Unity 6.3
0 онлайн 86 гостей 3 в системе
Вход
Модульная игровая архитектура на ScriptableObjects Глава 5 из 12 Оригинал, стр. 34

Паттерн расширяемых перечислений

Русский

Паттерн расширяемых перечислений

C#
При разработке игр часто приходится решать повторяющиеся или похожие задачи. К счастью, можно опереться на коллективный опыт инженеров-программистов, которые уже сталкивались с такими задачами и воплотили свои знания в паттернах проектирования. Паттерны проектирования предлагают универсальные решения для создания крупных масштабируемых приложений. Они повышают читаемость кода, делают кодовую базу чище и сокращают объем рефакторинга и время тестирования. Паттерн проектирования можно рассматривать как шаблон для решения распространенных задач, например: - эффективное хранение больших объемов данных;
- обмен данными между объектами из разных игровых систем;
- динамическая замена поведения во время выполнения. ScriptableObject помогает реализовать некоторые из этих паттернов. Вы уже видели, как он работает в роли контейнера данных, но его возможности не ограничиваются хранением значений и настроек. В следующих разделах рассматриваются более сложные варианты применения ScriptableObject.

Категории, подобные enum Чтобы приносить пользу, ScriptableObject вовсе не обязан содержать данные. Даже пустой ScriptableObject можно применять, например, для сравнения с другими объектами ScriptableObject.

Предположим, в игре создается несколько ассетов на основе пустого ScriptableObject GameItemSO:

