Игровой цикл — это код, который выполняется многократно (циклически), чтобы реагировать на ввод данных (взаимодействие игрока с помощью контроллера или мыши), обновлять состояние игры, а также выполнять действия, когда игрок повлиял на состояние игры, например нажал кнопку и включил свет. Цикл обычно заканчивается, когда игра завершается успешно, например игрок достигает цели, или неуспешно, например истекает время, отведённое на достижение цели.
В этом разделе урока по созданию игры «Гонка на время: доставка пиццы», вы узнаете, как создать игровой цикл и определить состояние успешного или неуспешного завершения игры.
Ниже приведён псевдокод цикла игры «Гонка на время: Доставка пиццы»:
loop:
race:
loop:
SelectNextPickupZone
WaitForPlayerToCompletePickupZone
block:
WaitForFirstPickup
SelectNextDeliveryZone
WaitForPlayerToCompleteDeliveryZone
Этот цикл должен завершиться, когда таймер обратного отсчёта закончит отсчёт или если в игре возникнет непредвиденная ошибка.
Создание основного игрового цикла
Внесите следующие изменения в файл game_coordinator_device.verse:
- Создайте метод с названием
PickupDeliveryLoop()
и спецификаторамиprivate
иsuspends
. Переместите цикл, который вы до этого создали вOnBegin()
, в этот новый метод.OnBegin<override>()<suspends> : void = SetupZones() PickupDeliveryLoop() PickupDeliveryLoop<private>()<suspends> : void = 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
- Определите максимальное количество уровней подбора по длине массива тегов и увеличивайте уровень
PickupLevel
каждый раз, когда игрок завершает зону подбора, пока уровень подбора не превысит максимальное количество уровней подбора.OnBegin<override>()<suspends> : void = SetupZones() PickupDeliveryLoop() 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 var PickupLevel : int = 0 loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone() PickupZone.ZoneCompletedEvent.Await() PickupZone.DeactivateZone() # Обновляем уровень подбора if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 else: Print("Следующая PickupZone не найдена") return
- Зона доставки должна активироваться после того, как игрок совершит первый подбор, при этом игрок должен иметь возможность при желании подобрать предметы перед тем, как отправиться в зону доставки. Для этого код зоны подбора и зоны доставки должен выполняться одновременно. В этом примере используется выражение одновременного выполнения
race
, так как:- Блок доставки должен отменять цикл зоны подбора, когда игрок завершает доставку.
- Цикл зоны подбора должен отменять блок доставки при возникновении проблемы с циклом подбора.
Вам также потребуется немного изменить процесс деактивации зоны. Метод
DeactivateZone()
не должен вызываться при отмене цикла или блока доставки, если сценарий ожидал завершения зоны.Так как код деактивации зоны выполняться не будет, сама зона останется активной, что повлечёт за собой программную ошибку.
-
Это можно исправить выражением
defer
. Оно откладывает выполнение содержащихся в нём выражений до завершения области видимости, в которой встречаетсяdefer
. Блокdefer
запускается, когда управление программой передаётся за пределы области видимости. Это может быть в результате стандартного выхода из области видимости (завершение работы функции), досрочного выхода (например использования операторовreturn
илиbreak
) либо отмены любой параллельной задачи или асинхронного выражения (например,race
). Это похоже на постановку в очередь операций, которые будут выполнены в самом конце независимо от того, что произойдёт. Поместите каждый вызов методаDeactivateZone
в блокdefer
и вызывайте его перед соответствующим вызовомZoneCompletedEvent.Await()
.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 var PickupLevel : int = 0 race: loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone() defer: PickupZone.DeactivateZone() PickupZone.ZoneCompletedEvent.Await() # Обновляем уровень подбора if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 else: Print("Следующая PickupZone не найдена") return block: 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) else: Logger.Print("Следующая зона DeliveryZone для выбора не найдена.", ?Level:=log_level.Error) return # Ошибка при выходе из PickupDeliveryLoop
- В предыдущем примере зона доставки активируется одновременно с зоной подбора, однако активация зоны доставки должна происходить только после выполнения первого подбора. Для этого добавьте событие и сделайте так, чтобы зона доставки ожидала это событие, прежде чем активироваться.
OnBegin<override>()<suspends> : void = SetupZones() PickupDeliveryLoop() 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(){} var PickupLevel : int = 0 var IsFirstPickup : logic = true race: loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone() defer: PickupZone.DeactivateZone() PickupZone.ZoneCompletedEvent.Await() # После первого подбора можно активировать зону доставки. if (IsFirstPickup?): set IsFirstPickup = false FirstPickupZoneCompletedEvent.Signal() # Обновляем уровень подбора if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 else: Print("Следующая PickupZone не найдена") return 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) else: Logger.Print("Следующая зона DeliveryZone для выбора не найдена.", ?Level := log_level.Error) return # Ошибка при выходе из PickupDeliveryLoop
- Создайте цикл, в котором выражение
race
для зоны подбора/доставки выполняется до завершения игры, чтобы игрок мог продолжать подбирать и доставлять предметы.OnBegin<override>()<suspends> : void = SetupZones() PickupDeliveryLoop() 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(){} loop: var PickupLevel : int = 0 var IsFirstPickup : logic = true race: loop: if (PickupZone : base_zone = PickupZones[PickupLevel].SelectNext[]): PickupZone.ActivateZone() defer: PickupZone.DeactivateZone() PickupZone.ZoneCompletedEvent.Await() # Обновляем уровень подбора if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 else: Print("Следующая PickupZone не найдена") return 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) else: Logger.Print("Следующая зона DeliveryZone для выбора не найдена.", ?Level:=log_level.Error) return # Ошибка при выходе из PickupDeliveryLoop
-
Теперь файл game_coordinator_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/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): DeliveryZoneSelector<private> : tagged_zone_selector = tagged_zone_selector{} var PickupZoneSelectors<private> : []tagged_zone_selector = array{} OnBegin<override>()<suspends> : void = SetupZones() PickupDeliveryLoop() 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 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(){} loop: var PickupLevel : int = 0 var IsFirstPickup : logic = true race: loop: if (PickupZone : base_zone = PickupZones[PickupLevel].SelectNext[]): PickupZone.ActivateZone() defer: PickupZone.DeactivateZone() PickupZone.ZoneCompletedEvent.Await() # Обновляем уровень подбора if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 else: Print("Следующая PickupZone не найдена") return 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) else: Logger.Print("Следующая зона DeliveryZone для выбора не найдена.", ?Level:=log_level.Error) return # Ошибка при выходе из PickupDeliveryLoop
Сохраните файлы Verse, скомпилируйте код и протестируйте уровень в игре.
Во время тестирования уровня одно из устройств «Генератор предметов» активируется в начале игры, а также после того, как игрок подберёт предмет. После того как игрок подберёт первый предмет, устройство «Генератор предметов» будет деактивировано, а затем активируется устройство «Область захвата». Данный цикл будет работать, пока вы не завершите игру вручную.
Определение состояний успешного и неуспешного завершения игрового цикла
Вы создали основной игровой цикл. Самое время определить состояния успешного и неуспешного завершения игрового цикла. Игра должна завершаться при одном из следующих условий:
- Когда завершается отсчёт таймер или
- Когда что-то идёт не так в игровом цикле.
Выполните следующие действия, чтобы задать состояния успешного и неуспешного завершения игры:
- Создайте экземпляр класса
countdown_timer
внутри классаgame_coordinator_device
со спецификаторомprivate
.game_coordinator_device<public> := class(creative_device): @editable EndGame<public> : end_game_device = end_game_device{} var CountdownTimer<private> : countdown_timer = countdown_timer{}
- Поскольку конструктор класса
countdown_timer
требует ссылки на игрока, добавьте опциональную переменную игрока для хранения ссылки на него в этой однопользовательской игре, а также создайте функциюFindPlayer()
для получения ссылки на игрока. Вызовите функциюFindPlayer()
в методеOnBegin()
перед настройкой зон.game_coordinator_device<public> := class(creative_device): @editable EndGame<public> : end_game_device = end_game_device{} var CountdownTimer<private> : countdown_timer = countdown_timer{} var MaybePlayer<private> : ?player = false OnBegin<override>()<suspends> : void = FindPlayer() SetupZones() FindPlayer<private>() : void = # Так как мы создаём однопользовательскую игру, первый игрок (с индексом 0) # будет единственным доступным. if (FirstPlayer := GetPlayspace().GetPlayers()[0]): set MaybePlayer = option{FirstPlayer} Logger.Print("Игрок найден") else: # Если не можем найти игрока, регистрируем ошибку. # Однако такого быть не должно, так как у нас всегда есть хотя бы один игрок. Logger.Print("Доступный игрок не найден", ?Level := log_level.Error)
- Создайте функцию
HandleCountdownEnd()
, которая будет ожидать завершения отсчёта таймера и активировать устройство завершения игры.HandleCountdownEnd<private>(InPlayer : agent)<suspends> : void = CountdownTimer.CountdownEndedEvent.Await() EndGame.Activate(InPlayer)
- Создайте функцию
StartGame()
и вызовите её послеSetupZones()
вOnBegin()
. Эта функция должна выполнять следующее:- Инициализировать таймер.
game_coordinator_device<public> := class(creative_device): # С какого момента таймер начнёт обратный отсчёт. @editable InitialCountdownTime<public> : float = 30.0 @editable EndGame<public> : end_game_device = end_game_device{} OnBegin<override>()<suspends> : void = FindPlayer() SetupZones() StartGame() StartGame<private>()<suspends> : void = Logger.Print("Пробуем запустить игру…") <# Создаём новый countdown_timer, который начнёт отсчёт с InitialCountdownTime сразу после запуска. Таймер countdown_timer требует наличия игрока для отображения интерфейса. К этому моменту у нас уже должен быть действующий игрок. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Игрок доступен, запускаем игру…") set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer) CountdownTimer.StartCountdown() else: Logger.Print("Доступный игрок не найден. Запуск игры прерван", ?Level := log_level.Error)
- Используйте выражение
race
как для вызоваHandleCountdownEnd(ValidPlayer)
, так и для вызоваPickupDeliveryLoop()
, чтобы добиться следующего:- игровой цикл останавливается при завершении отсчёта таймера или
- отсчёт времени прекращается при остановке игрового цикла.
StartGame<private>()<suspends> : void = Logger.Print("Пробуем запустить игру…") <# Создаём новый countdown_timer, который начнёт отсчёт с InitialCountdownTime сразу после запуска. Таймер countdown_timer требует наличия игрока для отображения интерфейса. К этому моменту у нас уже должен быть действующий игрок. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Игрок доступен, запускаем игру…") set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer) CountdownTimer.StartCountdown() # Ждём завершения отсчёта таймера. # В то же время запускаем игровой цикл подбора и доставки, на основе которого и строится игровой процесс. race: HandleCountdownEnd(ValidPlayer) PickupDeliveryLoop() else: Logger.Print("Доступный игрок не найден. Запуск игры прерван", ?Level := log_level.Error)
- Инициализировать таймер.
- Ваш файл 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/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 InitialCountdownTime<public> : float = 30.0 @editable EndGame<public> : end_game_device = end_game_device{} DeliveryZoneSelector<private> : tagged_zone_selector = tagged_zone_selector{} var PickupZoneSelectors<private> : []tagged_zone_selector = array{} OnBegin<override>()<suspends> : void = FindPlayer() SetupZones() StartGame() FindPlayer<private>() : void = # Так как мы создаём однопользовательскую игру, первый игрок (с индексом 0) # будет единственным доступным. if (FirstPlayer := GetPlayspace().GetPlayers()[0]): set MaybePlayer = option{FirstPlayer} Logger.Print("Игрок найден") else: # Если не можем найти игрока, регистрируем ошибку. # Однако такого быть не должно, так как у нас всегда есть хотя бы один игрок. Logger.Print("Доступный игрок не найден", ?Level := log_level.Error) 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 StartGame<private>()<suspends> : void = Logger.Print("Пробуем запустить игру…") <# Создаём новый countdown_timer, который начнёт отсчёт с InitialCountdownTime сразу после запуска. Таймер countdown_timer требует наличия игрока для отображения интерфейса. К этому моменту у нас уже должен быть действующий игрок. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Игрок доступен, запускаем игру…") set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer) CountdownTimer.StartCountdown() # Ждём завершения отсчёта таймера. # В то же время запускаем игровой цикл подбора и доставки, на основе которого и строится игровой процесс. race: HandleCountdownEnd(ValidPlayer) PickupDeliveryLoop() else: Logger.Print("Доступный игрок не найден. Запуск игры прерван", ?Level := log_level.Error) HandleCountdownEnd<private>(InPlayer : agent)<suspends> : void = CountdownTimer.CountdownEndedEvent.Await() 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(){} loop: var PickupLevel : int = 0 var IsFirstPickup : logic = true race: loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone() <# Это единственная отложенная операция, необходимая для любой активируемой зоны подбора PickupZone. Она либо деактивирует первую зону PickupZone в конце каждого внешнего цикла, либо деактивирует любую последующую зону PickupZone. Это происходит потому, что выражение оценивается в самом конце, когда переменная PickupZone уже привязана к новой зоне. #> defer: PickupZone.DeactivateZone() PickupZone.ZoneCompletedEvent.Await() Logger.Print("Подобрано", ?Level := log_level.Normal) # После первого подбора можно активировать зону доставки. if (IsFirstPickup?): set IsFirstPickup = false FirstPickupZoneCompletedEvent.Signal() # Обновляем уровень подбора if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 Logger.Print("Уровень PickupLevel повышен до {PickupLevel}", ?Level := log_level.Normal) 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) else: Logger.Print("Следующая зона DeliveryZone для выбора не найдена.", ?Level := log_level.Error) return # Ошибка при выходе из PickupDeliveryLoop
- Сохраните файлы Verse, скомпилируйте код и протестируйте уровень в игре.
Во время тестирования уровня игра будет работать так же, как в предыдущем разделе. Однако теперь есть таймер, который завершит игру, когда закончится обратный отсчёт или возникнет проблема в игровом цикле.