CodeDomを使用して、エディターのレイヤー名とコード間の対応の維持を自動化します

Unityを頻繁に使用する場合、名前でUnityエンティティ(コリジョンレイヤー、ソートレイヤー、タグ、入力軸、シーン)を使用する必要があります。 たとえば、エディターで名前の1つが変更された場合、それぞれコード内の名前を修正することを忘れてはなりません。そうしないと、エラーが発生します。 また、このエラーはコンパイル時ではなく、実行時、名前による呼び出し時に直接発生します。 少し自動化すれば、このような不快な驚きからあなたを救うでしょう。



一見、この問題を解決するためにT4テンプレートを使用することは論理的ですが、それらは(少なくともUnityプロジェクトで使用する場合は)不快に思えたため、別のアプローチを選択しました。 CodeDomを使用してこのような小さな問題を解決することは過剰なエンジニアリングのように思えるかもしれませんが、個人的な経験からこのアプローチの実行可能性が証明されました。時間のまともな秒。



この記事では 衝突 レイヤーの名前含む定数を使用して禁欲的なコードジェネレーターを作成することを検討します。 他のエンティティの名前の処理も同様に行われます。



一般的な行動計画は次のとおりです。





コリジョンレイヤー名のリストを取得します



ここでは、内部と呼ばれる場所に登ることを恐れないのであれば、すべてが簡単です。 より具体的には、コリジョンレイヤー名のリストは、「内部」クラスのフィールドとして保存されます。



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





コード自体を生成します



CodeGenの用語はわずかに異なります(たとえば、 Roslynの用語と比較してください)が、一般に、すべてがC#コードに固有の構文ツリーに対応しています。 ルートからリーフへの順序では、次を使用します。



  1. CodeCompilationUnitは、ここで構成するコードジェネレーターそのものです。
  2. CodeNamespaceは、クラスが配置される名前空間です。 クラスを明示的な名前空間でラップすることはしませんが、CodeNamespaceのインスタンスを作成する必要があります。
  3. CodeTypeDeclarationはクラス自体です。
  4. CodeMemberFieldはクラスのメンバーです(この場合、定数宣言)。
  5. CodePrimitiveExpressionは、リテラル(この場合、定数に割り当てられる文字列)を持つ式です。


名前と値がコリジョンレイヤーの名前と一致するパブリック文字列定数を生成します。



 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には小さな不便さが1つあります。静的クラスを作成できません。 これは、静的クラスがまだ「インポート」されていないC#言語のd明期に作成されたためです。 取得します。プライベートコンストラクターを使用して、シールクラスで静的クラスをシミュレートします。 初期のC#ユーザーの一部はこれを行っており、Java言語を使用しているユーザーは、現在これに頼らざるを得ません。



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





最後に、プライベートコンストラクターと定数を使用して、クラス自体を構築しましょう。



 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 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()); } } }
      
      





Unityに変更を即座に認識させる



これは最後のステップであり、大量のコードを必要としないため、ユーザー入力に応答する関数にこれを実行させます。



 [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(); }
      
      





結果



すべてをまとめる:



ジェネレーターコード
 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()); } } } } }
      
      





エディターフォルダーにユーティリティを配置し、[Habr]→[レイヤー定数を生成]をクリックすると、次の内容のファイルがプロジェクトに取得されます。



 // ------------------------------------------------------------------------------ // <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() { } }
      
      





さらなるアクション



結果のユーティリティには、次のものがありません。





独自の「バイク」を書く時間を無駄にしないために、私の「バイク」を使用することもできます。



All Articles