#if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; 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); } /// /// Generate C# code that for the given device layout called instantly creates /// an equivalent to what the input system would create by manually interpreting /// the given . /// /// Name of the device layout to generate code for. /// Null/empty or a valid expression for an #if conditional compilation statement. /// Prefix to prepend to the type name of . /// C# access modifier to use with the generated class. /// Namespace to put the generated class in. If null, namespace of type behind will be used. /// C# source code for a precompiled version of the device layout. /// /// The code generated by this method will be many times faster than the reflection-based /// 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 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 . 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. /// /// public static 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(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>(); var controlArrayProperties = new Dictionary>(); 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(); 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, {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> 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> 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 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 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(); } } } #endif // UNITY_EDITOR