#if UNITY_EDITOR using System; using System.Collections; using System.IO; using System.Linq; using System.Text; using UnityEngine.InputSystem.Utilities; using UnityEditor; ////TODO: option to allow referencing the original asset rather than embedding it ////TODO: emit indexer directly at toplevel so you can more easily look up actions dynamically ////TODO: put the generated code behind #if that depends on input system ////TODO: suffix map properties with Map or Actions (e.g. "PlayerMap" instead of "Player") ////TODO: unify the generated events so that performed, canceled, and started all go into a single event ////TODO: look up actions and maps by ID rather than by name ////TODO: only generate @something if @ is really needed ////TODO: allow having an unnamed or default-named action set which spills actions directly into the toplevel wrapper ////TODO: add cleanup for ActionEvents ////TODO: protect generated wrapper against modifications made to asset ////TODO: make capitalization consistent in the generated code ////TODO: instead of loading from JSON, generate the structure in code ////REVIEW: allow putting *all* of the data from the inputactions asset into the generated class? namespace UnityEngine.InputSystem.Editor { /// /// Utility to generate code that makes it easier to work with action sets. /// public static class InputActionCodeGenerator { private const int kSpacesPerIndentLevel = 4; private const string kClassExample = @"using namespace UnityEngine; using UnityEngine.InputSystem; // Example of using an InputActionMap named ""Player"" from a UnityEngine.MonoBehaviour implementing callback interface. public class Example : MonoBehaviour, MyActions.IPlayerActions { private MyActions_Actions m_Actions; // Source code representation of asset. private MyActions_Actions.PlayerActions m_Player; // Source code representation of action map. void Awake() { m_Actions = new MyActions_Actions(); // Create asset object. m_Player = m_Actions.Player; // Extract action map object. m_Player.AddCallbacks(this); // Register callback interface IPlayerActions. } void OnDestroy() { m_Actions.Dispose(); // Destroy asset object. } void OnEnable() { m_Player.Enable(); // Enable all actions within map. } void OnDisable() { m_Player.Disable(); // Disable all actions within map. } #region Interface implementation of MyActions.IPlayerActions // Invoked when ""Move"" action is either started, performed or canceled. public void OnMove(InputAction.CallbackContext context) { Debug.Log($""OnMove: {context.ReadValue()}""); } // Invoked when ""Attack"" action is either started, performed or canceled. public void OnAttack(InputAction.CallbackContext context) { Debug.Log($""OnAttack: {context.ReadValue()}""); } #endregion }"; public struct Options { public string className { get; set; } public string namespaceName { get; set; } public string sourceAssetPath { get; set; } } public static string GenerateWrapperCode(InputActionAsset asset, Options options = default) { if (asset == null) throw new ArgumentNullException(nameof(asset)); if (string.IsNullOrEmpty(options.sourceAssetPath)) options.sourceAssetPath = AssetDatabase.GetAssetPath(asset); if (string.IsNullOrEmpty(options.className) && !string.IsNullOrEmpty(asset.name)) options.className = CSharpCodeHelpers.MakeTypeName(asset.name); if (string.IsNullOrEmpty(options.className)) { if (string.IsNullOrEmpty(options.sourceAssetPath)) throw new ArgumentException("options.sourceAssetPath"); options.className = CSharpCodeHelpers.MakeTypeName(Path.GetFileNameWithoutExtension(options.sourceAssetPath)); } var writer = new Writer { buffer = new StringBuilder() }; // Header. writer.WriteLine(CSharpCodeHelpers.MakeAutoGeneratedCodeHeader("com.unity.inputsystem:InputActionCodeGenerator", InputSystem.version.ToString(), options.sourceAssetPath)); // Usings. writer.WriteLine("using System;"); writer.WriteLine("using System.Collections;"); writer.WriteLine("using System.Collections.Generic;"); writer.WriteLine("using UnityEngine.InputSystem;"); writer.WriteLine("using UnityEngine.InputSystem.Utilities;"); writer.WriteLine(""); // Begin namespace. var haveNamespace = !string.IsNullOrEmpty(options.namespaceName); if (haveNamespace) { writer.WriteLine($"namespace {options.namespaceName}"); writer.BeginBlock(); } // Begin class. writer.DocSummary($"Provides programmatic access to , " + ", and " + " instances defined " + $"in asset \"{options.sourceAssetPath}\"."); writer.DocRemarks("This class is source generated and any manual edits will be discarded if the associated asset is reimported or modified."); writer.DocExample(kClassExample); writer.WriteLine($"public partial class @{options.className}: IInputActionCollection2, IDisposable"); writer.BeginBlock(); writer.DocSummary("Provides access to the underlying asset instance."); writer.WriteLine($"public InputActionAsset asset {{ get; }}"); writer.WriteLine(); // Default constructor. writer.DocSummary("Constructs a new instance."); writer.WriteLine($"public @{options.className}()"); writer.BeginBlock(); writer.WriteLine($"asset = InputActionAsset.FromJson(@\"{asset.ToJson().Replace("\"", "\"\"")}\");"); var maps = asset.actionMaps; var schemes = asset.controlSchemes; foreach (var map in maps) { var mapName = CSharpCodeHelpers.MakeIdentifier(map.name); writer.WriteLine($"// {map.name}"); writer.WriteLine($"m_{mapName} = asset.FindActionMap(\"{map.name}\", throwIfNotFound: true);"); foreach (var action in map.actions) { var actionName = CSharpCodeHelpers.MakeIdentifier(action.name); writer.WriteLine($"m_{mapName}_{actionName} = m_{mapName}.FindAction(\"{action.name}\", throwIfNotFound: true);"); } } writer.EndBlock(); writer.WriteLine(); writer.WriteLine($"~@{options.className}()"); writer.BeginBlock(); foreach (var map in maps) { var mapName = CSharpCodeHelpers.MakeIdentifier(map.name); writer.WriteLine($"UnityEngine.Debug.Assert(!m_{mapName}.enabled, \"This will cause a leak and performance issues, {options.className}.{mapName}.Disable() has not been called.\");"); } writer.EndBlock(); writer.WriteLine(); writer.DocSummary("Destroys this asset and all associated instances."); writer.WriteLine("public void Dispose()"); writer.BeginBlock(); writer.WriteLine("UnityEngine.Object.Destroy(asset);"); writer.EndBlock(); writer.WriteLine(); var classNamePrefix = typeof(InputActionAsset).Namespace + "." + nameof(InputActionAsset) + "."; writer.DocInherit(classNamePrefix + nameof(InputActionAsset.bindingMask)); writer.WriteLine("public InputBinding? bindingMask"); writer.BeginBlock(); writer.WriteLine("get => asset.bindingMask;"); writer.WriteLine("set => asset.bindingMask = value;"); writer.EndBlock(); writer.WriteLine(); writer.DocInherit(classNamePrefix + nameof(InputActionAsset.devices)); writer.WriteLine("public ReadOnlyArray? devices"); writer.BeginBlock(); writer.WriteLine("get => asset.devices;"); writer.WriteLine("set => asset.devices = value;"); writer.EndBlock(); writer.WriteLine(); writer.DocInherit(classNamePrefix + nameof(InputActionAsset.controlSchemes)); writer.WriteLine("public ReadOnlyArray controlSchemes => asset.controlSchemes;"); writer.WriteLine(); writer.DocInherit(classNamePrefix + nameof(InputActionAsset.Contains) + "(InputAction)"); writer.WriteLine("public bool Contains(InputAction action)"); writer.BeginBlock(); writer.WriteLine("return asset.Contains(action);"); writer.EndBlock(); writer.WriteLine(); writer.DocInherit(classNamePrefix + nameof(InputActionAsset.GetEnumerator) + "()"); writer.WriteLine("public IEnumerator GetEnumerator()"); writer.BeginBlock(); writer.WriteLine("return asset.GetEnumerator();"); writer.EndBlock(); writer.WriteLine(); writer.DocInherit(nameof(IEnumerable) + "." + nameof(IEnumerable.GetEnumerator) + "()"); writer.WriteLine("IEnumerator IEnumerable.GetEnumerator()"); writer.BeginBlock(); writer.WriteLine("return GetEnumerator();"); writer.EndBlock(); writer.WriteLine(); writer.DocInherit(classNamePrefix + nameof(InputActionAsset.Enable) + "()"); writer.WriteLine("public void Enable()"); writer.BeginBlock(); writer.WriteLine("asset.Enable();"); writer.EndBlock(); writer.WriteLine(); writer.DocInherit(classNamePrefix + nameof(InputActionAsset.Disable) + "()"); writer.WriteLine("public void Disable()"); writer.BeginBlock(); writer.WriteLine("asset.Disable();"); writer.EndBlock(); writer.WriteLine(); writer.DocInherit(classNamePrefix + nameof(InputActionAsset.bindings)); writer.WriteLine("public IEnumerable bindings => asset.bindings;"); writer.WriteLine(); writer.DocInherit(classNamePrefix + nameof(InputActionAsset.FindAction) + "(string, bool)"); writer.WriteLine("public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false)"); writer.BeginBlock(); writer.WriteLine("return asset.FindAction(actionNameOrId, throwIfNotFound);"); writer.EndBlock(); writer.WriteLine(); writer.DocInherit(classNamePrefix + nameof(InputActionAsset.FindBinding) + "(InputBinding, out InputAction)"); writer.WriteLine("public int FindBinding(InputBinding bindingMask, out InputAction action)"); writer.BeginBlock(); writer.WriteLine("return asset.FindBinding(bindingMask, out action);"); writer.EndBlock(); // Action map accessors. var inputActionMapClassPrefix = typeof(InputActionMap).Namespace + "." + nameof(InputActionMap) + "."; foreach (var map in maps) { writer.WriteLine(); writer.WriteLine($"// {map.name}"); var mapName = CSharpCodeHelpers.MakeIdentifier(map.name); var mapTypeName = CSharpCodeHelpers.MakeTypeName(mapName, "Actions"); // Caching field for action map. writer.WriteLine($"private readonly InputActionMap m_{mapName};"); writer.WriteLine(string.Format("private List m_{0}CallbackInterfaces = new List();", mapTypeName)); // Caching fields for all actions. foreach (var action in map.actions) { var actionName = CSharpCodeHelpers.MakeIdentifier(action.name); writer.WriteLine($"private readonly InputAction m_{mapName}_{actionName};"); } // Struct wrapping access to action set. writer.DocSummary($"Provides access to input actions defined in input action map \"{map.name}\"."); writer.WriteLine($"public struct {mapTypeName}"); writer.BeginBlock(); writer.WriteLine($"private @{options.className} m_Wrapper;"); writer.WriteLine(); // Constructor. writer.DocSummary("Construct a new instance of the input action map wrapper class."); writer.WriteLine($"public {mapTypeName}(@{options.className} wrapper) {{ m_Wrapper = wrapper; }}"); // Getter for each action. foreach (var action in map.actions) { var actionName = CSharpCodeHelpers.MakeIdentifier(action.name); writer.DocSummary($"Provides access to the underlying input action \"{mapName}/{actionName}\"."); writer.WriteLine( $"public InputAction @{actionName} => m_Wrapper.m_{mapName}_{actionName};"); } // Action map getter. writer.DocSummary("Provides access to the underlying input action map instance."); writer.WriteLine($"public InputActionMap Get() {{ return m_Wrapper.m_{mapName}; }}"); // Enable/disable methods. writer.DocInherit(inputActionMapClassPrefix + nameof(InputActionMap.Enable) + "()"); writer.WriteLine("public void Enable() { Get().Enable(); }"); writer.DocInherit(inputActionMapClassPrefix + nameof(InputActionMap.Disable) + "()"); writer.WriteLine("public void Disable() { Get().Disable(); }"); writer.DocInherit(inputActionMapClassPrefix + nameof(InputActionMap.enabled)); writer.WriteLine("public bool enabled => Get().enabled;"); // Implicit conversion operator. writer.DocSummary($"Implicitly converts an to an instance."); writer.WriteLine( $"public static implicit operator InputActionMap({mapTypeName} set) {{ return set.Get(); }}"); // AddCallbacks method. writer.DocSummary("Adds , and callbacks provided via on all input actions contained in this map."); writer.DocParam("instance", "Callback instance."); writer.DocRemarks("If is null or have already been added this method does nothing."); writer.DocSeeAlso(mapTypeName); writer.WriteLine($"public void AddCallbacks(I{mapTypeName} instance)"); writer.BeginBlock(); // Initialize new interface. writer.WriteLine($"if (instance == null || m_Wrapper.m_{mapTypeName}CallbackInterfaces.Contains(instance)) return;"); writer.WriteLine($"m_Wrapper.m_{mapTypeName}CallbackInterfaces.Add(instance);"); foreach (var action in map.actions) { var actionName = CSharpCodeHelpers.MakeIdentifier(action.name); var actionTypeName = CSharpCodeHelpers.MakeTypeName(action.name); writer.WriteLine($"@{actionName}.started += instance.On{actionTypeName};"); writer.WriteLine($"@{actionName}.performed += instance.On{actionTypeName};"); writer.WriteLine($"@{actionName}.canceled += instance.On{actionTypeName};"); } writer.EndBlock(); writer.WriteLine(); // UnregisterCallbacks method. writer.DocSummary("Removes , and callbacks provided via on all input actions contained in this map."); writer.DocRemarks("Calling this method when have not previously been registered has no side-effects."); writer.DocSeeAlso(mapTypeName); writer.WriteLine($"private void UnregisterCallbacks(I{mapTypeName} instance)"); writer.BeginBlock(); foreach (var action in map.actions) { var actionName = CSharpCodeHelpers.MakeIdentifier(action.name); var actionTypeName = CSharpCodeHelpers.MakeTypeName(action.name); writer.WriteLine($"@{actionName}.started -= instance.On{actionTypeName};"); writer.WriteLine($"@{actionName}.performed -= instance.On{actionTypeName};"); writer.WriteLine($"@{actionName}.canceled -= instance.On{actionTypeName};"); } writer.EndBlock(); writer.WriteLine(); // RemoveCallbacks method. writer.DocSummary($"Unregisters and unregisters all input action callbacks via ."); writer.DocSeeAlso($"{mapTypeName}.UnregisterCallbacks(I{mapTypeName})"); writer.WriteLine($"public void RemoveCallbacks(I{mapTypeName} instance)"); writer.BeginBlock(); writer.WriteLine($"if (m_Wrapper.m_{mapTypeName}CallbackInterfaces.Remove(instance))"); writer.WriteLine($" UnregisterCallbacks(instance);"); writer.EndBlock(); writer.WriteLine(); // SetCallbacks method. writer.DocSummary($"Replaces all existing callback instances and previously registered input action callbacks associated with them with callbacks provided via ."); writer.DocRemarks($"If is null, calling this method will only unregister all existing callbacks but not register any new callbacks."); writer.DocSeeAlso($"{mapTypeName}.AddCallbacks(I{mapTypeName})"); writer.DocSeeAlso($"{mapTypeName}.RemoveCallbacks(I{mapTypeName})"); writer.DocSeeAlso($"{mapTypeName}.UnregisterCallbacks(I{mapTypeName})"); writer.WriteLine($"public void SetCallbacks(I{mapTypeName} instance)"); writer.BeginBlock(); ////REVIEW: this would benefit from having a single callback on InputActions rather than three different endpoints writer.WriteLine($"foreach (var item in m_Wrapper.m_{mapTypeName}CallbackInterfaces)"); writer.WriteLine($" UnregisterCallbacks(item);"); writer.WriteLine($"m_Wrapper.m_{mapTypeName}CallbackInterfaces.Clear();"); // Initialize new interface. writer.WriteLine("AddCallbacks(instance);"); writer.EndBlock(); writer.EndBlock(); // Getter for instance of struct. writer.DocSummary($"Provides a new instance referencing this action map."); writer.WriteLine($"public {mapTypeName} @{mapName} => new {mapTypeName}(this);"); } // Control scheme accessors. foreach (var scheme in schemes) { var identifier = CSharpCodeHelpers.MakeIdentifier(scheme.name); writer.WriteLine($"private int m_{identifier}SchemeIndex = -1;"); writer.DocSummary("Provides access to the input control scheme."); writer.DocSeeAlso(typeof(InputControlScheme).Namespace + "." + nameof(InputControlScheme)); writer.WriteLine($"public InputControlScheme {identifier}Scheme"); writer.BeginBlock(); writer.WriteLine("get"); writer.BeginBlock(); writer.WriteLine($"if (m_{identifier}SchemeIndex == -1) m_{identifier}SchemeIndex = asset.FindControlSchemeIndex(\"{scheme.name}\");"); writer.WriteLine($"return asset.controlSchemes[m_{identifier}SchemeIndex];"); writer.EndBlock(); writer.EndBlock(); } // Generate interfaces. var inputActionClassReference = typeof(InputAction).Namespace + "." + nameof(InputAction) + "."; foreach (var map in maps) { var typeName = CSharpCodeHelpers.MakeTypeName(map.name); writer.DocSummary($"Interface to implement callback methods for all input action callbacks associated with input actions defined by \"{map.name}\" which allows adding and removing callbacks."); writer.DocSeeAlso($"{typeName}Actions.AddCallbacks(I{typeName}Actions)"); writer.DocSeeAlso($"{typeName}Actions.RemoveCallbacks(I{typeName}Actions)"); writer.WriteLine($"public interface I{typeName}Actions"); writer.BeginBlock(); foreach (var action in map.actions) { var methodName = CSharpCodeHelpers.MakeTypeName(action.name); writer.DocSummary($"Method invoked when associated input action \"{action.name}\" is either , or ."); writer.DocSeeAlso(string.Concat(inputActionClassReference, "started")); writer.DocSeeAlso(string.Concat(inputActionClassReference, "performed")); writer.DocSeeAlso(string.Concat(inputActionClassReference, "canceled")); writer.WriteLine($"void On{methodName}(InputAction.CallbackContext context);"); } writer.EndBlock(); } // End class. writer.EndBlock(); // End namespace. if (haveNamespace) writer.EndBlock(); return writer.buffer.ToString(); } ////TODO: move this to a shared place internal struct Writer { public StringBuilder buffer; public int indentLevel; public void BeginBlock() { WriteIndent(); buffer.Append("{\n"); ++indentLevel; } public void EndBlock() { --indentLevel; WriteIndent(); buffer.Append("}\n"); } public void WriteLine() { buffer.Append('\n'); } public void WriteLine(string text) { if (!text.All(char.IsWhiteSpace)) { WriteIndent(); buffer.Append(text); } buffer.Append('\n'); } public void Write(string text) { buffer.Append(text); } public void WriteIndent() { for (var i = 0; i < indentLevel; ++i) { for (var n = 0; n < kSpacesPerIndentLevel; ++n) buffer.Append(' '); } } public void DocSummary(string text) { DocElement("summary", text); } public void DocParam(string paramName, string text) { WriteLine($"/// {text}"); } public void DocRemarks(string text) { DocElement("remarks", text); } public void DocInherit(string cref) { DocReference("inheritdoc", cref); } public void DocSeeAlso(string cref) { DocReference("seealso", cref: cref); } public void DocExample(string code) { DocComment(""); DocComment(""); foreach (var line in code.Split('\n')) DocComment(line.Replace("<", "<").Replace(">", ">")); DocComment(""); DocComment(""); } private void DocComment(string text) { if (string.IsNullOrEmpty(text)) WriteLine("///"); else WriteLine(string.Concat("/// ", text)); } private void DocElement(string tag, string text) { DocComment($"<{tag}>"); DocComment(text); DocComment($""); } private void DocReference(string tag, string cref) { DocInlineElement(tag, "cref", cref); } private void DocInlineElement(string tag, string property, string value) { DocComment($"<{tag} {property}=\"{value}\" />"); } } // Updates the given file with wrapper code generated for the given action sets. // If the generated code is unchanged, does not touch the file. // Returns true if the file was touched, false otherwise. public static bool GenerateWrapperCode(string filePath, InputActionAsset asset, Options options) { if (!Path.HasExtension(filePath)) filePath += ".cs"; // Generate code. var code = GenerateWrapperCode(asset, options); // Check if the code changed. Don't write if it hasn't. if (File.Exists(filePath)) { var existingCode = File.ReadAllText(filePath); if (existingCode == code || existingCode.WithAllWhitespaceStripped() == code.WithAllWhitespaceStripped()) return false; } // Write. EditorHelpers.CheckOut(filePath); File.WriteAllText(filePath, code); return true; } } } #endif // UNITY_EDITOR