Визуальный редактор логики для Unity3d. Часть 1

в 12:58, , рубрики: Без рубрики

Введение

Здравствуйте уважаемые читатели, в сегодняшней статье я хотел бы остановиться на таком феномене в разработке приложений на Unity3d, как визуальная разработка или если бы точнее, разработка с применением визуального представления кода и логики. И, прежде чем продолжить хочу сразу уточнить, речь не идет о визуальном программировании, от слова “совсем”, никаких Blueprint вариаций в мире Unity и никаких генераций C# кода. Так что же тогда подразумевается под визуальным редактором логики? Если вам интересен ответ на данный вопрос добро пожаловать под кат.

Что такое визуальный редактор логики

Очень часто, а некоторые утверждают что всегда, в ходе разработки программисты пишут много разного кода, который делает много разных вещей, от системных, до механик игрового процесса. Этот код, если программист “настоящий”, как правило делается унифицированным и изолированным, чтобы его можно было использовать повторно (в рамках Unity этим кодом являются компоненты, они же наследники MonoBehavior). Не трудно представить, что такого кода, может быть очень много, особенно, если это не первый проект. А теперь представим себе, что мы начинаем новый проект и нужно сделать много быстрых и разных прототипов, а команда программистов ограничена, и вся занята основным проектом или проектами. Гейм-дизайнеры негодуют, им надо тестировать, проверять, продюсеры бегают по менеджерам, пытаясь выцепить для себя программиста, деньги ограничены, время уходит и т. п. и т. д.

Другая сторона медали, мы (программисты) написали много кода в виде компонентов, они висят большим списком на разных объектах сцены. И вот мы расширяем команду, наняли нового программиста, он открывает сцену, чтобы разобраться и тонет в каше вопросов: кто кого вызывает, в каком порядке, какой компонент с каким связан и каким образом и т. п. Вы резонно скажите: — “ А документация?” Документация есть (хотя вовсе не факт), но тут речь идет о том, чтобы порог вхождения новых людей в команду был максимально низким, и время на этот процесс максимально коротким.

Как же решить описанные выше ситуации? Ответ в названии статьи – Визуальный Редактор Логики. Что же это такое? Это среда, которая позволяет в визуальном виде оперировать различными компонентами логики и настраивать их взаимосвязи (в “мягком” варианте), а также опосредованно от сцены оперировать объектами сцены. Если описать это в сказочно-простом виде, то это как в детстве собирать из кубиков разные конструкции (только в нашем случае, кубики не связаны между собой жестко, убрав нижней, наша конструкция не упадет).

Итак, с определением мы разобрались, но что это нам дает в итоге?

  • Можно собирать универсальные конструкции, которые можно переиспользовать в проектах, что уменьшает последующую рутину. Представим себе некое чисто поле, которое является нашим проектом, мы просто берем собранную конструкцию из другой игры, ставим ее на поле и всё.
  • Можно создать базу изолированных “кубиков” (механик, логик, функционала) из которых люди, не являющиеся программистами, могли бы собирать конструкции самостоятельно.
  • Можно налету заменять конструкции на другие, тем самым изменяя поведение логики.
  • Можно в отложенном режиме использовать конструкции, например, если NPC не присутствует сейчас в мире, то и никакая логика, связанная с ним, не будет существовать на нашем “поле”.
  • Поскольку наши кубики не связаны жесткими связями между собой, мы можем их отключать и включать как нам угодно и осуществлять сколь угодно сложное условное и безусловное ветвление.

Что ж, звучит все довольно неплохо? А что же в реальности? Если открыть Asset Store и посмотреть раздел Visual Scripting, то можно увидеть, в принципе, большое количество различных плагинов. Большинство из них — это вариации на тему Blueprint из Unreal Engine, т. е. по сути кодогенерация. Систем, которые подходят под понятия визуального редактора логики практически нет. Наиболее близкие по смыслу это:

  1. Playmaker. Да это FSM плагин, но тем не менее, он позволяет писать свои Action. Он не так удобен с точки зрения интерфейса, но для определенных вещей очень хорош. Blizzard не зря его использовали в Hearthstone.
  2. Behaviour Designer/MoonBehavior и т.п. плагины дерева состояний. Уже ближе к тому, что было описано выше, но много ограничений, все-таки дерево состояний, это не полноценная логика на компонентах.
  3. ICode. Это аналог playmaker’a, т. е. по сути тоже стейт-машина.

