- PVSM.RU - https://www.pvsm.ru -

Автоматизация поддержания соответствия между названиями слоев в редакторе и коде с помощью CodeDom

При работе с Unity [1] очень часто приходится обращаться к сущностям Unity (слоям коллизий, сортировочным слоям, тэгам, осям ввода, сценам) по их названиям. Если какую-то из них, например, переименовать в редакторе, то нужно не забыть, соответственно, подправить название в коде, иначе нас ждет ошибка. И ошибка эта возникнет не при компиляции, а во время выполнения, непосредественно в момент обращения по имени. Немного автоматизации спасет от таких неприятных сюрпризов.

На первый взгляд, для решения этой проблемы логично использовать шаблоны T4, [2] но они мне показались неудобными (по крайней мере, при использовании именно в Unity-проекте), поэтому я выбрал другой подход. Использование CodeDom [3] для решения такой мелкой проблемы может показаться оверинжинирингом, но личный опыт доказал состоятельность этого подхода: я написал свой незамысловатый генератор кода больше года назад, и с тех пор, не внося в него изменения, успешно пользовался им, что сэкономило мне немало нервов и порядочно секунд времени.

В данной статье мы рассмотрим написание аскетичного генератора кода с константами [4], содержащими названия слоев [5] коллизий [6]. Работа с названиями других сущностей делается аналогично.

Общий план действий таков:

  • Получить список названий слоев коллизий, имеющихся в проекте.
  • Сгенерировать сам код, содержащий класс с нужными константами.
  • Записать этот код в файл.
  • Побудить Unity немедленно скомпилировать добавленный/измененный файл.

Получаем список названий слоев коллизий

Тут все просто, если не бояться лезть в места, названные внутренними. Конкретнее, список имен слоев коллизий хранится как поле «внутреннего» класса.

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

Генерируем сам код

У CodeGen слегка своя терминология (сравните, например, с терминологией в Roslyn [7]), но, в целом, все соответствует синтаксическому дереву, присущему коду на 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 одно мелкое неудобство: он не умеет создавать статические классы. Связано это с тем, что он создавался на заре языка 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# названиям.
  • Генерации аналогичным образом констант для названий сортировочных слоев, сцен, тэгов и осей ввода.

Чтобы не тратить время на написание своего «велосипеда», вы также можете воспользоваться моим «велосипедом» [8].

Автор: dog_funtom

Источник [9]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/unity3d/182879

Ссылки в тексте:

[1] Unity: https://unity3d.com/ru/

[2] шаблоны T4,: http://www.google.ru/search?q=t4+template

[3] CodeDom: http://www.google.ru/search?q=codedom

[4] константами: https://msdn.microsoft.com/ru-ru/library/ms173119.aspx

[5] слоев: https://docs.unity3d.com/Manual/Layers.html

[6] коллизий: https://docs.unity3d.com/Manual/LayerBasedCollision.html

[7] Roslyn: https://github.com/dotnet/roslyn/wiki/Roslyn%20Overview

[8] моим «велосипедом»: https://gist.github.com/dogfuntom/00faf9f3598d845b1a5c

[9] Источник: https://habrahabr.ru/post/309128/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best