В основе этого обучения лежат концепции из раздела Сохраняемая статистика игрока, поэтому для начала рекомендуем ознакомиться именно с этим разделом.
Таблицы лидеров — это основной элемент соревновательных игр, позволяющий игрокам продемонстрировать свои навыки и заявить о себе. Они помогают игрокам ощущать собственный прогресс и побуждают их постоянно возвращаться, чтобы увидеть, насколько выше в списке они поднялись.
Verse Persistence — это инструмент, который позволяет создавать такие таблицы лидеров и добавлять соревновательные элементы в игры. Из руководства по сохраняемой статистике игрока вы уже узнали, как можно отслеживать сохраняемые данные между игровыми сеансами, а также как изменять и обновлять эти данные с учётом различных событий. Теперь можно применить эти знания для создания полноценных локальных таблиц лидеров, сортировки статистики игроков и объединения всех этих элементов в гоночной игре!
Используемые возможности языка Verse
-
Класс: в этом примере создан сохраняемый класс Verse, отслеживающий группу показателей одного игрока.
-
Конструктор: это специальная функция, которая создаёт экземпляр связанного с ней класса.
-
Weak_map: это простой слабый ассоциативный массив, итерация по которому не производится. Сохраняемые данные Verse должны храниться в weak_map.
Настройка уровня
В этом примере используются следующие объекты окружения и устройства:
-
3 устройства «Рекламный щит»: на них отображается статистика за всё игровое время каждого игрока, и вы будете сортировать игроков по набранным очкам, чтобы представить лучших игроков в лобби.
-
3 устройства «Упоминание игрока»: в сочетании с рекламными щитами устройства «Упоминание игрока» будут отображать имена лидеров, чтобы другие игроки знали, с кем стоит состязаться во время игры.
-
3 устройства «Контрольная точка»: это контрольные точки, через которые проезжают игроки, чтобы завершить гонку.
-
1 устройство «Управление гонками»: оно позволяет отслеживать моменты начала и завершения гонки игроками, а также начислять им очки с учётом места на финише.
-
1 устройство «Генератор машин Bear»: оно создаёт транспортное средство, которое вы будете использовать во время гонки, однако вы можете заменить его на любое другое транспортное средство, соответствующее тематике вашей игры.
Выполните следующие действия, чтобы настроить уровень:
Устройства «Рекламный щит» и «Упоминание игрока»
Для отображения статистики игроков нужно использовать комбинацию устройств «Рекламный щит» и «Упоминание игрока». На каждом рекламном щите будет отображаться статистика игрока за всё его игровое время, а устройство «Упоминание игрока» будет содержать визуальное представление об этом игроке. Чтобы добавить эти элементы, выполните следующие действия:
-
Добавьте три устройства Упоминание игрока на уровень и разместите их рядом друг с другом.
-
Выберите каждое устройство «Упоминание игрока» в Структуре. На панели Сведения в разделе Пользовательские настройки выберите для параметра Пользовательский цвет тот цвет, который вы хотите использовать для обозначения в лобби первого, второго и третьего игроков, показавших лучшие результаты. В этом примере используются золотой, серебряный и бронзовый цвета.
-
Добавьте три устройства Рекламный щит на уровень и разместите их напротив каждого устройства «Упоминание игрока». В начале игры мы добавим в каждое устройство статистику игрока с помощью Verse.
Устройства «Контрольная точка», «Генератор машин Bear» и «Управление гонками»
Поскольку мы с вами разрабатываем гоночную игру, нужно добавить состязательные элементы. Вам также понадобятся контрольные точки для проезда и устройство «Управление гонками», которое будет управлять гонкой во время игры. Чтобы добавить эти элементы, выполните следующие действия:
-
Добавьте три устройства Контрольная точка в гонках на уровень. Расположите их в том порядке, в котором вы хотите, чтобы игроки через них проезжали. Проверьте, чтобы на панели Структура Номер контрольной точки каждой контрольной точки соответствовал порядку прохождения игроками контрольных точек.
-
Добавьте одно устройство Управление гонками на уровень. Так вы сможете управлять гонкой и направлять игроков к контрольным точкам. Необходимо перехватывать событие
RaceCompletedEvent()
из этого устройства, чтобы знать, когда игрок заканчивает гонку. -
Добавьте одно устройство Генератор машин Bear на уровень. Транспортное средство добавлять необязательно, просто в этом руководстве мы используем пикап, чтобы воссоздать Шаблон гоночной трассы и предоставить игрокам средство передвижения.
Изменение статистической таблицы
В этом примере используется изменённая версия файла player_stats_table
из Сохраняемой статистики игрока. Этот файл будет похож на файл из того примера, однако в нём будут серьёзные отличия, из-за которых процесс реализации тоже будет изменён.
Выполните следующие действия, чтобы создать таблицу статистики игрока:
-
В классе
player_stats_table
:-
Удалите показатель
Losses
. -
Измените показатель
Score
наPoints
.
# Отслеживает различные сохраняемые показатели каждого игрока. player_stats_table<public>:= class<final><persistable>: # Версия текущей таблицы статистики. Version<public>:int = 0 # Очки игрока. Points<public>:int = 0 # Количество побед игрока. Wins<public>:int = 0
-
-
Измените функцию конструктора
MakePlayerStatsTable()
в вашем файле так, чтобы в ней отражалась обновлённая статистика.# Создаёт новую таблицу player_stats_table с теми же значениями, что и в предыдущей. MakePlayerStatsTable<constructor>(OldTable:player_stats_table)<transacts> := player_stats_table: Version := OldTable.Version Points := OldTable.Points Wins := OldTable.Wins
-
Добавьте новую структуру
player_and_stats
в файл player_stats_table.verse. Эта структура содержит ссылку наplayer
и его классplayer_stats_table
, что позволяет вам использовать данные каждого из них в функциях без необходимости их повторного извлечения. Готовая структураplayer_and_stats
должна выглядеть следующим образом:# Структура для передачи игрока и его статистики в качестве аргументов. player_and_stats<public> := struct: Player<public>:player StatsTable<public>:player_stats_table
Управление статистикой
Для управления изменениями в статистике игроков и регистрацией такой статистики мы будем использовать диспетчер файлов — как и в случае с Сохраняемой статистикой игрока.
Чтобы создать изменённый файл player_stats_manager
, выполните действия, описанные ниже.
-
Измените сигнатуру функции
InitializeAllPlayers()
иInitializePlayer()
наInitializeAllPlayerStats()
иInitializePlayerStat()
. Эти названия лучше отражают их отношение к функцииGetPlayerStat()
. Ваша обновлённая функция должна выглядеть следующим образом:# Инициализируем показатели каждого текущего игрока. InitializeAllPlayerStats<public>(Players:[]player):void = for (Player : Players): InitializePlayerStat(Player) # Инициализируем показатели заданного игрока. InitializePlayerStat<public>(Player:player):void= if: not PlayerStatsMap[Player] set PlayerStatsMap[Player] = player_stats_table{} else: Print("Невозможно инициализировать статистику игрока")
-
Измените сигнатуру функции
AddScore()
наAddPoints()
. Затем удалите функциюAddLosses()
, поскольку переменнаяplayer_stats_table
больше не содержит это значение. Файлplayer_stats_manager
в конечном итоге должен выглядеть следующим образом:# Этот файл обрабатывает код для инициализации, обновления и возврата player_stats_tables # для каждого игрока. Также в нём определён абстрактный класс stat_type, используемый для обновления статистики, и # модуль StatType, используемый при показе статистики. using { /Fortnite.com/Devices } using { /Verse.org/Simulation } using { /UnrealEngine.com/Temporary/Diagnostics } # Возвращаем player_stats_table для предоставленного агента. GetPlayerStats<public>(Agent:agent)<decides><transacts>:player_stats_table= var PlayerStats:player_stats_table = player_stats_table{} if: Player := player[Agent] PlayerStatsTable := PlayerStatsMap[Player] set PlayerStats = MakePlayerStatsTable(PlayerStatsTable) PlayerStats # Инициализируем показатели каждого текущего игрока. InitializeAllPlayerStats<public>(Players:[]player):void = for (Player : Players): InitializePlayerStat(Player) # Инициализируем показатели заданного игрока. InitializePlayerStat<public>(Player:player):void= if: not PlayerStatsMap[Player] set PlayerStatsMap[Player] = player_stats_table{} else: Print("Невозможно инициализировать статистику игрока") # Добавляется к очкам заданного агента и обновляет таблицу для обоих показателей # в PlayerStatsManager и на рекламном щите на уровне. AddPoints<public>(Agent:agent, NewPoints:int):void= if: Player := player[Agent] PlayerStatsTable := PlayerStatsMap[Player] CurrentPoints := PlayerStatsTable.Points set PlayerStatsMap[Player] = player_stats_table: MakePlayerStatsTable<constructor>(PlayerStatsTable) Points := CurrentPoints + NewPoints else: Print("Невозможно записать очки игрока") # Добавляется к победам заданного агента и обновляет таблицу для обоих показателей # в PlayerStatsManager и на рекламном щите на уровне. AddWin<public>(Agent:agent, NewWins:int):void= if: Player := player[Agent] PlayerStatsTable := PlayerStatsMap[Player] CurrentWins := PlayerStatsTable.Wins set PlayerStatsMap[Player] = player_stats_table: MakePlayerStatsTable<constructor>(PlayerStatsTable) Wins := CurrentWins + NewWins else: Print("Невозможно записать победы игрока")
Создание таблиц лидеров игроков
Для отображения данных игроков в таблице лидеров необходимо следующее. Нужно найти способ обновлять текст на рекламных щитах и игроков и устройствах «Упоминание игрока». А ещё эти устройства нужно сортировать, ведь нужно сделать так, чтобы имена лучших игроков выделялись среди всех в таблице лидеров. Описанные функции имеют схожую цель — они должны изменять устройства на уровне, поэтому рекомендуем сгруппировать их в общий файл.
Для создания функций, которые будут обновлять устройства на уровне, выполните следующие действия:
-
Создайте новый файл Verse под названием player_leaderboards.verse. В нём будут храниться функции, общие для обновления таблицы лидеров на уровне.
-
Для вывода текста на рекламном щите будем использовать сообщение, в которое можно передавать аргументы. Создайте новое сообщение с названием
StatsMessage
, которое принимает значенияCurrentPlayer
,Points
иWins
типаmessage
и возвращает объединённый текст в видеmessage
.# Сообщение для отображения на рекламном щите со статистикой. StatsMessage<localizes>(CurrentPlayer:message, Points:message, Wins:message):message= "{CurrentPlayer}:\n{Points}\n{Wins}"
-
Добавьте ещё три переменные
message
, по одной для каждого из входных данных дляStatsMessage
. СообщениеPlayerText
принимаетAgent
, сообщениеPointsText
принимает количество очков этого агента, а сообщениеWinsText
принимает количество побед этого агента.StatsMessage
создаст сообщение из всего вышеописанного для наглядного отображения ваших данных на уровне.# Сообщение для отображения на рекламном щите со статистикой. StatsMessage<localizes>(CurrentPlayer:message, Points:message, Wins:message):message= "{CurrentPlayer}:\n{Points}\n{Wins}" PlayerText<localizes>(CurrentPlayer:agent):message = "Игрок: {CurrentPlayer}" PointsText<localizes>(Points:int):message = "Всего очков: {Points}" WinsText<localizes>(Wins:int):message = "Всего побед: {Wins}"
-
Чтобы обновить рекламный щит, вызовите функцию
UpdateStatsBillboard()
из руководства «Сохраняемая статистика игрока». Эта функция определена в отдельном файле от устройства Verse, поэтому нужно добавитьStatsBillboard
в качестве дополнительного аргумента, чтобы указать, какой рекламный щит нужно будет обновлять.# Обновляет указанное устройство «Рекламный щит» для отображения показателей указанного игрока. UpdateStatsBillboard<public>(Player:agent, StatsBillboard:billboard_device):void=
-
Сначала получите статистику игрока, переданную в качестве аргумента, с помощью
GetPlayerStats[]
. Ссылка наplayer_stats_manager
не потребуется, поскольку он больше не является отдельным классом. Затем создайте новое сообщениеStatsMessage
с помощью игрока, а такжеPoints
иWins
из его статистикиCurrentPlayerStats
. Наконец, вызовитеSetText()
дляStatsBillboard
, чтобы обновить текст рекламного щита на уровне. Готовая функцияUpdateStatsBillboard()
должна выглядеть следующим образом:# Обновляет указанное устройство «Рекламный щит» для отображения показателей указанного игрока. UpdateStatsBillboard<public>(Player:agent, StatsBillboard:billboard_device):void= if: CurrentPlayerStats := GetPlayerStats[Player] then: PlayerStatsText := StatsMessage( PlayerText(Player), PointsText(CurrentPlayerStats.Points), WinsText(CurrentPlayerStats.Wins)) StatsBillboard.SetText(PlayerStatsText)
Сортировка игроков и отображение имени лучшего игрока
Прежде чем продолжить, подумайте о том, как вы хотите выполнять сортировку в этих рекламных щитах. Вы хотите, чтобы в верхней строчке был игрок, набравший наибольшее количество очков, или игрок с наибольшим количеством побед? Может, вы хотите сортировать игроков по их статистике? Вам нужен метод, который будет обрабатывать эти запросы, а также алгоритм сортировки. Используя алгоритм сортировки и функцию сравнения, вы сможете указать, по каким критериям вы хотите сортировать данные. Вы также сможете отсортировать рекламные щиты и устройства «Упоминание игроков», чтобы отобразить имена лучших игроков. В этом примере используется алгоритм Сортировки с объединением, однако вы можете реализовать свой собственный алгоритм.
Выполните следующие действия, чтобы добавить функции сравнения и сортировки в рекламные щиты и завершить обновление устройств на уровне.
-
В файле
player_stats_table
определим функции сравнения для каждого из показателей. Каждая из них принимает в качестве аргументов структурыLeft
иRight
типаplayer_and_stats
и сравнивает их по значениям конкретных показателей. Эти функции имеют модификаторы<decides><transacts>
, поэтому если сравнение окажется неудачным, не будет выполнена и функция, то есть, например, она не вернёт информацию о том, чтоLeft
меньшеRight
. Добавьте новую функциюMorePointsComparison()
в файл player_stats_table.verse. Эта функция проверяет, превышает ли значение
Left.Pointsзначение
Right.Points. Если не превышает, она не выполнится. Если же превышает, она вернёт
Left`.# Возвращает Left, если Left содержит больше очков, чем Right. MorePointsComparison<public>(Left:player_and_stats, Right:player_and_stats)<decides><transacts>:Left= Left.StatsTable.Points > Right.StatsTable.Points Слева
-
Создайте ещё три копии этой функции — одна будет использоваться для сравнения с целью определения меньшего числа очков, а две — для сравнения числа побед. Ваши функции сравнения должны выглядеть следующим образом:
# Возвращает Left, если Left содержит больше очков, чем Right. MorePointsComparison<public>(Left:player_and_stats, Right:player_and_stats)<decides><transacts>:player_and_stats= Left.StatsTable.Points > Right.StatsTable.Points Слева # Возвращает Left, если Left содержит меньше очков, чем Right. LessPointsComparison<public>(Left:player_and_stats, Right:player_and_stats)<decides><transacts>:player_and_stats= Left.StatsTable.Points < Right.StatsTable.Points Слева # Возвращает Left, если Left содержит больше призовых мест, чем Right. MorePodiumsComparison<public>(Left:player_and_stats, Right:player_and_stats)<decides><transacts>:player_and_stats= Left.StatsTable.Points > Right.StatsTable.Points Слева # Возвращает Left, если Left содержит меньше призовых мест, чем Right. LessPodiumsComparison<public>(Left:player_and_stats, Right:player_and_stats)<decides><transacts>:player_and_stats= Left.StatsTable.Points < Right.StatsTable.Points Слева
-
Добавим алгоритм сортировки с объединением. Вы можете поместить его в отдельный файл или модуль и протестировать такой алгоритм на предоставленном тестовом файле.
-
Теперь в
player_leaderboards
добавим новую функциюUpdateStatsBillboards()
. Эта функция принимает массив агентов и массив рекламных щитов, сортирует их и вызываетUpdateStatsBillboard()
для обновления каждого рекламного щита на уровне.# Обновляем рекламные щиты со статистикой, сортируя их на основе количества очков, набранных за всё игровое время # каждым игроком. UpdateStatsBillboards<public>(Players:[]agent, StatsBillboards:[]billboard_device):void=
-
В
UpdateStatsBillboards()
инициализируем новую переменную массиваplayer_and_stats
с именемPlayerAndStatsArray
. Присваиваем ей результат выраженияfor
. В этом выраженииfor
для каждогоagent
получаемplayer
для данногоagent
и получаем егоplayer_stats_table
с помощьюGetPlayerStats[]
. Затем возвращаем структуруplayer_and_stats
, построенную на основеplayer
и его таблицы статистики.UpdateStatsBillboards<public>(Players:[]agent, StatsBillboards:[]billboard_device):void= var PlayerAndStatsArray:[]player_and_stats = for: Agent:Players Player := player[Agent] PlayerStats := GetPlayerStats[Player] do: player_and_stats: Player := Player StatsTable := PlayerStats
-
Для сортировки
PlayerAndStatsArray
инициализируем новую переменнуюSortedPlayersAndStats
результатом вызоваMergeSort()
, передав массив иMorePointsComparison
. После сортировки в выраженииfor
перебираем все элементы вSortedPlayerAndStats
, сохраняя индекс элемента в переменнойPlayerIndex
. ИспользуемPlayerIndex
для обращения к определённому элементу массиваStatsBillboards
, а затем вызываемUpdateStatsBillboard
, передав игрока и рекламный щит для обновления. Готовая функцияUpdateStatsBillboards()
должна выглядеть следующим образом:# Обновляем рекламные щиты со статистикой, сортируя их на основе количества очков, набранных за всё игровое время # каждым игроком. UpdateStatsBillboards<public>(Players:[]agent, StatsBillboards:[]billboard_device):void= var PlayerAndStatsArray:[]player_and_stats = for: Agent:Players Player := player[Agent] PlayerStats := GetPlayerStats[Player] do: player_and_stats: Player := Player StatsTable := PlayerStats # Сравниваем и сортируем игроков по общему количеству очков, что позволяет выявить # лучшего игрока в лобби. Функцию сравнения здесь можно заменить с учётом # потребностей вашей игры. SortedPlayersAndStats := SortingAlgorithms.MergeSort( PlayerAndStatsArray, MorePointsComparison) for: PlayerIndex -> PlayerAndStats : SortedPlayersAndStats StatsBillboard := StatsBillboards[PlayerIndex] do: UpdateStatsBillboard(PlayerAndStats.Player, StatsBillboard)
-
Для обновления устройств «Упоминание игрока» воспользуемся очень похожей функцией, которая называется
UpdatePlayerReferences()
. Эта функция принимает массивplayer_reference_device
вместо рекламных щитов и вместо вызоваUpdateStatsBillboard()
в конце она вызываетRegister()
для устройства «Упоминание игрока» применительно к каждому игроку. Скопируйте свой кодUpdateStatsBillboard()
в новую функциюUpdatePlayerReferences()
, внеся изменения, указанные выше. Готовая функцияUpdatePlayerReferences()
должна выглядеть следующим образом:# Обновляем устройства «Упоминание игрока», сортируя их на основе количества # очков, набранных каждым игроком за всё игровое время UpdatePlayerReferences<public>(Players:[]player, PlayerReferences:[]player_reference_device):void= var PlayerAndStatsArray:[]player_and_stats = for: Agent:Players Player := player[Agent] PlayerStats := GetPlayerStats[Player] do: player_and_stats: Player := Player StatsTable := PlayerStats # Сравниваем и сортируем игроков по общему количеству очков, что позволяет выявить # лучшего игрока в лобби. Функцию сравнения здесь можно заменить с учётом # потребностей вашей игры. SortedPlayersAndStats := SortingAlgorithms.MergeSort( PlayerAndStatsArray, MorePointsComparison) for: PlayerIndex -> PlayerAndStats : SortedPlayersAndStats PlayerReference := PlayerReferences[PlayerIndex] do: PlayerReference.Register(PlayerAndStats.Player)
Таблицы лидеров игроков на вашем уровне
Теперь, когда всё настроено, пора показать игроков! Создадим устройство, которое будет присваивать очки игрокам при их взаимодействии с кнопкой, и отсортируем устройства «Упоминание игрока» и рекламные щиты, чтобы лучшие игроки оказались впереди по центру. Выполните следующие действия, чтобы создать устройство Verse для тестирования таблиц лидеров на вашем уровне:
-
Создайте новое устройство Verse и назовите его player_leaderboards_example. См. описание действий в разделе Создание собственного устройства с помощью Verse.
-
В начале определения класса
player_leaderboards_example
добавьте следующие поля:-
Редактируемый массив устройств «Упоминание игрока» с именем
PlayerReferences
. Эти устройства обеспечивают визуальное представление каждого игрока в гонке.# Визуальное представление каждого игрока. @editable PlayerReferences:[]player_reference_device = array{}
-
Редактируемый массив устройств «Рекламный щит» с именем
Leaderboards
. Они показывают статистику каждого игрока на рекламном щите на уровне.# Рекламные щиты, на которых отображается статистика по каждому игроку. @editable Leaderboards:[]billboard_device = array{}
-
Редактируемое устройство «Управление гонками» с именем
RaceManager
. Подпишемся на события от устройства «Управление гонками», чтобы знать, когда игрок заканчивает гонку.# Будем отслеживать момент завершения игроками гонки, когда первым игрокам присуждается победа. @editable RaceManager:race_manager_device = race_manager_device{}
-
Редактируемая целочисленная переменная с именем
PlacementRequiredForWin
. Это место игрок должен занять для присуждения ему победы.# Для присуждения победы место игрока должно иметь такое значение или меньше. @editable PlacementRequiredForWin:int = 1
-
Редактируемый массив целых чисел с именем
PointsPerPlace
. Это количество очков, которое каждый игрок получает в зависимости от его места.# Количество очков, которое получают игроки, занявшие каждое место. # Его можно изменить, чтобы начислять игрокам нужное количество очков # с учётом их места. @editable PointsPerPlace:[]int = array{5, 3, 1}
-
Целочисленная переменная с именем
CurrentFinishOrder
. Это место игрока, который завершил гонку только что.# Место игрока, который только что завершил гонку. # Первым трём игрокам, завершившим гонку, присуждается победа. var CurrentFinishOrder:int = 0
-
Начисление очков в зависимости от места
Как только игрок завершит гонку, нужно будет обновить его статистику с учётом его места. Игроки, занявшие высокие места, должны получить больше очков, а те из них, кто показал лучшие результаты, должны быть объявлены победителями.
Выполним следующие действия для начисления очков игрокам при завершении гонки:
-
Чтобы решить эту задачу, добавим новую функцию
RecordPlayerFinish()
в определение классаplayer_leaderboards_example
. Эта функция принимает игрока, которому необходимо начислить очки, как параметр.# Как только игрок закончит гонку, начисляем ему очки с учётом его места и присуждаем ему победу, если # его место окажется выше, чем PlacementRequiredForWin. RecordPlayerFinish(Player:agent):void=
-
В функции
RecordPlayerFinish()
получим место этого игрока, получив текущее значениеCurrentFinishOrder
в новой переменной типаint
с именемPlayerFinishOrder
. Затем увеличимCurrentFinishOrder
, чтобы следующий финиширующий игрок не оказался на том же месте.RecordPlayerFinish(Player:agent):void= PlayerFinishOrder:int = CurrentFinishOrder set CurrentFinishOrder += 1
-
А теперь пора начислять очки. Чтобы знать, сколько очков нужно присвоить определённому игроку, в выражении
if
обратимся к массивуPointsPerPlace
с помощьюPlayerFinishOrder
. Затем вызовемAddPoints()
для начисления игроку такого количества очков.set CurrentFinishOrder += 1 if: PointsToAward := PointsPerPlace[PlayerFinishOrder] then: AddPoints(Player, PointsToAward)
-
Если игрок достаточно опередил остальных, чтобы выиграть, нужно отразить победу в его таблице статистики. В другом выражении
if
убедитесь, что значениеPlayerFinishOrder
оказалось меньше значенияPlacementRequiredToWin
. При выполнении данного условия вызываемAddWin()
, передавая соответствующего игрока и присуждаемое ему место. Готовая функцияRecordPlayerFinish()
должна выглядеть следующим образом:# Как только игрок закончит гонку, начисляем ему очки с учётом его места и присуждаем ему победу, если # его место окажется выше, чем PlacementRequiredForWin. RecordPlayerFinish(Player:agent):void= PlayerFinishOrder:int = CurrentFinishOrder set CurrentFinishOrder += 1 if: PointsToAward := PointsPerPlace[PlayerFinishOrder] then: AddPoints(Player, PointsToAward) # Если занятое игроком место оказалось меньше значения PlacementRequiredToWin или равным ему, # присуждаем ему победу и записываем этот результат в его player_stats_table. if: PlayerFinishOrder < PlacementRequiredForWin then: AddWin(Player, 1)
Ожидание завершения гонки игроками
Запись статистики настроена, и теперь нужно определять момент завершения игроком гонки, чтобы эту статистику можно было обновить. Для этого будем ожидать события RaceCompletedEvent()
диспетчера гонки. Это событие возникает, когда какой-либо из игроков завершает гонку, поэтому его нужно прослушивать постоянно в асинхронной функции.
-
Добавим новую функцию
WaitForPlayerToFinishRace()
в наше определение классаplayer_leaderboards_example
. Эта функция принимает игрока и ожидает момента, когда он завершит гонку.# При завершении игроком гонки записываем завершение в его таблицу статистики. WaitForPlayerToFinishRace(Player:agent)<suspends>:void=
-
В функции
WaitForPlayerToFinishRace()
организуем два цикла loop в выраженииrace
. В первом будем ожидать завершения игроком гонки, а во втором обработаем ситуацию, когда игрок выходит из сеанса до финиша. Если игрок выйдет из игры, цикл уже не нужно будет выполнять бесконечно, а потому нужно решить, как его прервать.# При завершении игроком гонки записываем завершение в его таблицу статистики. WaitForPlayerToFinishRace(Player:agent)<suspends>:void= race: # Ожидаем завершения гонки этим игроком и затем регистрируем завершение. loop: # Ожидаем выхода этого игрока из игры. loop:
-
В первом цикле ожидаем
RaceManager.RaceCompletedEvent
и сохраняем результат в переменной с именемFinishingPlayer
. Так как это событие возникает всегда, когда один из игроков завершает гонку, важно, чтобы сохранённый игрок оказался тем игроком, которого мы отслеживаем. СравниваемFinishingPlayer
с игроком, который отслеживается в этом цикле. Если игроки совпадают, передаём игрока вRecordPlayerFinish()
и прерываем цикл.# Ожидаем завершения гонки этим игроком и затем регистрируем завершение. loop: FinishingPlayer := RaceManager.RaceCompletedEvent.Await() if: FinishingPlayer = Player then: RecordPlayerFinish(Player) break
-
Во втором цикле ожидаем событие игрового пространства
PlayerRemovedEvent()
. Как и раньше, получаем игрока, который только что вышел из игры, и сохраняем его в переменнойLeavingPlayer
. Если только что вышедший из игры игрок совпадает с ожидаемым в цикле, прерываем цикл. Готовая функцияWaitForPlayerToFinishRace()
должна выглядеть следующим образом:# При завершении игроком гонки записываем завершение в его таблицу статистики. WaitForPlayerToFinishRace(Player:agent)<suspends>:void= race: # Ожидаем завершения гонки этим игроком и затем регистрируем завершение. loop: FinishingPlayer := RaceManager.RaceCompletedEvent.Await() if: FinishingPlayer = Player then: RecordPlayerFinish(Player) break # Ожидаем выхода этого игрока из игры. loop: LeavingPlayer := GetPlayspace().PlayerRemovedEvent().Await() if: LeavingPlayer = Player then: break
Связывание всех элементов
Функции настроены, и теперь пора связать их с устройствами и запустить гонку!
Чтобы связать логику с устройствами, выполните следующие шаги:
-
В
OnBegin()
получите всех игроков в игровом пространстве с помощьюGetPlayers()
. Передайте этот массив вInitializeAllPlayerStats()
, чтобы настроитьplayer_stats_table
для каждого из них.# Выполняется при запуске устройства в работающей игре OnBegin<override>()<suspends>:void= # Получим игроков в текущей гонке и создадим таблицу player_stat_table # для каждого из них. Players := GetPlayspace().GetPlayers() InitializeAllPlayerStats(Players)
-
Вызовите функцию
UpdateStatsBillboards()
, передав массивыPlayers
иLeaderboards
для обновления имеющихся на уровне рекламных щитов в соответствии с текущими данными каждого игрока. Затем вызовите функциюUpdatePlayerReferences()
, чтобы обновить имеющиеся на уровне устройства «Упоминание игрока» в соответствии с результатами игроков. Наконец, в выраженииfor
создайте экземпляр функцииWaitForPlayerToFinishRace()
для каждого игрока. Готовая функцияOnBegin()
должна выглядеть следующим образом:# Выполняется при запуске устройства в работающей игре OnBegin<override>()<suspends>:void= # Получим игроков в текущей гонке и создадим таблицу player_stat_table # для каждого из них. Players := GetPlayspace().GetPlayers() InitializeAllPlayerStats(Players) UpdateStatsBillboards(Players, Leaderboards) UpdatePlayerReferences(Players, PlayerReferences) # Ожидаем, когда все игроки завершат гонку. for: Player:Players do: spawn{WaitForPlayerToFinishRace(Player)}
-
Сохраните свой код и скомпилируйте его.
Перетащите устройство player_leaderboards_example на уровень. Включите устройства «Упоминание игрока» в массив PlayerReferences в соответствующем порядке. Устройство «Упоминание игрока» с первым индексом должно соответствовать лучшему игроку, со вторым индексом — второму игроку и т. д. То же самое проделайте для рекламных щитов, не забывая о том, что они должны соответствовать устройствам «Упоминание игрока». Не забудьте также назначить устройство «Управление гонками»!

Тестирование сохраняемых таблиц лидеров
Вы можете протестировать сохраняемые данные в сеансе редактирования, но они будут сброшены при выходе из сеанса и его перезапуске. Чтобы данные сохранялись от сеанса к сеансу, нужно запустить сеанс тестирования и изменить некоторые настройки в разделе Настройки острова. Информация о настройке острова для тестирования сохраняемых данных как в режиме редактирования, так и в игровом тесте приведена в разделе «Тестирование с сохраняемыми данными». Кроме того, нужно изменить некоторые настройки в разделе «Настройки острова». Информация о настройке острова для тестирования сохраняемых данных как в режиме редактирования, так и в игровом тесте приведена в разделе «Тестирование с сохраняемыми данными».
После входа в сеанс в игровом тесте уровня игроки, завершающие гонку, должны получать очки с учётом их места. Им должна присуждаться победа, если их место достаточно высоко, и такая статистика должна сохраняться от одного игрового сеанса к другому. Игроки и их статистика должны сортироваться таким образом, чтобы игрок с наибольшим количеством очков находился на первом месте.

Самостоятельная работа
В данном руководстве вы узнали, как создавать таблицы лидеров, в которых отображается сохраняемая статистика по игрокам на уровне. Вы также узнали, как сортировать и обновлять эти таблицы лидеров, благодаря чему всем будет понятно, кто лидирует. Попробуйте применить в собственной игре знания, полученные в этом уроке, и покажите лучших в вашей игре!
Закончить код
player_stats_table.verse
# В этом файле определена таблица player_stats_table, которая содержит набор сохраняемой статистики игроков.
# Он также содержит функции для сравнения таблицы статистики по каждому показателю для ранжирования игроков
# при сортировке.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# Структура для передачи игрока и его статистики в качестве аргументов.
player_and_stats<public> := struct:
Player<public>:player
StatsTable<public>:player_stats_table
# Отслеживает различные сохраняемые показатели каждого игрока.
player_stats_table<public>:= class<final><persistable>:
# Версия текущей таблицы статистики.
Version<public>:int = 0
# Очки игрока.
Points<public>:int = 0
# Количество побед игрока.
Wins<public>:int = 0
# Возвращает Left, если Left содержит больше очков, чем Right.
MorePointsComparison<public>(Left:player_and_stats, Right:player_and_stats)<decides><transacts>:player_and_stats=
Left.StatsTable.Points > Right.StatsTable.Points
Слева
# Возвращает Left, если Left содержит меньше очков, чем Right.
LessPointsComparison<public>(Left:player_and_stats, Right:player_and_stats)<decides><transacts>:player_and_stats=
Left.StatsTable.Points < Right.StatsTable.Points
Слева
# Возвращает Left, если Left содержит больше побед, чем Right.
MoreWinsComparison<public>(Left:player_and_stats, Right:player_and_stats)<decides><transacts>:player_and_stats=
Left.StatsTable.Points > Right.StatsTable.Points
Слева
# Возвращает Left, если Left содержит меньше побед, чем Right.
LessWinsComparison<public>(Left:player_and_stats, Right:player_and_stats)<decides><transacts>:player_and_stats=
Left.StatsTable.Points < Right.StatsTable.Points
Слева
# Возвращает Left, если в Left время BestLapTime больше, чем в Right.
# Помните, что в отличие от других показателей, этот показатель учитывается в обратном порядке, так как чем меньше время круга, тем лучше.
SlowerLapTimeComparison(Left:player_and_stats, Right:player_and_stats)<decides><transacts>:player_and_stats=
Left.StatsTable.Points > Right.StatsTable.Points
Слева
# Возвращает Left, если в Left время BestLapTime меньше, чем в Right.
# Помните, что в отличие от других показателей, этот показатель учитывается в обратном порядке, так как чем меньше время круга, тем лучше.
FasterLapTimeComparison(Left:player_and_stats, Right:player_and_stats)<decides><transacts>:player_and_stats=
Left.StatsTable.Points < Right.StatsTable.Points
Слева
# Создаёт новую таблицу player_stats_table с теми же значениями, что и в предыдущей.
MakePlayerStatsTable<constructor>(OldTable:player_stats_table)<transacts> := player_stats_table:
Version := OldTable.Version
Points := OldTable.Points
Wins := OldTable.Wins
# Сопоставляет игроков с таблицей их показателей.
var PlayerStatsMap:weak_map(player, player_stats_table) = map{}
player_leaderboards.verse
# Этот файл содержит код, обновляющий рекламные щиты, устройства «Упоминание игрока» и интерфейс на острове
# для показа статистики игроков из их таблиц статистики. Он же обрабатывает данные, добавляя победы и очки
# в таблицу статистики игрока.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation}
using { PlayerStatistics }
# Сообщение для отображения на рекламном щите со статистикой.
StatsMessage<localizes>(CurrentPlayer:message, Points:message, Wins:message):message=
"{CurrentPlayer}:\n{Points}\n{Wins}"
PlayerText<localizes>(CurrentPlayer:agent):message = "Игрок: {CurrentPlayer}"
PointsText<localizes>(Points:int):message = "Всего очков: {Points}"
WinsText<localizes>(Wins:int):message = "Всего побед: {Wins}"
# Обновляет указанное устройство «Рекламный щит» для отображения показателей указанного игрока.
UpdateStatsBillboard<public>(Player:agent, StatsBillboard:billboard_device):void=
if:
CurrentPlayerStats := GetPlayerStats[Player]
then:
PlayerStatsText := StatsMessage(
PlayerText(Player),
PointsText(CurrentPlayerStats.Points),
WinsText(CurrentPlayerStats.Wins))
StatsBillboard.SetText(PlayerStatsText)
# Обновляем рекламные щиты со статистикой, сортируя их на основе количества очков, набранных за всё игровое время
# каждым игроком.
UpdateStatsBillboards<public>(Players:[]agent, StatsBillboards:[]billboard_device):void=
var PlayerAndStatsArray:[]player_and_stats =
for:
Agent:Players
Player := player[Agent]
PlayerStats := GetPlayerStats[Player]
do:
player_and_stats:
Player := Player
StatsTable := PlayerStats
# Сравниваем и сортируем игроков по общему количеству очков, что позволяет выявить
# лучшего игрока в лобби. Функцию сравнения здесь можно заменить с учётом
# потребностей вашей игры.
SortedPlayersAndStats := SortingAlgorithms.MergeSort(
MorePointsComparison,
PlayerAndStatsArray)
for:
PlayerIndex -> PlayerAndStats : SortedPlayersAndStats
StatsBillboard := StatsBillboards[PlayerIndex]
do:
UpdateStatsBillboard(PlayerAndStats.Player, StatsBillboard)
# Обновляем устройства «Упоминание игрока», сортируя их на основе количества
# очков, набранных каждым игроком за всё игровое время
UpdatePlayerReferences<public>(Players:[]player, PlayerReferences:[]player_reference_device):void=
var PlayerAndStatsArray:[]player_and_stats =
for:
Agent:Players
Player := player[Agent]
PlayerStats := GetPlayerStats[Player]
do:
player_and_stats:
Player := Player
StatsTable := PlayerStats
# Сравниваем и сортируем игроков по общему количеству очков, что позволяет выявить
# лучшего игрока в лобби. Функцию сравнения здесь можно заменить с учётом
# потребностей вашей игры.
SortedPlayersAndStats := SortingAlgorithms.MergeSort(
MorePointsComparison,
PlayerAndStatsArray)
for:
PlayerIndex -> PlayerAndStats : SortedPlayersAndStats
PlayerReference := PlayerReferences[PlayerIndex]
do:
PlayerReference.Register(PlayerAndStats.Player)
player_stats_manager.verse
# Этот файл обрабатывает код для инициализации, обновления и возврата player_stats_tables
# для каждого игрока. Также в нём определён абстрактный класс stat_type, используемый для обновления статистики, и
# модуль StatType, используемый при показе статистики.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# Возвращаем player_stats_table для предоставленного агента.
GetPlayerStats<public>(Agent:agent)<decides><transacts>:player_stats_table=
var PlayerStats:player_stats_table = player_stats_table{}
if:
Player := player[Agent]
PlayerStatsTable := PlayerStatsMap[Player]
set PlayerStats = MakePlayerStatsTable(PlayerStatsTable)
PlayerStats
# Инициализируем показатели каждого текущего игрока.
InitializeAllPlayerStats<public>(Players:[]player):void =
for (Player : Players):
InitializePlayerStat(Player)
# Инициализируем показатели заданного игрока.
InitializePlayerStat<public>(Player:player):void=
if:
not PlayerStatsMap[Player]
set PlayerStatsMap[Player] = player_stats_table{}
else:
Print("Невозможно инициализировать статистику игрока")
# Добавляется к указанным показателям StatToAdd агента и обновляет его таблицу статистики
# в PlayerStatsManager и на рекламном щите на уровне.
AddPoints<public>(Agent:agent, NewPoints:int):void=
if:
Player := player[Agent]
PlayerStatsTable := PlayerStatsMap[Player]
CurrentPoints := PlayerStatsTable.Points
set PlayerStatsMap[Player] = player_stats_table:
MakePlayerStatsTable<constructor>(PlayerStatsTable)
Points := CurrentPoints + NewPoints
else:
Print("Невозможно записать очки игрока")
# Добавляется к указанным показателям StatToAdd агента и обновляет его таблицу статистики
# в PlayerStatsManager и на рекламном щите на уровне.
AddWin<public>(Agent:agent, NewWins:int):void=
if:
Player := player[Agent]
PlayerStatsTable := PlayerStatsMap[Player]
CurrentWins := PlayerStatsTable.Wins
set PlayerStatsMap[Player] = player_stats_table:
MakePlayerStatsTable<constructor>(PlayerStatsTable)
Wins := CurrentWins + NewWins
else:
Print("Невозможно записать победы игрока")
player_leaderboards_example.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { PlayerStatistics }
using { PlayerLeaderboard }
# См. https://dev.epicgames.com/documentation/en-us/uefn/create-your-own-device-in-verse, чтобы узнать, как создать устройство Verse.
# Это Verse-устройство творческого режима, которое можно разместить на уровне
player_leaderboards_example := class(creative_device):
# Визуальное представление каждого игрока.
@editable
PlayerReferences:[]player_reference_device = array{}
# Рекламные щиты, на которых отображается статистика по каждому игроку.
@editable
Leaderboards:[]billboard_device = array{}
# Будем отслеживать момент завершения игроками гонки, когда первым игрокам присуждается победа.
@editable
RaceManager:race_manager_device = race_manager_device{}
# Для присуждения победы место игрока должно иметь такое значение или меньше.
@editable
PlacementRequiredForWin:int = 1
# Количество очков, которое получают игроки, занявшие каждое место.
# Его можно изменить, чтобы начислять игрокам нужное количество очков
# с учётом их места.
@editable
PointsPerPlace:[]int = array{5, 3, 1}
# Место игрока, который только что завершил гонку.
# Первым трём игрокам, завершившим гонку, присуждается победа.
var CurrentFinishOrder:int = 0
# Выполняется при запуске устройства в работающей игре
OnBegin<override>()<suspends>:void=
# Получим игроков в текущей гонке и создадим таблицу player_stat_table
# для каждого из них.
Players := GetPlayspace().GetPlayers()
InitializeAllPlayerStats(Players)
UpdateStatsBillboards(Players, Leaderboards)
UpdatePlayerReferences(Players, PlayerReferences)
# Ожидаем, когда все игроки завершат гонку.
for:
Player:Players
do:
spawn{WaitForPlayerToFinishRace(Player)}
# При завершении игроком гонки записываем завершение в его таблицу статистики.
WaitForPlayerToFinishRace(Player:agent)<suspends>:void=
race:
# Ожидаем завершения гонки этим игроком и затем регистрируем завершение.
loop:
FinishingPlayer := RaceManager.RaceCompletedEvent.Await()
if:
FinishingPlayer = Player
then:
RecordPlayerFinish(Player)
break
# Ожидаем выхода этого игрока из игры.
loop:
LeavingPlayer := GetPlayspace().PlayerRemovedEvent().Await()
if:
LeavingPlayer = Player
then:
break
# Как только игрок закончит гонку, начисляем ему очки с учётом его места и присуждаем ему победу, если
# его место окажется выше, чем PlacementRequiredForWin.
RecordPlayerFinish(Player:agent):void=
PlayerFinishOrder:int = CurrentFinishOrder
set CurrentFinishOrder += 1
if:
PointsToAward := PointsPerPlace[PlayerFinishOrder]
then:
AddPoints(Player, PointsToAward)
# Если занятое игроком место оказалось меньше значения PlacementRequiredToWin или равным ему,
# присуждаем ему победу и записываем этот результат в его player_stats_table.
if:
PlayerFinishOrder < PlacementRequiredForWin
then:
AddWin(Player, 1)