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