📜 ⬆️ ⬇️

Automating the maintenance of correspondence between the names of layers in the editor and code using CodeDom

When working with Unity, it is often necessary to refer to Unity entities (collision layers, sorting layers, tags, input axes, scenes) by their names. If some of them, for example, rename in the editor, then you need not forget, respectively, correct the name in the code, otherwise we will get an error. And this error will occur not at compilation, but at run time, immediately at the time of the appeal by name. A little automation will save from such unpleasant surprises.

At first glance, it is logical to use T4 templates to solve this problem , but they seemed inconvenient to me (at least when used in the Unity project), so I chose a different approach. Using CodeDom to solve such a small problem may seem to overingineering, but personal experience proved the viability of this approach: I wrote my simple code generator more than a year ago, and since then, without making changes to it, I successfully used it, which saved me a lot of nerves and decent seconds of time.

In this article, we will look at writing an ascetic code generator with constants containing the names of the collision layers . Working with the names of other entities is done similarly.

The general plan of action is as follows:
')

Get the list of collision layer names


Everything is simple, if not afraid to climb into places called internal. More specifically, the list of names of the collision layers is stored as a field of the “inner” class.

private static IEnumerable<string> GetAllLayers() { return InternalEditorUtility.layers; } 

We generate the code itself


CodeGen has its own terminology (compare, for example, with Roslyn terminology), but, in general, everything corresponds to the syntax tree inherent in C # code. In order from root to leaf, we will use the following:

  1. CodeCompilationUnit is the code generator itself, which we are configuring here, so to speak.
  2. CodeNamespace is the namespace in which our class will sit. We will not wrap the class in an explicit namespace, but we still have to create an instance of the CodeNamespace.
  3. CodeTypeDeclaration is the class itself.
  4. CodeMemberField is a member of a class (in this case, a constant declaration).
  5. CodePrimitiveExpression is an expression with a literal (in this case, a string that will be assigned to a constant).

We generate a public string constant whose name and value match the name of the collision layer.

 private static CodeMemberField GenerateConstant(string name) { name = name.Replace(" ", ""); var @const = new CodeMemberField( typeof(string), name); @const.Attributes &= ~MemberAttributes.AccessMask; @const.Attributes &= ~MemberAttributes.ScopeMask; @const.Attributes |= MemberAttributes.Public; @const.Attributes |= MemberAttributes.Const; @const.InitExpression = new CodePrimitiveExpression(name); return @const; } 

CodeGen has one minor inconvenience: it does not know how to create static classes. This is due to the fact that it was created at the dawn of the C # language, when static classes were not “brought” to it yet. We will get out: we will simulate a static class with a sealed class with a private constructor. So did some of the early C # users, and those using the Java language are forced to resort to it now.

 private static void ImitateStaticClass(CodeTypeDeclaration type) { @type.TypeAttributes |= TypeAttributes.Sealed; @type.Members.Add(new CodeConstructor { Attributes = MemberAttributes.Private | MemberAttributes.Final }); } 

Finally, let's assemble the class itself, with a private constructor and constants:

 private static CodeCompileUnit GenerateClassWithConstants( string name, IEnumerable<string> constants) { var compileUnit = new CodeCompileUnit(); var @namespace = new CodeNamespace(); var @class = new CodeTypeDeclaration(name); ImitateStaticClass(@class); foreach (var constantName in constants) { var @const = GenerateConstant(constantName); @class.Members.Add(@const); } @namespace.Types.Add(@class); compileUnit.Namespaces.Add(@namespace); return compileUnit; } 

Write the code to the file


 private static void WriteIntoFile(string fullPath, CodeCompileUnit code) { Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); using (var stream = new StreamWriter(fullPath, append: false)) { var writer = new IndentedTextWriter(stream); using (var codeProvider = new CSharpCodeProvider()) { codeProvider.GenerateCodeFromCompileUnit(code, writer, new CodeGeneratorOptions()); } } } 

