В этом последнем разделе создания игры «Гонка на время: Доставка пиццы» вы найдёте полный код игры, а также идеи для самостоятельной работы над игрой.
Закончить код
Этот проект содержит несколько файлов Verse:
- countdown_timer.verse: См. раздел Настраиваемый таймер обратного отсчёта, где представлен полный код файла.
- game_coordinator_device.verse: полный код файла приведён ниже.
- objective_marker.verse: См. раздел Движущийся маркер цели, где представлен полный код файла.
- pickup_delivery_zone.verse: полный код файла приведён ниже.
- score_manager.verse: Полный код файла представлен ниже.
game_coordinate_device.verse
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
using { /Fortnite.com/Vehicles }
using { /Fortnite.com/Characters }
using { /Fortnite.com/Playspaces }
using { /Verse.org/Native }
using { /Verse.org/Random }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/Curves }
using { /Verse.org/Simulation/Tags }
# Теги игровых зон
pickup_zone_tag<public> := class(tag):
pickup_zone_level_1_tag<public> := class(pickup_zone_tag):
pickup_zone_level_2_tag<public> := class(pickup_zone_tag):
pickup_zone_level_3_tag<public> := class(pickup_zone_tag):
delivery_zone_tag<public> := class(tag):
log_pizza_pursuit<internal> := class(log_channel){}
game_coordinator_device<public> := class(creative_device):
@editable
VehicleSpawner<public> : vehicle_spawner_atk_device = vehicle_spawner_atk_device{}
# С какого момента таймер начнёт обратный отсчёт.
@editable
InitialCountdownTime<public> : float = 30.0
@editable
EndGame<public> : end_game_device = end_game_device{}
# Сколько секунд добавлять к таймеру за доставку подобранного предмета.
@editable
DeliveryBonusSeconds<public> : float = 20.0
@editable
PickupMarker<public> : objective_marker = objective_marker{}
@editable
ScoreManagerDevice<public> : score_manager_device = score_manager_device{}
@editable
PizzaRemover<public> : item_remover_device = item_remover_device{}
@editable
# Показывает, сколько очков даёт подбор в зависимости от его уровня.
PointsForPickupLevel<public> : []int = array{1, 2, 3}
OnBegin<override>()<suspends> : void =
FindPlayer()
SetupZones()
# После того, как игрок сел в транспорт, он может выйти из него в любой момент;
# необходимо отслеживать этот момент каждый раз, чтобы возвращать игрока обратно в транспорт.
VehicleSpawner.AgentExitsVehicleEvent.Subscribe(HandlePlayerExitsVehicle)
# Требуется вывод уведомления только в первый раз, когда игрок садится в транспорт, чтобы начать игру.
# Функция StartGameOnPlayerEntersVehicle будет ожидать это событие, после чего запустит цикл игрового процесса.
StartGameOnPlayerEntersVehicle()
Logger<private> : log = log{Channel := log_pizza_pursuit}
var MaybePlayer<private> : ?player = false
var CountdownTimer<private> : countdown_timer = countdown_timer{}
var ScoreManager<private> : score_manager = score_manager{}
DeliveryZoneSelector<private> : tagged_zone_selector = tagged_zone_selector{}
var PickupZones<private> : []tagged_zone_selector = array{}
FindPlayer<private>() : void =
# Поскольку мы работаем с одиночной игрой, первый игрок (0)
# будет единственным доступным.
Playspace := Self.GetPlayspace()
if (FirstPlayer := Playspace.GetPlayers()[0]):
set MaybePlayer = option{FirstPlayer}
Logger.Print("Игрок найден")
else:
# Если не можем найти игрока, регистрируем ошибку.
# Однако такого быть не должно, так как у нас всегда есть хотя бы один игрок.
Logger.Print("Доступный игрок не найден", ?Level := log_level.Error)
SetupZones<private>() : void =
# Существует только один тип зоны доставки, поскольку они не различаются по уровням сложности.
DeliveryZoneSelector.InitZones(delivery_zone_tag{})
# Мы используем теги игрового процесса для выбора зон (представленных устройствами) на основе их уровня сложности.
# Использование массива облегчает изменение уровней сложности: мы можем добавлять больше
уровней, увеличивать/уменьшать шаг между уровнями, а также менять их порядок без необходимости изменять код в других местах.
# Создайте один tagged_zone_selector для каждого тега уровня сложности, чтобы все устройства с одинаковым тегом (т. е. с одинаковым уровнем сложности)
# попадали в одну область выбора.
LevelTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}}
set PickupZones = for (ZoneTag : LevelTags):
NewZone := tagged_zone_selector{}
NewZone.InitZones(ZoneTag)
NewZone
StartGameOnPlayerEntersVehicle<private>()<suspends> : void =
VehiclePlayer := VehicleSpawner.AgentEntersVehicleEvent.Await()
Logger.Print("Игрок сел в транспорт")
set MaybePlayer = option{player[VehiclePlayer]}
StartGame()
HandlePlayerExitsVehicle<private>(VehiclePlayer : agent) : void =
Logger.Print("Игрок вышел из транспорта. Возвращаю игрока в транспорт")
VehicleSpawner.AssignDriver(VehiclePlayer)
StartGame<private>()<suspends> : void =
Logger.Print("Пробуем запустить игру…")
<# Создаём новый countdown_timer, который начнёт отсчёт с InitialCountdownTime сразу после запуска.
Также создайте новый score_manager, который будет отслеживать счёт и уровень подбора игрока.
Таймер countdown_timer и score_manager требуют наличия игрока для отображения интерфейса.
К этому моменту у нас уже должен быть действующий игрок: тот, кто сел в машину, инициировав начало игры. #>
if (ValidPlayer := MaybePlayer?):
Logger.Print("Игрок доступен, запускаем игру…")
set ScoreManager = MakeScoreManager(ValidPlayer, ScoreManagerDevice)
ScoreManager.AddScoreManagerToUI()
set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer)
CountdownTimer.StartCountdown()
# Ждём завершения отсчёта таймера.
# В то же время запускаем игровой цикл подбора и доставки, на основе которого и строится игровой процесс.
race:
HandleCountdownEnd(ValidPlayer)
PickupDeliveryLoop()
else:
Logger.Print("Доступный игрок не найден. Запуск игры прерван", ?Level := log_level.Error)
HandleCountdownEnd<private>(InPlayer : player)<suspends> : void =
TotalTime := CountdownTimer.CountdownEndedEvent.Await()
ScoreManager.AwardScore()
EndGame.Activate(InPlayer)
PickupDeliveryLoop<private>()<suspends> : void =
PickupZonesTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}}
MaxPickupLevel := PickupZonesTags.Length - 1
FirstPickupZoneCompletedEvent := event(){}
<# Откладываем отключение объекта MapIndicator, чтобы завершение цикла PickupDeliveryLoop всегда приводило к отключению маркера.
Отложенный код также выполняется при условии, если цикл PickupDeliveryLoop отменяется. #>
defer:
if (ValidPlayer := MaybePlayer?):
PickupMarker.MapIndicator.DeactivateObjectivePulse(ValidPlayer)
PickupMarker.MapIndicator.Disable()
PickupMarker.MapIndicator.Enable()
loop:
var PickupLevel : int = 0
var IsFirstPickup : logic = true
<# Каждый раз, когда цикл начинается заново, необходимо сбрасывать элемент интерфейса, соответствующий уровню подбора, через ScoreManager.
Уровень подбора в интерфейсе начинается с 1 (не с 0). Если он будет начинаться с 0, то у некоторых игроков это может вызвать недоумение.
Нужно начать индексирование с 0, поэтому PickupLevel=0 будет соответствовать уровню 1 в интерфейсе. #>
ScoreManager.UpdatePickupLevel(PickupLevel + 1)
race:
loop:
if (PickupZone:base_zone = PickupZones[PickupLevel].SelectNext[]):
PickupZone.ActivateZone()
Sleep(0.0)
PickupMarker.MoveMarker(PickupZone.GetTransform(), ?OverTime := 0.0)
if (ValidPlayer := MaybePlayer?):
PickupMarker.MapIndicator.ActivateObjectivePulse(ValidPlayer)
<# Это единственная отложенная операция, необходимая для любой активируемой зоны подбора PickupZone. Она либо деактивирует первую зону PickupZone в конце каждого внешнего цикла,
либо деактивирует любую последующую зону PickupZone. Это происходит потому, что выражение оценивается в самом конце, когда переменная PickupZone уже привязана к новой зоне. #>
defer:
PickupZone.DeactivateZone()
PickupZone.ZoneCompletedEvent.Await()
Logger.Print("Подобрано", ?Level:=log_level.Normal)
<# Забираем пиццу из инвентаря игрока, чтобы избежать её скапливания и сбрасывания при переполнении. #>
if (RemovingPlayer := MaybePlayer?):
PizzaRemover.Remove(RemovingPlayer)
# После первого подбора можно активировать зону доставки.
if (IsFirstPickup?):
set IsFirstPickup = false
FirstPickupZoneCompletedEvent.Signal()
if (PickupPoints := PointsForPickupLevel[PickupLevel]):
ScoreManager.UpdatePendingScore(PickupPoints)
# Обновляем уровень подбора и ScoreManager.
if (PickupLevel < MaxPickupLevel):
set PickupLevel += 1
ScoreManager.UpdatePickupLevel(PickupLevel + 1)
else:
Logger.Print("Следующая зона PickupZone для выбора не найдена.", ?Level := log_level.Error)
return # Ошибка при выходе из PickupDeliveryLoop
block:
FirstPickupZoneCompletedEvent.Await()
if (DeliveryZone := DeliveryZoneSelector.SelectNext[]):
DeliveryZone.ActivateZone()
# Мы откладываем деактивацию зоны, поэтому прерывание PickupDeliveryLoop также приводит к деактивации любой активной зоны доставки.
defer:
Logger.Print("Деактивация зоны доставки.", ?Level:=log_level.Normal)
DeliveryZone.DeactivateZone()
DeliveryZone.ZoneCompletedEvent.Await()
Logger.Print("Доставлено", ?Level:=log_level.Normal)
PointsCommitted := ScoreManager.AddPendingScoreToTotalScore()
BonusTime : float = DeliveryBonusSeconds * PointsCommitted
CountdownTimer.AddRemainingTime(BonusTime)
else:
Logger.Print("Следующая зона DeliveryZone для выбора не найдена.", ?Level:=log_level.Error)
return # Ошибка при выходе из PickupDeliveryLoop
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)]
score_manager.verse
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/UI }
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
MakeScoreManager<constructor><public>(InPlayer : player, InScoreManagerDevice : score_manager_device) := score_manager:
MaybePlayer := option{InPlayer}
MaybePlayerUI := option{GetPlayerUI[InPlayer]}
score_manager := class:
<# Поскольку рабочая область не будет создаваться повторно во время существования панели управления счётом, добавляйте это выражение один раз
при каждом создании объекта этого типа. #>
block:
set Canvas = canvas:
Slots := array:
canvas_slot:
Widget := stack_box:
Orientation := orientation.Vertical
Slots := array:
stack_box_slot:
Widget := TotalGameScoreWidget
stack_box_slot:
Widget := PendingScoreWidget
stack_box_slot:
Widget := PickupLevelWidget
Offsets := margin{ Top:=0.0, Left:=500.0 }
AddScoreManagerToUI<public>() : void =
if (PlayerUI := MaybePlayerUI?):
PlayerUI.AddWidget(Canvas)
UpdateUI()
<# Добавляет PendingScore к TotalGameScore и сбрасывает PendingScore на 0.
Возвращает общее число добавленных очков подбора. #>
AddPendingScoreToTotalScore<public>() : int =
set TotalGameScore += PendingScore
defer:
set PendingScore = 0
UpdateUI()
PendingScore
<# Добавляет заданное количество очков к очкам, набранным за сессию. #>
UpdatePendingScore<public>(Points : int) : void =
set PendingScore += Points
UpdateUI()
UpdatePickupLevel<public>(Level : int) : void =
set PickupLevel = Level
UpdateUI()
<# Присваивает игроку счёт при помощи ScoreManagerDevice путём его активации. #>
AwardScore<public>() : void =
ScoreManagerDevice.SetScoreAward(TotalGameScore)
if (AwardedPlayer := MaybePlayer?):
ScoreManagerDevice.Activate(AwardedPlayer)
MaybePlayer<internal> : ?player = false
MaybePlayerUI<internal> : ?player_ui = false
ScoreManagerDevice<internal> : score_manager_device = score_manager_device{}
var Canvas<internal> : canvas = canvas{}
TotalGameScoreWidget<internal> : text_block = text_block{}
PendingScoreWidget<internal> : text_block = text_block{}
PickupLevelWidget<internal> : text_block = text_block{}
PickupLevelText<private><localizes>(InLevel : int) : message = "Уровень подбора: {InLevel}"
PendingScoreText<private><localizes>(InPoints : int) : message = "Набрано очков: {InPoints}"
TotalGameScoreText<private><localizes>(InPoints : int) : message = "Всего очков: {InPoints}"
var TotalGameScore<private> : int = 0
var PendingScore<private> : int = 0
var PickupLevel<private> : int = 0
UpdateUI<private>() : void =
if (PlayerUI := MaybePlayerUI?):
PickupLevelWidget.SetText(PickupLevelText(PickupLevel))
PendingScoreWidget.SetText(PendingScoreText(PendingScore))
TotalGameScoreWidget.SetText(TotalGameScoreText(TotalGameScore))
Самостоятельная работа
Вы успешно изучили данное руководство и теперь знаете, как создать полноценную игру на время («Доставка пиццы») при помощи Verse.
Используя полученные знания, попробуйте сделать следующее:
- Добавьте ещё несколько уровней зон подбора.
- Добавьте разные типы зон доставки. Расширьте класс
base_zone
таким образом, чтобы игроку для завершения зоны нужно было активировать какое-либо другое устройство, например кнопку. - Сделайте так, чтобы игроку для доставки пиццы было необходимо выходить из транспорта и проходить короткую полосу препятствий пешком.
- Активируйте несколько зон одновременно.
- Измените критерии выбора зоны с учётом расстояния до игрока.