Ostatnim krokiem jest złożenie wszystkich elementów za pomocą menedżera gry. Menedżer gry steruje przypisywaniem obiektów rozgrywki do graczy i przepływem pętli gry. W szczególności menedżer gry:
Przypisuje graczom obiekty rozgrywki, takie jak plansza i miniplansza.
Steruje logiką pętli gry (włącznie z tym, co dzieje się, gdy zostanie wykonany ruch).
Określa, kiedy gracz zwycięża i kończy się rozgrywka.
Definiowanie typów ruchu
Podczas swojej tury gracz wybiera współrzędne na planszy. Po wybraniu współrzędnych gracz może wykonać jeden z dwóch typów ruchu:
Atakuj: próba zniszczenia pionka w podanej lokalizacji.
Ujawnij: ujawnienie wszystkich pionków w określonym promieniu od podanej lokalizacji.
W module DataTypes dodaj następujący typ wyliczenia (enum) definiujący typy ruchu:
using{/Verse.org/Simulation}
using{/Verse.org/Random}
using{/UnrealEngine.com/Temporary/SpatialMath}
DataTypes<public> := module:
...
move_type<public> := enum<open>:
Attack
Ten typ wyliczeniowy jest otwarty, więc w przyszłości zawsze możesz dodać więcej typów ruchu.
Tworzenie menedżera gry
Następnie utwórz nowy plik Verse o nazwie game_manager.verse i dodaj nowe urządzenie trybu kreatywnego o nazwie game_manager. Będzie to pojedynczy obiekt istniejący w świecie gry, który będzie sterować przepływem gry.
Definiowanie obiektów dla poszczególnych graczy
Z każdym graczem jest powiązanych kilka obiektów (w tym plansza i miniplansza), a także informacje o tym, które pole zaatakował, zdarzenie wskazujące wykonanie ruchu i zdarzenie wskazujące, że wybrana współrzędna uległa zmianie. Zdefiniuj nową klasę o nazwie 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){}Definiowanie menedżera gry
Klasa menedżera gry musi wiązać graczy z ich obiektami graczy. Idealnym sposobem na powiązanie gracza z obiektem w Verse jest użycie mapy weak_map. Dodaj następujące pola do klasy menedżera gry:
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){}
Przypisywanie obiektów graczy
W miarę dołączania graczy każdemu z nich przypisuj obiekty graczy z obiektu PerPlayerObjects do obiektu PerPlayerManagement. Najpierw uzyskaj wszystkich graczy biorących udział w grze, a następnie przypisz im obiekty graczy. Jeśli nie ma wystarczającej liczby obiektów graczy, obsłuż błąd. Do menedżera gry dodaj następującą funkcję AssignPlayerObjects:
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
...
game_manager := class(creative_device):
AssignPlayerObjects():void =
for (Index -> Player : GetPlayspace().GetPlayers()):
if:
Definiowanie warunku zwycięstwa
Następnie należy ustalić, kiedy gracz spełnia warunek zwycięstwa. Gracz wygrywa, jeśli na planszy nie ma więcej pionków do znalezienia i zniszczenia. W tym celu jest wykonywane bezpośrednie zapytanie o długość tablicy Pawns (tj. o liczbę pionków znajdujących się na planszy). Do menedżera gry dodaj następującą funkcję 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}")
Działanie tej funkcji kończy się powodzeniem tylko wtedy, gdy nie ma już więcej pionków do znalezienia i usunięcia dla gracza wejściowego.
W momencie wykonania ruchu Atakuj
Już wiesz, jak przypisać obiekty gry do każdego gracza i ustalić, czy gracz wygrał, więc kolejnym krokiem będzie zdefiniowanie, co ma się dziać podczas ruchu Atakuj. Gdy gracz atakuje jedno z pól przeciwnika, są wykonywane następujące kroki:
Ustal, czy na planszy znajduje się pionek, którego lokalizacja odpowiada współrzędnym ataku.
Jeśli tak:
Usuń pionek z planszy gracza.
Ustaw znacznik trafienia na miniplanszy przeciwnika.
Jeśli nie:
Ustaw znacznik chybienia na miniplanszy przeciwnika.
Do klasy game_manager dodaj funkcję OnAttack z następującą definicją:
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}")
W momencie wykonania ruchu Ujawnij
Innym typem ruchu jest ruch Ujawnij. W porównaniu do ruchu Atakuj ten ruch wymaga dodatkowej konfiguracji. Najpierw do modułu UtilityFunctions dodaj trzy nowe funkcje:
operator'-': definiuje operację binarną odejmowania dla dwóch obiektówtile_coordinate.Abs: uzyskuje komponentową wartość bezwzględną z wartościtile_coordinate.ManhattanDistance: pobiera odległość miejską (metryka miejska, znana także jako metryka Manhattan lub metryka taksówkowa) między dwoma obiektamitile_coordinate.
UtilityFunctions<public> := module:
using{DataTypes}
...
Abs(TileCoordinate:tile_coordinate)<transacts>:tile_coordinate =
tile_coordinate:
Left := Abs(TileCoordinate.Left)
Forward := Abs(TileCoordinate.Forward)
Funkcja Manhattan Distance oblicza odległość między dwoma obiektami tile_coordinate, poruszając się w głównych kierunkach na siatce pól. Aby dowiedzieć się więcej, patrz: https://en.wikipedia.org/wiki/Taxicab_geometry.
Po zdefiniowaniu funkcji narzędziowych zdefiniuj zachowanie funkcji OnReveal. Gdy gracz postanawia ujawnić pionki w określonym promieniu od pola tile_coordinate, wykonywane są następujące kroki:
Na planszy gracza znajdź wszystkie pionki, które znajdują się w odległości
RevealDistanceod współrzędnych wejściowych zgodnie z funkcjąManhattanDistance.W przypadku każdego pionka w tej odległości odtwórz efekt ujawniania.
Do klasy game_manager dodaj funkcję OnReveal z następującą definicją:
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:
Tura gracza
Teraz określ, jak ma wyglądać tura gracza. Gdy nadejdzie tura gracza, poczekaj na jedno z dwóch zdarzeń, które mogą być sygnalizowane: MoveEvent lub CoordinateChangeEvent. Za każdym razem, gdy zostanie zasygnalizowane jedno z tych zdarzeń, porzuć drugie zdarzenie i umieść te zdarzenia obok siebie w warunku race. Po zasygnalizowaniu zmiany współrzędnych ten sam gracz powinien grać dalej, dopóki nie wybierze typu ruchu. Dlatego do kolejnego gracza przechodź tylko wtedy, gdy zostanie wybrany ruch Atakuj lub Ujawnij.
Do klasy game_manager dodaj funkcję OnTurn z następującą definicją:
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)
Definiowanie pętli gry
Teraz, gdy tura gracza jest już zdefiniowana, możesz skonstruować podstawową pętlę gry. W pętli gry są wykonywane następujące kroki:
Pobierz wszystkich graczy.
Przypisz jednego gracza do tury, a drugiego do oczekiwania na ruch pierwszego gracza.
Wykonuj pętlę tak długo, aż wygra jeden z graczy, przy czym mają oni swoje tury na przemian.
W tym celu do klasy game_manager dodaj następującą funkcję GameLoop:
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]):
Rozpoczynanie rozgrywki
Ostatnią rzeczą do zrobienia jest przypisanie obiektów graczy do każdego gracza i rozpoczęcie pętli gry. Aby to zautomatyzować, do funkcji OnBegin dodaj wywołania funkcji AssignPlayerObjects i GameLoop:
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
AssignPlayerObjects()
GameLoop()Podsumowanie
Podsumowując, na tej stronie zaprezentowano następujące kroki:
Zdefiniuj ruchy w grze.
Skonstruuj pętlę gry.
Określ, kiedy jest spełniony warunek zwycięstwa.
Istnieje jeszcze wiele czynności, które możesz wykonać, aby ta przygoda była unikatowa. Obejmuje to:
Projektowanie i implementowanie interfejsu użytkownika.
Definiowanie, kiedy i jak ruchy gracza są sygnalizowane menedżerowi gry.
Projektowanie świata gry i scenerii.
Tworzenie efektów ataku i ujawnienia.
Dodawanie muzyki i dekoracji.
Dowolnie korzystaj z tych podstawowych klas rozgrywki, zmieniaj ich konstrukcję, używaj ich fragmentów i dostosowuj je do swoich potrzeb.
Pliki
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