На первый взгляд, для решения этой проблемы логично использовать шаблоны T4, но они мне показались неудобными (по крайней мере, при использовании именно в Unity-проекте), поэтому я выбрал другой подход. Использование CodeDom для решения такой мелкой проблемы может показаться оверинжинирингом, но личный опыт доказал состоятельность этого подхода: я написал свой незамысловатый генератор кода больше года назад, и с тех пор, не внося в него изменения, успешно пользовался им, что сэкономило мне немало нервов и порядочно секунд времени.
В данной статье мы рассмотрим написание аскетичного генератора кода с константами, содержащими названия слоев коллизий. Работа с названиями других сущностей делается аналогично.
Общий план действий таков:
- Получить список названий слоев коллизий, имеющихся в проекте.
- Сгенерировать сам код, содержащий класс с нужными константами.
- Записать этот код в файл.
- Побудить Unity немедленно скомпилировать добавленный/измененный файл.
Получаем список названий слоев коллизий
Тут все просто, если не бояться лезть в места, названные внутренними. Конкретнее, список имен слоев коллизий хранится как поле «внутреннего» класса.
private static IEnumerable<string> GetAllLayers() { return InternalEditorUtility.layers; }
Генерируем сам код
У CodeGen слегка своя терминология (сравните, например, с терминологией в Roslyn), но, в целом, все соответствует синтаксическому дереву, присущему коду на C#. В порядке от корня к листьям, мы будем использовать следующее:
- CodeCompilationUnit — это сам генератор кода, который мы здесь, так сказать, конфигурируем.
- CodeNamespace — это пространство имен, в котором будет сидеть наш класс. Мы не будем оборачивать класс в явное пространство имен, но создать экземпляр CodeNamespace, все равно, придется.
- CodeTypeDeclaration — это сам класс.
- CodeMemberField — это член класса (в данном случае, объявление константы).
- 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 одно мелкое неудобство: он не умеет создавать статические классы. Связано это с тем, что он создавался на заре языка C#, когда в него еще не «завезли» статические классы. Будем выкручиваться: сымитируем статический класс запечатанным классом с приватным конструктором. Так поступали некоторые ранние пользователи 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()); } } } } }
Кладем нашу утилиту в папку Editor, нажимаем Habr → Generate layers constants, получаем в проекте файл со следующим содержанием:
// ------------------------------------------------------------------------------ // <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() { } }
Дальнейшие действия
Полученной утилите не хватает следующих вещей:
- Чуть более удобного интерфейса с чуть более гибкими настройками.
- Устойчивости к невалидным в C# названиям.
- Генерации аналогичным образом констант для названий сортировочных слоев, сцен, тэгов и осей ввода.
Чтобы не тратить время на написание своего «велосипеда», вы также можете воспользоваться моим «велосипедом».

