mvaganov

InputControlBindingBehaviour.cs

Sep 10th, 2025 (edited)
188
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 13.62 KB | Software | 0 0
  1. // Unlicensed: this code is explicitly placed into the public domain.
  2. // I wrote this because the Unity InputSystem is confusing to setup, and this is more intuitive for me.
  3. using UnityEngine;
  4. using UnityEngine.InputSystem;
  5. using System;
  6. using System.Collections.Generic;
  7. using UnityEngine.Events;
  8. using ControlCallback = System.Action<UnityEngine.InputSystem.InputAction.CallbackContext>;
  9. #if UNITY_EDITOR
  10. using UnityEditor;
  11. using System.Linq;
  12. using UnityEngine.InputSystem.Layouts;
  13. using StringDict = System.Collections.Generic.Dictionary<string, string>;
  14. #endif
  15.  
  16. public class InputControlBindingBehaviour : MonoBehaviour {
  17.     public InputControlBinding input = new InputControlBinding();
  18.     private void Reset() {
  19.         input.Binding = new List<InputControlBinder>{
  20.             new InputControlBinder("jump", "<Keyboard>/space", this, DefaultInputListener, DefaultInputListener),
  21.             new InputControlBinder("attack", "<Mouse>/leftButton", this, DefaultInputListener, null)
  22.         };
  23.     }
  24.     private void Start() { input.Init(); input.Enable(); }
  25.     private void OnEnable() => input.Enable();
  26.     private void OnDisable() => input.Disable();
  27.     private void OnDestroy() => input.Destroy();
  28.     public void DefaultInputListener(InputAction.CallbackContext context) {
  29.         Debug.Log(context.action.name + " -> input " + (context.canceled ? "released" : "pressed") + ": " +
  30.             context.control.ToString() + "\n" + context.phase + " " + context.ReadValueAsObject());
  31.     }
  32.     [ContextMenu("Preview Runtime Action Map")] private void PreviewRuntimeActionMap() { input.Init(); }
  33. }
  34.  
  35. [Serializable] public class InputControlBinding {
  36.     public List<InputControlBinder> Binding = new List<InputControlBinder>();
  37.     [SerializeField] public InputActionMap InputActionMapRuntime;
  38.     public void Init() {
  39.         Disable(); Destroy(); InputActionMapRuntime = new InputActionMap();
  40.         foreach (InputControlBinder binding in Binding) { AddBindingInternal(binding); }
  41.     }
  42.     private InputAction AddBindingInternal(InputControlBinder binding) {
  43.         InputAction inputAction = InputActionMapRuntime.AddAction(binding.name, InputActionType.Button);
  44.         inputAction.AddBinding(binding.key);
  45.         binding.BindCallbacksInto(inputAction);
  46.         if (InputActionMapRuntime.enabled) { inputAction.Enable(); }
  47.         return inputAction;
  48.     }
  49.     public InputAction Bind(InputControlBinder binding) {
  50.         Binding.Add(binding);
  51.         if (InputActionMapRuntime.actions.Count > 0 || InputActionMapRuntime.enabled) {
  52.             return AddBindingInternal(binding);
  53.         }
  54.         return null;
  55.     }
  56.     public void Enable() => InputActionMapRuntime.Enable();
  57.     public void Disable() => InputActionMapRuntime.Disable();
  58.     public void Destroy() {
  59.         ExplicitlyRemoveCallbacksFromInputActions(); // may be necessary, shouldn't harm data integrity if not necessary
  60.         InputActionMapRuntime.Dispose();
  61.     }
  62.     private void ExplicitlyRemoveCallbacksFromInputActions() {
  63.         foreach (var inputAction in InputActionMapRuntime.actions) {
  64.             List<InputControlBinder> bindings = Binding; // Binding.FindAll(info => info.name == action.name);
  65.             bindings.ForEach(binding => binding.UnbindCallbacksFrom(inputAction));
  66.         }
  67.     }
  68. }
  69.  
  70. [Serializable] public class InputControlBinder {
  71.     [Serializable] public class UnityEvent_CallbackContext : UnityEvent<InputAction.CallbackContext> { }
  72.     public string name;
  73.     [InputControlPath] public string key;
  74.     [Serializable] public class EventCallbacks {
  75.         public UnityEvent_CallbackContext onPress = new UnityEvent_CallbackContext();
  76.         public UnityEvent_CallbackContext onRelease = new UnityEvent_CallbackContext();
  77.     }
  78.     public EventCallbacks callbacks = new EventCallbacks();
  79.     public InputControlBinder(string name, string key, UnityEngine.Object callbackOwner,
  80.         ControlCallback onPressCallback, ControlCallback onReleaseCallback) {
  81.         this.name = name; this.key = key;
  82.         Bind(callbacks.onPress, callbackOwner, onPressCallback);
  83.         Bind(callbacks.onRelease, callbackOwner, onReleaseCallback);
  84.     }
  85.     public static void Bind<T>(UnityEvent<T> dest, UnityEngine.Object funcOwner, Action<T> func) {
  86. #if UNITY_EDITOR
  87.         if (funcOwner != null) try {
  88.             var unityAction = (UnityAction<T>)Delegate.CreateDelegate(typeof(UnityAction<T>), funcOwner, func.Method);
  89.             UnityEditor.Events.UnityEventTools.AddPersistentListener(dest, unityAction);
  90.         } catch (Exception e) { Debug.LogException(e); }
  91. #endif
  92.         dest.AddListener(new UnityAction<T>(func));
  93.     }
  94.     public void BindCallbacksInto(InputAction action) {
  95.         action.started += callbacks.onPress.Invoke;
  96.         action.canceled += callbacks.onRelease.Invoke;
  97.     }
  98.     public void UnbindCallbacksFrom(InputAction action) {
  99.         action.started -= callbacks.onPress.Invoke;
  100.         action.canceled -= callbacks.onRelease.Invoke;
  101.     }
  102. }
  103. public static class ControlInputs {
  104.     public const string Keyboard = "Keyboard", Mouse = "Mouse", Gamepad = "Gamepad", Touchscreen = "Touchscreen";
  105.     public static readonly (string name, string[] fallback)[] Data = new (string name, string[] fallback)[] {
  106.         (Keyboard, new string[] {
  107.             "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u",
  108.             "v", "w", "x", "y", "z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
  109.             "space", "enter", "escape", "tab", "shift", "ctrl", "alt", "leftArrow", "rightArrow", "upArrow", "downArrow"
  110.         }), (Mouse, new string[] {
  111.             "leftButton", "rightButton", "middleButton", "forwardButton", "backButton", "scroll/up", "scroll/down"
  112.         }), (Gamepad, new string[] {
  113.             "buttonSouth", "buttonEast", "buttonWest", "buttonNorth", "leftShoulder", "rightShoulder",
  114.             "leftTrigger", "rightTrigger", "leftStickPress", "rightStickPress", "start", "select",
  115.             "dpad/up", "dpad/down", "dpad/left", "dpad/right",
  116.         }), (Touchscreen, new string[]{ "Press" })
  117.     };
  118.     private static string[] _names;
  119.     public static string[] Names => _names != null ? _names : _names = Array.ConvertAll(Data, d => d.name);
  120. }
  121.  
  122. public class InputControlPathAttribute : PropertyAttribute {
  123.     private bool[] explicitlyInclude;
  124.     /// <param name="controlsToExplicitlyInclude">should be const string value from <see cref="ControlInputs"/></param>
  125.     public InputControlPathAttribute(params string[] controlsToExplicitlyInclude) {
  126.         if (controlsToExplicitlyInclude == null || controlsToExplicitlyInclude.Length == 0) { return; }
  127.         string[] names = ControlInputs.Names;
  128.         explicitlyInclude = new bool[ControlInputs.Names.Length];
  129.         for (int i = 0; i < controlsToExplicitlyInclude.Length; i++) {
  130.             explicitlyInclude[ConvertNameToIndex(controlsToExplicitlyInclude[i])] = true;
  131.         }
  132.     }
  133.     public bool Includes(string controlName) {
  134.         if (explicitlyInclude == null || explicitlyInclude.Length == 0) { return true; }
  135.         return explicitlyInclude[ConvertNameToIndex(controlName)];
  136.     }
  137.     public static int ConvertNameToIndex(string controlName) {
  138.         int index = Array.IndexOf(ControlInputs.Names, controlName);
  139.         if (index < 0) { throw new Exception($"{controlName} not in [{string.Join(", ", ControlInputs.Names)}]"); }
  140.         return index;
  141.     }
  142.     public bool Includes(int controlInputIndex) => explicitlyInclude == null || explicitlyInclude[controlInputIndex];
  143. }
  144.  
  145. #if UNITY_EDITOR
  146. [CustomPropertyDrawer(typeof(InputControlBinder))]
  147. public class InputBindInfoDrawer : PropertyDrawer {
  148.     public override void OnGUI(Rect guiRect, SerializedProperty property, GUIContent label) {
  149.         EditorGUI.BeginProperty(guiRect, label, property);
  150.         SerializedProperty nameProperty = property.FindPropertyRelative(nameof(InputControlBinder.name));
  151.         SerializedProperty keyProperty = property.FindPropertyRelative(nameof(InputControlBinder.key));
  152.         SerializedProperty callbacksProperty = property.FindPropertyRelative(nameof(InputControlBinder.callbacks));
  153.         Rect firstLineRect = new Rect(guiRect.x, guiRect.y, guiRect.width, EditorGUIUtility.singleLineHeight);
  154.         float nameWidth = firstLineRect.width * 0.4f, keyWidth = firstLineRect.width * 0.6f - 5f;
  155.         Rect nameRect = new Rect(firstLineRect.x, firstLineRect.y, nameWidth, firstLineRect.height);
  156.         Rect keyRect = new Rect(firstLineRect.x + nameWidth + 5f, firstLineRect.y, keyWidth, firstLineRect.height);
  157.         EditorGUI.PropertyField(nameRect, nameProperty, GUIContent.none);
  158.         EditorGUI.PropertyField(keyRect, keyProperty, GUIContent.none);
  159.         float y = guiRect.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
  160.         Rect callbacksRect = new Rect(guiRect.x, y, guiRect.width, EditorGUI.GetPropertyHeight(callbacksProperty, true));
  161.         EditorGUI.PropertyField(callbacksRect, callbacksProperty, true);
  162.         EditorGUI.EndProperty();
  163.     }
  164.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
  165.         SerializedProperty callbacksProperty = property.FindPropertyRelative(nameof(InputControlBinder.callbacks));
  166.         return EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing +
  167.             EditorGUI.GetPropertyHeight(callbacksProperty, true);
  168.     }
  169. }
  170.  
  171. [CustomPropertyDrawer(typeof(InputControlPathAttribute))]
  172. public class InputControlPathDrawer : PropertyDrawer {
  173.     private const string None = "None";
  174.     private static Dictionary<string, StringDict> SpecificControlPaths = new Dictionary<string, StringDict>();
  175.     private static StringDict allControlPaths;
  176.     static InputControlPathDrawer() => RefreshControlPaths();
  177.     private static void RefreshControlPaths() {
  178.         allControlPaths = new StringDict();
  179.         foreach (var inputDefinition in ControlInputs.Data) {
  180.             StringDict controls = SpecificControlPaths[inputDefinition.name] = new StringDict();
  181.             PopulateControlPaths(inputDefinition.name, controls, inputDefinition.fallback);
  182.             AddDictionaryToDictionary(allControlPaths, controls);
  183.         }
  184.     }
  185.     private static void AddDictionaryToDictionary<K, V>(Dictionary<K, V> destination, Dictionary<K, V> valuesToCopy) {
  186.         foreach (var kvp in valuesToCopy) { destination[kvp.Key] = kvp.Value; }
  187.     }
  188.     private static void PopulateControlPaths(string inputName, StringDict controlPaths, string[] fallback) {
  189.         try {
  190.             InputControlLayout controlLayout = InputSystem.LoadLayout(inputName);
  191.             foreach (InputControlLayout.ControlItem control in controlLayout.controls) {
  192.                 string path = $"<{inputName}>/{control.name}";
  193.                 string controlName = (string.IsNullOrEmpty(control.displayName) ? control.name : control.displayName);
  194.                 string safeDisplayName = $"{inputName}: {controlName}".Replace("/", "\u2215");
  195.                 controlPaths[path] = safeDisplayName;
  196.             }
  197.         } catch {
  198.             foreach (string key in fallback) {
  199.                 string path = $"<{inputName}>/{key}".Replace("/", "\u2215");
  200.                 controlPaths[path] = ConvertPathToHumanReadableDisplayName(path);
  201.             }
  202.         }
  203.     }
  204.     private static string ConvertPathToHumanReadableDisplayName(string controlPath) {
  205.         int indexOfInputNameEnd = controlPath.IndexOf('/');
  206.         if (indexOfInputNameEnd >= 0) {
  207.             string device = controlPath.Substring(0, indexOfInputNameEnd).Trim('<', '>');
  208.             string control = controlPath.Substring(indexOfInputNameEnd + 1);
  209.             return $"{device}: {control}".Replace("/", "\u2215");
  210.         }
  211.         return controlPath;
  212.     }
  213.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
  214.         EditorGUI.BeginProperty(position, label, property);
  215.         StringDict filteredPaths = GetFilteredPaths();
  216.         StringDict visiblePaths = new StringDict { { "", None } };
  217.         AddDictionaryToDictionary(visiblePaths, filteredPaths);
  218.         string currentPath = property.stringValue;
  219.         string currentDisplayName = GetDisplayName(currentPath);
  220.         if (!visiblePaths.ContainsKey(currentPath) && !string.IsNullOrEmpty(currentPath)) {
  221.             currentDisplayName += " (filtered)";
  222.         }
  223.         Rect rect = EditorGUI.PrefixLabel(position, label);
  224.         if (EditorGUI.DropdownButton(rect, new GUIContent(currentDisplayName), FocusType.Keyboard)) {
  225.             GenericMenu menu = CreateControlPathDropdown(currentPath, property, (InputControlPathAttribute)attribute);
  226.             menu.ShowAsContext();
  227.         }
  228.         EditorGUI.EndProperty();
  229.     }
  230.     private GenericMenu CreateControlPathDropdown(string path, SerializedProperty prop, InputControlPathAttribute attr) {
  231.         GenericMenu menu = new GenericMenu();
  232.         menu.AddItem(new GUIContent(None), string.IsNullOrEmpty(path), () => SetPropertyValue(prop, ""));
  233.         menu.AddSeparator("");
  234.         for(int i = 0; i < ControlInputs.Names.Length; ++i) {
  235.             if (attr.Includes(i)) { AddInputControlPathSubmenu(ControlInputs.Names[i]); }
  236.         }
  237.         void AddInputControlPathSubmenu(string inputName) {
  238.             StringDict controlPathNames = SpecificControlPaths[inputName];
  239.             if (controlPathNames.Count == 0) { return; }
  240.             menu.AddSeparator($"{inputName}/");
  241.             foreach (var kvp in controlPathNames.OrderBy(x => x.Value)) {
  242.                 string controlName = ExtractControlName(kvp.Value);
  243.                 string menuPath = $"{inputName}/{controlName}";
  244.                 menu.AddItem(new GUIContent(menuPath), path == kvp.Key, () => SetPropertyValue(prop, kvp.Key));
  245.             }
  246.         }
  247.         return menu;
  248.     }
  249.     private StringDict GetFilteredPaths() {
  250.         InputControlPathAttribute attr = (InputControlPathAttribute)attribute;
  251.         StringDict filteredPaths = new StringDict();
  252.         for (int i = 0; i < ControlInputs.Names.Length; ++i) {
  253.             if (attr.Includes(i)) { AddDictionaryToDictionary(filteredPaths, SpecificControlPaths[ControlInputs.Names[i]]); }
  254.         }
  255.         return filteredPaths;
  256.     }
  257.     private string GetDisplayName(string controlPath) {
  258.         if (string.IsNullOrEmpty(controlPath)) { return None; }
  259.         if (allControlPaths.TryGetValue(controlPath, out string displayName)) { return displayName; }
  260.         return ConvertPathToHumanReadableDisplayName(controlPath);
  261.     }
  262.     private string ExtractControlName(string displayName) {
  263.         int colonIndex = displayName.IndexOf(": ");
  264.         return colonIndex >= 0 ? displayName.Substring(colonIndex + 2) : displayName;
  265.     }
  266.     private void SetPropertyValue(SerializedProperty property, string value) {
  267.         property.stringValue = value;
  268.         property.serializedObject.ApplyModifiedProperties();
  269.     }
  270. }
  271. #endif
  272.  
Advertisement
Add Comment
Please, Sign In to add comment