-
Notifications
You must be signed in to change notification settings - Fork 318
/
Copy pathCustomComposite.cs
174 lines (160 loc) · 7.93 KB
/
CustomComposite.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
using System;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
// Let's say we want to have a composite that takes an axis and uses
// it's value to multiply the length of a vector from a stick. This could
// be used, for example, to have the right trigger on the gamepad act as
// a strength multiplier on the value of the left stick.
//
// We start by creating a class that is based on InputBindingComposite<>.
// The type we give it is the type of value that we will compute. In this
// case, we will consume a Vector2 from the stick so that is the type
// of value we return.
//
// NOTE: By advertising the type of value we return, we also allow the
// input system to filter out our composite if it is not applicable
// to a specific type of action. For example, if an action is set
// to "Value" as its type and its "Control Type" is set to "Axis",
// our composite will not be shown as our value type (Vector2) is
// incompatible with the value type of Axis (float).
//
// We can customize the way display strings are formed for our composite by
// annotating it with DisplayStringFormatAttribute. The string is simply a
// list with elements to be replaced enclosed in curly braces. Everything
// outside those will taken verbatim. The fragments inside the curly braces
// in this case refer to the binding composite parts by name. Each such
// instance is replaced with the display text for the corresponding
// part binding.
//
// NOTE: We don't supply a name for the composite here. The default logic
// will take the name of the type ("CustomComposite" in our case)
// and snip off "Composite" if used as a suffix (which is the case
// for us) and then use that as the name. So in our case, we are
// registering a composite called "Custom" here.
//
// If we were to use our composite with the AddCompositeBinding API,
// for example, it would look like this:
//
// myAction.AddCompositeBinding("Custom")
// .With("Stick", "<Gamepad>/leftStick")
// .With("Multiplier", "<Gamepad>/rightTrigger");
[DisplayStringFormat("{multiplier}*{stick}")]
public class CustomComposite : InputBindingComposite<Vector2>
{
// So, we need two parts for our composite. The part that delivers the stick
// value and the part that delivers the axis multiplier. Note that each part
// may be bound to multiple controls. The input system handles that for us
// by giving us an integer identifier for each part that reads a single value
// from however many controls are bound to the part.
//
// In our case, this could be used, for example, to bind the "multiplier" part
// to both the left and the right trigger on the gamepad.
// To tell the input system of a "part" binding that we need for a composite,
// we add a public field with an "int" type and annotated with an [InputControl]
// attribute. We set the "layout" property on the attribute to tell the system
// what kind of control we expect to be bound to the part.
//
// NOTE: These part binding need to be *public fields* for the input system
// to find them.
//
// So this is introduces a part to the composite called "multiplier" and
// expecting an "Axis" control. The value of the field will be set by the
// input system. It will be some internal, unique numeric ID for the part
// which we can then use with InputBindingCompositeContext.ReadValue to
// read out the value of just that part.
[InputControl(layout = "Axis")]
public int multiplier;
// The other part we need is for the stick.
//
// NOTE: We could use "Stick" here but "Vector2" is a little less restrictive.
[InputControl(layout = "Vector2")]
public int stick;
// We may also expose "parameters" on our composite. These can be configured
// graphically in the action editor and also through AddCompositeBinding.
//
// Let's say we want to allow the user to specify an additional scale factor
// to apply to the value of "multiplier". We can do so by simply adding a
// public field of type float. Any public field that is not annotated with
// [InputControl] will be treated as a possible parameter.
//
// If we added a composite with AddCompositeBinding, we could configure the
// parameter like so:
//
// myAction.AddCompositeBinding("Custom(scaleFactor=0.5)"
// .With("Multiplier", "<Gamepad>/rightTrigger")
// .With("Stick", "<Gamepad>/leftStick");
public float scaleFactor = 1;
// Ok, so now we have all the configuration in place. The final piece we
// need is the actual logic that reads input from "multiplier" and "stick"
// and computes a final input value.
//
// We can do that by defining a ReadValue method which is the actual workhorse
// for our composite.
public override Vector2 ReadValue(ref InputBindingCompositeContext context)
{
// We read input from the parts we have by simply
// supplying the part IDs that the input system has set up
// for us to ReadValue.
//
// NOTE: Vector2 is a less straightforward than primitive value types
// like int and float. If there are multiple controls bound to the
// "stick" part, we need to tell the input system which one to pick.
// We do so by giving it an IComparer. In this case, we choose
// Vector2MagnitudeComparer to return the Vector2 with the greatest
// length.
var stickValue = context.ReadValue<Vector2, Vector2MagnitudeComparer>(stick);
var multiplierValue = context.ReadValue<float>(multiplier);
// The rest is simple. We just scale the vector we read by the
// multiple from the axis and apply our scale factor.
return stickValue * (multiplierValue * scaleFactor);
}
}
// Our custom composite is complete and fully functional. We could stop here and
// call it a day. However, for the sake of demonstration, let's say we also want
// to customize how the parameters for our composite are edited. We have "scaleFactor"
// so let's say we want to replace the default float inspector with a slider.
//
// We can replace the default UI by simply deriving a custom InputParameterEditor
// for our composite.
#if UNITY_EDITOR
public class CustomCompositeEditor : InputParameterEditor<CustomComposite>
{
public override void OnGUI()
{
// Using the 'target' property, we can access an instance of our composite.
var currentValue = target.scaleFactor;
// The easiest way to lay out our UI is to simply use EditorGUILayout.
// We simply assign the changed value back to the 'target' object. The input
// system will automatically detect a change in value.
target.scaleFactor = EditorGUILayout.Slider(m_ScaleFactorLabel, currentValue, 0, 2);
}
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
var slider = new Slider(m_ScaleFactorLabel.text, 0, 2)
{
value = target.scaleFactor,
showInputField = true
};
// Note: For UIToolkit sliders, as of Feb 2022, we can't register for the mouse up event directly
// on the slider because an element inside the slider captures the event. The workaround is to
// register for the event on the slider container. This will be fixed in a future version of
// UIToolkit.
slider.Q("unity-drag-container").RegisterCallback<MouseUpEvent>(evt =>
{
target.scaleFactor = slider.value;
onChangedCallback?.Invoke();
});
root.Add(slider);
}
#endif
private GUIContent m_ScaleFactorLabel = new GUIContent("Scale Factor");
}
#endif