A zone is an area of the map (represented by a device) where the player can pick up items or deliver items. By completing this step in the Time Trial: Pizza Pursuit tutorial, you’ll learn how to create these pickup and delivery zones and activate / deactivate them for the player.
Using Abstraction for Creating a Zone Class
Abstraction is a programming principle where unnecessary details are hidden from a user where the user doesn't need to understand the hidden complexities. Abstraction describes what something is without knowing how it works. For example, you can put money in a vending machine and get a treat out without understanding how the mechanics function.
In Time Trial: Pizza Pursuit, there are two kinds of zones: pickup zones, which use the Item Spawner device, and delivery zones, which use the Capture Area device. Since these zones will behave the same way even though they’re different devices — meaning that both can be activated and deactivated — you can create a class to abstract this behavior into a generic zone object that handles the specific device interactions.
Abstracting this behavior into a class means you have only one place to change out what kind of devices you use. This implementation also means that you can change its specifics without changing any of your other code, because any code that uses this class only knows about the activate / deactivate functions.
Follow these steps to create this zone class:
- Create a new empty Verse file named pickup_delivery_zone.verse and open it in Visual Studio Code.
- In the Verse file, create a new class named
base_zone
with thepublic
specifier and add:- A
creative_object_interface
constant namedActivatorDevice
with thepublic
specifier, to store the device used in the zone. - An event named
ZoneCompletedEvent
that has thepublic
specifier, to signal when the player interacts with this zone, such as picking up items or delivering them. - A function named
ActivateZone()
that has thevoid
return type and thepublic
specifier, to enable the device used for the zone. -
A function named
DeactivateZone()
that has thevoid
return type and thepublic
specifier, to disable the device used for the zone.base_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")
When a class and its members have the
public
specifier, then they are universally accessible from other code. For more details, see Specifiers and Attributes.
- A
- In the
ActivateZone()
function, check whether theActivatorDevice
is a Capture Area device or Item Spawner device by type casting theActivatorDevice
to the different types and call theEnable()
function on the converted device. Do the same with theDeactivateZone()
function, except call theDisable()
function.base_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() DeactivateZone<public>() : void = Print("Zone deactivated") if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Disable() else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Disable()
- Create a function named
WaitForZoneCompleted()
that has theprivate
specifier andsuspends
specifier. This function will signal theZoneCompletedEvent
when the device-specific event occurs. This setup means that other code just needs to wait for theZoneCompletedEvent
and not care what kind of event the underlying device uses.WaitForZoneCompleted<private>(ZoneDeviceCompletionEventOpt : ?awaitable(agent))<suspends> : void = if (DeviceEvent := ZoneDeviceCompletionEventOpt?): DeviceEvent.Await() ZoneCompletedEvent.Signal(Self)
This function must have the
suspends
effect to be able to callDeviceEvent.Await()
. - Update
ActivateZone()
with aspawn
expression that callsWaitForZoneCompleted()
with the appropriate device event for the player interacting with the device:AgentEntersEvent
for the Capture Area device andItemPickedUpEvent
for the Item Spawner device.WaitForZoneCompleted
expects anoption
(indicated by?
) parameter of typeawaitable(agent)
, so we can pass any type that implements theawaitable
interface, with its parametric type equals toagent
. BothCaptureArea.AgentEntersEvent
andItemSpawner.ItemPickedUpEvent
respect this condition, so we can use them as the parameter. This is another example of abstraction.ActivateZone<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}) }
- Add another event named
ZoneDeactivatedEvent
that has theprotected
specifier. This event is necessary to terminate theWaitForZoneCompleted()
function if the zone is deactivated before the player completes it. Signal this event in theDeactivateZone()
function.ZoneDeactivatedEvent<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()
- Update
WaitForZoneCompleted()
with arace
expression so the function will wait for either the player to complete the zone, or the zone is deactivated. With therace
expression, theZoneDeactivatedEvent.Await()
asynchronous function call and theblock
expression with the device event andZoneCompletedEvent
signal will run at the same time but the expression that doesn’t finish first is canceled.WaitForZoneCompleted<private>(ZoneDeviceCompletionEventOpt : ?awaitable(agent))<suspends> : void = if (DeviceEvent := ZoneDeviceCompletionEventOpt?): race: block: DeviceEvent.Await() ZoneCompletedEvent.Signal(Self) ZoneDeactivatedEvent.Await()
- Finally, make a constructor for the
base_zone
class which will initialize theActivatorDevice
field.MakeBaseZone<constructor><public>(InActivatorDevice : creative_object_interface) := base_zone: ActivatorDevice := InActivatorDevice
- The following is the complete code for the
base_zone
class.<# 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() <# Activates the Zone. You should enable devices and any visual indicators for the zone here. #> ActivateZone<public>() : void = # The base zone can handle zones defined as item spawners or capture areas. # Try and cast to each type to see which we're dealing with. 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}) } <# Deactivates the Zone. You should disable devices and any visual indicators for the zone here. #> DeactivateZone<public>() : void = if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Disable() else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Disable() ZoneDeactivatedEvent.Signal() <# This event is necessary to terminate the WaitForZoneCompleted coroutine if the zone is deactivated without being completed. #> 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
Finding Zones at Runtime with Gameplay Tags
Now that you have a way to create zones and activate / deactivate them, let’s add a way to initialize all the zones that you tagged in the level and how to select the next one to activate.
This example shows how to do this with a class that is responsible for creating the zones and selecting the next zone to activate.
Follow these steps to create the class for creating and selecting zones:
- Create a new class named
tagged_zone_selector
in the pickup_delivery_zone.verse file. Add a variable array to store all the zones in the level.tagged_zone_selector<public> := class: var Zones<protected> : []base_zone = array{}
- Add a method named
InitZones()
that has thepublic
specifier and atag
parameter to find all zones associated with that Gameplay Tag and cache them.InitZones<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)
- Add a method named
SelectNext()
that has thedecides
andtransacts
specifiers so the method will either find another zone or fail. Select the zone at a random index in the array usingGetRandomInt(0, Zones.Length - 1)
for the index.SelectNext<public>()<transacts><decides> : base_zone = Zones[GetRandomInt(0, Zones.Length - 1)]
- The complete code for pickup_delivery_zone.verse file should now look like:
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 } <# 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() <# Activates the Zone. You should enable devices and any visual indicators for the zone here. #> ActivateZone<public>() : void = # The base zone can handle zones defined as item spawners or capture areas. # Try and cast to each type to see which we're dealing with. 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}) } <# Deactivates the Zone. You should disable devices and any visual indicators for the zone here. #> DeactivateZone<public>() : void = if (CaptureArea := capture_area_device[ActivatorDevice]): CaptureArea.Disable() else if (ItemSpawner := item_spawner_device[ActivatorDevice]): ItemSpawner.Disable() ZoneDeactivatedEvent.Signal() <# This event is necessary to terminate the WaitForZoneCompleted coroutine if the zone is deactivated without being completed. #> 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 # The tagged_zone_selector creates zones based on triggers tagged with the tag passed to InitZones. tagged_zone_selector<public> := class: var Zones<protected> : []base_zone = array{} InitZones<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<public>()<transacts><decides> : base_zone = Zones[GetRandomInt(0, Zones.Length - 1)]
Testing Pickup and Delivery Zones
Now that you've created two classes, it's a good idea to test your code and make sure that your zone selection works the way you expect.
Follow these steps to update your game_coordinator_device.verse file:
- Add a constant for the delivery zone selector and a variable array for the pickup zone selectors to the
game_coordinator_device
. Since the game will later increase the pickup level after each pizza pickup, you will need onetagged_zone_selector
for each pickup level you want in the game, hence thePickupZoneSelectors
array. Each zone selector holds all the pickup zones of a certain level. It needs to be a variable because its setup is determined by the number ofpickup_zone_tag
s inPickupZoneLevelTags
. - Use this setup to extend the number of pickup levels with minimal changes to the code: you just need to update the
PickupZoneLevelTags
with additional ones deriving frompickup_zone_tag
, then tag the devices in the editor.game_coordinator_device<public> := class<concrete>(creative_device): DeliveryZoneSelector<private> : tagged_zone_selector = tagged_zone_selector{} var PickupZoneSelectors<private> : []tagged_zone_selector = array{}
- Add a method named
SetupZones()
and call the method inOnBegin()
:- Set the method to have the
private
specifier and avoid
return type. - Initialize the delivery zone selector with the
delivery_zone_tag
. -
Create the pickup zone level tags and initialize the pickup zone selectors.
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
- Set the method to have the
- Create a loop in
OnBegin()
that selects the next pickup zone, activates it, waits for the player to complete the zone, and then deactivates the zone.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("Can't find next PickupZone to select") return if (DeliveryZone := DeliveryZoneSelector.SelectNext[]): DeliveryZone.ActivateZone() DeliveryZone.ZoneCompletedEvent.Await() DeliveryZone.DeactivateZone() else: Print("Can't find next DeliveryZone to select") return
- Save your Verse files, compile your code, and playtest your level.
When you playtest your level, one of the Item Spawner devices will activate at the start of the game. After you pick up the item, the Item Spawner device will deactivate and a Capture Area device will then activate. This continues until you manually end the game.