#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($"{tag}>");
}
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