Игровой цикл — это код, который выполняется многократно (циклически), чтобы реагировать на ввод данных (взаимодействие игрока с помощью контроллера или мыши), обновлять состояние игры, а также выполнять действия, когда игрок повлиял на состояние игры, например нажал кнопку и включил свет. Цикл обычно заканчивается, когда игра завершается успешно, например игрок достигает цели, или неуспешно, например истекает время, отведённое на достижение цели.
В этом разделе урока по созданию игры Гонка на время: доставка пиццы вы узнаете, как создать игровой цикл и определить состояние успешного или неуспешного завершения игры.
Ниже приведён псевдокод цикла игры «Гонка на время: Доставка пиццы»:
loop:
race:
loop:
SelectNextPickupZone
WaitForPlayerToCompletePickupZone
block:
WaitForFirstPickup
SelectNextDeliveryZone
WaitForPlayerToCompleteDeliveryZone
Этот цикл должен завершиться, когда таймер обратного отсчёта закончит отсчёт или если в игре возникнет непредвиденная ошибка.
Создание основного игрового цикла
Внесите следующие изменения в файл game_coordinator_device.verse:
Создайте метод с именем
PickupDeliveryLoop()и спецификаторамиprivateиsuspends. Переместите цикл, который вы до этого создали вOnBegin(), в этот новый метод.VerseOnBegin<override>;()<suspends> : void = SetupZones() PickupDeliveryLoop() PickupDeliveryLoop<private>()<suspends> : void = var PickupLevel : int = 0 loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]):Определите максимальное количество уровней подбора по длине массива тегов и увеличивайте уровень
PickupLevelкаждый раз, когда игрок завершает зону подбора, пока уровень подбора не превысит максимальное количество уровней подбора.VerseOnBegin<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Зона доставки должна активироваться после того, как игрок совершит первый подбор, при этом игрок должен иметь возможность при желании подобрать предметы перед тем, как отправиться в зону доставки. Для этого код зоны подбора и зоны доставки должен выполняться одновременно. В этом примере используется выражение одновременного выполнения race, так как:
Блок доставки должен отменять цикл зоны подбора, когда игрок завершает доставку.
Цикл зоны подбора должен отменять блок доставки при возникновении проблемы с циклом подбора.
Вам также потребуется немного изменить процесс деактивации зоны. Метод
DeactivateZone()не должен вызываться при отмене цикла или блока доставки, если сценарий ожидал завершения зоны.Так как код деактивации зоны выполняться не будет, сама зона останется активной, что повлечёт за собой программную ошибку.
Это можно исправить выражением defer.
deferоткладывает выполнение содержащихся в нём выражений до завершения области видимости, в которой встречаетсяdefer. Блокdeferзапускается, когда управление программой передаётся за пределы области видимости. Это может быть в результате стандартного выхода из области видимости (завершение работы функции), досрочного выхода (например использования операторов return или break) либо отмены любой параллельной задачи или асинхронного выражения (например,race). Это похоже на постановку в очередь операций, которые будут выполнены в самом конце независимо от того, что произойдёт. Поместите каждый вызов методаDeactivateZoneв блокdeferи вызывайте его перед соответствующим вызовомZoneCompletedEvent.Await().VersePickupDeliveryLoop<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()В предыдущем примере зона доставки активируется одновременно с зоной подбора, однако активация зоны доставки должна происходить только после выполнения первого подбора. Для этого добавьте событие и сделайте так, чтобы зона доставки ожидала это событие, прежде чем активироваться.
VerseOnBegin<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(){}Создайте цикл, в котором выражение `race` для зоны подбора/доставки выполняется до завершения игры, чтобы игрок мог продолжать подбирать и доставлять предметы.
VerseOnBegin<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(){}Ваш файл game_coordinator_device.verse сейчас должен выглядеть следующим образом:
Verseusing { /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 }
Сохраните файлы 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{}Versegame_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()перед настройкой зон.Versegame_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 = # Since this is a single player experience, the first player (at index 0) # should be the only one available. if (FirstPlayer := GetPlayspace().GetPlayers()[0]): set MaybePlayer = option{FirstPlayer} Logger.Print("Player found") else: # Log an error if we can't find a player. # This shouldn't happen because at least one player is always present. Logger.Print("Can't find valid player", ?Level := log_level.Error)Создайте функцию под названием
HandleCountdownEnd(), которая будет ожидать завершения отсчёта таймера и активировать устройство завершения игры.VerseHandleCountdownEnd<private>(InPlayer : agent)<suspends> : void = CountdownTimer.CountdownEndedEvent.Await() EndGame.Activate(InPlayer)Создайте функцию
StartGame()и вызовите её послеSetupZones()вOnBegin(). Эта функция должна выполнять следующее:Инициализировать таймер.
Versegame_coordinator_device<public> := class(creative_device): # How long the countdown timer will start counting down from. @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("Trying to start the game...") <# We construct a new countdown_timer that'll countdown from InitialCountdownTime once started. The countdown_timer requires a player to show their UI to. We should have a valid player by now. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Valid player, starting game...") set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer) CountdownTimer.StartCountdown() else: Logger.Print("Can't find valid player. Aborting game start", ?Level := log_level.Error)Используйте выражение
raceкак для вызоваHandleCountdownEnd(ValidPlayer), так и для вызоваPickupDeliveryLoop(), чтобы добиться следующего:Игровой цикл останавливается при завершении отсчёта таймера или
Отсчёт времени прекращается при остановке игрового цикла.
VerseStartGame<private>()<suspends> : void = Logger.Print("Trying to start the game...") <# We construct a new countdown_timer that'll countdown from InitialCountdownTime once started. The countdown_timer requires a player to show their UI to. We should have a valid player by now. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Valid player, starting game...") set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer) CountdownTimer.StartCountdown() # We wait for the countdown to end. # At the same time, we also run the Pickup and Delivery game loop that constitutes the core gameplay. race: HandleCountdownEnd(ValidPlayer) PickupDeliveryLoop() else: Logger.Print("Can't find valid player. Aborting game start", ?Level := log_level.Error)
Ваш файл game_coordinate_device.verse сейчас должен выглядеть следующим образом:
Verseusing { /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 }Сохраните файлы Verse, скомпилируйте код и протестируйте уровень в игре.
Во время тестирования уровня игра будет работать так же, как в предыдущем разделе. Однако теперь есть таймер, который завершит игру, когда закончится обратный отсчёт или возникнет проблема в игровом цикле.