W tym ostatnim kroku samouczka Próba czasu: Pogoń za pizzą (Time Trial: Pizza Pursuit) znajdziesz kompletny kod gry oraz pomysły na własne eksperymenty z tą grą.
Kompletny kod
W tym projekcie istnieje wiele plików Verse:
- countdown_timer.verse: Kompletny kod pliku, patrz Niestandardowy licznik czasu odliczania.
- game_coordinator_device.verse: Kompletny kod pliku, patrz niżej.
- objective_marker.verse: Kompletny kod pliku, patrz samouczek Ruchomy znacznik celu.
- pickup_delivery_zone.verse: Kompletny kod pliku, patrz niżej.
- score_manager.verse: Kompletny kod pliku, patrz niżej.
game_coordinate_device.verse
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
using { /Fortnite.com/Vehicles }
using { /Fortnite.com/Characters }
using { /Fortnite.com/Playspaces }
using { /Verse.org/Native }
using { /Verse.org/Random }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/Curves }
using { /Verse.org/Simulation/Tags }
# Tagi stref gry
pickup_zone_tag<public> := class(tag):
pickup_zone_level_1_tag<public> := class(pickup_zone_tag):
pickup_zone_level_2_tag<public> := class(pickup_zone_tag):
pickup_zone_level_3_tag<public> := class(pickup_zone_tag):
delivery_zone_tag<public> := class(tag):
log_pizza_pursuit<internal> := class(log_channel){}
game_coordinator_device<public> := class(creative_device):
@editable
VehicleSpawner<public> : vehicle_spawner_atk_device = vehicle_spawner_atk_device{}
# Od jakiej wartości licznik czasu odliczania rozpocznie odliczanie w dół.
@editable
InitialCountdownTime<public> : float = 30.0
@editable
EndGame<public> : end_game_device = end_game_device{}
# Ile sekund dodać do licznika czasu odliczania, gdy odebrany przedmiot zostanie dostarczony.
@editable
DeliveryBonusSeconds<public> : float = 20.0
@editable
PickupMarker<public> : objective_marker = objective_marker{}
@editable
ScoreManagerDevice<public> : score_manager_device = score_manager_device{}
@editable
PizzaRemover<public> : item_remover_device = item_remover_device{}
@editable
# Mapuje liczbę punktów, jaka przysługuje za odbiór, w zależności od poziomu odbioru.
PointsForPickupLevel<public> : []int = array{1, 2, 3}
OnBegin<override>()<suspends> : void =
FindPlayer()
SetupZones()
# Po wejściu do pojazdu gracz może go w dowolnej chwili opuścić.
# Chcemy wykrywać każde takie zdarzenie i umieszczać gracza z powrotem w pojeździe.
VehicleSpawner.AgentExitsVehicleEvent.Subscribe(HandlePlayerExitsVehicle)
# Chcemy jedynie otrzymać powiadomienie, gdy gracz wejdzie do pojazdu po raz pierwszy, aby rozpocząć grę.
# Funkcja StartGameOnPlayerEntersVehicle będzie oczekiwać na to zdarzenie, a następnie uruchomi pętlę rozgrywki.
StartGameOnPlayerEntersVehicle()
Logger<private> : log = log{Channel := log_pizza_pursuit}
var MaybePlayer<private> : ?player = false
var CountdownTimer<private> : countdown_timer = countdown_timer{}
var ScoreManager<private> : score_manager = score_manager{}
DeliveryZoneSelector<private> : tagged_zone_selector = tagged_zone_selector{}
var PickupZones<private> : []tagged_zone_selector = array{}
FindPlayer<private>() : void =
# Gra odbywa się w trybie jednego gracza, dlatego pierwszy gracz (0)
# powinien być jedynym dostępnym graczem.
Playspace := Self.GetPlayspace()
if (FirstPlayer := Playspace.GetPlayers()[0]):
set MaybePlayer = option{FirstPlayer}
Logger.Print("Znaleziono gracza")
else:
# Zarejestruj błąd, jeśli nie można znaleźć gracza.
# To nie powinno się zdarzyć, ponieważ co najmniej jeden gracz jest zawsze obecny.
Logger.Print("Nie znaleziono prawidłowego gracza", ?Level := log_level.Error)
SetupZones<private>() : void =
# Istnieje tylko jeden typ strefy dostarczania, ponieważ nie skaluje się ona według poziomu trudności.
DeliveryZoneSelector.InitZones(delivery_zone_tag{})
# Strefy (reprezentowane przez urządzenia) wybiera się za pomocą tagów rozgrywki w oparciu o poziom ich trudności.
# Zastosowanie tablicy ułatwia modyfikowanie poziomów trudności: można dodać więcej
# poziomów, zwiększać lub zmniejszać poziom ich szczegółowości bądź zmieniać ich kolejność bez ingerencji w kod.
# Utwórz jeden element tagged_zone_selector dla każdego tagu poziomu trudności, aby wszystkie urządzenia oznaczone tym samym tagiem (tj. o takim samym poziomie trudności)
# trafiły do tej samej puli wyboru.
LevelTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}}
set PickupZones = for (ZoneTag : LevelTags):
NewZone := tagged_zone_selector{}
NewZone.InitZones(ZoneTag)
NewZone
StartGameOnPlayerEntersVehicle<private>()<suspends> : void =
VehiclePlayer := VehicleSpawner.AgentEntersVehicleEvent.Await()
Logger.Print("Gracz wszedł do pojazdu")
set MaybePlayer = option{player[VehiclePlayer]}
StartGame()
HandlePlayerExitsVehicle<private>(VehiclePlayer : agent) : void =
Logger.Print("Gracz opuścił pojazd. Ponowne przypisywanie gracza do pojazdu")
VehicleSpawner.AssignDriver(VehiclePlayer)
StartGame<private>()<suspends> : void =
Logger.Print("Próba rozpoczęcia gry...")
<# Konstruujemy nowy licznik czasu odliczania countdown_timer, który po uruchomieniu zacznie odliczać w dół od wartości InitialCountdownTime.
Utwórz również nową klasę score_manager, która będzie śledzić wynik gracza oraz poziom odbioru.
Klasy countdown_timer i score_manager wymagają gracza, któremu można wyświetlić UI.
Na tym etapie powinniśmy już mieć prawidłowego gracza: tego, który wszedł do pojazdu, aktywując rozpoczęcie gry. #>
if (ValidPlayer := MaybePlayer?):
Logger.Print("Prawidłowy gracz, rozpoczynanie gry...")
set ScoreManager = MakeScoreManager(ValidPlayer, ScoreManagerDevice)
ScoreManager.AddScoreManagerToUI()
set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer)
CountdownTimer.StartCountdown()
# Czekamy na zakończenie odliczania.
# Jednocześnie uruchamiamy również pętlę gry dotyczącą odbioru i dostarczania, która stanowi główną rozgrywkę.
race:
HandleCountdownEnd(ValidPlayer)
PickupDeliveryLoop()
else:
Logger.Print("Nie znaleziono prawidłowego gracza. Przerwanie uruchamiania gry", ?Level := log_level.Error)
HandleCountdownEnd<private>(InPlayer : player)<suspends> : void =
TotalTime := CountdownTimer.CountdownEndedEvent.Await()
ScoreManager.AwardScore()
EndGame.Activate(InPlayer)
PickupDeliveryLoop<private>()<suspends> : void =
PickupZonesTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}}
MaxPickupLevel := PickupZonesTags.Length - 1
FirstPickupZoneCompletedEvent := event(){}
<# Opóźnij wyłączenie urządzenia MapIndicator tak, aby zakończenie pętli PickupDeliveryLoop zawsze kończyło się wyłączeniem znacznika.
Wyrażenie defer wykonywane jest także w przypadku anulowania pętli PickupDeliveryLoop. #>
defer:
if (ValidPlayer := MaybePlayer?):
PickupMarker.MapIndicator.DeactivateObjectivePulse(ValidPlayer)
PickupMarker.MapIndicator.Disable()
PickupMarker.MapIndicator.Enable()
loop:
var PickupLevel : int = 0
var IsFirstPickup : logic = true
<# Za każdym razem, gdy pętla zostanie ponownie uruchomiona, UI poziomu odbioru powinien być resetowany przez ScoreManager.
Początkowym poziomem odbioru w UI jest 1 (nie 0). Jeśli odliczanie rozpocznie się od 0, niektórzy gracze będą zdezorientowani.
Indeksujemy od 0, zatem PickupLevel=0 oznacza poziom 1 w UI. #>
ScoreManager.UpdatePickupLevel(PickupLevel + 1)
race:
loop:
if (PickupZone:base_zone = PickupZones[PickupLevel].SelectNext[]):
PickupZone.ActivateZone()
Sleep(0.0)
PickupMarker.MoveMarker(PickupZone.GetTransform(), ?OverTime := 0.0)
if (ValidPlayer := MaybePlayer?):
PickupMarker.MapIndicator.ActivateObjectivePulse(ValidPlayer)
<# To jedyne wyrażenie defer, jakiego potrzebujemy dla dowolnej aktywowanej strefy PickupZone. Spowoduje ono dezaktywowanie pierwszej strefy PickupZone na końcu każdej pętli zewnętrznej
lub dezaktywację dowolnej późniejszej strefy PickupZone. Dzieje się tak dlatego, że wyrażenie jest poddawane ocenie na końcu, gdy zmienna PickupZone została już powiązana z nowszą strefą. #>
defer:
PickupZone.DeactivateZone()
PickupZone.ZoneCompletedEvent.Await()
Logger.Print("Odebrano", ?Level:=log_level.Normal)
<# Usuwamy odebrane przez gracza pizze z jego ekwipunku, aby uniknąć ich kumulacji i konieczności upuszczenia ich na ziemię po przepełnieniu stosu. #>
if (RemovingPlayer := MaybePlayer?):
PizzaRemover.Remove(RemovingPlayer)
# Po pierwszym odbiorze możemy włączyć strefę dostarczania.
if (IsFirstPickup?):
set IsFirstPickup = false
FirstPickupZoneCompletedEvent.Signal()
if (PickupPoints := PointsForPickupLevel[PickupLevel]):
ScoreManager.UpdatePendingScore(PickupPoints)
# Zaktualizuj poziom odbioru i wartość ScoreManager.
if (PickupLevel < MaxPickupLevel):
set PickupLevel += 1
ScoreManager.UpdatePickupLevel(PickupLevel + 1)
else:
Logger.Print("Nie znaleziono następnej strefy PickupZone do wybrania", ?Level := log_level.Error)
return # Błąd pętli PickupDeliveryLoop
block:
FirstPickupZoneCompletedEvent.Await()
if (DeliveryZone := DeliveryZoneSelector.SelectNext[]):
DeliveryZone.ActivateZone()
# Opóźniamy dezaktywację strefy, aby anulowanie pętli PickupDeliveryLoop kończyło się także dezaktywacją każdej aktywnej strefy dostarczania.
defer:
Logger.Print("Dezaktywacja strefy dostarczania.", ?Level:=log_level.Normal)
DeliveryZone.DeactivateZone()
DeliveryZone.ZoneCompletedEvent.Await()
Logger.Print("Dostarczono", ?Level:=log_level.Normal)
PointsCommitted := ScoreManager.AddPendingScoreToTotalScore()
BonusTime : float = DeliveryBonusSeconds * PointsCommitted
CountdownTimer.AddRemainingTime(BonusTime)
else:
Logger.Print("Nie znaleziono następnej strefy DeliveryZone do wybrania.", ?Level:=log_level.Error)
return # Błąd pętli PickupDeliveryLoop
pickup_delivery_zone.verse
using { /Verse.org/Simulation }
using { /Verse.org/Random }
using { /Verse.org/Concurrency }
using { /Verse.org/Simulation/Tags }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /Fortnite.com/Devices }
<# Strefa jest obszarem mapy (reprezentowanym przez urządzenie), który można aktywować/dezaktywować oraz który jest źródłem zdarzeń
sygnalizowanych, gdy strefa zostanie "ukończona" (nie można ukończyć jej po raz kolejny do czasu ponownej aktywacji).
"Ukończona" strefa zależy od typu urządzenia (ActivatorDevice) użytego w tej strefie.
Sugerowane użycie: ActivateZone() -> ZoneCompletedEvent.Await() -> DeactivateZone() #>
base_zone<public> := class:
ActivatorDevice<public> : creative_object_interface
ZoneCompletedEvent<public> : event(base_zone) = event(base_zone){}
GetTransform<public>() : transform =
ActivatorDevice.GetTransform()
<# Aktywuje strefę.
W tym miejscu włącz urządzenia i wszelkie wskaźniki wizualne strefy. #>
ActivateZone<public>() : void =
# Strefa bazowa może obsługiwać strefy zdefiniowane jako generatory przedmiotów lub strefy punktowane.
# Wypróbuj każdy typ i wykonaj do niego rzutowanie, aby sprawdzić, z czym mamy do czynienia.
if (CaptureArea := capture_area_device[ActivatorDevice]):
CaptureArea.Enable()
spawn { WaitForZoneCompleted(option{CaptureArea.AgentEntersEvent}) }
else if (ItemSpawner := item_spawner_device[ActivatorDevice]):
ItemSpawner.Enable()
spawn { WaitForZoneCompleted(option{ItemSpawner.ItemPickedUpEvent}) }
<# Dezaktywuje strefę.
W tym miejscu wyłącz urządzenia i wszelkie wskaźniki wizualne strefy. #>
DeactivateZone<public>() : void =
if (CaptureArea := capture_area_device[ActivatorDevice]):
CaptureArea.Disable()
else if (ItemSpawner := item_spawner_device[ActivatorDevice]):
ItemSpawner.Disable()
ZoneDeactivatedEvent.Signal()
<# Zdarzenie to jest konieczne do zakończenia koprocedury WaitForZoneCompleted, jeśli strefa zostanie dezaktywowana bez ukończenia. #>
ZoneDeactivatedEvent<protected> : event() = event(){}
WaitForZoneCompleted<private>(ZoneDeviceCompletionEventOpt : ?awaitable(agent))<suspends> : void =
if (DeviceEvent := ZoneDeviceCompletionEventOpt?):
race:
block:
DeviceEvent.Await()
ZoneCompletedEvent.Signal(Self)
ZoneDeactivatedEvent.Await()
MakeBaseZone<constructor><public>(InActivatorDevice : creative_object_interface) := base_zone:
ActivatorDevice := InActivatorDevice
# tagged_zone_selector tworzy strefy w oparciu o aktywatory oznaczone tagiem przekazanym do InitZones.
tagged_zone_selector<public> := class:
var Zones<protected> : []base_zone = array{}
InitZones<public>(ZoneTag : tag) : void =
<# Przy tworzeniu wybieracza stref znajdź wszystkie dostępne strefy
i zapisz je w pamięci podręcznej, abyśmy nie tracili czasu na wyszukiwanie otagowanych urządzeń
za każdym razem, gdy wybierana jest kolejna strefa. #>
ZoneDevices := GetCreativeObjectsWithTag(ZoneTag)
set Zones = for (ZoneDevice : ZoneDevices):
MakeBaseZone(ZoneDevice)
SelectNext<public>()<transacts><decides> : base_zone =
Zones[GetRandomInt(0, Zones.Length-1)]
score_manager.verse
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/UI }
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
MakeScoreManager<constructor><public>(InPlayer : player, InScoreManagerDevice : score_manager_device) := score_manager:
MaybePlayer := option{InPlayer}
MaybePlayerUI := option{GetPlayerUI[InPlayer]}
score_manager := class:
<# W czasie życia menedżera wyników nie będziemy ponownie tworzyć kanwy, dlatego należy utworzyć ją raz
przy każdym tworzeniu obiektu tego typu. #>
block:
set Canvas = canvas:
Slots := array:
canvas_slot:
Widget := stack_box:
Orientation := orientation.Vertical
Slots := array:
stack_box_slot:
Widget := TotalGameScoreWidget
stack_box_slot:
Widget := PendingScoreWidget
stack_box_slot:
Widget := PickupLevelWidget
Offsets := margin{ Top:=0.0, Left:=500.0 }
AddScoreManagerToUI<public>() : void =
if (PlayerUI := MaybePlayerUI?):
PlayerUI.AddWidget(Canvas)
UpdateUI()
<# Dodaje wartość wyniku PendingScore do wartości wyniku TotalGameScore i resetuje wartość PendingScore do 0.
Zwraca łączną liczbę dodanych punktów za odbiór. #>
AddPendingScoreToTotalScore<public>() : int =
set TotalGameScore += PendingScore
defer:
set PendingScore = 0
UpdateUI()
PendingScore
<# Dodaje konkretną liczbę punktów do punktów oczekujących. #>
UpdatePendingScore<public>(Points : int) : void =
set PendingScore += Points
UpdateUI()
UpdatePickupLevel<public>(Level : int) : void =
set PickupLevel = Level
UpdateUI()
<# Przyznaje graczowi wynik za pomocą urządzenia ScoreManagerDevice, aktywując to urządzenie. #>
AwardScore<public>() : void =
ScoreManagerDevice.SetScoreAward(TotalGameScore)
if (AwardedPlayer := MaybePlayer?):
ScoreManagerDevice.Activate(AwardedPlayer)
MaybePlayer<internal> : ?player = false
MaybePlayerUI<internal> : ?player_ui = false
ScoreManagerDevice<internal> : score_manager_device = score_manager_device{}
var Canvas<internal> : canvas = canvas{}
TotalGameScoreWidget<internal> : text_block = text_block{}
PendingScoreWidget<internal> : text_block = text_block{}
PickupLevelWidget<internal> : text_block = text_block{}
PickupLevelText<private><localizes>(InLevel : int) : message = "Poziom odbioru: {InLevel}"
PendingScoreText<private><localizes>(InPoints : int) : message = "Punkty oczekujące: {InPoints}"
TotalGameScoreText<private><localizes>(InPoints : int) : message = "Łączna liczba punktów: {InPoints}"
var TotalGameScore<private> : int = 0
var PendingScore<private> : int = 0
var PickupLevel<private> : int = 0
UpdateUI<private>() : void =
if (PlayerUI := MaybePlayerUI?):
PickupLevelWidget.SetText(PickupLevelText(PickupLevel))
PendingScoreWidget.SetText(PendingScoreText(PendingScore))
TotalGameScoreWidget.SetText(TotalGameScoreText(TotalGameScore))
We własnym zakresie
Dzięki tej instrukcji wiesz już, jak wykorzystać język Verse do utworzenia pełnej czasówki Pizza Pursuit.
Wykorzystując zdobytą wiedzę, spróbuj wykonać następujące czynności:
- Dodaj więcej poziomów stref odbioru.
- Dodaj różne typy stref dostarczania. Rozbuduj klasę
base_zone
tak, aby gracz musiał aktywować jakieś inne urządzenie, na przykład przycisk, aby ukończyć strefę. - Wymuś na graczu konieczność wyjścia z wózka i pokonania pieszo krótkiego toru przeszkód w celu dostarczenia pizzy.
- Aktywuj wiele stref równocześnie.
- Moduluj kryteria wyboru strefy na podstawie odległości od gracza.