При завершении этого шага в игре «Гонка на время: Доставка пиццы» вы узнаете, как управлять количеством заработанных очков, когда игрок подбирает предметы и доставляет их, а также как обновлять пользовательский интерфейс для отображения этих очков. Процесс создания внутриигрового пользовательского интерфейса при помощи Verse более подробно описан в разделе Создание внутриигрового интерфейса.
Панель управления счётом будет отслеживать и отображать следующее:
- Всего очков: отображает общее количество очков, которые игрок заработал в игре.
- Набрано очков: отображает очки, которые игрок набрал за текущую серию подборов.
- Уровень подбора: отображает текущий уровень подбора.

Создание интерфейса
Ниже пошагово описан процесс создания интерфейса для панели управления счётом в Verse:
- Создайте пустой файл Verse и назовите его score_manager.verse.
- Создайте новый класс
score_manager
и добавьте к нему следующие поля:- Опциональную переменную типа
agent
с именемMaybePlayer
для хранения ссылки на игрока.MaybePlayer<internal> : ?agent = false
- Опциональную переменную
player_ui
с именемMaybePlayerUI
для хранения ссылки на интерфейс игрока.MaybePlayerUI<internal> : ?player_ui = false
- Класс
score_manager_device
для хранения ссылки на устройство «Панель управления счётом», вокруг которого и строится этот класс. (Обратите внимание: это не редактируемая ссылка, потому что она должна быть связана с Verse-устройствомgame_coordinator_device
.ScoreManagerDevice<internal> : score_manager_device = score_manager_device{}
- Целочисленную переменную
TotalGameScore
, в которой будут храниться все очки, которые заработал игрок в игре за всё время.var TotalGameScore<private> : int = 0
- Целочисленную переменную
PendingScore
, в которой будет храниться количество очков, которое игрок получил за текущую серию подборов.var PendingScore<private> : int = 0
- Целочисленную переменную
PickupLevel
, в которой будет храниться текущий уровень подбора.var PickupLevel<private> : int = 0
- Опциональную переменную типа
- Ваш класс score_manager должен выглядеть следующим образом:
score_manager := class: MaybePlayer<internal> : ?agent = false MaybePlayerUI<internal> : ?player_ui = false ScoreManagerDevice<internal> : score_manager_device = score_manager_device{} var TotalGameScore<private> : int = 0 var PendingScore<private> : int = 0 var PickupLevel<private> : int = 0
- Создайте интерфейс при первом создании класса. Это можно сделать, добавив в определение класса выражение с
block
, которое будет выполняться всякий раз, когда вы создаёте экземпляр класса. Добавьте следующие переменные для генерации интерфейса:- Переменную типа
canvas
с именемCanvas
с внутренним спецификаторомinternal
для хранения пользовательского виджета рабочей области.var Canvas<internal> : canvas = canvas{}
- Класс
text_block
с именемTotalGameScoreWidget
и спецификаторомinternal
для хранения текстового виджета для отображения всех очков, набранных игроком в игре в целом (представлено переменнойTotalGameScore
). Для текстового блока установите белый цвет текста по умолчанию.TotalGameScoreWidget<internal> : text_block = text_block{DefaultTextColor := NamedColors.White}
- Класс
PendingScoreWidget
типаtext_block
со спецификаторомinternal
для хранения текстового виджета для отображения очков, которые игрок набрал за текущую серию подборов (представлено переменнойPendingScore
). Для текстового блока установите белый цвет текста по умолчанию.PendingScoreWidget<internal> : text_block = text_block{}
- Класс
PickupLevelWidget
типаtext_block
со спецификаторомinternal
для хранения текстового виджета для отображения текущего уровня подбора (представлено переменнойPickupLevel
). Для текстового блока установите белый цвет текста по умолчанию.PickupLevelWidget<internal> : text_block = text_block{}
- Функцию, возвращающую объект
TotalGameScoreText
классаmessage
и создающую локализуемый текст с общим количеством очков, набранных игроком в игре, который может быть отображён в интерфейсе.TotalGameScoreText<localizes>(CurrentTotalGameScore : int) : message = "Всего очков: {CurrentTotalGameScore}"
- Функцию, возвращающую объект
PendingScoreText
классаmessage
и создающую локализуемый текст с количеством очков, набранных игроком за текущую серию подборов, который может быть отображён в интерфейсе.PendingScoreText<localizes>(CurrentPendingScore : int) : message = "Набрано очков: {CurrentPendingScore}"
- Функцию, возвращающую объект
PickupLevelText
классаmessage
и создающую локализуемый текст, который может быть отображён в интерфейсе для текущего уровня подбора.PickupLevelText<localizes>(CurrentPickupLevel : int) : message = "Уровень подбора: {CurrentPickupLevel}"
- Добавьте выражение
block
, которое создаст виджет рабочей области и расположит текст вертикально в левой части экрана.<# Поскольку рабочая область не будет создаваться повторно во время существования панели управления счётом, добавляйте это выражение один раз при каждом создании объекта этого типа. #> block: set Canvas = canvas: Slots := array: canvas_slot: Anchors := anchors{Minimum := vector2{X := 0.0, Y := 0.25}, Maximum := vector2{X := 0.0, Y := 0.25} } Offsets := margin{Top := 0.0, Left := 25.0, Right := 0.0, Bottom := 0.0} Alignment := vector2{X := 0.0, Y := 0.0} SizeToContent := true Widget := stack_box: Orientation := orientation.Vertical Slots := array: stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := TotalGameScoreWidget stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := PendingScoreWidget stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := PickupLevelWidget
- Переменную типа
- Класс
score_manager
должен выглядеть следующим образом:using { /UnrealEngine.com/Temporary/UI } using { /Fortnite.com/UI } using { /Verse.org/Colors } score_manager := class: var Canvas<internal> : canvas = canvas{} TotalGameScoreWidget<internal> : text_block = text_block{DefaultTextColor := NamedColors.White} PendingScoreWidget<internal> : text_block = text_block{DefaultTextColor := NamedColors.White} PickupLevelWidget<internal> : text_block = text_block{DefaultTextColor := NamedColors.White} MaybePlayer<internal> : ?agent = false MaybePlayerUI<internal> : ?player_ui = false ScoreManagerDevice<internal> : score_manager_device = score_manager_device{} 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 <# Поскольку рабочая область не будет создаваться повторно во время существования панели управления счётом, добавляйте это выражение один раз при каждом создании объекта этого типа. #> block: set Canvas = canvas: Slots := array: canvas_slot: Anchors := anchors{Minimum := vector2{X := 0.0, Y := 0.25}, Maximum := vector2{X := 0.0, Y := 0.25} } Offsets := margin{Top := 0.0, Left := 25.0, Right := 0.0, Bottom := 0.0} Alignment := vector2{X := 0.0, Y := 0.0} SizeToContent := true Widget := stack_box: Orientation := orientation.Vertical Slots := array: stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := TotalGameScoreWidget stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := PendingScoreWidget stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := PickupLevelWidget
- Создайте функцию
UpdateUI()
со спецификаторомprivate
, которая будет подставлять в текст интерфейса актуальное количество очков и текущий уровень подбора.UpdateUI<private>() : void = if (PlayerUI := MaybePlayerUI?): PickupLevelWidget.SetText(PickupLevelText(PickupLevel)) PendingScoreWidget.SetText(PendingScoreText(PendingScore)) PendingScoreWidget.SetText(TotalGameScoreText(TotalGameScore))
- Создайте функцию
AddScoreManagerToUI()
, которая добавит интерфейс панели управления счётом к интерфейсу игрока.AddScoreManagerToUI<public>() : void = if (PlayerUI := MaybePlayerUI?): PlayerUI.AddWidget(Canvas) UpdateUI()
- Создайте функцию для каждого значения, отображаемого в интерфейсе, чтобы игровой цикл мог обновлять значения:
- Функцию
AddPendingScoreToTotalScore()
со спецификаторомpublic
. Эта функция должна добавлять набранные очки за текущую сессию к общему счёту игры и сбрасывать значение набранных очков до0
. С помощьюdefer
можно отложить сброс переменнойPendingScore
вместе с обновлением интерфейса и выполнить это уже после обновления переменнойTotalGameScore
. Это позволит избежать использования временной переменной для хранения значенияPendingScore
до его сброса.<# Прибавляет значение PendingScore к TotalGameScore и присваивает PendingScore значение 0.#> AddPendingScoreToTotalScore<public>() : void = defer: set PendingScore = 0 UpdateUI() set TotalGameScore += PendingScore
- Функцию
UpdatePendingScore()
со спецификатромpublic
и целочисленным параметромPoints
, который функция будет добавлять к текущему количеству очков, набранных за сессию.<# Добавляет заданное количество очков к очкам, набранным за сессию. #> UpdatePendingScore<public>(Points : int) : void = set PendingScore += Points UpdateUI()
- Функцию
UpdatePickupLevel
со спецификатромpublic
и целочисленным параметромLevel
, который будет определять текущий уровень подбора.UpdatePickupLevel<public>(Level : int) : void = set PickupLevel = Level UpdateUI()
- Создайте функцию
AwardScore()
со спецификаторомpublic
. Эта функция будет начислять очки игроку при помощи устройства «Панель управления счётом» и активировать устройство.<# Начисляет очки игроку при помощи устройства «Панель управления счётом», активируя его. #> AwardScore<public>() : void = ScoreManagerDevice.SetScoreAward(TotalGameScore) if (AwardedPlayer := MaybePlayer?): ScoreManagerDevice.Activate(AwardedPlayer)
- Функцию
- Класс
score_manager
должен выглядеть следующим образом:score_manager := class: <# Поскольку рабочая область не будет создаваться повторно во время существования панели управления счётом, добавляйте это выражение один раз при каждом создании объекта этого типа. #> block: set Canvas = canvas: Slots := array: canvas_slot: Anchors := anchors{Minimum := vector2{X := 0.0, Y := 0.25}, Maximum := vector2{X := 0.0, Y := 0.25} } Offsets := margin{Top := 0.0, Left := 25.0, Right := 0.0, Bottom := 0.0} Alignment := vector2{X := 0.0, Y := 0.0} SizeToContent := true Widget := stack_box: Orientation := orientation.Vertical Slots := array: stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := TotalGameScoreWidget stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := PendingScoreWidget stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := PickupLevelWidget AddScoreManagerToUI<public>() : void = if (PlayerUI := MaybePlayerUI?): PlayerUI.AddWidget(Canvas) UpdateUI() <# Прибавляет PendingPickupPoints к TotalPickupPoints и присваивает PendingPickupPoints значение 0. Возвращает общее число добавленных очков подбора. #> AddPendingScoreToTotalScore<public>() : void = set TotalGameScore += PendingScore defer: set PendingScore = 0 UpdateUI() <# Добавляет заданное количество очков к очкам, набранным за сессию. #> UpdatePendingScore<public>(Points : int) : void = set PendingScore += Points UpdateUI() UpdatePickupLevel<public>(Level : int) : void= set PickupLevel = Level UpdateUI() <# Начисляет очки игроку при помощи устройства «Панель управления счётом», активируя его. #> AwardScore<public>() : void = ScoreManagerDevice.SetScoreAward(TotalGameScore) if (AwardedPlayer := MaybePlayer?): ScoreManagerDevice.Activate(AwardedPlayer) MaybePlayer<internal> : ?agent = 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{DefaultTextColor := NamedColors.White} PendingScoreWidget<internal> : text_block = text_block{DefaultTextColor := NamedColors.White} PickupLevelWidget<internal> : text_block = text_block{DefaultTextColor := NamedColors.White} 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)) PendingScoreWidget.SetText(TotalGameScoreText(TotalGameScore))
- Класс
score_manager
успешно создан, и теперь можно заняться конструктором для класса, чтобы инициализировать переменные игрока из игры. Обратите внимание: чтобы получить ссылку на интерфейс игрока, вам будет необходимо выполнить приведение типов для ссылки на игрока с типаagent
к типуplayer
.MakeScoreManager<constructor><public>(InPlayer : agent, InScoreManagerDevice : score_manager_device) := score_manager: MaybePlayer := option{InPlayer} MaybePlayerUI := option{GetPlayerUI[player[InPlayer]]}
- Ваш файл score_manager.verse теперь должен выглядеть следующим образом:
using { /UnrealEngine.com/Temporary/SpatialMath } using { /UnrealEngine.com/Temporary/UI } using { /Fortnite.com/Devices } using { /Fortnite.com/UI } using { /Verse.org/Colors } using { /Verse.org/Simulation } MakeScoreManager<constructor><public>(InPlayer : agent, InScoreManagerDevice : score_manager_device) := score_manager: MaybePlayer := option{InPlayer} MaybePlayerUI := option{GetPlayerUI[player[InPlayer]]} score_manager := class: <# Поскольку рабочая область не будет создаваться повторно во время существования панели управления счётом, добавляйте это выражение один раз при каждом создании объекта этого типа. #> block: set Canvas = canvas: Slots := array: canvas_slot: Anchors := anchors{Minimum := vector2{X := 0.0, Y := 0.25}, Maximum := vector2{X := 0.0, Y := 0.25} } Offsets := margin{Top := 0.0, Left := 25.0, Right := 0.0, Bottom := 0.0} Alignment := vector2{X := 0.0, Y := 0.0} SizeToContent := true Widget := stack_box: Orientation := orientation.Vertical Slots := array: stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := TotalGameScoreWidget stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := PendingScoreWidget stack_box_slot: HorizontalAlignment := horizontal_alignment.Left Widget := PickupLevelWidget AddScoreManagerToUI<public>() : void = if (PlayerUI := MaybePlayerUI?): PlayerUI.AddWidget(Canvas) UpdateUI() <# Прибавляет PendingPickupPoints к TotalPickupPoints и присваивает PendingPickupPoints значение 0. Возвращает общее число добавленных очков подбора. #> AddPendingScoreToTotalScore<public>() : int = set TotalGameScore += PendingScore defer: set PendingScore = 0 UpdateUI() return PendingScore <# Добавляет заданное количество очков к очкам, набранным за сессию. #> UpdatePendingScore<public>(Points : int) : void = set PendingScore += Points UpdateUI() UpdatePickupLevel<public>(Level : int) : void= set PickupLevel = Level UpdateUI() <# Начисляет очки игроку при помощи устройства «Панель управления счётом», активируя его. #> AwardScore<public>() : void = ScoreManagerDevice.SetScoreAward(TotalGameScore) if (AwardedPlayer := MaybePlayer?): ScoreManagerDevice.Activate(AwardedPlayer) MaybePlayer<internal> : ?agent = 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{DefaultTextColor := NamedColors.White} PendingScoreWidget<internal> : text_block = text_block{DefaultTextColor := NamedColors.White} PickupLevelWidget<internal> : text_block = text_block{DefaultTextColor := NamedColors.White} PickupLevelText<localizes><internal>(CurrentPickupLevel : int) : message = "Уровень подбора: {CurrentPickupLevel}" PendingScoreText<localizes><internal>(CurrentPendingScore : int) : message = "Набрано очков: {CurrentPendingScore}" TotalGameScoreText<localizes><internal>(CurrentTotalGameScore : int) : message = "Всего очков: {CurrentTotalGameScore}" 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)) PendingScoreWidget.SetText(TotalGameScoreText(TotalGameScore))
Обновление счёта и интерфейса в игровом цикле
Внесите следующие изменения в файл game_coordinator_device.verse, чтобы создать и обновлять интерфейс во время игры:
- Добавьте классу
game_coordinator_device
следующие атрибуты:- Переменную
ScoreManager
типаscore_manager
со спецификаторомprivate
. Этот экземпляр будет управлять интерфейсом и счётом игрока.var ScoreManager<private> : score_manager = score_manager{}
- Редактируемый объект
score_manager_device
, который вы можете задать для устройства «Панель управления счётом» на уровне. Это устройство, которое будет использоваться классомscore_manager
.@editable ScoreManagerDevice<public> : score_manager_device = score_manager_device{}
- Редактируемый массив целочисленных значений
PointsForPickupLevel
со спецификаторомpublic
для определения очков, которые игрок может набрать за каждый уровень подбора.@editable # Показывает, сколько очков даёт подбор в зависимости от его уровня. PointsForPickupLevel<public> : []int = array{1, 2, 3}
- Переменную
-
В функции
StartGame
инициализируйте переменную панели управления счётом, вызвав конструкторMakeScoreManager()
со ссылкой на игрока и устройство «Панель управления счётом», а также создайте пользовательский интерфейс для игрока.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)
- В функции игрового цикла
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 <# Каждый раз, когда цикл начинается заново, необходимо сбрасывать элемент интерфейса, соответствующий уровню подбора, через ScoreManager. Уровень подбора в интерфейсе начинается с 1 (не с 0). Если он будет начинаться с 0, то у некоторых игроков это может вызвать недоумение. Нужно начать индексирование с 0, поэтому PickupLevel=0 будет соответствовать уровню 1 в интерфейсе. #> ScoreManager.UpdatePickupLevel(PickupLevel + 1) race: loop: if (PickupZone:base_zone = PickupZones[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 (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) ScoreManager.AddPendingScoreToTotalScore() else: Logger.Print("Следующая зона DeliveryZone для выбора не найдена.", ?Level := log_level.Error) return # Ошибка при выходе из PickupDeliveryLoop
- Когда обратный отсчёт заканчивается, игроку следует присвоить итоговый счёт. Через метод
HandleCountdownEnd()
вызовитеScoreManager.AwardScore()
.HandleCountdownEnd<private>(InPlayer : player)<suspends>:void= TotalTime := CountdownTimer.CountdownEndedEvent.Await() ScoreManager.AwardScore() EndGame.Activate(InPlayer)
- Теперь файл 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 { /EpicGames.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{} @editable ScoreManagerDevice<public> : score_manager_device = score_manager_device{} @editable # Показывает, сколько очков даёт подбор в зависимости от его уровня. PointsForPickupLevel<public> : []int = array{1, 2, 3} OnBegin<override>()<suspends> : void = FindPlayer() SetupZones() StartGame() Logger<private> : log = log{Channel := log_pizza_pursuit} var MaybePlayer<private> : ?agent = false var CountdownTimer<private> : countdown_timer = countdown_timer{} var ScoreManager<private> : score_manager = score_manager{} DeliveryZoneSelector<private> : tagged_zone_selector = tagged_zone_selector{} var PickupZoneSelectors<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 PickupZoneSelectors = for (ZoneTag : LevelTags): NewZone := tagged_zone_selector{} NewZone.InitZones(ZoneTag) NewZone 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 : agent)<suspends> : void = 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(){} 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 = 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 (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) ScoreManager.AddPendingScoreToTotalScore() else: Logger.Print("Следующая зона DeliveryZone для выбора не найдена.", ?Level := log_level.Error) return # Ошибка при выходе из PickupDeliveryLoop