Больше гибкости, меньше кода — продуктивнее разработка.

Уже долгое время Unity3D — мой любимый инструмент разработки игр, которым я пользуюсь уже более 8 лет — и для профессиональных продуктов, и для личных проектов, и при обучении программированию и гейм-дизайну. Более того, я писал на Unity почти на всех гейм-джемах, в которых участвовал, благодаря чему получалось создавать основу игры всего за несколько часов.

Как вы, наверное, знаете, гейм-джем — это конкурс разработчиков игр, участники которого делают игру с нуля за короткий период времени. Гейм-джем обычно идет от 24 до 72 часов, но бывают и более длительные — например, GitHub Game Off, который длится весь ноябрь.

После участия в различных гейм-джемах, в том числе и с самодельным движком моей группы на C++ (к сожалению, он только на португальском языке), я составил список правил быстрого прототипирования, которые вскоре превратились в мой основной принцип разработки ПО: меньше кода — быстрее работа.

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

  1. Защита от ошибок: чем меньше размер кода, тем меньше вероятность сделать ошибку.

  2. Экономия времени: при каждом изменении кода нужны обновления и тесты, что требует времени.

А для Unity есть и третья причина: каждое изменение в коде запускает обновление Unity, на что регулярно тратится некоторое количество времени.

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

Для справки: Unity 3D Technologies мне ничего не платили (пока что).

1. Сериализация классов и структур

Сериализация — это процесс автоматического преобразования структур данных или состояний объекта в другой формат. В случае Unity это упрощает хранение и реконструкцию данных.

Класс и структуру можно пометить как сериализуемые — указав [Serializable] над именем. Ниже — пример из документации Unity:

[Serializable]
public struct PlayerStats
{
   public int movementSpeed;
   public int hitPoints;
   public bool hasHealthPotion;
}

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

Список характеристик игрока в инспекторе свойств Unity
Список характеристик игрока в инспекторе свойств Unity

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

То же можно сделать для перечислений (их использование повышает безопасность типов) и более сложных конструкций — например, спрайтов. Оба случая приведены на изображении и в коде ниже:

Список характеристик игрока со спрайтами и перечислениями в инспекторе Unity
Список характеристик игрока со спрайтами и перечислениями в инспекторе Unity
public enum PlayerType
{
    ARCHER, KNIGHT
}

[Serializable]
public struct PlayerStats
{
    public int movementSpeed;
    public int hitPoints;
    public bool hasHealthPotion;
    public Sprite face;
    public PlayerType type;
}

Если использовать перечисление в сериализованной структуре, то в качестве приятного дополнения вы получите его идентификаторы в инспекторе Unity в красивом и удобном выпадающем списке — и больше не нужно запоминать строки.

2. По возможности используйте RequireComponent

Сценарии с зависимостями от компонентов — обычное дело. Например, сценарий контроллера игрока, скорее всего, будет зависеть от Rigidbody и коллайдеров игрока. Самый безопасный способ обеспечить наличие всех зависимостей у игрового объекта во время выполнения сценария — это пометить его атрибутом RequireComponent.

У этого атрибута три основных функции:

  1. Обеспечить наличие необходимых компонентов у игрового объекта.

  2. Заблокировать удаление необходимых компонентов.

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

Пример кода с использованием RequireComponent показан ниже:

[RequireComponent(typeof(Rigidbody))]
public class PlayerScript : MonoBehaviour
{
    Rigidbody rigidbody;

    void Awake()
    {
        rigidbody = GetComponent<Rigidbody>();
    }
}

Эта функциональность значительно повышает безопасность кода и снижает вероятность непредвиденного исключения пустого указателя при попытке получить доступ к недействительным или несуществующим компонентам. Компоненты можно непосредственно прикрепить в окне редактора или получить их с помощью методов Awake или Start, как показано в примере кода выше.

С точки зрения прототипирования этот подход ускоряет подготовку объектов. Более того, у классов и структур может быть несколько атрибутов RequireComponent сразу. Достаточно указать их в сценарии, а затем просто добавить его в игровой объект — и все компоненты будут прикреплены.

А учитывая, что эти компоненты никогда не будут недействительными при обычных обстоятельствах, в сценарии не нужно проверять на значение null, — значит, и кода писать нужно меньше.

3. Кнопки интерфейса для нескольких событий

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

Само по себе это не плохо, но в обоих случаях появляется сильная зависимость от изменений в кодовой базе — которые наверняка будут.

Лучше всего перечислять все эффекты кнопки прямо в ее списке событий OnClick: в нем может быть столько событий, сколько нужно, и к ним легче получить доступ и изменить их.