Making Unity immediately “aware” of changes


This is the last step, and it does not require a significant amount of code, so let it be the function that will respond to user input.

 [MenuItem("Habr/Generate layers constants")] private static void GenerateAndForceImport() { const string path = @"Auto/Layers.cs"; var fullPath = Path.Combine(Application.dataPath, path); var className = Path.GetFileNameWithoutExtension(fullPath); var code = GenerateClassWithConstants(className, GetAllLayers()); WriteIntoFile(fullPath, code); AssetDatabase.ImportAsset("Assets/" + path, ImportAssetOptions.ForceUpdate); AssetDatabase.Refresh(); } 

Result


Putting it all together:

Total generator code
 namespace Habr { using Microsoft.CSharp; using System.CodeDom; using System.CodeDom.Compiler; using System.Collections.Generic; using System.IO; using System.Reflection; using UnityEditor; using UnityEditorInternal; using UnityEngine; internal static class HabrCodeGen { [MenuItem("Habr/Generate layers constants")] private static void GenerateAndForceImport() { const string path = @"Auto/Layers.cs"; var fullPath = Path.Combine(Application.dataPath, path); var className = Path.GetFileNameWithoutExtension(fullPath); var code = GenerateClassWithConstants(className, GetAllLayers()); WriteIntoFile(fullPath, code); AssetDatabase.ImportAsset("Assets/" + path, ImportAssetOptions.ForceUpdate); AssetDatabase.Refresh(); } private static CodeCompileUnit GenerateClassWithConstants( string name, IEnumerable<string> constants) { var compileUnit = new CodeCompileUnit(); var @namespace = new CodeNamespace(); var @class = new CodeTypeDeclaration(name); ImitateStaticClass(@class); foreach (var constantName in constants) { var @const = GenerateConstant(constantName); @class.Members.Add(@const); } @namespace.Types.Add(@class); compileUnit.Namespaces.Add(@namespace); return compileUnit; } private static CodeMemberField GenerateConstant(string name) { name = name.Replace(" ", ""); var @const = new CodeMemberField( typeof(string), name); @const.Attributes &= ~MemberAttributes.AccessMask; @const.Attributes &= ~MemberAttributes.ScopeMask; @const.Attributes |= MemberAttributes.Public; @const.Attributes |= MemberAttributes.Const; @const.InitExpression = new CodePrimitiveExpression(name); return @const; } private static IEnumerable<string> GetAllLayers() { return InternalEditorUtility.layers; } private static void ImitateStaticClass(CodeTypeDeclaration type) { @type.TypeAttributes |= TypeAttributes.Sealed; @type.Members.Add(new CodeConstructor { Attributes = MemberAttributes.Private | MemberAttributes.Final }); } private static void WriteIntoFile(string fullPath, CodeCompileUnit code) { Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); using (var stream = new StreamWriter(fullPath, append: false)) { var tw = new IndentedTextWriter(stream); using (var codeProvider = new CSharpCodeProvider()) { codeProvider.GenerateCodeFromCompileUnit(code, tw, new CodeGeneratorOptions()); } } } } } 

Put our utility in the Editor folder, click Habr → Generate layers constants, we get a file in the project with the following content:

 // ------------------------------------------------------------------------------ // <autogenerated> // This code was generated by a tool. // Mono Runtime Version: 2.0.50727.1433 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </autogenerated> // ------------------------------------------------------------------------------ public sealed class Layers { public const string Default = "Default"; public const string TransparentFX = "TransparentFX"; public const string IgnoreRaycast = "IgnoreRaycast"; public const string Water = "Water"; public const string UI = "UI"; public const string Habr = "Habr"; private Layers() { } } 

Next steps


The resulting utility lacks the following things:


In order not to waste time writing your “bicycle”, you can also use my “bicycle” .

Source: https://habr.com/ru/post/309128/


All Articles