Зона — это участок карты (представленный устройством), где игрок может подбирать предметы или куда он может их доставлять. По завершении этого раздела урока Гонка на время: доставка пиццы вы узнаете, как создавать зоны подбора и доставки, а также деактивировать и активировать их для игрока.
Использование абстракции для создания класса зоны
Абстракция — это принцип в программировании, когда от пользователя скрываются более сложные детали, поскольку для него нет необходимости в их понимании. Абстракция описывает то, чем что-то является, без объяснения, как это работает. К примеру, вы можете купить кофе в автомате, не вникая в принципы его работы.
В игре Гонка на время: доставка пиццы есть два вида зон: зоны подбора, в которых используется устройство Генератор предметов, и зоны доставки, в которых используется устройство Область захвата. Поскольку поведение этих зон является аналогичным даже несмотря на то, что это разные устройства (то есть оба могут быть активированы и деактивированы), вы можете создать класс, чтобы абстрагировать это поведение через стандартный объект зоны, который будет обрабатывать взаимодействие с конкретным устройством.
Абстрагирование поведения путём создания класса означает, что в коде будет лишь одно место для изменения типа используемых устройств. Благодаря подобной реализации вам будет достаточно вносить изменения всего в одном месте без необходимости изменять код где-то ещё, так как любой код, где используется этот класс, будет «знать» только о функциях активации/деактивации.
Ниже пошагово описан процесс создания класса зоны:
Создайте новый пустой файл 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, для деактивации устройства, используемого в зоне.Versebase_zone<public> := class: ActivatorDevice<public> : creative_object_interface ZoneCompletedEvent<public> : event(base_zone) = event(base_zone){} ActivateZone<public>() : void = Print("Zone activated") DeactivateZone<public>() : void = Print("Zone deactivated")Если класс и его члены имеют спецификатор
public, то они всегда доступны из других частей кода. Более подробно это описано в разделе Спецификаторы и атрибуты.
В функции
ActivateZone()проверьте, является лиActivatorDeviceустройством «Область захвата» или «Генератор предметов», путём приведения типаActivatorDeviceк другим типам и вызова функцииEnable()для уже преобразованного устройства. Выполните то же самое с функциейDeactivateZone(), только в этом случае вызовите функциюDisable().Versebase_zone<public> := class: ActivatorDevice<public> : creative_object_interface ActivateZone<public>() : void = Print("Zone activated") if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Enable() else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Enable()Создайте функцию с именем
WaitForZoneCompleted()со спецификаторамиprivateиsuspends. Эта функция инициирует событиеZoneCompletedEventпри возникновении события для конкретного устройства. Благодаря такому подходу коду в других местах будет достаточно дождатьсяZoneCompletedEventбез необходимости проверять, какое конкретно событие используется задействованным устройством.VerseWaitForZoneCompleted<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отвечают данному условию, поэтому мы можем использовать их в качестве параметра. Это ещё один пример абстракции.VerseActivateZone<public>() : void = Print("Zone activated") 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().VerseZoneDeactivatedEvent<protected> : event() = event(){} DeactivateZone<public>() : void = Print("Zone deactivated") if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Disable() else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Disable() ZoneDeactivatedEvent.Signal()Добавьте в функцию
WaitForZoneCompleted()выражениеrace, чтобы функция ожидала либо завершения зоны игроком, либо деактивации зоны. При использовании выраженияraceасинхронный вызов функцииZoneDeactivatedEvent.Await(), а такжеблочноевыражение с событием устройства и сигналомZoneCompletedEventбудут выполняться одновременно, при этом выполнение выражения, которое не завершится первым, будет отменено.VerseWaitForZoneCompleted<private>(ZoneDeviceCompletionEventOpt : ?awaitable(agent))<suspends> : void = if (DeviceEvent := ZoneDeviceCompletionEventOpt?): race: block: DeviceEvent.Await() ZoneCompletedEvent.Signal(Self) ZoneDeactivatedEvent.Await()Наконец, создайте конструктор для класса
base_zone, который будет инициализировать полеActivatorDevice.VerseMakeBaseZone<constructor><public>(InActivatorDevice : creative_object_interface) := base_zone: ActivatorDevice := InActivatorDeviceНиже приведён полный код класса
base_zone.Verse# A zone is an area of the map (represented by a device) that can be Activated/Deactivated and that provides events to signal when the zone has been "Completed" (can't be completed anymore until next activation). # Zone "Completed" depends on the device type (ActivatorDevice) for the zone. # Suggested usage: 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()
Поиск зон в среде выполнения с помощью тегов игрового процесса
Теперь, когда у нас есть возможность создавать зоны и активировать/деактивировать их, добавим способ инициализации всех зон на уровне, которым вы присвоили теги, а также способ выбора следующей зоны для активации.
Рассмотрим это ниже на примере класса, отвечающего за создание зон и выбор следующей зоны для активации.
Ниже пошагово описан процесс создания класса для создания и выбора зон:
Создайте новый класс с именем
tagged_zone_selectorв файле pickup_delivery_zone.verse. Добавьте массив переменных для хранения всех зон на уровне.tagged_zone_selector<public> := class: var Zones<protected> : []base_zone = array{}Добавьте метод
InitZones()со спецификаторомpublicи параметромtag, чтобы найти все зоны, связанные с этим тегом игрового процесса, и кэшировать их.VerseInitZones<public>(ZoneTag : tag) : void = # On creation of a zone selector, find all available zones and cache them so we don't consume time searching for tagged devices every time the next zone is selected. ZoneDevices := GetCreativeObjectsWithTag(ZoneTag) set Zones = for (ZoneDevice : ZoneDevices): MakeBaseZone(ZoneDevice)Добавьте метод с именем
SelectNext()со спецификаторамиdecidesиtransacts, чтобы метод либо находил другую зону, либо завершался с ошибкой. Выберите зону со случайным индексом в массиве при помощиGetRandomInt(0, Zones.Length - 1).VerseSelectNext<public>()<transacts><decides> : base_zone = Zones[GetRandomInt(0, Zones.Length - 1)]Теперь файл pickup_delivery_zone.verse должен выглядеть следующим образом:
Verseusing { /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 } <# A zone is an area of the map (represented by a device) that can be Activated/Deactivated and that provides events to signal when the zone has been "Completed" (can't be completed anymore until next activation). Zone "Completed" depends on the device type (ActivatorDevice) for the zone.
Тестирование зон подбора и доставки
Мы успешно создали два класса, поэтому хорошо бы протестировать написанный код и убедиться, что выбор зоны работает, как задумано.
Внесите следующие изменения в файл game_coordinator_device.verse:
Добавьте константу для селектора зоны доставки и массив переменных для селекторов зон подбора в
game_coordinator_device. Поскольку в дальнейшем игра будет повышать уровень подбора после каждого подбора пиццы, вам понадобится одинtagged_zone_selectorдля каждого уровня подбора, который должен быть в игре, в связи с чем мы и используем массивPickupZoneSelectors. Каждый селектор зоны содержит все зоны подбора определённого уровня. Это должна быть переменная, так как при её создании учитывается количество теговpickup_zone_tagв массивеPickupZoneLevelTags.Используйте следующий способ для увеличения количества уровней подбора, внеся минимум изменений в код: достаточно будет добавить в
PickupZoneLevelTagsдополнительные теги, производные отpickup_zone_tag, а затем присвоить соответствующие теги устройствам в редакторе.Versegame_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.Создайте теги уровней зоны подбора и инициализируйте селекторы зоны подбора.
VerseOnBegin<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{}
Создайте цикл в
OnBegin(), который будет выбирать следующую зону подбора, активировать её, ожидать, когда игрок завершит зону, после чего деактивировать зону.VerseOnBegin<override>()<suspends> : void = SetupZones() var PickupLevel : int = 0 loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone() PickupZone.ZoneCompletedEvent.Await() PickupZone.DeactivateZone()Сохраните файлы Verse, скомпилируйте код и протестируйте уровень в игре.
Когда вы запустите игровой тест уровня, в начале игры активируется одно из устройств «Генератор предметов». После того как вы подберёте предмет, устройство «Генератор предметов» будет деактивировано, а затем активируется устройство «Область захвата». Данный цикл будет работать, пока вы не завершите игру вручную.