В последнем шаге нужно объединить все части воедино с помощью диспетчера игры. Диспетчер игры управляет назначением объектов игрового процесса игрокам и потоком игрового цикла. В частности, диспетчер игры:
назначает игрокам объекты игрового процесса, такие как доска и мини-доска;
управляет логикой игрового цикла, в том числе происходящим при ходе;
определяет, когда игрок победил и игровой процесс завершается.
Определение типов перемещения
Во время хода игрок выбирает координату на игровой доске. После выбора координаты предусмотрены два типа перемещения:
атака: попробовать уничтожить пешку в указанном месте;
раскрытие: показать все пешки в определённом радиусе от заданного местоположения.
В модуль DataTypes добавьте следующий enum для определения типов перемещения:
using{/Verse.org/Simulation}
using{/Verse.org/Random}
using{/UnrealEngine.com/Temporary/SpatialMath}
DataTypes<public> := module:
...
move_type<public> := enum<open>:
Attack
Поскольку этот enum открыт, вы всегда можете добавить новые типы перемещения в будущем.
Создание диспетчера игры
Далее создайте новый файл Verse с именем game_manager.verse и добавьте новое устройство творческого режима с именем game_manager. Это будет одиночный объект в игровом мире, управляющий игровым процессом.
Определение объектов для каждого игрока
У каждого игрока есть несколько связанных с ним объектов, включая игровую доску и мини-доску, а также данные о том, какие плитки он атаковал, событие для обозначения сделанного хода и событие для обозначения изменения выбранной координаты. Определите новый класс с именем per_player_objects:
using { /Verse.org/Simulation }
per_player_objects<public> := class:
@editable
Board<public>:board
@editable
Miniboard<public>:miniboard
var AttackedTiles<public>:[]tile_coordinate = array{}
MoveEvent<public>:event(tuple(tile_coordinate, move_type)) = event(tuple(tile_coordinate, move_type)){}
CoordinateChangeEvent<public>:event(tile_coordinate) = event(tile_coordinate){}Определение диспетчера игры
Класс диспетчера игры должен связать игроков с их объектами. Идеальный способ связать игрока с объектом в Verse — использовать weak_map. Добавьте следующие поля в класс диспетчера игры:
using { /Verse.org/Simulation }
per_player_objects<public> := class:
@editable
Board<public>:board
@editable
Miniboard<public>:miniboard
var AttackedTiles<public>:[]tile_coordinate = array{}
MoveEvent<public>:event(tuple(tile_coordinate, move_type)) = event(tuple(tile_coordinate, move_type)){}
CoordinateChangeEvent<public>:event(tile_coordinate) = event(tile_coordinate){}
Назначение объектов игроку
По мере присоединения игроков назначайте каждому игроку объекты игрока из PerPlayerObjects в объект PerPlayerManagement. Сначала получите всех игроков в игре, а затем назначьте каждому игроку объекты игроков. Если объектов игрока не хватает, обработайте ошибку. Добавьте следующую функцию AssignPlayerObjects в свой диспетчер игры:
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
...
game_manager := class(creative_device):
AssignPlayerObjects():void =
for (Index -> Player : GetPlayspace().GetPlayers()):
if:
Определение условия победы
Теперь нужно определить, когда игрок выполнил условие победы. Игрок выиграл, если на доске больше нет пешек, которые он мог бы найти и уничтожить. Это делается путём прямого запроса длины массива Pawns на доске. Добавьте следующую функцию WinConditionMet в диспетчер игры:
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
...
game_manager := class(creative_device):
WinConditionMet(Player:player)<decides><transacts>:void =
# Player wins if no pawns remain
Print("Pawns remaining: {PerPlayerManagement[Player].Board.Pawns.Length}")
Эта функция выполняется успешно тогда и только тогда, когда у указанного игрока больше нет пешек, которые он мог бы найти и уничтожить.
Перемещение при атаке
Теперь, когда вы знаете, как назначать игровые объекты каждому игроку и определять, победил ли игрок, нужно описать, что происходит при атаке. Когда игрок атакует одну из плиток соперника, выполняется следующий алгоритм:
определить, есть ли на доске пешка в точке координат атаки;
если да:
удалить пешку с доски игрока;
установить маркер попадания на мини-доске соперника;
если нет:
установить маркер промаха на мини-доске соперника.
Добавьте функцию OnAttack в класс game_manager со следующим определением:
OnAttack(Instigator:player, Recipient:player, TileCoordinate:tile_coordinate):void =
if:
InstigatorObjects := PerPlayerManagement[Instigator]
RecipientObjects := PerPlayerManagement[Recipient]
then:
# Determine if the attack is a hit
var MarkerType:marker_type = marker_type.Miss
Print("Attack coordinate: Left: {TileCoordinate.Left}, Forward: {TileCoordinate.Forward}")
Перемещение при раскрытии
Ещё один тип перемещения — раскрытие. В данном случае для перемещения при атаке потребуется дополнительная настройка. Для начала добавьте в модуль UtilityFunctions три новые функции:
operator'-': определяет двоичную операцию вычитания для двух объектовtile_coordinate;Abs: получает покомпонентное абсолютное значениеtile_coordinate;ManhattanDistance: получает манхэттенское расстояние между двумя объектамиtile_coordinate.
UtilityFunctions<public> := module:
using{DataTypes}
...
Abs(TileCoordinate:tile_coordinate)<transacts>:tile_coordinate =
tile_coordinate:
Left := Abs(TileCoordinate.Left)
Forward := Abs(TileCoordinate.Forward)
ManhattanDistance рассчитывает расстояние между двумя объектами tile_coordinate, проходя по основным направлениям сетки плиток. Подробнее см. в статье по адресу https://en.wikipedia.org/wiki/Taxicab_geometry.
После определения служебных элементов следует определить поведение функции OnReveal. Когда игрок решает открыть пешки противника в определённом радиусе от tile_coordinate, выполняется следующий алгоритм:
найти все пешки на доске игрока, которые находятся в пределах заданного расстояния
RevealDistanceот входной координаты в соответствии сManhattanDistance;на каждой пешке в пределах этого расстояния воспроизвести эффект раскрытия.
Добавьте функцию OnReveal в класс game_manager со следующим определением:
OnReveal(Instigator:player, Recipient:player, TileCoordinate:tile_coordinate):void =
if:
InstigatorObjects := PerPlayerManagement[Instigator]
RecipientObjects := PerPlayerManagement[Recipient]
then:
for:
Pawn : InstigatorObjects.Board.Pawns
PawnTileCoordinate := InstigatorObjects.Board.GetTileCoordinate[Pawn]
ManhattanDistance(PawnTileCoordinate, TileCoordinate) < RevealDistance
do:
Ход игрока
Теперь соберём ход игрока воедино. Когда наступит очередь игрока, нужно дождаться одного из двух событий, о которых можно сообщить: MoveEvent или CoordinateChangeEvent. Всякий раз, когда одно из этих событий срабатывает, другое следует отменить, поместив оба события в условие race. Когда получен сигнал об изменении координат, тот же игрок должен продолжать игру, пока не выберет тип перемещения. Поэтому к следующему игроку нужно переходить только при выборе атаки или раскрытия.
Добавьте функцию OnTurn в класс game_manager со следующим определением:
OnTurn(Player:player, Opponent:player)<suspends>:void =
if (PlayerObjects := PerPlayerManagement[Player]):
loop:
var Continue:logic = false
race:
block:
# Listens for a call to PerPlayerManager[Player].CoordinateChangeEvent.Signal(:tile_coordinate)
TileCoordinate := PlayerObjects.CoordinateChangeEvent.Await()
block:
# Listens for a call to PerPlayerManager[Player].MoveEvent.Signal(:tile_coordinate,:move_type)
Определение игрового цикла
Теперь, когда ход игрока определён, можно построить основной игровой цикл. Игровой цикл воспроизводит следующий алгоритм:
получить всех игроков;
назначить одному игроку выполнять ход, а другому — ждать, пока первый выполнит перемещение;
повторять цикл, где все игроки ходят по очереди, пока один из них не выиграет.
Для этого добавьте следующую функцию GameLoop в класс game_manager:
GameLoop()<suspends>:void =
Players := GetPlayspace().GetPlayers()
if :
Players.Length = 2
var TurnPlayer:player = Players[0]
var OtherPlayer:player = Players[1]
then:
loop:
OnTurn(TurnPlayer, OtherPlayer)
if (WinConditionMet[TurnPlayer]):
Начало игрового процесса
Осталось только назначить каждому игроку его объекты и запустить игровой цикл. Сделайте это автоматически, добавив вызовы AssignPlayerObjects и GameLoop в функцию OnBegin:
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
AssignPlayerObjects()
GameLoop()Описание
Подведём итог. На этой странице вы выполнили следующие шаги:
определить игровые перемещения;
создать игровой цикл;
определить, когда будет выполнено условие победы.
Есть ещё много способов сделать этот опыт по-настоящему уникальным, включая следующие:
разработка и реализация пользовательского интерфейса;
настройка метода и сроков передачи ходов игрока диспетчеру игры;
разработка игрового мира и окружения;
создание эффектов для атак и раскрытия объектов;
добавление музыки и оформления окружения.
Не стесняйтесь развивать эти базовые игровые классы, перестраивать их, использовать их части и создавать на их основе что-то своё.
Файлы
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { DataTypes }
using { UtilityFunctions }
per_player_objects<public> := class:
@editable
Board<public>:board
@editable