Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /Editor /InputLayoutCodeGenerator.cs
| using System; | |
| using System.Collections.Generic; | |
| using System.IO; | |
| using System.Linq; | |
| using System.Reflection; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| using Unity.Collections.LowLevel.Unsafe; | |
| using UnityEngine.InputSystem.Controls; | |
| using UnityEngine.InputSystem.Layouts; | |
| using UnityEngine.InputSystem.Processors; | |
| using UnityEngine.InputSystem.Utilities; | |
| namespace UnityEngine.InputSystem.Editor | |
| { | |
| internal static class InputLayoutCodeGenerator | |
| { | |
| public static string GenerateCodeFileForDeviceLayout(string layoutName, string fileName, string prefix = "Fast") | |
| { | |
| string defines = null; | |
| string @namespace = null; | |
| var visibility = "public"; | |
| // If the file already exists, read out the changes we preserve. | |
| if (File.Exists(fileName)) | |
| { | |
| var lines = File.ReadLines(fileName).Take(50).ToList(); | |
| // Read out #defines. | |
| for (var i = 0; i < (lines.Count - 1); ++i) | |
| { | |
| var line = lines[i].Trim(); | |
| if (line.StartsWith("#if ")) | |
| defines = line.Substring("#if ".Length); | |
| else if (line.StartsWith("namespace ")) | |
| @namespace = line.Substring("namespace ".Length); | |
| } | |
| if (lines.Any(x => x.Contains("internal partial class " + prefix))) | |
| visibility = "internal"; | |
| } | |
| return GenerateCodeForDeviceLayout(layoutName, | |
| defines: defines, visibility: visibility, @namespace: @namespace, namePrefix: prefix); | |
| } | |
| /// <summary> | |
| /// Generate C# code that for the given device layout called <paramref name="layoutName"/> instantly creates | |
| /// an <see cref="InputDevice"/> equivalent to what the input system would create by manually interpreting | |
| /// the given <see cref="InputControlLayout"/>. | |
| /// </summary> | |
| /// <param name="layoutName">Name of the device layout to generate code for.</param> | |
| /// <param name="defines">Null/empty or a valid expression for an #if conditional compilation statement.</param> | |
| /// <param name="namePrefix">Prefix to prepend to the type name of <paramref name="layoutName"/>.</param> | |
| /// <param name="visibility">C# access modifier to use with the generated class.</param> | |
| /// <param name="namespace">Namespace to put the generated class in. If <c>null</c>, namespace of type behind <paramref name="layoutName"/> will be used.</param> | |
| /// <returns>C# source code for a precompiled version of the device layout.</returns> | |
| /// <remarks> | |
| /// The code generated by this method will be many times faster than the reflection-based <see cref="InputDevice"/> | |
| /// creation normally performed by the input system. It will also create less GC heap garbage. | |
| /// | |
| /// The downside to the generated code is that the makeup of the device is hardcoded and can no longer | |
| /// be changed by altering the <see cref="InputControlLayout"/> setup of the system. | |
| /// | |
| /// Note that it is possible to use this method with layouts generated on-the-fly by layout builders such as | |
| /// the one employed for <see cref="HID"/>. However, this must be done at compile/build time and can thus not | |
| /// be done for devices dynamically discovered at runtime. When this is acceptable, it is a way to dramatically | |
| /// speed up the creation of these devices. | |
| /// </remarks> | |
| /// <seealso cref="InputSystem.RegisterPrecompiledLayout{T}"/> | |
| public static unsafe string GenerateCodeForDeviceLayout(string layoutName, string defines = null, string namePrefix = "Fast", string visibility = "public", string @namespace = null) | |
| { | |
| if (string.IsNullOrEmpty(layoutName)) | |
| throw new ArgumentNullException(nameof(layoutName)); | |
| // Produce a device from the layout. | |
| var device = InputDevice.Build<InputDevice>(layoutName, noPrecompiledLayouts: true); | |
| // Get info about base type. | |
| var baseType = device.GetType(); | |
| var baseTypeName = baseType.Name; | |
| var baseTypeNamespace = baseType.Namespace; | |
| // Begin generating code. | |
| var writer = new InputActionCodeGenerator.Writer | |
| { | |
| buffer = new StringBuilder() | |
| }; | |
| writer.WriteLine(CSharpCodeHelpers.MakeAutoGeneratedCodeHeader("com.unity.inputsystem:InputLayoutCodeGenerator", | |
| InputSystem.version.ToString(), | |
| $"\"{layoutName}\" layout")); | |
| // Defines. | |
| if (defines != null) | |
| { | |
| writer.WriteLine($"#if {defines}"); | |
| writer.WriteLine(); | |
| } | |
| if (@namespace == null) | |
| @namespace = baseTypeNamespace; | |
| writer.WriteLine("using UnityEngine.InputSystem;"); | |
| writer.WriteLine("using UnityEngine.InputSystem.LowLevel;"); | |
| writer.WriteLine("using UnityEngine.InputSystem.Utilities;"); | |
| writer.WriteLine(""); | |
| writer.WriteLine("// Suppress warnings from local variables for control references"); | |
| writer.WriteLine("// that we don't end up using."); | |
| writer.WriteLine("#pragma warning disable CS0219"); | |
| writer.WriteLine(""); | |
| if (@namespace != "") | |
| writer.WriteLine("namespace " + @namespace); | |
| writer.BeginBlock(); | |
| writer.WriteLine($"{visibility} partial class {namePrefix}{baseTypeName} : {baseTypeNamespace}.{baseTypeName}"); | |
| writer.BeginBlock(); | |
| // "Metadata". ATM this is simply a flat, semicolon-separated list of names for layouts and processors that | |
| // we depend on. If any of them are touched, the precompiled layout should be considered invalidated. | |
| var internedLayoutName = new InternedString(layoutName); | |
| var allControls = device.allControls; | |
| var usedControlLayouts = allControls.Select(x => x.m_Layout).Distinct().ToList(); | |
| var layoutDependencies = string.Join(";", | |
| usedControlLayouts.SelectMany(l => InputControlLayout.s_Layouts.GetBaseLayouts(l)) | |
| .Union(InputControlLayout.s_Layouts.GetBaseLayouts(internedLayoutName))); | |
| var processorDependencies = string.Join(";", | |
| allControls.SelectMany(c => c.GetProcessors()).Select(p => InputProcessor.s_Processors.FindNameForType(p.GetType())) | |
| .Where(n => !n.IsEmpty()).Distinct()); | |
| var metadata = string.Join(";", processorDependencies, layoutDependencies); | |
| writer.WriteLine($"public const string metadata = \"{metadata}\";"); | |
| // Constructor. | |
| writer.WriteLine($"public {namePrefix}{baseTypeName}()"); | |
| writer.BeginBlock(); | |
| var usagesForEachControl = device.m_UsagesForEachControl; | |
| var usageToControl = device.m_UsageToControl; | |
| var aliasesForEachControl = device.m_AliasesForEachControl; | |
| var controlCount = allControls.Count; | |
| var usageCount = usagesForEachControl?.Length ?? 0; | |
| var aliasCount = aliasesForEachControl?.Length ?? 0; | |
| // Set up device control info. | |
| writer.WriteLine($"var builder = this.Setup({controlCount}, {usageCount}, {aliasCount})"); | |
| writer.WriteLine($" .WithName(\"{device.name}\")"); | |
| writer.WriteLine($" .WithDisplayName(\"{device.displayName}\")"); | |
| writer.WriteLine($" .WithChildren({device.m_ChildStartIndex}, {device.m_ChildCount})"); | |
| writer.WriteLine($" .WithLayout(new InternedString(\"{device.layout}\"))"); | |
| writer.WriteLine($" .WithStateBlock(new InputStateBlock {{ format = new FourCC({(int)device.stateBlock.format}), sizeInBits = {device.stateBlock.sizeInBits} }});"); | |
| if (device.noisy) | |
| writer.WriteLine("builder.IsNoisy(true);"); | |
| // Add controls to device. | |
| writer.WriteLine(); | |
| foreach (var layout in usedControlLayouts) | |
| writer.WriteLine($"var k{layout}Layout = new InternedString(\"{layout}\");"); | |
| for (var i = 0; i < controlCount; ++i) | |
| { | |
| var control = allControls[i]; | |
| var controlVariableName = MakeControlVariableName(control); | |
| writer.WriteLine(""); | |
| writer.WriteLine($"// {control.path}"); | |
| var parentName = "this"; | |
| if (control.parent != device) | |
| parentName = MakeControlVariableName(control.parent); | |
| writer.WriteLine($"var {controlVariableName} = {NameOfControlMethod(controlVariableName)}(k{control.layout}Layout, {parentName});"); | |
| } | |
| // Initialize usages array. | |
| if (usageCount > 0) | |
| { | |
| writer.WriteLine(); | |
| writer.WriteLine("// Usages."); | |
| for (var i = 0; i < usageCount; ++i) | |
| writer.WriteLine( | |
| $"builder.WithControlUsage({i}, new InternedString(\"{usagesForEachControl[i]}\"), {MakeControlVariableName(usageToControl[i])});"); | |
| } | |
| // Initialize aliases array. | |
| if (aliasCount > 0) | |
| { | |
| writer.WriteLine(); | |
| writer.WriteLine("// Aliases."); | |
| for (var i = 0; i < aliasCount; ++i) | |
| writer.WriteLine($"builder.WithControlAlias({i}, new InternedString(\"{aliasesForEachControl[i]}\"));"); | |
| } | |
| // Emit initializers for control getters and control arrays. This is usually what's getting set up | |
| // in FinishSetup(). We hardcode the look results here. | |
| var controlGetterProperties = new Dictionary<Type, List<PropertyInfo>>(); | |
| var controlArrayProperties = new Dictionary<Type, List<PropertyInfo>>(); | |
| writer.WriteLine(); | |
| writer.WriteLine("// Control getters/arrays."); | |
| writer.EmitControlArrayInitializers(device, "this", controlArrayProperties); | |
| writer.EmitControlGetterInitializers(device, "this", controlGetterProperties); | |
| for (var i = 0; i < controlCount; ++i) | |
| { | |
| var control = allControls[i]; | |
| var controlVariableName = MakeControlVariableName(control); | |
| writer.EmitControlArrayInitializers(control, controlVariableName, controlArrayProperties); | |
| writer.EmitControlGetterInitializers(control, controlVariableName, controlGetterProperties); | |
| } | |
| // State offset to control index map. | |
| if (device.m_StateOffsetToControlMap != null) | |
| { | |
| writer.WriteLine(); | |
| writer.WriteLine("// State offset to control index map."); | |
| writer.WriteLine("builder.WithStateOffsetToControlIndexMap(new uint[]"); | |
| writer.WriteLine("{"); | |
| ++writer.indentLevel; | |
| var map = device.m_StateOffsetToControlMap; | |
| var entryCount = map.Length; | |
| for (var index = 0; index < entryCount;) | |
| { | |
| if (index != 0) | |
| writer.WriteLine(); | |
| // 10 entries a line. | |
| writer.WriteIndent(); | |
| for (var i = 0; i < 10 && index < entryCount; ++index, ++i) | |
| writer.Write((index != 0 ? ", " : "") + map[index] + "u"); | |
| } | |
| writer.WriteLine(); | |
| --writer.indentLevel; | |
| writer.WriteLine("});"); | |
| } | |
| writer.WriteLine(); | |
| if (device.m_ControlTreeNodes != null) | |
| { | |
| if (device.m_ControlTreeIndices == null) | |
| throw new InvalidOperationException( | |
| $"Control tree indicies was null. Ensure the '{device.displayName}' device was created without errors."); | |
| writer.WriteLine("builder.WithControlTree(new byte[]"); | |
| writer.WriteLine("{"); | |
| ++writer.indentLevel; | |
| writer.WriteLine("// Control tree nodes as bytes"); | |
| var nodePtr = (byte*)UnsafeUtility.AddressOf(ref device.m_ControlTreeNodes[0]); | |
| var byteCount = device.m_ControlTreeNodes.Length * UnsafeUtility.SizeOf<InputDevice.ControlBitRangeNode>(); | |
| for (var i = 0; i < byteCount;) | |
| { | |
| if (i != 0) | |
| writer.WriteLine(); | |
| writer.WriteIndent(); | |
| for (var j = 0; j < 30 && i < byteCount; j++, i++) | |
| { | |
| writer.Write((i != 0 ? ", " : "") + *(nodePtr + i)); | |
| } | |
| } | |
| writer.WriteLine(); | |
| --writer.indentLevel; | |
| writer.WriteLine("}, new ushort[]"); | |
| ++writer.indentLevel; | |
| writer.WriteLine("{"); | |
| ++writer.indentLevel; | |
| writer.WriteLine("// Control tree node indicies"); | |
| writer.WriteLine(); | |
| for (var i = 0; i < device.m_ControlTreeIndices.Length;) | |
| { | |
| if (i != 0) | |
| writer.WriteLine(); | |
| writer.WriteIndent(); | |
| for (var j = 0; j < 30 && i < device.m_ControlTreeIndices.Length; j++, i++) | |
| { | |
| writer.Write((i != 0 ? ", " : "") + device.m_ControlTreeIndices[i]); | |
| } | |
| } | |
| writer.WriteLine(); | |
| --writer.indentLevel; | |
| writer.WriteLine("});"); | |
| --writer.indentLevel; | |
| } | |
| writer.WriteLine(); | |
| writer.WriteLine("builder.Finish();"); | |
| writer.EndBlock(); | |
| for (var i = 0; i < controlCount; ++i) | |
| { | |
| var control = allControls[i]; | |
| var controlType = control.GetType(); | |
| var controlVariableName = MakeControlVariableName(control); | |
| var controlFieldInits = control.GetInitializersForPublicPrimitiveTypeFields(); | |
| writer.WriteLine(); | |
| EmitControlMethod(writer, controlVariableName, controlType, controlFieldInits, i, control); | |
| } | |
| writer.EndBlock(); | |
| writer.EndBlock(); | |
| if (defines != null) | |
| writer.WriteLine($"#endif // {defines}"); | |
| return writer.buffer.ToString(); | |
| } | |
| private static string NameOfControlMethod(string controlVariableName) | |
| { | |
| return $"Initialize_{controlVariableName}"; | |
| } | |
| // We emit this as a separate method instead of directly inline to avoid generating a single massive constructor method | |
| // as these can lead to large build times with il2cpp and C++ compilers (https://fogbugz.unity3d.com/f/cases/1282090/). | |
| private static void EmitControlMethod(InputActionCodeGenerator.Writer writer, string controlVariableName, Type controlType, | |
| string controlFieldInits, int i, InputControl control) | |
| { | |
| var controlTypeName = controlType.FullName.Replace('+', '.'); | |
| writer.WriteLine($"private {controlTypeName} {NameOfControlMethod(controlVariableName)}(InternedString k{control.layout}Layout, InputControl parent)"); | |
| writer.BeginBlock(); | |
| writer.WriteLine($"var {controlVariableName} = new {controlTypeName}{controlFieldInits};"); | |
| writer.WriteLine($"{controlVariableName}.Setup()"); | |
| writer.WriteLine($" .At(this, {i})"); | |
| writer.WriteLine(" .WithParent(parent)"); | |
| if (control.children.Count > 0) | |
| writer.WriteLine($" .WithChildren({control.m_ChildStartIndex}, {control.m_ChildCount})"); | |
| writer.WriteLine($" .WithName(\"{control.name}\")"); | |
| writer.WriteLine($" .WithDisplayName(\"{control.m_DisplayNameFromLayout.Replace("\\", "\\\\")}\")"); | |
| if (!string.IsNullOrEmpty(control.m_ShortDisplayNameFromLayout)) | |
| writer.WriteLine( | |
| $" .WithShortDisplayName(\"{control.m_ShortDisplayNameFromLayout.Replace("\\", "\\\\")}\")"); | |
| writer.WriteLine($" .WithLayout(k{control.layout}Layout)"); | |
| if (control.usages.Count > 0) | |
| writer.WriteLine($" .WithUsages({control.m_UsageStartIndex}, {control.m_UsageCount})"); | |
| if (control.aliases.Count > 0) | |
| writer.WriteLine($" .WithAliases({control.m_AliasStartIndex}, {control.m_AliasCount})"); | |
| if (control.noisy) | |
| writer.WriteLine(" .IsNoisy(true)"); | |
| if (control.synthetic) | |
| writer.WriteLine(" .IsSynthetic(true)"); | |
| if (control.dontReset) | |
| writer.WriteLine(" .DontReset(true)"); | |
| if (control is ButtonControl) | |
| writer.WriteLine(" .IsButton(true)"); | |
| writer.WriteLine(" .WithStateBlock(new InputStateBlock"); | |
| writer.WriteLine(" {"); | |
| writer.WriteLine($" format = new FourCC({(int) control.stateBlock.format}),"); | |
| writer.WriteLine($" byteOffset = {control.stateBlock.byteOffset},"); | |
| writer.WriteLine($" bitOffset = {control.stateBlock.bitOffset},"); | |
| writer.WriteLine($" sizeInBits = {control.stateBlock.sizeInBits}"); | |
| writer.WriteLine(" })"); | |
| if (control.hasDefaultState) | |
| writer.WriteLine($" .WithDefaultState({control.m_DefaultState})"); | |
| if (control.m_MinValue != default || control.m_MaxValue != default) | |
| writer.WriteLine($" .WithMinAndMax({control.m_MinValue}, {control.m_MaxValue})"); | |
| foreach (var processor in control.GetProcessors()) | |
| { | |
| var isEditorWindowSpaceProcessor = processor is EditorWindowSpaceProcessor; | |
| if (isEditorWindowSpaceProcessor) | |
| writer.WriteLine(" #if UNITY_EDITOR"); | |
| var processorType = processor.GetType().FullName.Replace("+", "."); | |
| var valueType = InputProcessor.GetValueTypeFromType(processor.GetType()); | |
| var fieldInits = processor.GetInitializersForPublicPrimitiveTypeFields(); | |
| writer.WriteLine( | |
| $" .WithProcessor<InputProcessor<{valueType}>, {valueType}>(new {processorType}{fieldInits})"); | |
| if (isEditorWindowSpaceProcessor) | |
| writer.WriteLine(" #endif"); | |
| } | |
| writer.WriteLine(" .Finish();"); | |
| if (control is KeyControl key) | |
| writer.WriteLine($"{controlVariableName}.keyCode = UnityEngine.InputSystem.Key.{key.keyCode};"); | |
| else if (control is DpadControl.DpadAxisControl dpadAxis) | |
| writer.WriteLine($"{controlVariableName}.component = {dpadAxis.component};"); | |
| writer.WriteLine($"return {controlVariableName};"); | |
| writer.EndBlock(); | |
| } | |
| private static string MakeControlVariableName(InputControl control) | |
| { | |
| return "ctrl" + CSharpCodeHelpers.MakeIdentifier(control.path); | |
| } | |
| private static void EmitControlGetterInitializers(this InputActionCodeGenerator.Writer writer, InputControl control, | |
| string controlVariableName, Dictionary<Type, List<PropertyInfo>> controlGetterPropertyTable) | |
| { | |
| var type = control.GetType(); | |
| if (!controlGetterPropertyTable.TryGetValue(type, out var controlGetterProperties)) | |
| { | |
| controlGetterProperties = GetControlGetterProperties(type); | |
| controlGetterPropertyTable[type] = controlGetterProperties; | |
| } | |
| foreach (var property in controlGetterProperties) | |
| { | |
| var value = (InputControl)property.GetValue(control); | |
| if (value == null) | |
| continue; | |
| writer.WriteLine($"{controlVariableName}.{property.Name} = {MakeControlVariableName(value)};"); | |
| } | |
| } | |
| private static void EmitControlArrayInitializers(this InputActionCodeGenerator.Writer writer, InputControl control, | |
| string controlVariableName, Dictionary<Type, List<PropertyInfo>> controlArrayPropertyTable) | |
| { | |
| var type = control.GetType(); | |
| if (!controlArrayPropertyTable.TryGetValue(type, out var controlArrayProperties)) | |
| { | |
| controlArrayProperties = GetControlArrayProperties(type); | |
| controlArrayPropertyTable[type] = controlArrayProperties; | |
| } | |
| foreach (var property in controlArrayProperties) | |
| { | |
| var array = (Array)property.GetValue(control); | |
| if (array == null) | |
| continue; | |
| var arrayLength = array.Length; | |
| var arrayElementType = array.GetType().GetElementType(); | |
| writer.WriteLine($"{controlVariableName}.{property.Name} = new {arrayElementType.FullName.Replace('+','.')}[{arrayLength}];"); | |
| for (var i = 0; i < arrayLength; ++i) | |
| { | |
| var value = (InputControl)array.GetValue(i); | |
| if (value == null) | |
| continue; | |
| writer.WriteLine($"{controlVariableName}.{property.Name}[{i}] = {MakeControlVariableName(value)};"); | |
| } | |
| } | |
| } | |
| private static List<PropertyInfo> GetControlGetterProperties(Type type) | |
| { | |
| return type.GetProperties(BindingFlags.Instance | BindingFlags.Public) | |
| .Where(x => typeof(InputControl).IsAssignableFrom(x.PropertyType) && x.CanRead && x.CanWrite && | |
| x.GetIndexParameters().LengthSafe() == 0 && x.Name != "device" && x.Name != "parent").ToList(); | |
| } | |
| private static List<PropertyInfo> GetControlArrayProperties(Type type) | |
| { | |
| return type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) | |
| .Where(x => x.PropertyType.IsArray && typeof(InputControl).IsAssignableFrom(x.PropertyType.GetElementType()) && x.CanRead && x.CanWrite && | |
| x.GetIndexParameters().LengthSafe() == 0).ToList(); | |
| } | |
| } | |
| } | |