Есть ли выход спросите вы, уважаемые читатели? Я нашел только один, написать свою систему, что я и сделал, но путь к ней был довольно долгий и тернистый.

Путь

Идея разработать плагин визуального редактора логики для Unity3D возникла давно. Сначала это были лишь мысли, что, если бы вот так, то было бы круто. Мысли эти появились в процессе работы над проектом, в котором было очень много однотипных игр, более 20 штук, которые надо было делать очень и очень быстро. Первая реализация была ужасная в плане интерфейса, хотя, конечно, и позволила успешно разработать весь набор игр с заданной скоростью.

Для следующего проекта было решено делать полноценный визуальный редактор, однако в итоге из-за малого опыта, реализация оказалась не удачной, все дико тормозило, кол-во связей и т. п. вещей зашкаливало настолько, что разобраться что и куда было невозможно (см. скриншот и не пугайтесь).

image

После этого на какое-то время идея была отложена. Следующие проекты уже я делал на чистом коде, но в голове по-прежнему витала идея. Постепенно с учетом прошлых ошибок формировалось окончательное (как мне казалось) видение и список требований. И в 2017 году, после завершения очередного фриланс-проекта, я решил, что могу себе позволить потратить 6-7 месяцев на работу над этим плагином и попробовать его выложить в Asset Store (он и до сих пор лежит, и называется Panthea VS). С точки зрения опыта работы над таким сложным проектом, было все очень круто, финансовая стороны увы печальна, все-таки уметь программировать и уметь продавать, это вещи разные. Это был ноябрь 2017 года, после чего я слегка потерял мотивацию, развелся, сменил город, поменял полностью жизнь, и чтобы не впасть в самоедство решил посмотреть под другим углом на тему визуального редактора логики. Итогом стал uViLEd, который я решил выложить бесплатно. Поскольку я подписал фуллтайм контракт, то работать над ней пришлось по выходным вечерам и праздникам и заняло это весь 2018 и начало 2019 года. uViLEd — это большое переосмысление Panthea VS, полный рефакторинг кода под компилятор Roslyn (C# 7+), поэтому работает все только начиная с версии Unity3d 2018.3.

Примечание: на Panthea VS вышли несколько проектов (Android и iOS, в частности Грузовичок Лёва и машинки), в принципе опыт использования удачный, но всплыл момент, что одно дело написать редактор, другое дело научиться правильно его использовать (как бы это странно не звучало ).

uViLEd и как им пользоваться

Введение

Итак, что получилось у меня в итоге, сначала смотрим картинку, а затем продолжим (далее картинок будет больше).

image

Что же из себя представляет в своей основе визуальный редактор логики.

image

Здесь:

  • Компоненты — это наш код, которые реализует тот или иной функционал, по сути, аналог MonoBehaviour, только в нашем случае все компоненты наследуются от LogicComponent класса, который в свою очередь является ScriptableObject.
  • Переменные — это такие специальные ScriptableObject, которые позволяются хранить данные (любые, включая, кастомные структуры и классы, ссылки на объекты сцены, префабы и ассеты). Переменные нужны, если необходимо сделать данные разделяемыми между компонентами, т. е. каждый компонент, может ссылаться на переменную нужного типа, при этом она будет одна.
  • Связи – это описание в визуальном виде, того, какие компоненты, каким образом и в каком порядке вызывают методы друг друга. Связи между компонентами определяются с помощью двух специализированных полей типов INPUT_POINT и OUTPUT_POINT. Связь всегда формируется как выходная точка компонента во входную точку другого компонента. Эти связи не являются жесткими, т. е. для программиста они не видны и в коде их тоже нет. Они присутствуют, только в редакторе, а затем при запуске сцены, контроллер логики, сам разбирается с кодом. Как это происходит мы поговорим в отдельной статье.

Все в купе – компоненты, переменные и связи – формируют логику. В целом ничего сверхсложного, программист пишет код компонентов и переменных, а гейм-дизайнер или другой (а может и тот же самый) программист/скриптер формирует логику, путем размещения этих компонентов в редакторе и настройки связей и параметров.

Ключевые возможности uViLEd

  • Запуск логики (набор компонентов, переменных и связей) в отложенном режиме, включая запуск из внешнего источника (жёсткий диск или сервер)
  • Настройка связей между компонентами (порядок вызова, активация и деактивация)
  • Простая интеграция компонентов и любого другого кода, включая наследников MonoBehavior
  • Переопределение внешнего вида компонентов в редакторе (аналог CustomPropertyDrawer)
  • Настройка параметров компонентов через инспектор Unity3d
  • Простое добавление компонентов в логику через Drag&Drop файла скрипта или через каталог
  • Группировка компонентов в редакторе логики
  • Настройка отображения компонентов в редакторе (инверсия, минимизация, активация и деактивация)
  • Открытие редактора кода компонента напрямую из редактора логики
  • Отображение отладочных данных напрямую в редакторе логики во время запуска в редакторе
  • Масштабирование визуального редактора логики
  • Если вдруг в логике содержится большое количество компонентов, то существует возможность их поиска с фокусировкой при выборе (это относится и к переменным)
  • Пошаговая отладка в режиме запуска сцены в редакторе Unity3d с отслеживанием всех передаваемых данных между компонентами и значений переменных
  • Поддержка MonoBehaviour методов и установка порядка их вызова. Примечание: здесь имеется в виду, что по умолчанию в SO нет методов типа Start, Update и т.п. поэтому их поддержка добавлена в сами компоненты. При этом используя атрибут ExecuteOrder, можно настраивать порядок вызова методов Start, Update и т. п.
  • Поддержка Coroutine, async/await и всех Unity3d атрибутов для инспектора, а также поддержка CustomPropertyDrawer

Работа с редактором

Для начала работы с редактором необходимо открыть сцену, а затем запустить сам редактор.

image

После запуска редактора инициализируем сцену (кнопка обновления в редакторе), после чего появится возможно создавать или добавлять существую логику в сцену.
После создания логики (файл, который будет описывать компоненты, их параметры, связи между компонентами, переменные и их значения), можно наполнять его смыслом. Для добавления компонента или переменной, достаточно перетащить соответствующий скрипт в область редактора логики. Альтернативный вариант воспользоваться каталогом, который формируется автоматически с использованием атрибута ComponentDefinition.

image

После того как мы добавили несколько компонентов в логику, их можно перемещать, в том числе группами или объединять в визуальную группу.

image

Рассмотрим подробнее что из себя представляем сам компонент в визуальном редакторе.

image

Здесь:

  • Кнопка меню компонента, открывает выпадающее меню, с помощью которого можно:
    • Активировать или деактивировать компонент
    • Минимизировать компонент (также это можно сделать двойным кликом в заголовок)
    • Инвертировать компонент (поменять местами входные и выходные точки)
    • Скрыть или показать область параметров компонента
    • Открыть редактор кода для компонента
  • Область параметров компонента, это место, где выводятся значения ключевых параметров компонента, состав которых зависит от программиста

Для настройки параметров (публичных полей или полей с атрибутом SerializeField), необходимо выделить компонент в редакторе логики и открыть инспектор Unity3d.

image

Здесь:

  • В верхнем правом углу находится кнопка, которая позволяет изменять цвет заголовка, который отображается в редакторе логики
  • Name – поле для установки имени экземпляра компонента
  • Comment – поле для установки комментария к экземпляру компонента, отображается в редакторе логики при наведении курсора мыши на компонент
  • Component Parameters – область где отображаются параметры компонента (публичные поля и поля помеченные SerializeField)
  • Variable Links – область для установки ссылок на переменные (подробнее о них будет сказано в разделе работы с переменными).

Для того, чтобы визуально сгруппировать объекты необходимо их выделить, а затем нажать правую кнопку и выбрать соответствующий пункт в меню. Группы можно переименовывать, а также менять их цветовую гамму.

image

Для масштабирования визуального представления компонентов используется колесико мыши, тут все достаточно просто.

И последнее, на что хочу обратить внимание, это работа со связями между компонентами.
Для установки связи необходимо соединить выходную точку одного компонента с входной точкой другого.

image

image

image

Связи устанавливаются на основе правила совпадения типов, которые передает и принимает точка. Исключения составляется входная точка, которая не принимает данных, с ней можно соединить любую выходную точку. При установке связи система автоматически проверяет совпадение типов и показывает можно или нет эту связь установить. Входные и выходные точки задаются в коде компонента с помощью следующих классов:

INPUT_POINT 
OUTPUT_POINT
INPUT_POINT<T>
OUTPUT_POINT<T>

Первые два класса используются для входной и выходной точки, которые не принимают параметры, вторые соответственно наоборот. Т может быть любым типом.

public INPUT_POINT <float> InputFloatValue = new INPUT_POINT<float>();
public OUTPUT_POINT <float> OutputFloatValue = new OUTPUT_POINT<float>();

Для того, чтобы вызвать цепочку связей, необходимо использовать функцию Execute.

OutputFloatValue.Execute(5f);

Для того, чтобы обработать такой вызов, в коде компонента необходимо установить обработчик для входной точки (о том, где именно мы поговорим чуть позже).

InputFloatValue.Handler = value => Debug.Log(value);

И в завершении, хочу упомянуть важный момент о связях. Если связей из одной точки выходит несколько, то в редакторе есть возможность настроить порядок их вызова.

Работа с переменными

Как было сказано ранее, переменные — это специальные объекты, которые позволяют разделять данные между компонентами через ссылки на них. Переменные, как и компоненты, создаются программистами.

[ComponentDefinition(Name = "Float", 
Path = "uViLEd Components/Base/Variable/Base", 
Tooltip = "Variable for a floating-point number", 
Color = VLEColor.Cyan)]
public class VariableFloat : Variable<float> { }

Как видно базовым классом для переменных является generic класс Variable, где T тип данных, которые заключены в переменной, Т может быть любым типом, который может быть сериализован.

В редакторе переменные отображаются в следующем виде:

image

Для изменения отображения значений переменной достаточно переопределить метод ToString в типе Т.

public struct CustomData
{
    public readonly int Value01;
    public readonly int Value02;

    public CustomData (int value01, int value02)
    {
        Value01= value01;
        Value02= value02;
    }

    public override string ToString()
    {
        var stringBuilder = new StringBuilder();

        stringBuilder.AppendLine("Value01 = {0}".Fmt(Value01));
        stringBuilder.Append("Value02 = {0}".Fmt(Value02));

        return stringBuilder.ToString();
    }
}

Таким образом переменная данного типа будет выглядеть как:

public class VariableCustomData : Variable<CustomData> { }

Для того, чтобы добавить ссылку на переменную в компонент, необходимо использовать специальный класс.

public VARIABLE_LINK<CustomData> CustomVariableLink = new VARIABLE_LINK<CustomData>();

После этого, ссылку можно установить в инспекторе, при этом, в выпадающем меню, буду показаны только переменные типа CustomData, что значительно упрощает работу с ними.

Для удобства работы с переменными существуют специальные методы, позволяющие определить, когда переменная поменяла свое значение или когда в нее были установлены любые данные.

CustomVariableLink.AddSetEventHandler(CustomDataSet);
CustomVariableLink.AddChangedEventHandler(CustomDataChanged);

Здесь стоит учитывать, что Changed работает по условию Equals, поэтому, если используются структуры и классы, этот метод необходимо переопределять, чтобы гарантировать правильность работы.

Работа с объектами Unity

Из-за особенностей работы системы uViLEd прямые ссылки на объекты Unity не могут быть использованы в ней, поскольку они не могут быть восстановлены при загрузке логики. Чтобы решить эту проблему, была создана специализированная оболочка VLObject, которая позволяет создавать такие ссылки, а также сохранять и загружать их. Помимо прочего, эта оболочка имеет специальный редактор свойств, который позволяет вам получать компоненты из любого объекта сцены (см. Рисунок ниже), если вы хотите обратиться к ним. С VLObject вы можете хранить ссылки не только на объекты сцены и их компоненты, но также на префабы и файлы ресурсов, такие как текстуры, звуки и т. п.

image

Примечание: если существующая логика используется в другой сцене, ссылки на объекты будут потеряны, включая ссылки на префабы, потому что сцена действует как их хранилище. Это также необходимо учитывать, если вы планируете использовать логику в качестве шаблона, в этом случае наилучшим вариантом будет передача необходимых ссылок на него извне (например, из логики, привязанной к сцене).

Также существует возможность ограничить тип объекта Unity, который будет установлен в VLObject. Это влияет только на инспектор Unity и используется для удобства работы с ними.

[SerializeField] [TypeConstraint(typeof(Button))] private VLObject _button;

Создание компонента логики

Для того, чтобы создать компонент логики, программисту достаточно добавить в проект простой C# скрипт-файл (также можно создать сразу компонент или переменную через специальное меню во вкладке Project) и изменить в нем код на следующий:

[ComponentDefinition(Name = "MyComponent", Path = "MyFolder/MySubfolder", Tooltip = "this my logic component", Color = VSEColor.Green)]
public class MyLogicComponent : LogicComponent
{

}

Как было сказано ранее ComponentDefinition это атрибут, который позволяет автоматически формировать каталог компонентов, здесь следует учесть, что Color (цвет заголовка) задается в строке в виде HEX-формата.

LogicComponent – это базовый класс всех компонентов, в свою очередь являющийся наследником ScripatableObject.

Ниже приведен простой пример компонента, который делает ветвление по входящему значению типа bool:

public class IfBool : LogicComponent
{
          public INPUT_POINT<bool> ValueToBeChecked = new INPUT_POINT<bool>();            
          public OUTPUT_POINT True = new OUTPUT_POINT();
          public OUTPUT_POINT False = new OUTPUT_POINT();

          public override void Constructor()
          {
                  ValueToBeChecked.Handler = ValueToBeCheckedHandler;
          }

          private void ValueToBeCheckedHandler(bool value)
          {
                  if(value)
                  {
                    	  True.Execute();
                  }else
                  {
                          False.Execute();
                  }
         }
}

Итак, как видно из кода, мы создали входную точку компонента, которая принимает значение типа bool и две выходных точки, которые вызываются в зависимости, какое значение мы получили.

Наверное, у вас сейчас возникает вопрос, что это за Constructor такой? Поясняю. По умолчанию ScriptableObject не поддерживает методы типа Start, Update и т. п., но при этом поддерживаются методы Awake, OnEnable, OnDisable и OnDestroy. Так вот Awake (как и OnEnable), в случае если ScriptableObject создается через метод CreateInstance вызывается всегда, и в этом, собственно, и есть проблема. Из-за того, что объект создается в памяти для сериализации в режиме редактора необходимо было исключить работу кода компонента в этот момент, поэтому и был добавлен аналог Awake в виде метода Constructor, тоже самое касается и методов OnDisable и OnDestroy при удалении объекта в редакторе. Если необходимо корректно обработать удаление компонента (при выгрузке сцены, например), то необходимо использовать интерфейс IDisposable.

В целом, как видно, ничего сложно в создании компонентов нет. Это обычный класс, в котором может быть любой код, какой только захотите. В частном случае компоненты вообще могут не содержать входных и выходных точек, а общаться с помощью глобальный сообщений. Кстати, для этого в uViLEd присутствует класс GlobalEvent — это система сообщений, основанная на типах данных (подробнее о ней можно почитать в моей статье).

Последнее, о чем хотелось бы упомянуть, это возможность конфигурировать входные и выходные точки компонента в зависимости от параметров компонента.

image

Для этого в коде компонента, достаточно имплементировать один или оба интерфейса IInputPointParse и IOutputPointParse. Ниже приведет пример кода абстрактного generic-класса для компонентов ветвления Switch, здесь автоматически создаются выходные точки, в зависимости от параметра SwitchValues.

Код класса SwitchAbstract

public abstract class SwitchAbstract<T> : LogicComponent, IOutputPointParse
{
    [Tooltip("input point for transmitting value, which should be checked")]
    public INPUT_POINT<T> ValueToBeChecked = new INPUT_POINT<T>();

    [Tooltip("set of values for branching")]
    public List<T> SwitchValues = new List<T>();

    protected Dictionary<string, object> outputPoints = new Dictionary<string, object>();

    public override void Constructor()
    {
        ValueToBeChecked.Handler = ValueToBeCheckedHandler;
    }

    protected virtual bool CompareEqual(T first, T second)
    {
        return first.Equals(second);
    }

    protected virtual string GetValueString(T value)
    {
        var outputPontName = value.ToString();

#if UNITY_EDITOR
        if (!UnityEditor.EditorApplication.isPlaying)
        {
            if (outputPoints.ContainsKey(outputPontName))
            {
                outputPontName += " ({0})".Fmt(outputPoints.Count);
            }
        }
#endif

        return outputPontName;
    }

    private void ValueToBeCheckedHandler(T checkedValue)
    {
        foreach (var value in SwitchValues)
        {
            if (CompareEqual(checkedValue, value))
            {
                ((OUTPUT_POINT)outputPoints[GetValueString(value)]).Execute();

                return;
            }
        }
    }

    public IDictionary<string, object> GetOutputPoints()
    {

#if UNITY_EDITOR
        if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
        {
            outputPoints.Clear();
        }
#endif
        if (outputPoints.Count == 0)
        {
            foreach (var value in SwitchValues)
            {
                outputPoints.Add(GetValueString(value), new OUTPUT_POINT());
            }
        }

        return outputPoints;
    }
}

Отладка логики

Для отладки логики в uViLEd предусмотрено несколько механизмов:

  1. Возможность отображать внутренние переменные и их значения в редакторе логики в режиме запуска сцены. Для этого используется атрибут ViewInDebugMode
  2. Возможность просматривать значения переменных логики в режиме запуска сцены
  3. Возможность пошаговой отладки вызова связей между компонентами и просмотра данных, которые передаются между ними

Для последнего пункта в uViLEd предусмотрен специальный режим, который включается при запуске сцены.

image

У этого режима, к сожалению, есть определенные ограничения, связанные с переходом между сценами. В этом случае отладочные данные с предыдущей сцены и логик будут утеряны, а в новой они начнут отображаться, только с момента, когда логика будет активирована в редакторе.

Заключение

В данной статье я постарался вкратце познакомить вас с подходом в разработке который я применяю в своих текущих проектах. Несмотря на изначальный скепсис (и мой в том числе) практика показывает значительно удобство его применения, особенно при прототипировании. Помимо прочего значительно упростилась работа гейм-дизайнеров, они не лезут в сцену, не тыкают в объекты для настройки игрового процесса, для них создается отдельная логика с набором данных переменных и компонентов, в которых они могут спокойно все настраивать. Также большое преимущество дает тот факт, что в моих проектах, часто контент загружается извне. С применением визуального редактора логики я могу обновлять баланс игрового процесса без обновления основного приложения, в некоторых случаях, можно менять и саму логику.

Для себя я решил, что подобный подход к разработке вполне имеет место быть, конечно он не применим к большим проектам, но он может быть использован там для каких-то скриптов игрового процесса для оживления мира, в дизайне уровней и т. п. В моих текущих проектах (детский сегмент) он, пока что, отлично проявляет себя.

Что же дальше?

Это была первая часть из серии статей про визуальный редактор логики uViLEd, далее будут части про:

  1. Ядро системы: как происходит загрузка логики, почему выбран ScriptableObject, как устроен API, что позволяет делать и т. п., какие трудности возникли и как все решалось.
  2. Редактор: как разрабатывался, как устроен, какие проблемы и какие решения и т. п. вещи, что бы переделал сейчас.

Напишите в комментариях, если у вас появятся конкретные вопросы, которые хочется, чтобы я освятил в последующих статьях.

PS: я постарался рассказать о ключевых моментах uViLEd, если у вас появится желание вы можете ознакомится с ним скачав плагин из Asset Store, там присутствует полная документация (правда на английском): руководство пользователя, гайд для программистов и API.

Визуальный редактор логики uViLEd
Стать о глобальных системах сообщений

Автор: Ichimitsu

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js