- PVSM.RU - https://www.pvsm.ru -
При работе с Unity [1] очень часто приходится обращаться к сущностям Unity (слоям коллизий, сортировочным слоям, тэгам, осям ввода, сценам) по их названиям. Если какую-то из них, например, переименовать в редакторе, то нужно не забыть, соответственно, подправить название в коде, иначе нас ждет ошибка. И ошибка эта возникнет не при компиляции, а во время выполнения, непосредественно в момент обращения по имени. Немного автоматизации спасет от таких неприятных сюрпризов.
На первый взгляд, для решения этой проблемы логично использовать шаблоны T4, [2] но они мне показались неудобными (по крайней мере, при использовании именно в Unity-проекте), поэтому я выбрал другой подход. Использование CodeDom [3] для решения такой мелкой проблемы может показаться оверинжинирингом, но личный опыт доказал состоятельность этого подхода: я написал свой незамысловатый генератор кода больше года назад, и с тех пор, не внося в него изменения, успешно пользовался им, что сэкономило мне немало нервов и порядочно секунд времени.
В данной статье мы рассмотрим написание аскетичного генератора кода с константами [4], содержащими названия слоев [5] коллизий [6]. Работа с названиями других сущностей делается аналогично.
Общий план действий таков:
Тут все просто, если не бояться лезть в места, названные внутренними. Конкретнее, список имен слоев коллизий хранится как поле «внутреннего» класса.
private static IEnumerable<string> GetAllLayers()
{
return InternalEditorUtility.layers;
}
У CodeGen слегка своя терминология (сравните, например, с терминологией в Roslyn [7]), но, в целом, все соответствует синтаксическому дереву, присущему коду на C#. В порядке от корня к листьям, мы будем использовать следующее:
Генерируем публичную строковую константу, у которой имя и значение совпадают с именем слоя коллизий.
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());
}
}
}
Это последний шаг, и он не требует существенного количества кода, поэтому пусть этим занимается та функция, которая будет реагировать на ввод пользователя.
[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() {
}
}
Полученной утилите не хватает следующих вещей:
Чтобы не тратить время на написание своего «велосипеда», вы также можете воспользоваться моим «велосипедом» [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
Нажмите здесь для печати.