Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Unity_в_действии_Джозеф_Хокинг_Рус.pdf
Скачиваний:
83
Добавлен:
21.06.2022
Размер:
26.33 Mб
Скачать

8.3. Управление инвентаризационными данными и состоянием игры      199

ВНИМАНИЕ  Метод Destroy() должен вызываться для параметра this.gameObject, а не this! Не путайте эти вещи; ключевое слово this ссылается только на компонент сценария, в то время как выражение this.gameObject ссылается на объект, к которому присоединен сценарий.

Добавленная в код переменная должна появиться на панели Inspector. Введите в соответствующее поле имя, чтобы идентифицировать элемент; я выбрал для первого элемента имя energy. Затем создайте несколько копий элемента и поменяйте их имена; я использовал имена ore, health и key (точность написания имен крайне важна, так как впоследствии они появятся в коде). Заодно назначьте каждому элементу собственный материал, чтобы они имели разные цвета. Я выбрал голубой цвет для элемента energy, темно-серый — для элемента ore, розовый — для элемента health и желтый — для элемента key.

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

Теперь превратите все элементы в шаблоны экземпляров, чтобы упростить их добавление в игровой уровень. В главе 3 вы узнали, что эта операция осуществляется перетаскиванием объектов со вкладки Hierarchy на вкладку Project.

ПРИМЕЧАНИЕ  После этой операции имя объекта на панели Hierarchy выделяется синим цветом — это признак объектов, которые являются экземплярами данного шаблона экземпляров. Щелкните правой кнопкой мыши на имени такого объекта и выберите команду Select Prefab для выделения шаблона, экземпляром которого является объект.

Перетаскивая шаблоны, расположите элементы на открытых участках уровня; для тестирования создавайте по несколько экземпляров одного элемента. Запустите игру и «соберите» элементы. Все выглядит здорово, но пока ничего не происходит. Давайте начнем отслеживать собранные элементы; для этого нам нужно создать структуру кода, отвечающую за управление инвентарем.

8.3. Управление инвентаризационными данными и состоянием игры

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

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

200      Глава 8. Добавление в игру интерактивных устройств и элементов

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

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

Но сейчас перед нами стоит задача по управлению инвентарем игрока. Давайте запрограммируем соответствующую структуру кода.

8.3.1. Настраиваем диспетчеры игрока и инвентаря

Нам нужно разбить управление данными на отдельные хорошо продуманные модули, у каждого из которых своя зона ответственности. Для управления состоянием игрока (например, его здоровьем) мы создадим модуль PlayerManager, а для управления инвентарем — модуль InventoryManager. Диспетчеры данных будут вести себя как модели в MVC-архитектуре; контроллером в большинстве сцен послужит невидимый объект (в данном случае он не требовался, но вспомните объект SceneController из предыдущих глав), остальная же часть сцены выступит аналогом представления.

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

ДОСТУП К ЦЕНТРАЛИЗОВАННЫМ МОДУЛЯМ СОВМЕСТНОГО ИСПОЛЬЗОВАНИЯ

За прошедшие годы для решения проблемы соединения частей программы с централизованными модулями общего доступа было создано множество паттернов проектирования. Кпримеру, паттерн одиночки (singleton) упоминался еще в первой книге «Банды четырех».

Однако этот паттерн разонравился многим разработчикам ПО, предпочитающим пользоваться такими альтернативами, как локатор служб (service locator) и инъекция зависимостей (dependency injection). Мой код представляет собой компромисс между простотой статических переменных и гибкостью локатора служб.

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

Чтобы главный диспетчер ссылался на другие модули одним и тем же способом, все эти модули должны унаследовать свойства от общей основы. Мы обеспечим

8.3. Управление инвентаризационными данными и состоянием игры      201