С помощью этого подхода можно, например, использовать onClick одной кнопки для отображения панели Unity, воспроизведения звука и запуска анимации. Конкретно для этих задач дополнительный код не потребуется: отображаем панель — ( setActive(true) ), воспроизводим звук — ( play() ) и вызываем Animator — ( setTrigger() ). Примеры вызова этих методов — ниже.

Пример списка с событиями OnClick
Пример списка с событиями OnClick

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

4. Широкое применение событий Unity

В Unity есть специальный класс с именем UnityEvent, который ведет как себя метод OnClick кнопки интерфейса Unity. Переменная UnityEvent, доступ к которой есть у сценария, дает тот же интерфейс, что и метод OnClick:

Одна переменная UnityEvent с именем EventsToBeCalled
Одна переменная UnityEvent с именем EventsToBeCalled

Такая переменная используется практически так же, за исключением того, что для выполнения списка событий переменной UnityEvent ее нужно вызывать через сценарий. Во фрагменте кода ниже показано, как добавить переменную UnityEvent в сценарий и как пишется простая функция вызова Invoke:

using UnityEngine;
using UnityEngine.Events;
public class CallEventsScript : MonoBehaviour
{
   public UnityEvent eventsToBeCalled;
   public void CallEvents()
   {
      eventsToBeCalled.Invoke();
   }
}

Метод CallEvents вызывает список событий для переменной UnityEvent. Это общедоступный метод, поэтому к нему имеют доступ другие сценарии, в том числе сигналы временной шкалы и события анимации. Для этих двух случаев писать код для доступа к методу не нужно — всё делается простым перетаскиванием.

Временная шкала анимации с добавленным событием, которое вызывает метод CallEvents
Временная шкала анимации с добавленным событием, которое вызывает метод CallEvents

UnityEvent также может использоваться для создания очень гибких сценариев, например, для вызова списка событий в таких методах, как Awake, Start, OnEnable, OnDisable и т. д. Например, можно написать сценарий, который выполняет список событий в методе Start, — это позволит быстро создать функции без необходимости писать код.

Пример из практики — триггерный ящик, как я его называю: это игровой объект с коллайдером, который выполняет одно или несколько действий при столкновении с другими игровыми объектами. Его можно легко реализовать с помощью UnityEvent:

[RequireComponent(typeof(Collider))]
public class TriggerBoxScript : MonoBehaviour
{
    public UnityEvent eventsToBeCalledOnCollision;
    public List<string> objectsTagToActivate;

    private void OnCollisionEnter(Collision other)
    {
        if (OtherHasWantedTag(other.gameObject))
        {
            InvokeEvents();
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (OtherHasWantedTag(other.gameObject))
        {
            InvokeEvents();
        }
    }

    private bool OtherHasWantedTag(GameObject other)
    {
        var found = objectsTagToActivate.Find(other.CompareTag);
        return found != null;
    }

    private void InvokeEvents()
    { 
        eventsToBeCalledOnCollision.Invoke();   
    }
}

Пример выше работает и для триггеров, и для коллайдеров без триггера (метод вызывается и через OnTriggerEnter, и через OnCollisionEnter). При этом его могут использовать только игровые объекты, у которых есть коллайдер.

Пример необходимых компонентов для триггерного ящика
Пример необходимых компонентов для триггерного ящика

В примере выше показан игровой объект с триггерным ящиком, использующий этот подход. В этом примере всякий раз, когда игровой объект «Player» сталкивается с объектом — триггерным ящиком, последний воспроизведет звук и деактивируется. Возможности такой структуры почти безграничны: активация врагов, изменение фоновой музыки, точки появления, точки сохранения и т. д.

Заключение

Если коротко, то два главных результата использования описанных подходов — это снижение количества кода и расширение возможностей инспектора Unity. Больше гибкость — шире простор для действий.

В случае событий (OnClick и UnityEvent) разработчику не нужно беспокоиться о настройке зависимостей объекта (методы объектов можно вызывать напрямую из списков) и проверке их действительности (в списке можно будет привязать только действительные существующие объекты).

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

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

Благодарю за внимание.

Побережье, коричневый песок. Альберт Эдельфельт (1935) [USEUM]
Побережье, коричневый песок. Альберт Эдельфельт (1935) [USEUM]

О переводчике

Перевод статьи выполнен в Alconost.

Alconost занимается локализацией игр, приложений и сайтов на 70 языков. Переводчики-носители языка, лингвистическое тестирование, облачная платформа с API, непрерывная локализация, менеджеры проектов 24/7, любые форматы строковых ресурсов.

Мы также делаем рекламные и обучающие видеоролики — для сайтов, продающие, имиджевые, рекламные, обучающие, тизеры, эксплейнеры, трейлеры для Google Play и App Store.