В этом разделе приведён готовый код для добавления в созданные файлы Verse.
Закончить код
Этот проект содержит несколько файлов Verse.
-
heartbeat.verse: полный код файла представлен ниже.
-
base_team.verse: полный код файла представлен ниже.
-
hunter_team.verse: полный код файла представлен ниже.
-
prop_team.verse: полный код файла представлен ниже.
-
round_timer.verse: полный код файла представлен ниже.
-
waiting_for_more_players.verse: полный код файла представлен ниже.
heartbeat.verse
using { /Fortnite.com/Characters }
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/UI }
using { /Verse.org/Colors }
using { /Verse.org/Simulation }
log_heart_beat := class(log_channel){}
# Эти сообщения используются для отображения (или скрытия) уведомления агенту прячущейся команды, когда ему необходимо начать двигаться, чтобы не допустить появления привязанного к нему эффекта сердцебиения.
HeartBeatWarningMessage<localizes>(Time:int):message = "Сердцебиение через {Time} сек. Двигайтесь!"
HeartBeatWarningClear<localizes>:message = ""
# Этот класс предоставил редактируемые свойства эффекта сердцебиения устройству prop_hunt.
heart_beat := class<concrete>():
Logger:log = log{Channel:=log_heart_beat}
@editable # Время в секундах, до истечения которого агент прячущейся команды должен сдвинуться с места, чтобы сердцебиение не раскрыло его местоположение.
MoveTime:float = 15.0
@editable # Время в секундах, остающееся до момента, когда появится предупреждение о сердцебиении. Эта величина не должна превышать величину HeartBeatTimer.
WarningTime:float = 5.0
@editable # Массив устройств ВЭ «сердцебиение». По одному устройству на каждого из игроков.
AgentVFX:[]heartbeat_vfx = array{}
@editable # Устройство «Аудиопроигрыватель», используемое для воспроизведения звуковых эффектов сердцебиения (спецэффектов).
SFXPlayer:radio_device = radio_device{}
# Этот ассоциативный массив связывает интерфейс, с помощью которого отображается предупреждение о сердцебиении, с каждым агентом прячущейся команды.
var WarningUI:[agent]heartbeat_warning_ui = map{}
# Отслеживает количество игроков с активным сигналом сердцебиения, чтобы мы могли управлять устройством спецэффектов.
var NumberOfHeartBeats:int = 0
# Настраивает для агента интерфейс, отображающий сердцебиение.
SetUpUI(PropAgent:agent):void =
if:
not WarningUI[PropAgent]
AsPlayer := player[PropAgent]
PlayerUI := GetPlayerUI[AsPlayer]
then:
UIData:heartbeat_warning_ui = heartbeat_warning_ui{}
UIData.CreateCanvas()
PlayerUI.AddWidget(UIData.Canvas, player_ui_slot{ZOrder := 1})
if (set WarningUI[PropAgent] = UIData) {}
# Активирует визуальный эффект «сердцебиение» и спецэффект для указанного игрока.
Enable(PropAgent:agent, HeartBeatVFXData:heartbeat_vfx):void =
if:
# Получаем персонажа, используемого для поиска местонахождения агента прячущейся команды в сцене.
Character := PropAgent.GetFortCharacter[]
then:
# Устанавливаем появление визуального эффекта «сердцебиение» в месте нахождения агента прячущейся команды.
HeartBeatVFXData.Activate(Character.GetTransform())
# Увеличиваем количество сигналов сердцебиения; если сердцебиение воспроизводится первый раз, для его запуска нужно воспроизвести аудиофайл.
set NumberOfHeartBeats += 1
if (NumberOfHeartBeats = 1) then SFXPlayer.Play()
# Регистрируем агента прячущейся команды в устройстве «Аудиопроигрыватель», чтобы в месте его нахождения воспроизводился звук сердцебиения.
SFXPlayer.Register(PropAgent)
else:
Logger.Print("Персонаж, индекс или HeartBeatVFXData не обнаружены. Невозможно воспроизвести сердцебиение")
# Убирает визуальный эффект «сердцебиение» и спецэффект для указанного агента прячущейся команды.
Disable(PropAgent:agent, HeartBeatVFXData:heartbeat_vfx):void =
Logger.Print("Отключение сердцебиения.")
# Отключаем визуальные эффекты.
HeartBeatVFXData.Deactivate()
# Отменяем регистрацию агента прячущейся команды в устройстве «Аудиопроигрыватель», чтобы звук сердцебиения прекратил воспроизводиться.
SFXPlayer.Unregister(PropAgent)
# Уменьшаем количество сигналов сердцебиения. Значение этого счётчика не должно быть меньше 0.
set NumberOfHeartBeats -= 1
if (NumberOfHeartBeats < 0) then set NumberOfHeartBeats = 0
# Убирает все визуальные эффекты «сердцебиение» и спецэффекты, использовавшиеся для указанных агентов прячущейся команды.
DisableAll():void =
Logger.Print("Отключение всех сигналов сердцебиения.")
# Проходим по всем визуальным эффектам и меняем их значения на 0,0,0.
for (HeartBeatVFXDevice : AgentVFX):
HeartBeatVFXDevice.Deactivate()
# Отменяем регистрацию всех игроков в устройстве воспроизведения сердцебиения.
SFXPlayer.UnregisterAll()
# Сбрасываем счётчик сигналов сердцебиения до 0
set NumberOfHeartBeats = 0
# Класс heartbeat_warning_ui содержит структуру данных для отслеживания рабочей области интерфейса и text_block для каждого игрока, а также функцию создания новой рабочей области интерфейса для предупреждений о сердцебиении.
heartbeat_warning_ui := class:
var Canvas:canvas = canvas{}
var Text:text_block = text_block{}
# Создаёт рабочую область интерфейса для предупреждений.
CreateCanvas():void =
set Text = text_block{DefaultTextColor := NamedColors.White, DefaultShadowColor := NamedColors.Black}
set Canvas = canvas:
Slots := array:
canvas_slot:
Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.75}, Maximum := vector2{X := 0.5, Y := 0.75}}
Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
Alignment := vector2{X := 0.5, Y := 1.0}
SizeToContent := true
Widget := Text
# Класс heartbeat_vfx содержит структуру данных для отслеживания корня ВЭ и объектов vfx_spawner_device для каждого игрока, а также функции для установки ВЭ в нужное положение или его сброса.
heartbeat_vfx := class<concrete>:
@editable # Генератор ВЭ для каждого сигнала «сердцебиение».
VFXDevice:vfx_spawner_device = vfx_spawner_device{}
# Данное смещение используется для размещения сигнала сердцебиения над головой агента прячущейся команды.
HeartBeatVFXOffset:vector3 = vector3{X := 0.0, Y := 0.0, Z := 110.0}
# Задаёт положение визуального эффекта «сердцебиение», а затем включает его.
Activate(Transform:transform):void =
VFXPosition := Transform.Translation + HeartBeatVFXOffset
if (VFXDevice.TeleportTo[VFXPosition, Transform.Rotation]):
VFXDevice.Enable()
# Отключает визуальный эффект, и сердцебиение больше не отображается.
Deactivate():void =
VFXDevice.Disable()
base_team.verse
using { /Fortnite.com/Characters }
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/UI }
using { /Verse.org/Colors }
using { /Verse.org/Simulation }
log_team := class(log_channel){}
# Этот класс определяет устройства, необходимые для разных команд в этом режиме.
# Этот класс является абстрактным, поэтому сам по себе не используется. Он должен наследоваться другим классом.
base_team := class<abstract>:
Logger:log = log{Channel:=log_team}
@editable # Используется для настройки игрока для команды.
ClassSelector:class_and_team_selector_device = class_and_team_selector_device{}
@editable # Используется для присвоения очков агентам команды.
ScoreManager:score_manager_device = score_manager_device{}
@editable # Используется для отображения заголовка задания команды.
TeamTitle:hud_message_device = hud_message_device{}
@editable # Используется для отображения описания задания команды.
TeamDescription:hud_message_device = hud_message_device{}
@editable # Используется для подписки на события устранения члена (прячущейся) команды или команды врага (охотников).
TeamManager:team_settings_and_inventory_device = team_settings_and_inventory_device{}
# Это массив агентов команды.
var TeamAgents<private>:[]agent = array{}
# Сигнал о таком событии подаётся в момент, когда массив TeamAgents становится пустым (сигнал об окончании раунда).
TeamEmptyEvent:event() = event(){}
# Возвращает текущий массив TeamAgents.
# Нужен, потому что массив TeamAgents является закрытым, из-за чего другие классы не могут обратиться к нему напрямую.
GetAgents()<decides><transacts>:[]agent =
TeamAgents
# Вернуть размер массива TeamAgents
# Для этого требуется функция, поскольку массив TeamAgents является закрытым, из-за чего другие классы не могут обратиться к нему напрямую.
Count()<transacts>:int =
TeamAgents.Length
# Возвращает индекс агента в массиве TeamAgents; в противном случае операция завершается ошибкой.
FindOnTeam(Agent:agent)<decides><transacts>: int =
Index := TeamAgents.Find[Agent]
# Назначаем агента в команду и уведомляем игрока.
InitializeAgent(Agent:agent):void =
AddAgentToTeam(Agent)
ClassSelector.ChangeTeamAndClass(Agent)
DisplayTeamInformation(Agent)
# Добавляем агента в TeamAgents.
AddAgentToTeam(AgentToAdd:agent):void =
if (not FindOnTeam[AgentToAdd]):
Logger.Print("Добавление агента в команду.")
set TeamAgents += array{AgentToAdd}
# Активирует устройства отображения сообщений в HUD-интерфейсе, по которым игрок определяет, в какой он находится команде
DisplayTeamInformation(Agent:agent):void =
TeamTitle.Show(Agent)
TeamDescription.Show(Agent)
# Если агент покинет матч, удаляем его из массива TeamAgents и выполняем проверку на окончание раунда.
EliminateAgent(Agent:agent)<suspends>:void =
Sleep(0.0) # Выполняем задержку на 1 игровой такт для возможности возрождения игрока перед тем, как продолжить игру.
RemoveAgentFromTeam(Agent)
# Удаляем агента из TeamAgents.
# Если удалённый агент оказался последним, передаём сигнал о событии TeamEmptyEvent.
RemoveAgentFromTeam(AgentToRemove:agent):void =
set TeamAgents = TeamAgents.RemoveAllElements(AgentToRemove)
Logger.Print("Осталось агентов в команде: {Count()}.")
if (Count() < 1):
Logger.Print("Агентов в команде больше нет. Раунд завершён.")
TeamEmptyEvent.Signal()
hunter_team.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Simulation }
# Класс hunter_team, наследующийся от base_team, содержит определения устройств и функции, связанные с командой охотников и её агентами.
hunter_team := class<concrete>(base_team):
@editable # В каждом раунде создаётся по одному агенту охотников на каждые n игроков. Пример: HunterTeamPerNumberOfPlayers = 5.0 — 1 на 5 игроков. Если игроков = 6, создаётся 2 агента охотников.
HunterAgentPerNumberOfPlayers:float = 5.0 # Используется минимум значение 1.1 для возможности создания хотя бы одного агента прячущейся команды.
@editable # Количество секунд, остающееся до создания агентов охотников, которое даёт агентам прячущейся команды возможность заранее спрятаться.
SpawnDelay:float = 15.0
@editable # Максимальное количество базовых очков, получаемых агентом охотника за устранение агента прячущейся команды. Эти очки делятся на количество оставшихся агентов прячущейся команды.
MaxEliminationScore:int = 5000
@editable # Устройство «Таймер» позволяет прячущимся игрокам успеть спрятаться.
WaitTimer:timer_device = timer_device{}
# Назначаем этого агента агентом охотника.
InitializeAgent<override>(NewHunterAgent:agent):void =
Logger.Print("Настройка нового агента охотника.")
(super:)InitializeAgent(NewHunterAgent)
# Если агент охотника покинет матч, удаляем его из массива HunterAgents и выполняем проверку на выполнение условия окончание раунда.
# Обратите внимание: мы переопределяем эту функцию потому, что нам здесь не нужно передавать дополнительные данные, как в случае с прячущейся командой.
EliminateAgent<override>(HunterAgent:agent)<suspends>:void =
Logger.Print("Агент охотника устранён.")
(super:)EliminateAgent(HunterAgent)
prop_team.verse
using { /Fortnite.com/Characters }
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/UI }
using { /Verse.org/Colors }
using { /Verse.org/Simulation }
# Это сообщение используется для вывода информации о количестве оставшихся прячущихся игроков для всех игроков во время раунда.
PropAgentsRemainingMessage<localizes>(Count:int):message = "Осталось прячущихся игроков: {Count}"
# Класс prop_team, наследующийся от base_team, содержит определения устройств и функции, связанные с командой прячущихся и её агентами.
# Обратите внимание: в этом же классе реализовано поведение эффекта сердцебиения агента прячущейся команды.
prop_team := class<concrete>(base_team):
@editable # Количество очков, получаемых агентом прячущейся команды в секунду.
ScorePerSecond:int = 10
@editable # Минимальное расстояние, которое агент прячущейся команды должен пройти для сброса таймера сердцебиения.
MinimumMoveDistance:float = 100.0
@editable # Устройство «Таймер», используемое для начисления очков прячущемуся игроку.
ScoreTimer:timer_device = timer_device{}
@editable # Устройство «Управление заданиями», используемое для отображения количества оставшихся спрятавшихся игроков на экране.
PropsRemainingTracker:tracker_device = tracker_device{}
@editable # Получаем свойства из класса heart_beat.
HeartBeat:heart_beat = heart_beat{}
# Назначаем этого агента агентом прячущейся команды и связываем его с интерфейсом предупреждения о сердцебиении.
InitializeAgent<override>(NewPropAgent:agent):void =
Logger.Print("Настройка нового агента прячущейся команды.")
(super:)InitializeAgent(NewPropAgent)
# При завершении работы PropScoreTimer начисляем очки всем агентам прячущейся команды. PropInstigator требуется для подписки на событие, но он не используется.
OnPropsScore(PropInstigator:?agent):void =
if (PropAgents := GetAgents[]):
for (PropAgent : PropAgents):
ScoreManager.Activate(PropAgent)
# Если агента охотника устраняют или он покидает матч, удаляем его из массива PropAgents и выполняем проверку на выполнение условия окончание раунда.
# Обратите внимание, что переопределение здесь не выполняется, поскольку мы передаём всех игроков в функцию для обновления сообщения об оставшемся количестве прячущихся игроков.
EliminateAgent<override>(PropAgent:agent)<suspends>:void =
Logger.Print("Агент прячущейся команды устранён.")
(super:)EliminateAgent(PropAgent)
# Обновляем оставшееся количество прячущихся игроков.
UpdatePropsRemainingTracker()
# Обновляет значение, используемое в устройстве «Управление заданиями», которое показывает оставшееся количество прячущихся игроков.
UpdatePropsRemainingTracker():void =
PropsRemainingTracker.SetValue(Count())
# Если агент прячущейся команды перестанет двигаться, запускаем конкурентное выполнение, чтобы проконтролировать перемещение агента за пределы расстояния MinimumMoveDistance, завершение отсчёта таймера эффекта сердцебиения или устранение агента.
RunPropGameLoop(PropAgent:agent)<suspends>:void =
Logger.Print("Запуск игрового цикла агента прячущейся команды.")
# Выполняем цикл бесконечно с учётом поведения спрятавшегося игрока до тех пор, пока агент прячущейся команды не будет устранён или игрок не покинет сеанс.
race:
PropAgent.AwaitNoLongerAProp()
loop:
# Дожидаемся, пока агент прячущейся команды не переместится на расстояние меньше минимального, и переходим к дальнейшим действиям.
PropAgent.AwaitStopMoving(MinimumMoveDistance)
# Пока агент прячущейся команды не переместится на расстояние больше минимального, отсчитываем время до сердцебиения, а затем непрерывно воспроизводим эффект сердцебиения.
race:
PropAgent.AwaitStartMoving(MinimumMoveDistance)
block:
CountdownTimer(PropAgent)
PropAgent.StartHeartbeat()
Sleep(0.0) # Как только конкурентное выполнение завершится (агент прячущейся команды начнёт двигаться), вновь запустить цикл.
# Повторяем цикл до тех пор, пока агент прячущейся команды не будет удалён из массива PropAgents. Удаление происходит тогда, когда агент прячущейся команды устраняется и превращается в охотника, либо когда игрок покидает сеанс.
(PropAgent:agent).AwaitNoLongerAProp()<suspends>:void =
loop:
if (not FindOnTeam[PropAgent]):
Logger.Print("Отмена поведения агента прячущейся команды.")
break
Sleep(0.0) # Переходим к следующему игровому такту.
# Цикл повторяется до тех пор, пока агент не переместится на расстояние, которое меньше MinimumDistance.
(PropAgent:agent).AwaitStopMoving(MinimumDistance:float)<suspends>:void =
Logger.Print("Проверка, переместился ли агент на расстояние, которое меньше минимума.")
# Получаем исходное положение агента от его персонажа в сцене.
if (Tracked := PropAgent.GetFortCharacter[]):
var StartPosition:vector3 = Tracked.GetTransform().Translation
loop:
Sleep(0.0) # Получаем местоположение агента в следующем игровом такте.
NewPosition := Tracked.GetTransform().Translation
# Если расстояние до нового местоположения от начального местоположения меньше величины MinimumDistance, значит, агент не двигался, и мы прерываем цикл.
if (Distance(StartPosition, NewPosition) < MinimumDistance):
Logger.Print("Агент передвинулся на расстояние меньше минимального.")
break
# В противном случае мы сбрасываем StartPosition, чтобы обеспечить перемещение игрока с нового местоположения.
else:
set StartPosition = NewPosition
# Цикл повторяется до тех пор, пока агент не переместится на расстояние, превышающее MinimumDistance.
(PropAgent:agent).AwaitStartMoving(MinimumDistance:float)<suspends>:void =
Logger.Print("Проверка перемещения агента на расстояние больше минимального.")
# Получаем исходное положение агента от его персонажа в сцене.
if (Tracked := PropAgent.GetFortCharacter[]):
StartPosition:vector3 = Tracked.GetTransform().Translation
loop:
Sleep(0.0) # Получаем местоположение агента в следующем игровом такте.
NewPosition := Tracked.GetTransform().Translation
# Если расстояние до нового местоположения от начального местоположения превышает значение MinimumDistance или равно ему, значит, агент совершил перемещение и мы прерываем цикл.
if (Distance(StartPosition, NewPosition) >= MinimumDistance):
Logger.Print("Агент передвинулся на расстояние, которое больше минимального или равно ему.")
break
# Выполняет задержку до запуска HeartBeatWarningTime. Затем ведёт обратный отсчёт с помощью HeartBeatWarningTime и задаёт текст обратного отсчёта. При отложенном выполнении текст удаляется.
CountdownTimer(PropAgent:agent)<suspends>:void =
Logger.Print("Запуск обратного отсчёта до начала эффекта сердцебиения.")
if (UIData := HeartBeat.WarningUI[PropAgent]):
Sleep(HeartBeat.MoveTime - HeartBeat.WarningTime) # Бездействие в течение этого времени до появления предупреждения.
Logger.Print("Запуск предупреждения о сердцебиении.")
var WarningTimeRemaining:int = 0
if (set WarningTimeRemaining = Ceil[HeartBeat.WarningTime]) {}
# Отложенное выполнение возникает при завершении выполнения функции или её отмене, если, например, при конкурентном выполнении первой была выполнена другая функция.
# В этом случае текст предупреждения удаляется по завершении обратного отсчёта или когда агент прячущейся команды начинает двигаться до завершения обратного отсчёта.
defer:
UIData.Text.SetText(HeartBeatWarningClear)
# Задаём для текста предупреждения остающееся время, ожидаем секунду и затем уменьшаем оставшееся время. Если обратный отсчёт завершится, прерываем цикл.
loop:
Logger.Print("Сердцебиение через {WarningTimeRemaining} сек.")
UIData.Text.SetText(HeartBeatWarningMessage(WarningTimeRemaining))
Sleep(1.0)
set WarningTimeRemaining -= 1
if (WarningTimeRemaining <= 0):
break
else:
Logger.Print("Данные UIData не обнаружены.")
# Включает визуальный эффект и спецэффект сердцебиения. Ожидает бесконечно до тех пор, пока это действие не будет отложено, и затем отключает визуальный эффект и спецэффект сердцебиения.
(PropAgent:agent).StartHeartbeat()<suspends>:void =
Logger.Print("Появление сигнала сердцебиения.")
# Сохраняем данные эффекта сердцебиения для их последующей передачи в отложенном вызове после того, как PropAgent будет уничтожен или покинет игру.
var HeartBeatVFXData:heartbeat_vfx = heartbeat_vfx{}
if:
# Получаем индекс агента прячущейся команды в массиве PropAgents для того, чтобы потом обратиться к соответствующему актору ВЭ сердцебиения.
Index := FindOnTeam[PropAgent]
set HeartBeatVFXData = HeartBeat.AgentVFX[Index]
then:
HeartBeat.Enable(PropAgent, HeartBeatVFXData)
# Когда эта функция отменяется при движении с места устраняемого агента прячущейся команды, либо когда игрок покидает сеанс, отключаем сердцебиение.
defer:
HeartBeat.Disable(PropAgent, HeartBeatVFXData)
Sleep(Inf) # Не прекращаем задержку до тех пор, пока не завершится одно из выполняемых конкурентно выражений.
round_timer.verse
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/UI }
using { /Verse.org/Colors }
using { /Verse.org/Simulation }
log_round_timer_device := class(log_channel){}
# Целочисленное значение, которое допускает значения в указанных пределах. Этот тип необходим для player_ui_slot.ZOrder.
round_int_clamped := type{_X:int where 0 <= _X, _X <= 2147483647}
# Это сообщение используется для вывода времени до конца раунда.
TimeRemainingMessage<localizes>(Minutes:string, Seconds:string):message = "{Minutes}:{Seconds}"
<#
Этот класс содержит всю логику управления временем раунда и отображения времени на экране.
Это устройство можно использовать с round_settings_device для фактического завершения раунда.
Это устройство управляет временем без использования таймера.
Для использования этого класса:
1) добавьте файл в свой проект;
2) скомпилируйте код Verse из меню Verse на панели инструментов;
3) перетащите это устройство на свой остров из папки устройств «Контент»/«Творческий режим» своего острова в Каталоге ресурсов;
4) включите класс waiting_for_more_players в другом сценарии Verse, включая следующее:
@editable
RoundTimer:round_timer = round_timer{}
5) скомпилируйте код Verse из меню Verse на панели инструментов;
6) подключите устройство, созданное на шаге 3, к устройству Verse;
7) запустите таймер раунда при помощи следующего кода Verse:
RoundTimer.Start()
8) перезапустите и остановите таймер аналогичными функциями;
9) подождите, пока запустится таймер при помощи:
RoundTimer.AwaitStart()
10) подождите, пока таймер завершит работу при помощи:
RoundTimer.AwaitEnd()
Вызовите функцию EndRound в round_settings_device для фактического завершения раунда игры.
#>
round_timer := class(creative_device):
Logger:log = log{Channel:=log_prop_hunt_device}
@editable # Длительность раунда в минутах.
RoundTimeInMinutes:float = 5.0
@editable # Горизонтальное и вертикальное положение интерфейса таймера на экране. X 0–1 — это слева направо, а Y 0–1 — сверху вниз.
UIPosition:vector2 = vector2{X:= 0.98, Y:=0.13}
@editable # Горизонтальное и вертикальное положение интерфейса таймера на экране. X 0–1 — это слева направо, а Y 0–1 — сверху вниз.
UIAlignment:vector2 = vector2{X := 1.0, Y := 0.0}
@editable # Порядок размещения интерфейса по оси Z по отношению к другим элементам интерфейса.
UIZOrder:round_int_clamped = 4
# Срабатывает при запуске раунда.
RoundStarted:event() = event(){}
# Срабатывает, когда раунд почти завершился.
RoundEndedEvent:event() = event(){}
# Этот ассоциативный массив связывает текстовый блок для отображения времени с каждым игроком.
var TimeRemainingTextBlocks:[player]text_block = map{}
# Время, оставшееся до конца раунда (целое число).
var TimeRemainingInSeconds:int = 0
# Дожидается запуска таймера раунда.
AwaitStart()<suspends>:void =
RoundStarted.Await()
Logger.Print("Таймер раунд запущен.")
# Используется для запуска таймера раунда.
Start():void =
Logger.Print("Запускается таймер раунда.")
RoundStarted.Signal()
set TimeRemainingInSeconds = GetRoundTimeInSeconds()
spawn{ Running() }
# Перезапускает таймер с учётом RoundTime
Restart():void =
Logger.Print("Перезапуск таймера раунда.")
set TimeRemainingInSeconds = GetRoundTimeInSeconds()
# Выполняет логику таймера.
Running()<suspends>:void =
Logger.Print("Таймер раунда работает.")
loop:
UpdateTimeUI()
Sleep(1.0)
# Уменьшаем оставшееся время (TimeRemaining) на 1 секунду, а потом проверяем, истекло ли время. Если истекло, завершаем раунд.
set TimeRemainingInSeconds -= 1
if (TimeRemainingInSeconds < 0):
Stop()
break
# Останавливает таймер и завершает раунд.
Stop():void =
Logger.Print("Завершение работы таймера раунда.")
# Получаем игрока из числа оставшихся игроков в сцене для завершения раунда.
Players:[]player = GetPlayspace().GetPlayers()
if (Instigator := Players[0]):
RoundEndedEvent.Signal()
# Дожидается момента, когда таймер раунда почти завершил работу.
AwaitEnd()<suspends>:void =
RoundEndedEvent.Await()
Logger.Print("Время таймера раунда истекло.")
# Принимает значение времени в минутах и возвращает его в секундах.
GetRoundTimeInSeconds():int =
var InSeconds:int = 0
if (set InSeconds = Round[RoundTimeInMinutes * 60.0]) {}
InSeconds
# При завершении работы таймера обновляем оставшееся время и проверяем, истекло ли оно.
UpdateTimeUI():void =
# Задаём количество минут в TimeRemainingInSeconds/60 без остатка.
var Minutes:int = 0
if (set Minutes = Floor(TimeRemainingInSeconds / 60)) {}
# Устанавливаем в секундах остаток TimeRemainingInSeconds/60.
var Seconds:int = 0
if (set Seconds = Mod[TimeRemainingInSeconds, 60]) {}
# Преобразуем минуты и секунды в строки.
MinutesAsString := string("{Minutes}")
# Если количество секунд меньше 10, перед этим значением нам нужно добавить 0, чтобы оно отображалось как :0# вместо :#
SecondsAsString := if (Seconds < 10) then Join(array{string("{0}"),string("{Seconds}")},string()) else string("{Seconds}")
# Перебираем всех игроков и проверяем, есть ли у них блок TimeRemainingTextBlock; если нет, предоставляем его им. Затем обновляем текст.
Players:[]player = GetPlayspace().GetPlayers()
for (Player : Players):
var TextBlock:text_block = text_block{}
if (set TextBlock = TimeRemainingTextBlocks[Player]) {}
else:
set TextBlock = SetUpTimeRemainingUI(Player)
TextBlock.SetText(TimeRemainingMessage(MinutesAsString, SecondsAsString))
# Принимает игрока, затем добавляет рабочую область интерфейса времени раунда к его экрану, а также сохраняет его блок TimeRemainingTextBlock для последующего обновления.
SetUpTimeRemainingUI(Player:player):text_block =
Logger.Print("Добавляем интерфейс таймера раунда игроку.")
# Это блок text_block, который выводит текст с оставшимся временем на экран.
TextBlock:text_block = text_block:
DefaultTextColor := NamedColors.White
DefaultShadowColor := NamedColors.Black
if (PlayerUI := GetPlayerUI[Player]):
if (set TimeRemainingTextBlocks[Player] = TextBlock) {}
# Это рабочая область, которая содержит блок text_block и размещает его на экране.
Canvas := canvas:
Slots := array:
canvas_slot:
Anchors := anchors{Minimum := UIPosition, Maximum := UIPosition}
Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
Alignment := UIAlignment
SizeToContent := true
Widget := TextBlock
# Рабочая область назначена игроку.
PlayerUI.AddWidget(Canvas, player_ui_slot{ZOrder := UIZOrder})
# Блок text_block возвращается для возможности его сохранения в ассоциативный массив и последующего обновления по мере обратного отсчёта.
return TextBlock
waiting_for_more_players.verse
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/UI }
using { /Verse.org/Colors }
using { /Verse.org/Simulation }
log_waiting_for_more_players_device := class(log_channel){}
# Целочисленное значение, которое допускает значения в указанных пределах. Этот тип необходим для player_ui_slot.ZOrder.
waiting_int_clamped := type{_X:int where 0 <= _X, _X <= 2147483647}
# Это сообщение используется для вывода количества игроков, требуемого до начала раунда.
WaitingForMorePlayersMessage<localizes>(Count:int):message = "Ожидание. Требуется ещё игроков: {Count}"
# Этот класс используется для отображения количества игроков, необходимых для запуска раунда.
waiting_for_more_players_ui := class:
var Canvas:canvas
var TextBlock:text_block
<#
Этот класс содержит всю логику настройки минимального количества игроков и проверки, достаточно ли их для запуска раунда.
Для использования этого класса:
1) добавьте файл в свой проект;
2) скомпилируйте код Verse из меню Verse на панели инструментов;
3) перетащите это устройство на свой остров из папки устройств «Контент»/«Творческий режим» своего острова в Каталоге ресурсов;
4) подключите устройство «Таймер» к свойству WaitingForMorePlayersTimer этого устройства;
5) включите класс waiting_for_more_players в другой сценарий Verse, включая следующее:
@editable
WaitingForMorePlayers:waiting_for_more_players = waiting_for_more_players{}
6) скомпилируйте код Verse из меню Verse на панели инструментов;
7) подключите устройство, созданное на шаге 3, к устройству Verse и свойству, предоставленному на шаге 6;
8) дождитесь, пока функции CheckForMinimumNumberOfPlayers будут переданы игроку; Например:
Players = GetPlayspace().GetPlayers()
CheckForMinimumNumberOfPlayers(Players)
9) задайте в IslandSettings значение 0.0 для обратного отсчёта перед запуском игры.
#>
waiting_for_more_players := class(creative_device):
Logger:log = log{Channel:=log_waiting_for_more_players_device}
@editable # Минимальное количество игроков в матче, чтобы можно было запустить раунд.
MinimumNumberOfPlayers:int = 2
@editable # Горизонтальное и вертикальное положение интерфейса таймера на экране. X 0–1 — это слева направо, а Y 0–1 — сверху вниз.
UIPosition:vector2 = vector2{X:= 0.5, Y:=0.4}
@editable # Горизонтальное и вертикальное положение интерфейса таймера на экране. X 0–1 — это слева направо, а Y 0–1 — сверху вниз.
UIAlignment:vector2 = vector2{X := 0.5, Y := 0.5}
@editable # Порядок размещения интерфейса по оси Z по отношению к другим элементам интерфейса.
UIZOrder:waiting_int_clamped = 3
@editable # Этот таймер используется для обратного отсчёта до начала раунда после ожидания присоединения игроков к матчу.
WaitingForMorePlayersTimer:timer_device = timer_device{}
# Этот ассоциативный массив связывает рабочую область интерфейса для отображения количества игроков, необходимого для запуска раунда, с каждым игроком.
var WaitingForMorePlayersUI:[player]?waiting_for_more_players_ui = map{}
# Проверяем, достаточно ли игроков для запуска раунда. В противном случае дожидаемся, когда количество игроков достигнет или превысит значение MinimumNumberOfPlayers.
WaitForMinimumNumberOfPlayers(Players:[]player)<suspends>:[]player =
Logger.Print("Ожидание достаточного количества игроков для запуска раунда.")
# Создаём новую переменную, которую можно изменить по мере присоединения к игре новых игроков. Инициализируем её с массивом игроков, передаваемым функции.
var PlayersWaiting:[]player = Players
# Если количество игроков меньше минимума, необходимого для запуска раунда…
if (PlayersWaiting.Length < MinimumNumberOfPlayers):
loop: # Выполняем цикл до тех пор, пока количество игроков не достигнет или не превысит необходимый минимум.
Logger.Print("Количество игроков, необходимое для запуска раунда: {PlayersWaiting.Length}/{MinimumNumberOfPlayers}.")
# Обновляем интерфейс ожидания дополнительного количества игроков.
DisplayWaitingForMorePlayers(PlayersWaiting)
Sleep(2.0) # Ожидаем появления новых игроков в матче, затем проверяем, достаточно ли игроков для запуска раунда.
set PlayersWaiting = GetPlayspace().GetPlayers()
if (PlayersWaiting.Length >= MinimumNumberOfPlayers):
# Если игроков уже достаточно, очищаем интерфейс ожидания дополнительного количества игроков,
Logger.Print("Количество игроков в раунде, готовящихся к его запуску: {PlayersWaiting.Length}/{MinimumNumberOfPlayers}.")
ClearWaitingForMorePlayers()
# После этого выходим из цикла.
break
# запускам обратный отсчёт до начала раунда и ожидаем его завершения.
WaitingForMorePlayersTimer.Start()
WaitingForMorePlayersTimer.SuccessEvent.Await()
Logger.Print("Раунд начинается.")
# Возвращаем список игроков в сеансе.
return PlayersWaiting
# Отображает сообщение интерфейса «Ожидание других игроков» для каждого игрока, если до этого оно не отображалось. Обновляет показание счётчика игроков для всех игроков.
DisplayWaitingForMorePlayers(Players:[]player):void =
PlayersNeededCount := MinimumNumberOfPlayers - Players.Length
Logger.Print("Количество игроков в раунде: {Players.Length}. Ожидаем ещё игроков: {PlayersNeededCount}.")
for (Player : Players):
# Если у игрока есть WaitingForMorePlayersUI, получаем TextBlock и обновляем текст таким образом, чтобы он показывал точное количество игроков, требуемое для запуска раунда.
if (UIData := WaitingForMorePlayersUI[Player]?):
UIData.TextBlock.SetText(WaitingForMorePlayersMessage(PlayersNeededCount))
# В противном случае создаём WaitingForMorePlayersUI для игрока.
else:
SetUpWaitingForMorePlayersUI(Player, PlayersNeededCount)
# Принимает игрока и player_ui, а также добавляет на экран рабочую область интерфейса ожидания других игроков.
SetUpWaitingForMorePlayersUI(Player:player, PlayersNeededCount:int):void =
Logger.Print("Создание интерфейса ожидания других игроков.")
if (PlayerUI := GetPlayerUI[Player]):
# Это блок text_block для вывода на экран текста об ожидании других игроков.
TextBlock:text_block = text_block:
DefaultText := WaitingForMorePlayersMessage(PlayersNeededCount)
DefaultTextColor := NamedColors.White
DefaultShadowColor := NamedColors.Black
# Это рабочая область, которая содержит блок text_block и размещает его на экране.
Canvas := canvas:
Slots := array:
canvas_slot:
Anchors := anchors{Minimum := UIPosition, Maximum := UIPosition}
Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
Alignment := UIAlignment
SizeToContent := true
Widget := TextBlock
# Рабочая область назначена игроку.
PlayerUI.AddWidget(Canvas, player_ui_slot{ZOrder := UIZOrder})
# Блок text_block сохраняется в ассоциативном массиве, чтобы в дальнейшем можно было обновлять текст по мере появления в игре новых игроков.
if (set WaitingForMorePlayersUI[Player] = option{ waiting_for_more_players_ui{Canvas := Canvas, TextBlock := TextBlock} }) {}
# Удаляет сообщение интерфейса «Ожидание других игроков» для каждого игрока, у которого он имеется.
ClearWaitingForMorePlayers():void =
Logger.Print("Очистка интерфейса ожидания других игроков.")
for (Player -> UIData : WaitingForMorePlayersUI):
if:
PlayerUI := GetPlayerUI[Player]
Canvas := UIData?.Canvas
set WaitingForMorePlayersUI[Player] = false
then:
PlayerUI.RemoveWidget(Canvas)