#if UNITY_EDITOR using System; 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; 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.WriteLine($"public partial class @{options.className}: IInputActionCollection2, IDisposable"); writer.BeginBlock(); writer.WriteLine($"public InputActionAsset asset {{ get; }}"); // Default constructor. 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("public void Dispose()"); writer.BeginBlock(); writer.WriteLine("UnityEngine.Object.Destroy(asset);"); writer.EndBlock(); writer.WriteLine(); writer.WriteLine("public InputBinding? bindingMask"); writer.BeginBlock(); writer.WriteLine("get => asset.bindingMask;"); writer.WriteLine("set => asset.bindingMask = value;"); writer.EndBlock(); writer.WriteLine(); writer.WriteLine("public ReadOnlyArray? devices"); writer.BeginBlock(); writer.WriteLine("get => asset.devices;"); writer.WriteLine("set => asset.devices = value;"); writer.EndBlock(); writer.WriteLine(); writer.WriteLine("public ReadOnlyArray controlSchemes => asset.controlSchemes;"); writer.WriteLine(); writer.WriteLine("public bool Contains(InputAction action)"); writer.BeginBlock(); writer.WriteLine("return asset.Contains(action);"); writer.EndBlock(); writer.WriteLine(); writer.WriteLine("public IEnumerator GetEnumerator()"); writer.BeginBlock(); writer.WriteLine("return asset.GetEnumerator();"); writer.EndBlock(); writer.WriteLine(); writer.WriteLine("IEnumerator IEnumerable.GetEnumerator()"); writer.BeginBlock(); writer.WriteLine("return GetEnumerator();"); writer.EndBlock(); writer.WriteLine(); writer.WriteLine("public void Enable()"); writer.BeginBlock(); writer.WriteLine("asset.Enable();"); writer.EndBlock(); writer.WriteLine(); writer.WriteLine("public void Disable()"); writer.BeginBlock(); writer.WriteLine("asset.Disable();"); writer.EndBlock(); writer.WriteLine(); writer.WriteLine("public IEnumerable bindings => asset.bindings;"); writer.WriteLine(); writer.WriteLine("public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false)"); writer.BeginBlock(); writer.WriteLine("return asset.FindAction(actionNameOrId, throwIfNotFound);"); writer.EndBlock(); writer.WriteLine(); 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. 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.WriteLine($"public struct {mapTypeName}"); writer.BeginBlock(); // Constructor. writer.WriteLine($"private @{options.className} m_Wrapper;"); 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.WriteLine( $"public InputAction @{actionName} => m_Wrapper.m_{mapName}_{actionName};"); } // Action map getter. writer.WriteLine($"public InputActionMap Get() {{ return m_Wrapper.m_{mapName}; }}"); // Enable/disable methods. writer.WriteLine("public void Enable() { Get().Enable(); }"); writer.WriteLine("public void Disable() { Get().Disable(); }"); writer.WriteLine("public bool enabled => Get().enabled;"); // Implicit conversion operator. writer.WriteLine( $"public static implicit operator InputActionMap({mapTypeName} set) {{ return set.Get(); }}"); // AddCallbacks method. 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.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.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.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.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.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. foreach (var map in maps) { var typeName = CSharpCodeHelpers.MakeTypeName(map.name); writer.WriteLine($"public interface I{typeName}Actions"); writer.BeginBlock(); foreach (var action in map.actions) { var methodName = CSharpCodeHelpers.MakeTypeName(action.name); 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(' '); } } } // 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