Больше гибкости, меньше кода — продуктивнее разработка.
Уже долгое время Unity3D — мой любимый инструмент разработки игр, которым я пользуюсь уже более 8 лет — и для профессиональных продуктов, и для личных проектов, и при обучении программированию и гейм-дизайну. Более того, я писал на Unity почти на всех гейм-джемах, в которых участвовал, благодаря чему получалось создавать основу игры всего за несколько часов.
Как вы, наверное, знаете, гейм-джем — это конкурс разработчиков игр, участники которого делают игру с нуля за короткий период времени. Гейм-джем обычно идет от 24 до 72 часов, но бывают и более длительные — например, GitHub Game Off, который длится весь ноябрь.
После участия в различных гейм-джемах, в том числе и с самодельным движком моей группы на C++ (к сожалению, он только на португальском языке), я составил список правил быстрого прототипирования, которые вскоре превратились в мой основной принцип разработки ПО: меньше кода — быстрее работа.
Основная идея — писать меньше кода (или, иначе говоря, держать меньшую кодовую базу) — решает две задачи:
Защита от ошибок: чем меньше размер кода, тем меньше вероятность сделать ошибку.
Экономия времени: при каждом изменении кода нужны обновления и тесты, что требует времени.
А для Unity есть и третья причина: каждое изменение в коде запускает обновление Unity, на что регулярно тратится некоторое количество времени.
В этой статье я расскажу о нескольких приемах, позволяющих легко реализовать этот принцип в Unity3D и таким образом ускорить прототипирование и создание игр в целом.
Для справки: Unity 3D Technologies мне ничего не платили (пока что).
1. Сериализация классов и структур
Сериализация — это процесс автоматического преобразования структур данных или состояний объекта в другой формат. В случае Unity это упрощает хранение и реконструкцию данных.
Класс и структуру можно пометить как сериализуемые — указав [Serializable] над именем. Ниже — пример из документации Unity:
[Serializable]
public struct PlayerStats
{
public int movementSpeed;
public int hitPoints;
public bool hasHealthPotion;
}
Главное преимущество этого подхода в том, что он дает прямой доступ к соответствующим свойствам через инспектор, что особенно удобно при использовании списков и массивов.
В вашем проекте наверняка будут повторяющиеся структуры: например, задания, предметы или даже диалоги — их можно реализовать в виде сериализованных классов или структур, что позволит с легкостью изменять их список значений в инспекторе.
То же можно сделать для перечислений (их использование повышает безопасность типов) и более сложных конструкций — например, спрайтов. Оба случая приведены на изображении и в коде ниже:
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.
У этого атрибута три основных функции:
Обеспечить наличие необходимых компонентов у игрового объекта.
Заблокировать удаление необходимых компонентов.
Автоматически добавлять необходимые компоненты к игровому объекту, когда к нему прикрепляется сценарий.
Пример кода с использованием 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() ). Примеры вызова этих методов — ниже.
Довольно часто этот подход используется для программирования переходов по меню. Каждое меню активирует следующее и деактивируется само. При возврате или закрытии меню каждое из них снова деактивирует себя и активирует предыдущее. Самое приятное здесь то, что не понадобится ни строчки кода. Повторюсь: меньше кода — меньше ошибок, быстрее работа.
4. Широкое применение событий Unity
В Unity есть специальный класс с именем UnityEvent, который ведет как себя метод OnClick кнопки интерфейса Unity. Переменная UnityEvent, доступ к которой есть у сценария, дает тот же интерфейс, что и метод OnClick:
Такая переменная используется практически так же, за исключением того, что для выполнения списка событий переменной UnityEvent ее нужно вызывать через сценарий. Во фрагменте кода ниже показано, как добавить переменную UnityEvent в сценарий и как пишется простая функция вызова Invoke:
using UnityEngine;
using UnityEngine.Events;
public class CallEventsScript : MonoBehaviour
{
public UnityEvent eventsToBeCalled;
public void CallEvents()
{
eventsToBeCalled.Invoke();
}
}
Метод CallEvents вызывает список событий для переменной UnityEvent. Это общедоступный метод, поэтому к нему имеют доступ другие сценарии, в том числе сигналы временной шкалы и события анимации. Для этих двух случаев писать код для доступа к методу не нужно — всё делается простым перетаскиванием.
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, для которых в таких ситуациях может быть удобно назначить конкретные методы.
В любом случае, гибкость описанных подходов наверняка перевесит их недостатки — и, конечно же, их легко игнорировать или устранить. Более того, эти приемы позволяют быстро создать прототип и проверить идею, и только после этого приступать к написанию более стабильного и эффективного кода.
Благодарю за внимание.
О переводчике
Перевод статьи выполнен в Alconost.
Alconost занимается локализацией игр, приложений и сайтов на 70 языков. Переводчики-носители языка, лингвистическое тестирование, облачная платформа с API, непрерывная локализация, менеджеры проектов 24/7, любые форматы строковых ресурсов.
Мы также делаем рекламные и обучающие видеоролики — для сайтов, продающие, имиджевые, рекламные, обучающие, тизеры, эксплейнеры, трейлеры для Google Play и App Store.
indiega
Говорят, что к людям, с годами приходит мудрость. Но иногда возраст приходит один. Похоже, это тот самый случай, если это все, к чему автор пришел, за 8, с хвостиком лет. Знания черезчур сакральные, нельзя такими делиться. Люди до них еще не доросли. )