A game about forced loneliness, made by TACStudios
at master 430 lines 18 kB view raw
1#if UNITY_EDITOR 2using System; 3using System.IO; 4using System.Linq; 5using System.Text; 6using UnityEngine.InputSystem.Utilities; 7using UnityEditor; 8 9////TODO: option to allow referencing the original asset rather than embedding it 10 11////TODO: emit indexer directly at toplevel so you can more easily look up actions dynamically 12 13////TODO: put the generated code behind #if that depends on input system 14 15////TODO: suffix map properties with Map or Actions (e.g. "PlayerMap" instead of "Player") 16 17////TODO: unify the generated events so that performed, canceled, and started all go into a single event 18 19////TODO: look up actions and maps by ID rather than by name 20 21////TODO: only generate @something if @ is really needed 22 23////TODO: allow having an unnamed or default-named action set which spills actions directly into the toplevel wrapper 24 25////TODO: add cleanup for ActionEvents 26 27////TODO: protect generated wrapper against modifications made to asset 28 29////TODO: make capitalization consistent in the generated code 30 31////TODO: instead of loading from JSON, generate the structure in code 32 33////REVIEW: allow putting *all* of the data from the inputactions asset into the generated class? 34 35namespace UnityEngine.InputSystem.Editor 36{ 37 /// <summary> 38 /// Utility to generate code that makes it easier to work with action sets. 39 /// </summary> 40 public static class InputActionCodeGenerator 41 { 42 private const int kSpacesPerIndentLevel = 4; 43 44 public struct Options 45 { 46 public string className { get; set; } 47 public string namespaceName { get; set; } 48 public string sourceAssetPath { get; set; } 49 } 50 51 public static string GenerateWrapperCode(InputActionAsset asset, Options options = default) 52 { 53 if (asset == null) 54 throw new ArgumentNullException(nameof(asset)); 55 56 if (string.IsNullOrEmpty(options.sourceAssetPath)) 57 options.sourceAssetPath = AssetDatabase.GetAssetPath(asset); 58 if (string.IsNullOrEmpty(options.className) && !string.IsNullOrEmpty(asset.name)) 59 options.className = 60 CSharpCodeHelpers.MakeTypeName(asset.name); 61 62 if (string.IsNullOrEmpty(options.className)) 63 { 64 if (string.IsNullOrEmpty(options.sourceAssetPath)) 65 throw new ArgumentException("options.sourceAssetPath"); 66 options.className = 67 CSharpCodeHelpers.MakeTypeName(Path.GetFileNameWithoutExtension(options.sourceAssetPath)); 68 } 69 70 var writer = new Writer 71 { 72 buffer = new StringBuilder() 73 }; 74 75 // Header. 76 writer.WriteLine(CSharpCodeHelpers.MakeAutoGeneratedCodeHeader("com.unity.inputsystem:InputActionCodeGenerator", 77 InputSystem.version.ToString(), 78 options.sourceAssetPath)); 79 80 // Usings. 81 writer.WriteLine("using System;"); 82 writer.WriteLine("using System.Collections;"); 83 writer.WriteLine("using System.Collections.Generic;"); 84 writer.WriteLine("using UnityEngine.InputSystem;"); 85 writer.WriteLine("using UnityEngine.InputSystem.Utilities;"); 86 writer.WriteLine(""); 87 88 // Begin namespace. 89 var haveNamespace = !string.IsNullOrEmpty(options.namespaceName); 90 if (haveNamespace) 91 { 92 writer.WriteLine($"namespace {options.namespaceName}"); 93 writer.BeginBlock(); 94 } 95 96 // Begin class. 97 writer.WriteLine($"public partial class @{options.className}: IInputActionCollection2, IDisposable"); 98 writer.BeginBlock(); 99 100 writer.WriteLine($"public InputActionAsset asset {{ get; }}"); 101 102 // Default constructor. 103 writer.WriteLine($"public @{options.className}()"); 104 writer.BeginBlock(); 105 writer.WriteLine($"asset = InputActionAsset.FromJson(@\"{asset.ToJson().Replace("\"", "\"\"")}\");"); 106 107 var maps = asset.actionMaps; 108 var schemes = asset.controlSchemes; 109 foreach (var map in maps) 110 { 111 var mapName = CSharpCodeHelpers.MakeIdentifier(map.name); 112 writer.WriteLine($"// {map.name}"); 113 writer.WriteLine($"m_{mapName} = asset.FindActionMap(\"{map.name}\", throwIfNotFound: true);"); 114 115 foreach (var action in map.actions) 116 { 117 var actionName = CSharpCodeHelpers.MakeIdentifier(action.name); 118 writer.WriteLine($"m_{mapName}_{actionName} = m_{mapName}.FindAction(\"{action.name}\", throwIfNotFound: true);"); 119 } 120 } 121 writer.EndBlock(); 122 writer.WriteLine(); 123 124 writer.WriteLine($"~@{options.className}()"); 125 writer.BeginBlock(); 126 foreach (var map in maps) 127 { 128 var mapName = CSharpCodeHelpers.MakeIdentifier(map.name); 129 writer.WriteLine($"UnityEngine.Debug.Assert(!m_{mapName}.enabled, \"This will cause a leak and performance issues, {options.className}.{mapName}.Disable() has not been called.\");"); 130 } 131 writer.EndBlock(); 132 writer.WriteLine(); 133 134 writer.WriteLine("public void Dispose()"); 135 writer.BeginBlock(); 136 writer.WriteLine("UnityEngine.Object.Destroy(asset);"); 137 writer.EndBlock(); 138 writer.WriteLine(); 139 140 writer.WriteLine("public InputBinding? bindingMask"); 141 writer.BeginBlock(); 142 writer.WriteLine("get => asset.bindingMask;"); 143 writer.WriteLine("set => asset.bindingMask = value;"); 144 writer.EndBlock(); 145 writer.WriteLine(); 146 147 writer.WriteLine("public ReadOnlyArray<InputDevice>? devices"); 148 writer.BeginBlock(); 149 writer.WriteLine("get => asset.devices;"); 150 writer.WriteLine("set => asset.devices = value;"); 151 writer.EndBlock(); 152 writer.WriteLine(); 153 154 writer.WriteLine("public ReadOnlyArray<InputControlScheme> controlSchemes => asset.controlSchemes;"); 155 writer.WriteLine(); 156 157 writer.WriteLine("public bool Contains(InputAction action)"); 158 writer.BeginBlock(); 159 writer.WriteLine("return asset.Contains(action);"); 160 writer.EndBlock(); 161 writer.WriteLine(); 162 163 writer.WriteLine("public IEnumerator<InputAction> GetEnumerator()"); 164 writer.BeginBlock(); 165 writer.WriteLine("return asset.GetEnumerator();"); 166 writer.EndBlock(); 167 writer.WriteLine(); 168 169 writer.WriteLine("IEnumerator IEnumerable.GetEnumerator()"); 170 writer.BeginBlock(); 171 writer.WriteLine("return GetEnumerator();"); 172 writer.EndBlock(); 173 writer.WriteLine(); 174 175 writer.WriteLine("public void Enable()"); 176 writer.BeginBlock(); 177 writer.WriteLine("asset.Enable();"); 178 writer.EndBlock(); 179 writer.WriteLine(); 180 181 writer.WriteLine("public void Disable()"); 182 writer.BeginBlock(); 183 writer.WriteLine("asset.Disable();"); 184 writer.EndBlock(); 185 writer.WriteLine(); 186 187 writer.WriteLine("public IEnumerable<InputBinding> bindings => asset.bindings;"); 188 writer.WriteLine(); 189 190 writer.WriteLine("public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false)"); 191 writer.BeginBlock(); 192 writer.WriteLine("return asset.FindAction(actionNameOrId, throwIfNotFound);"); 193 writer.EndBlock(); 194 writer.WriteLine(); 195 196 writer.WriteLine("public int FindBinding(InputBinding bindingMask, out InputAction action)"); 197 writer.BeginBlock(); 198 writer.WriteLine("return asset.FindBinding(bindingMask, out action);"); 199 writer.EndBlock(); 200 201 // Action map accessors. 202 foreach (var map in maps) 203 { 204 writer.WriteLine(); 205 writer.WriteLine($"// {map.name}"); 206 207 var mapName = CSharpCodeHelpers.MakeIdentifier(map.name); 208 var mapTypeName = CSharpCodeHelpers.MakeTypeName(mapName, "Actions"); 209 210 // Caching field for action map. 211 writer.WriteLine($"private readonly InputActionMap m_{mapName};"); 212 writer.WriteLine(string.Format("private List<I{0}> m_{0}CallbackInterfaces = new List<I{0}>();", mapTypeName)); 213 214 // Caching fields for all actions. 215 foreach (var action in map.actions) 216 { 217 var actionName = CSharpCodeHelpers.MakeIdentifier(action.name); 218 writer.WriteLine($"private readonly InputAction m_{mapName}_{actionName};"); 219 } 220 221 // Struct wrapping access to action set. 222 writer.WriteLine($"public struct {mapTypeName}"); 223 writer.BeginBlock(); 224 225 // Constructor. 226 writer.WriteLine($"private @{options.className} m_Wrapper;"); 227 writer.WriteLine($"public {mapTypeName}(@{options.className} wrapper) {{ m_Wrapper = wrapper; }}"); 228 229 // Getter for each action. 230 foreach (var action in map.actions) 231 { 232 var actionName = CSharpCodeHelpers.MakeIdentifier(action.name); 233 writer.WriteLine( 234 $"public InputAction @{actionName} => m_Wrapper.m_{mapName}_{actionName};"); 235 } 236 237 // Action map getter. 238 writer.WriteLine($"public InputActionMap Get() {{ return m_Wrapper.m_{mapName}; }}"); 239 240 // Enable/disable methods. 241 writer.WriteLine("public void Enable() { Get().Enable(); }"); 242 writer.WriteLine("public void Disable() { Get().Disable(); }"); 243 writer.WriteLine("public bool enabled => Get().enabled;"); 244 245 // Implicit conversion operator. 246 writer.WriteLine( 247 $"public static implicit operator InputActionMap({mapTypeName} set) {{ return set.Get(); }}"); 248 249 // AddCallbacks method. 250 writer.WriteLine($"public void AddCallbacks(I{mapTypeName} instance)"); 251 writer.BeginBlock(); 252 253 // Initialize new interface. 254 writer.WriteLine($"if (instance == null || m_Wrapper.m_{mapTypeName}CallbackInterfaces.Contains(instance)) return;"); 255 writer.WriteLine($"m_Wrapper.m_{mapTypeName}CallbackInterfaces.Add(instance);"); 256 257 foreach (var action in map.actions) 258 { 259 var actionName = CSharpCodeHelpers.MakeIdentifier(action.name); 260 var actionTypeName = CSharpCodeHelpers.MakeTypeName(action.name); 261 262 writer.WriteLine($"@{actionName}.started += instance.On{actionTypeName};"); 263 writer.WriteLine($"@{actionName}.performed += instance.On{actionTypeName};"); 264 writer.WriteLine($"@{actionName}.canceled += instance.On{actionTypeName};"); 265 } 266 267 writer.EndBlock(); 268 writer.WriteLine(); 269 270 // UnregisterCallbacks method. 271 writer.WriteLine($"private void UnregisterCallbacks(I{mapTypeName} instance)"); 272 writer.BeginBlock(); 273 foreach (var action in map.actions) 274 { 275 var actionName = CSharpCodeHelpers.MakeIdentifier(action.name); 276 var actionTypeName = CSharpCodeHelpers.MakeTypeName(action.name); 277 278 writer.WriteLine($"@{actionName}.started -= instance.On{actionTypeName};"); 279 writer.WriteLine($"@{actionName}.performed -= instance.On{actionTypeName};"); 280 writer.WriteLine($"@{actionName}.canceled -= instance.On{actionTypeName};"); 281 } 282 writer.EndBlock(); 283 writer.WriteLine(); 284 285 // RemoveCallbacks method. 286 writer.WriteLine($"public void RemoveCallbacks(I{mapTypeName} instance)"); 287 writer.BeginBlock(); 288 writer.WriteLine($"if (m_Wrapper.m_{mapTypeName}CallbackInterfaces.Remove(instance))"); 289 writer.WriteLine($" UnregisterCallbacks(instance);"); 290 writer.EndBlock(); 291 writer.WriteLine(); 292 293 // SetCallbacks method. 294 writer.WriteLine($"public void SetCallbacks(I{mapTypeName} instance)"); 295 writer.BeginBlock(); 296 297 ////REVIEW: this would benefit from having a single callback on InputActions rather than three different endpoints 298 299 writer.WriteLine($"foreach (var item in m_Wrapper.m_{mapTypeName}CallbackInterfaces)"); 300 writer.WriteLine($" UnregisterCallbacks(item);"); 301 writer.WriteLine($"m_Wrapper.m_{mapTypeName}CallbackInterfaces.Clear();"); 302 303 // Initialize new interface. 304 writer.WriteLine("AddCallbacks(instance);"); 305 writer.EndBlock(); 306 writer.EndBlock(); 307 308 // Getter for instance of struct. 309 writer.WriteLine($"public {mapTypeName} @{mapName} => new {mapTypeName}(this);"); 310 } 311 312 // Control scheme accessors. 313 foreach (var scheme in schemes) 314 { 315 var identifier = CSharpCodeHelpers.MakeIdentifier(scheme.name); 316 317 writer.WriteLine($"private int m_{identifier}SchemeIndex = -1;"); 318 writer.WriteLine($"public InputControlScheme {identifier}Scheme"); 319 writer.BeginBlock(); 320 writer.WriteLine("get"); 321 writer.BeginBlock(); 322 writer.WriteLine($"if (m_{identifier}SchemeIndex == -1) m_{identifier}SchemeIndex = asset.FindControlSchemeIndex(\"{scheme.name}\");"); 323 writer.WriteLine($"return asset.controlSchemes[m_{identifier}SchemeIndex];"); 324 writer.EndBlock(); 325 writer.EndBlock(); 326 } 327 328 // Generate interfaces. 329 foreach (var map in maps) 330 { 331 var typeName = CSharpCodeHelpers.MakeTypeName(map.name); 332 writer.WriteLine($"public interface I{typeName}Actions"); 333 writer.BeginBlock(); 334 335 foreach (var action in map.actions) 336 { 337 var methodName = CSharpCodeHelpers.MakeTypeName(action.name); 338 writer.WriteLine($"void On{methodName}(InputAction.CallbackContext context);"); 339 } 340 341 writer.EndBlock(); 342 } 343 344 // End class. 345 writer.EndBlock(); 346 347 // End namespace. 348 if (haveNamespace) 349 writer.EndBlock(); 350 351 return writer.buffer.ToString(); 352 } 353 354 ////TODO: move this to a shared place 355 internal struct Writer 356 { 357 public StringBuilder buffer; 358 public int indentLevel; 359 360 public void BeginBlock() 361 { 362 WriteIndent(); 363 buffer.Append("{\n"); 364 ++indentLevel; 365 } 366 367 public void EndBlock() 368 { 369 --indentLevel; 370 WriteIndent(); 371 buffer.Append("}\n"); 372 } 373 374 public void WriteLine() 375 { 376 buffer.Append('\n'); 377 } 378 379 public void WriteLine(string text) 380 { 381 if (!text.All(char.IsWhiteSpace)) 382 { 383 WriteIndent(); 384 buffer.Append(text); 385 } 386 buffer.Append('\n'); 387 } 388 389 public void Write(string text) 390 { 391 buffer.Append(text); 392 } 393 394 public void WriteIndent() 395 { 396 for (var i = 0; i < indentLevel; ++i) 397 { 398 for (var n = 0; n < kSpacesPerIndentLevel; ++n) 399 buffer.Append(' '); 400 } 401 } 402 } 403 404 // Updates the given file with wrapper code generated for the given action sets. 405 // If the generated code is unchanged, does not touch the file. 406 // Returns true if the file was touched, false otherwise. 407 public static bool GenerateWrapperCode(string filePath, InputActionAsset asset, Options options) 408 { 409 if (!Path.HasExtension(filePath)) 410 filePath += ".cs"; 411 412 // Generate code. 413 var code = GenerateWrapperCode(asset, options); 414 415 // Check if the code changed. Don't write if it hasn't. 416 if (File.Exists(filePath)) 417 { 418 var existingCode = File.ReadAllText(filePath); 419 if (existingCode == code || existingCode.WithAllWhitespaceStripped() == code.WithAllWhitespaceStripped()) 420 return false; 421 } 422 423 // Write. 424 EditorHelpers.CheckOut(filePath); 425 File.WriteAllText(filePath, code); 426 return true; 427 } 428 } 429} 430#endif // UNITY_EDITOR