Зона — это участок карты (представленный устройством), где игрок может подбирать предметы или куда он может их доставлять. В этом разделе урока по созданию игры «Гонка на время: доставка пиццы» вы узнаете, как создавать зоны подбора и доставки, а также деактивировать и активировать их для игрока.
Использование абстракции для создания класса зоны
Абстрагирование — это принцип в программировании, согласно которому необязательные сложные детали скрываются от пользователя, когда ему нет нужны понимать их. Абстракция описывает то, чем что-то является, без объяснения, как это работает. К примеру, вы можете купить кофе в автомате, не вникая в принципы его работы.
В игре Гонка на время: доставка пиццы» есть два вида зон: зоны подбора, в которых используется устройство Генератор предметов, и зоны доставки, в которых используется устройство Область захвата . Поскольку поведение этих зон является аналогичным даже несмотря на то, что это разные устройства (то есть оба могут быть активированы и деактивированы), вы можете создать класс, чтобы абстрагировать это поведение через стандартный объект зоны, который будет обрабатывать взаимодействие с конкретным устройством.
Абстрагирование поведения путём создания класса означает, что в коде будет лишь одно место для изменения типа используемых устройств. Благодаря подобной реализации вам будет достаточно вносить изменения всего в одном месте без необходимости изменять код где-то ещё, так как любой код, где используется этот класс, будет «знать» только о функциях активации/деактивации.
Ниже пошагово описан процесс создания класса зоны:
- Создайте новый пустой файл Verse с именем pickup_delivery_zone.verse и откройте его в Visual Studio Code.
- В файле Verse создайте новый класс с именем
base_zone
и спецификаторомpublic
, после чего добавьте в него следующее:- Константу
creative_object_interface
с именемActivatorDevice
и спецификаторомpublic
для хранения устройства, используемого в зоне. - Событие
ZoneCompletedEvent
со спецификаторомpublic
для сигнализации о взаимодействии игрока с этой зоной (подбор или доставка предмета). - Функцию
ActivateZone()
, возвращающую значение типаvoid
и имеющую спецификаторpublic
, для активации устройства, используемого в зоне. - Функцию
DeactivateZone()
, возвращающую значение типаvoid
и имеющую спецификаторpublic
, для деактивации устройства, используемого в зоне.
base_zone<public> := class: ActivatorDevice<public> : creative_object_interface ZoneCompletedEvent<public> : event(base_zone) = event(base_zone){} ActivateZone<public>() : void = Print("Зона активирована") DeactivateZone<public>() : void = Print("Зона деактивирована")
Если класс и его члены имеют спецификатор
public
, то они всегда доступны из других частей кода. Более подробно это описано в разделе Спецификаторы и атрибуты. - Константу
- В функции
ActivateZone()
проверьте, является лиActivatorDevice
устройством «Область захвата» или «Генератор предметов», путём приведения типаActivatorDevice
к другим типам и вызова функцииEnable()
для уже преобразованного устройства. Выполните то же самое с функциейDeactivateZone()
, только в этом случае вызовите функциюDisable()
.base_zone<public> := class: ActivatorDevice<public> : creative_object_interface ActivateZone<public>() : void = Print("Зона активирована") if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Enable() else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Enable() DeactivateZone<public>() : void = Print("Зона деактивирована") if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Disable() else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Disable()
- Создайте функцию с названием
WaitForZoneCompleted()
, а также спецификаторамиprivate
иsuspends
. Эта функция инициирует событиеZoneCompletedEvent
при возникновении события для конкретного устройства. Благодаря такому подходу коду в других местах будет достаточно дождатьсяZoneCompletedEvent
без необходимости проверять, какое конкретно событие используется задействованным устройством.WaitForZoneCompleted<private>(ZoneDeviceCompletionEventOpt : ?awaitable(agent))<suspends> : void = if (DeviceEvent := ZoneDeviceCompletionEventOpt?): DeviceEvent.Await() ZoneCompletedEvent.Signal(Self)
Чтобы иметь возможность вызова
DeviceEvent.Await()
, данная функция должна иметь эффектsuspends
. - Обновите функцию
ActivateZone()
, добавив выражениеspawn
, которое вызывает функциюWaitForZoneCompleted()
с соответствующим событием устройства для игрока, взаимодействующего с устройством, —AgentEntersEvent
для устройства «Область захвата» иItemPickedUpEvent
для устройства «Генератор объектов».WaitForZoneCompleted
ожидает параметрoption
(на что указывает?
) типаawaitable(agent)
, поэтому мы можем передать параметр любого типа, в котором реализован интерфейсawaitable
, с параметрическим типом, эквивалентнымagent
. КакCaptureArea.AgentEntersEvent
, так иItemSpawner.ItemPickedUpEvent
отвечают данному условию, поэтому мы можем использовать их в качестве параметра. Это ещё один пример абстракции.ActivateZone<public>() : void = Print("Зона активирована") if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Enable() spawn { WaitForZoneCompleted(option{CaptureArea.AgentEntersEvent})} else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Enable() spawn { WaitForZoneCompleted(option{ItemSpawner.ItemPickedUpEvent}) }
- Добавьте ещё одно событие
ZoneDeactivatedEvent
со спецификаторомprotected
. Это событие необходимо для завершения функцииWaitForZoneCompleted()
, если зона деактивируется до того, как игрок её завершит. Сообщите об этом событии внутри функцииDeactivateZone()
.ZoneDeactivatedEvent<protected> : event() = event(){} DeactivateZone<public>() : void = Print("Зона деактивирована") if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Disable() else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Disable() ZoneDeactivatedEvent.Signal()
- Добавьте в функцию
WaitForZoneCompleted()
выражениеrace
, чтобы функция ожидала либо завершения зоны игроком, либо деактивации зоны. При использовании выраженияrace
асинхронный вызов функцииZoneDeactivatedEvent.Await()
, а также выражениеblock
с событием устройства и сигналомZoneCompletedEvent
будут выполняться одновременно, при этом выполнение выражения, которое не завершится первым, будет отменено.WaitForZoneCompleted<private>(ZoneDeviceCompletionEventOpt : ?awaitable(agent))<suspends> : void = if (DeviceEvent := ZoneDeviceCompletionEventOpt?): race: block: DeviceEvent.Await() ZoneCompletedEvent.Signal(Self) ZoneDeactivatedEvent.Await()
- Наконец, создайте конструктор для класса
base_zone
, который будет инициализировать полеActivatorDevice
.MakeBaseZone<constructor><public>(InActivatorDevice : creative_object_interface) := base_zone: ActivatorDevice := InActivatorDevice
- Ниже приведён полный код класса
base_zone
.<# Зона — это область карты (представленная устройством), которая может быть активирована/деактивирована и которая формирует события, сигнализирующие о том, что зона «пройдена» (зона не может быть пройдена до следующей активации). Статус «Пройдена» зависит от типа устройства (ActivatorDevice) для данной зоны. Рекомендации по использованию: ActivateZone() -> ZoneCompletedEvent.Await() -> DeactivateZone() #> base_zone<public> := class: ActivatorDevice<public> : creative_object_interface ZoneCompletedEvent<public> : event(base_zone) = event(base_zone){} GetTransform<public>() : transform = ActivatorDevice.GetTransform() <# Активирует зону. Здесь следует активировать устройства и все визуальные индикаторы для зоны. #> ActivateZone<public>() : void = # Базовая зона может работать с зонами, определёнными как «генераторы предметов» или «зоны захвата». # Выполняем приведение типов, чтобы определить, с чем мы имеем дело. if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Enable() spawn { WaitForZoneCompleted(option{CaptureArea.AgentEntersEvent}) } else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Enable() spawn { WaitForZoneCompleted(option{ItemSpawner.ItemPickedUpEvent}) } <# Деактивирует зону. Здесь следует деактивировать устройства и все визуальные индикаторы для зоны. #> DeactivateZone<public>() : void = if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Disable() else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Disable() ZoneDeactivatedEvent.Signal() <# Это событие необходимо для завершения работы сопрограммы WaitForZoneCompleted, если зона деактивируется без завершения. #> ZoneDeactivatedEvent<protected> : event() = event(){} WaitForZoneCompleted<private>(ZoneDeviceCompletionEventOpt : ?awaitable(agent))<suspends> : void = if (DeviceEvent := ZoneDeviceCompletionEventOpt?): race: block: DeviceEvent.Await() ZoneCompletedEvent.Signal(Self) ZoneDeactivatedEvent.Await() MakeBaseZone<constructor><public>(InActivatorDevice : creative_object_interface) := base_zone: ActivatorDevice := InActivatorDevice
Поиск зон в среде выполнения с помощью тегов игрового процесса
Теперь, когда у нас есть возможность создавать зоны и активировать/деактивировать их, добавим способ инициализации всех зон на уровне, которым вы присвоили теги, а также способ выбора следующей зоны для активации.
Рассмотрим это ниже на примере класса, отвечающего за создание зон и выбор следующей зоны для активации.
Ниже пошагово описан процесс создания класса для создания и выбора зон:
- Создайте новый класс с названием
tagged_zone_selector
в файле pickup_delivery_zone.verse. Добавьте массив переменных для хранения всех зон на уровне.tagged_zone_selector<public> := class: var Zones<protected> : []base_zone = array{}
- Добавьте метод
InitZones()
со спецификаторомpublic
и параметромtag
, чтобы найти все зоны, связанные с этим тегом игрового процесса, и кэшировать их.InitZones<public>(ZoneTag : tag) : void = <# При создании селектора зоны найдём все доступные зоны и кэшируем их, чтобы не тратить время на поиск устройств с тегами всякий раз, когда выбирается следующая зона. #> ZoneDevices := GetCreativeObjectsWithTag(ZoneTag) set Zones = for (ZoneDevice : ZoneDevices): MakeBaseZone(ZoneDevice)
- Добавьте метод
SelectNext()
со спецификаторамиdecides
иtransacts
, чтобы метод либо находил другую зону, либо завершался с ошибкой. Выберите зону со случайным индексом в массиве при помощи функцииGetRandomInt(0, Zones.Length - 1)
.SelectNext<public>()<transacts><decides> : base_zone = Zones[GetRandomInt(0, Zones.Length - 1)]
- Теперь файл pickup_delivery_zone.verse должен выглядеть следующим образом:
using { /Verse.org/Simulation } using { /Verse.org/Random } using { /Verse.org/Concurrency } using { /Verse.org/Simulation/Tags } using { /UnrealEngine.com/Temporary/SpatialMath } using { /Fortnite.com/Devices } <# Зона — это область карты (представленная устройством), которая может быть активирована/деактивирована и которая формирует события, сигнализирующие о том, что зона «пройдена» (зона не может быть пройдена до следующей активации). Статус «Пройдена» зависит от типа устройства (ActivatorDevice) для данной зоны. Рекомендации по использованию: ActivateZone() -> ZoneCompletedEvent.Await() -> DeactivateZone() #> base_zone<public> := class: ActivatorDevice<public> : creative_object_interface ZoneCompletedEvent<public> : event(base_zone) = event(base_zone){} GetTransform<public>() : transform = ActivatorDevice.GetTransform() <# Активирует зону. Здесь следует активировать устройства и все визуальные индикаторы для зоны. #> ActivateZone<public>() : void = # Базовая зона может работать с зонами, определёнными как «генераторы предметов» или «зоны захвата». # Выполняем приведение типов, чтобы определить, с чем мы имеем дело. if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Enable() spawn { WaitForZoneCompleted(option{CaptureArea.AgentEntersEvent}) } else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Enable() spawn { WaitForZoneCompleted(option{ItemSpawner.ItemPickedUpEvent}) } <# Деактивирует зону. Здесь следует деактивировать устройства и все визуальные индикаторы для зоны. #> DeactivateZone<public>() : void = if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Disable() else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Disable() ZoneDeactivatedEvent.Signal() <# Это событие необходимо для завершения работы сопрограммы WaitForZoneCompleted, если зона деактивируется без завершения. #> ZoneDeactivatedEvent<protected> : event() = event(){} WaitForZoneCompleted<private>(ZoneDeviceCompletionEventOpt : ?awaitable(agent))<suspends> : void = if (DeviceEvent := ZoneDeviceCompletionEventOpt?): race: block: DeviceEvent.Await() ZoneCompletedEvent.Signal(Self) ZoneDeactivatedEvent.Await() MakeBaseZone<constructor><public>(InActivatorDevice : creative_object_interface) := base_zone: ActivatorDevice := InActivatorDevice # Класс tagged_zone_selector создаёт зоны на основе триггеров с тегами, передаваемыми методу InitZones. tagged_zone_selector<public> := class: var Zones<protected> : []base_zone = array{} InitZones<public>(ZoneTag : tag) : void = <# При создании селектора зоны найдём все доступные зоны и кэшируем их, чтобы не тратить время на поиск устройств с тегами всякий раз, когда выбирается следующая зона. #> ZoneDevices := GetCreativeObjectsWithTag(ZoneTag) set Zones = for (ZoneDevice : ZoneDevices): MakeBaseZone(ZoneDevice) SelectNext<public>()<transacts><decides> : base_zone = Zones[GetRandomInt(0, Zones.Length - 1)]
Тестирование зон подбора и доставки
Мы успешно создали два класса, поэтому хорошо бы протестировать написанный код и убедиться, что выбор зоны работает, как задумано.
Внесите следующие изменения в файл game_coordinator_device.verse:
- Добавьте в класс
game_coordinator_device
константу для селектора зоны доставки и массив переменных для селекторов зон подбора. Поскольку в дальнейшем игра будет повышать уровень подбора после каждого подбора пиццы, вам понадобится один селекторtagged_zone_selector
для каждого уровня подбора в игре. По этой причине мы и используем массивPickupZoneSelectors
. Каждый селектор зоны содержит все зоны подбора определённого уровня. Это должна быть переменная, так как при её создании учитывается количество теговpickup_zone_tag
в массивеPickupZoneLevelTags
. - Используйте следующий способ для увеличения количества уровней подбора, внеся минимум изменений в код: достаточно будет добавить в
PickupZoneLevelTags
дополнительные теги, производные отpickup_zone_tag
, а затем присвоить соответствующие теги устройствам в редакторе.game_coordinator_device<public> := class<concrete>(creative_device): DeliveryZoneSelector<private> : tagged_zone_selector = tagged_zone_selector{} var PickupZoneSelectors<private> : []tagged_zone_selector = array{}
- Добавьте метод
SetupZones()
и вызовите его вOnBegin()
:- Задайте для метода спецификатор
private
и тип возвращаемого значенияvoid
. - Инициализируйте селектор зоны доставки тегом
delivery_zone_tag
. - Создайте теги уровней зоны подбора и инициализируйте селекторы зоны подбора.
OnBegin<override>()<suspends> : void = SetupZones() SetupZones<private>() : void = DeliveryZoneSelector.InitZones(delivery_zone_tag{}) PickupZoneLevelTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}} set PickupZoneSelectors = for(PickupZoneTag : PickupZoneLevelTags): PickupZone := tagged_zone_selector{} PickupZone.InitZones(PickupZoneTag) PickupZone
- Задайте для метода спецификатор
- Создайте цикл в
OnBegin()
, который будет выбирать следующую зону подбора, активировать её, ожидать, когда игрок завершит зону, а после этого деактивировать зону.OnBegin<override>()<suspends> : void = SetupZones() var PickupLevel : int = 0 loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone() PickupZone.ZoneCompletedEvent.Await() PickupZone.DeactivateZone() else: Print("Следующая PickupZone не найдена") return if (DeliveryZone := DeliveryZoneSelector.SelectNext[]): DeliveryZone.ActivateZone() DeliveryZone.ZoneCompletedEvent.Await() DeliveryZone.DeactivateZone() else: Print("Следующая DeliveryZone не найдена") return
- Сохраните файлы Verse, скомпилируйте код и протестируйте уровень в игре.
Когда вы запустите игровой тест уровня, в начале игры активируется одно из устройств «Генератор предметов». После того как вы подберёте предмет, устройство «Генератор предметов» будет деактивировано, а затем активируется устройство «Область захвата». Данный цикл будет работать, пока вы не завершите игру вручную.