соблюдение этого условия с помощью интерфейса; многие языки программирования (в том числе C#) позволяют задавать своего рода сценарий, которому должны следовать остальные классы. Как PlayerManager, так и InventoryManager будут реализовывать общий интерфейс (он будет называться IGameManager), и основной объект Managers сможет воспринимать их как тип IGameManager. Иллюстрация этой схемы показана на рис. 8.5.

 

 

 

PlayerManager

 

 

Д

 

 

 

 

 

 

 

 

 

• static Player

 

 

IGameManager

 

 

 

• static Inventory

 

 

 

 

InventoryManager

 

 

 

 

• List<IGameManager>

 

 

 

 

 

 

 

 

 

 

IGameManager

 

 

 

 

 

 

Рис. 8.5. Схема связи модулей

 

 

 

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

8.3.2. Программирование диспетчеров

Итак, теперь, когда я объяснил все концепции, которыми мы будем пользоваться, пришло время написать код. Первым делом создайте новый сценарий IGameManager и скопируйте в него код следующего листинга.

Листинг 8.9. Базовый интерфейс, который будут реализовывать диспетчеры данных

public interface IGameManager {

ManagerStatus status {get;} ¬ Это перечисление, которое нам нужно определить.

void Startup();

}

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

Свойство читается откуда угодно, но задается только в этом сценарии.

202      Глава 8. Добавление в игру интерактивных устройств и элементов

Обратите внимание, что свойство относится к типу ManagerStatus; это пока еще не написанное нами перечисление. Создайте сценарий ManagerStatus.cs и скопируйте в него код из следующего листинга.

Листинг 8.10. ManagerStatus: возможные состояния для состояния IGameManager

public enum ManagerStatus { Shutdown,

Initializing, Started

}

Это еще один файл с практически отсутствующим кодом. На этот раз мы перечисляем возможные состояния диспетчеров, принудительно устанавливая для свойства status одно из указанных значений.

Теперь, когда у нас есть интерфейс IGameManager, мы можем реализовывать его в других сценариях. Листинги 8.11 и 8.12 содержат код сценариев PlayerManager

и InventoryManager.

Листинг 8.11. InventoryManager

using UnityEngine;

using System.Collections;

using System.Collections.Generic; ¬ Импорт новых структур данных (используемых в листинге 8.14).

public class InventoryManager : MonoBehaviour, IGameManager { public ManagerStatus status {get; private set;} ¬

public void Startup() { Сюда идут все задачи запуска с долгим

Debug.Log("Inventory manager starting..."); ¬ временем выполнения.

status = ManagerStatus.Started; ¬ Для задач с долгим временем выполнения используем состояние 'Initializing’.

}

}

Листинг 8.12. PlayerManager

using UnityEngine;

using System.Collections;

using System.Collections.Generic;

public class PlayerManager : MonoBehaviour, IGameManager { ¬ public ManagerStatus status {get; private set;}

public int health {get; private set;} public int maxHealth {get; private set;}

public void Startup() {

Debug.Log("Player manager starting...");

Наследуем класс и реализуем интерфейс.

health = 50;

Эти значения могут быть инициализированы

maxHealth = 100;

сохраненными данными.

status = ManagerStatus.Started;

}

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

8.3. Управление инвентаризационными данными и состоянием игры      203

public void

ChangeHealth(int value) { ¬ Другие сценарии не могут напрямую задавать переменную health,

health +=

value;

но могут вызывать эту функцию.

if (health

> maxHealth) {

 

health =

maxHealth;

 

} else if

(health < 0) {

 

health =

0;

 

}

 

 

Debug.Log("Health: " + health + "/" + maxHealth);

}

}

Пока что InventoryManager — это оболочка, заполнением которой мы займемся позднее, в то время как PlayerManager уже обладает всей необходимой для нашего проекта функциональностью. Оба диспетчера наследуют от класса MonoBehaviour и реализуют интерфейс IGameManager. Это означает, что оба диспетчера получили всю функциональность­ MonoBehaviour, но при этом обязаны реализовывать структуру, заданную интерфейсом IGameManager. Эта структура состоит из одного свойства и одного метода, которые и определяют наши диспетчеры.

Свойство status доступно для чтения из любого места (функция чтения общедоступна), но задается только внутри нашего сценария (функция записи закрыта). Кроме того, оба диспетчера определяют метод Startup(). Их инициализация завершается сразу же (InventoryManager пока ничего не делает, в то время как PlayerManager задает пару значений), поэтому состоянию присваивается значение Started. Но частью инициализации модулей данных могут оказаться длительные задания (например, загрузка сохраненных данных), тогда загрузкой этих заданий займется метод Startup(), а состоянию диспетчера будет присвоено значение Initializing. После завершения заданий измените состояние на Started.

Замечательно — мы наконец готовы связать все вместе внутри одного главного диспетчера! Создайте сценарий Managers и введите в него код следующего листинга.

Листинг 8.13. Диспетчер диспетчеров!

using UnityEngine;

using System.Collections;

using System.Collections.Generic;

[RequireComponent(typeof(PlayerManager))] ¬ Гарантируем существование различных диспетчеров.

[RequireComponent(typeof(InventoryManager))]

public class Managers : MonoBehaviour {

public static PlayerManager Player {get; private set;} ¬ public static InventoryManager Inventory {get; private set;}

private List<IGameManager> _startSequence; ¬ Список диспетчеров, который просматривается в цикле во время стартовой последовательности.

void Awake() {

Player = GetComponent<PlayerManager>(); Inventory = GetComponent<InventoryManager>();

_startSequence = new List<IGameManager>(); _startSequence.Add(Player); _startSequence.Add(Inventory);

204      Глава 8. Добавление в игру интерактивных устройств и элементов

StartCoroutine(StartupManagers()); ¬ Асинхронно загружаем стартовую последовательность.

}

private IEnumerator StartupManagers() {

foreach (IGameManager manager in _startSequence) { manager.Startup();

}

yield return null;

int numModules = _startSequence.Count; int numReady = 0;

while (numReady < numModules) { ¬ Продолжаем цикл, пока не начнут работать все диспетчеры. int lastReady = numReady;

numReady = 0;

foreach (IGameManager manager in _startSequence) { if (manager.status == ManagerStatus.Started) {

numReady++;

}

}

if (numReady > lastReady)

Debug.Log("Progress: " + numReady + "/" + numModules); yield return null; ¬ Остановка на один кадр перед следующей проверкой.

}

Debug.Log("All managers started up");

}

}

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

Awake().

СОВЕТ  Подобно методам Start() и Update(), метод Awake() автоматически предоставляется классом MonoBehaviour. Как и метод Start(), он однократно запускается в начале выполнения кода. Но в принятой в Unity последовательности выполнения кода метод Awake() располагается перед методом Start(), отвечая за связанные с инициализацией задания, которые должны запускаться перед любыми другими модулями кода.

Кроме того, метод Awake() выводит последовательность запуска, а затем загружает сопрограмму, начинающую работу всех диспетчеров. А именно он создает объект List и прибегает к методу List.Add() для добавления всех диспетчеров.

ОПРЕДЕЛЕНИЕ  Объект List представляет собой такую структуру данных, как коллекция из языка C#. Списки аналогичны массивам: в них хранятся последовательные наборы элементов определенного типа. Но размер списка может быть изменен в процессе работы, в то время как у массивов этот параметр не редактируется.