C#
using UnityEngine;
C#
[CreateAssetMenu(fileName="GameItem")] public class GameItemSO : ScriptableObject {

Пустые объекты ScriptableObject работают как значения enum.

Так можно создать в проекте любое количество ассетов. Даже если ScriptableObject не содержит данных, сам объект способен обозначать категорию или тип предмета подобно значению enum. Если две переменные ссылаются на один ScriptableObject, они обозначают один и тот же тип предмета. Если ссылки различаются, различаются и типы. Например, ScriptableObject может обозначать особый эффект урона, такой как холод, жар, электричество или магия, либо один из вариантов для игры с нулевой суммой «камень, ножницы, бумага».

Камень

Камень

Камень

Бумага

Ножницы

Сравнение объектов ScriptableObject

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

Ячейки инвентаря распределены по категориям ScriptableObject

Категории на основе ScriptableObject можно назначать перетаскиванием

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

Расширение поведения Применение ScriptableObject в роли enum становится еще интереснее, если расширить объект дополнительными данными. В отличие от обычного enum, ScriptableObject может содержать дополнительные поля и методы. Ниже показана адаптированная версия GameItem для игры «камень, ножницы, бумага». Ассет ScriptableObject по-прежнему задает категорию, подобную enum, но теперь он уже не пуст.

C#
public class GameItem : ScriptableObject {
    public GameItem weakness;
    public bool IsWinner(GameItem other) { return other.weakness == this; }
}

Теперь ScriptableObject содержит поле weakness, которое определяет, какой другой предмет победит при взаимодействии. Помимо данных, каждый ScriptableObject хранит простую логику сравнения в методе IsWinner. Каждому игровому предмету требуется MonoBehaviour со ссылкой на определенный ассет ScriptableObject. В этом примере роль контроллера выполняет следующий скрипт:

C#
public class GameItemController : MonoBehaviour {
    // камень, ножницы, бумага

    public GameItem gameItem;
C#
private void OnTriggerEnter(Collider other) {
    GameItemController otherController = other.GetComponent<GameItemController>();
    GameItem otherGameItem = otherController.gameItem;
    if (gameItem.IsWinner(otherGameItem)) {
        Debug.Log(gameItem.name + " побеждает " + otherGameItem.name);
C#
}
}
}

Скрипт хранит ссылку на ScriptableObject в поле. В OnTriggerEnter метод IsWinner позволяет определить победителя, когда gameItem соприкасается с другим предметом. Так создается основа для столкновения в духе игры «камень, ножницы, бумага». В отличие от enum, ScriptableObject легко расширять. Не нужно создавать отдельную таблицу поиска или связывать перечисление с новым массивом данных. Достаточно добавить поле и (или) метод с необходимой логикой.

Камень

Ножницы

Объекты ScriptableObject с логикой сравнения. Источник: Flaticon

Сравните это с поддержкой традиционного enum. Если длинный список его значений не имеет явной нумерации, вставка или удаление элемента может изменить порядок остальных. Такая перенумерация способна вызвать трудноуловимые ошибки или непредвиденное поведение. У перечислений на основе ScriptableObject этой проблемы нет. Добавляйте в проект новые ассеты или удаляйте существующие - остальные элементы продолжат работать. Предположим, предмет нужно сделать экипируемым в RPG. Для этого в ScriptableObject достаточно добавить логическое поле. Нужно запретить некоторым персонажам брать определенные предметы? Добавить магические свойства или особые способности? Перечисления на основе ScriptableObject поддерживают и такие сценарии.

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

English

The Extendable enums pattern

Game development often requires the task of solving recurring or similar problems. Fortunately, you can tap into the collective knowledge of software engineers who’ve already “been there and done that” with design patterns. Design patterns are general solutions that can help you build larger, scalable applications. They can improve code readability and make your codebase cleaner. Design patterns reduce refactoring and the time spent testing. Think of a design pattern as template for solving common issues like: —

Storing a lot of data efficiently

Getting objects from different game systems to speak to each other

Swapping out behavior on the fly at runtime

ScriptableObjects can help implement some of these patterns. You’ve already seen how they can function as data containers, but they can do more than simply save values or settings. The next few sections explore how you can go beyond using ScriptableObjects to save data.

Enum-like categories In fact, ScriptableObjects actually don’t have to contain anything at all to be useful. If you create an empty ScriptableObject, you’ll discover that it still has utility, even if it’s only used for comparing against other ScriptableObjects.

In your game application, suppose you make a number of assets from an empty GameItemSO ScriptableObject, like so: Using UnityEngine;

C#
[CreateAssetMenu(fileName="GameItem")] public class GameItemSO : ScriptableObject {

Empty ScriptableObjects work as enums.

This allows you to generate any number of assets within the project. Even without containing any data, the ScriptableObject itself can represent a category or item type, similar to an enum. Do two variables refer to the same ScriptableObject? Then they’re the same item type. Otherwise, they’re not. So, you could have a ScriptableObject that defines special damage effects (e.g., cold, heat, electrical, magic, and so on) or rock-paper-scissors designations from your favorite zero-sum game.

Comparing ScriptableObjects

If your application requires an inventory system to equip gameplay items, ScriptableObjects can represent item types or weapon slots. The fields in the Inspector then function as a dragand-drop interface for setting them up.

Drag and drop ScriptableObject-based categories

This artist-friendly UI allows your designers how to modify and extend gameplay data without extra support from a developer. Giving the design team the means and responsibility of maintaining gameplay data allows everyone to focus on what they do best.

Extending behavior Using ScriptableObjects as enums becomes more interesting when you want to extend them by adding more data. Unlike normal enums, ScriptableObjects can have extra fields and methods. Here’s the adapted rock-paper-scissors GameItem. The ScriptableObject asset itself still defines the enum-like category, but this time it’s no longer empty.

C#
public class GameItem : ScriptableObject {
    public GameItem weakness;
    public bool IsWinner(GameItem other) { return other.weakness == this; }
}

The ScriptableObject now contains a weakness field that determines which other item wins in a potential interaction. In addition to storing data, each ScriptableObject also contains simple comparison logic in IsWinner. Each gameplay item then needs a MonoBehaviour that references a specific ScriptableObject asset. This example works as a controller script:

C#
public class GameItemController : MonoBehaviour {
    // rock, paper, scissors

    public GameItem gameItem;
C#
private void OnTriggerEnter(Collider other) {
    GameItemController otherController = other.GetComponent<GameItem Controller>();
    GameItem otherGameItem = otherController.gameItem;
C#
if (gameItem.IsWinner(otherGameItem)) {
    Debug.Log(gameItem.name + " beats " + otherGameItem.name);
}
}
}

It references a ScriptableObject as a field. In OnTriggerEnter, you can check the IsWinner method to see which emerges victorious when the gameItem comes in contact with another. This sets the stage for some Rochambeau-like conflict. Unlike enums, ScriptableObjects are easy to extend. There’s no need to have a separate lookup table or to correlate with a new array of data. Simply add an extra field and/or method to handle the logic.

ScriptableObjects with comparison logic Source: Flatticon

Compare that with maintaining a traditional enum. If you have a long list of enum values without explicit numbering, inserting or removing an enum can change their order. This reordering can introduce subtle bugs or unintended behavior. ScriptableObject-based enums have no such issues. Add more to your project (or delete existing ones), and everything just works. Suppose you wanted to make the item equippable in an RPG. You could append an extra boolean field to the ScriptableObject to do that. Are certain characters not allowed to hold certain items? Are some items magical or have special abilities? ScriptableObject-based enums can do that. Your gameplay data can thus evolve as you work to implement the game design. While you’ll need to coordinate how to set up fields initially, later the designers can fill out the details independently.