In diesem letzten Schritt des Tutorials für Time Trial: Pizza Pursuit findest du den vollständigen Code fürs Spiel und weitere Ideen, um das Beispiel zu ändern.
Vollständiger Code
Es gibt mehrere Verse-Dateien in diesem Projekt:
- countdown_timer.verse: Siehe Benutzerdefinierter Countdown-Timer für den vollständigen Code der Datei.
- game_coordinator_device.verse: Siehe unten für den vollständigen Code der Datei.
- objective_marker.verse: Siehe das Tutorial Bewegliche Zielmarkierung für den vollständigen Code der Datei.
- pickup_delivery_zone.verse: Siehe unten für den vollständigen Code der Datei.
- score_manager.verse: Siehe unten für den vollständigen Code der Datei.
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 }
# Spielzonen-Tags
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{}
# Wie lange der Countdown-Timer abwärts zählen soll.
@editable
InitialCountdownTime<public> : float = 30.0
@editable
EndGame<public> : end_game_device = end_game_device{}
# Wie viele Sekunden zum Countdown-Timer hinzugefügt werden sollen, wenn eine Abholung zugestellt wird.
@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
# Zeigt an, wie viele Punkte eine Abholung auf der Grundlage ihres Abhol-Levels wert ist.
PointsForPickupLevel<public> : []int = array{1, 2, 3}
OnBegin<override>()<suspends> : void =
FindPlayer()
SetupZones()
# Nachdem der Spieler das Fahrzeug bestiegen hat, kann er jederzeit aussteigen; wir
# wollen dies jedes Mal erkennen, wenn es passiert, um sie wieder in das Fahrzeug zu setzen.
VehicleSpawner.AgentExitsVehicleEvent.Subscribe(HandlePlayerExitsVehicle)
# Wir wollen nur benachrichtigt werden, wenn der Spieler zum ersten Mal das Fahrzeug besteigt, um das Spiel zu starten.
# StartGameOnPlayerEntersVehicle wartet auf dieses Event und startet dann die Gameplay-Schleife.
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 =
# Da dies ein Einzelspielererlebnis ist, sollte nur der erste Spieler [0]
# verfügbar sein
Playspace := Self.GetPlayspace()
if (FirstPlayer := Playspace.GetPlayers()[0]):
set MaybePlayer = option{FirstPlayer}
Logger.Print("Spieler gefunden")
else:
# Wir protokollieren einen Fehler wenn kein Spieler gefunden wurde.
# Dies sollte nicht passieren, da immer mindestens ein Spieler anwesend ist.
Logger.Print("Kein gültiger Spieler gefunden", ?Level := log_level.Error)
SetupZones<private>() : void =
# Es gibt nur eine Art von Lieferzone, da sie nicht nach Schwierigkeitsgrad gestaffelt sind.
DeliveryZoneSelector.InitZones(delivery_zone_tag{})
# Wir verwenden Gameplay-Tags, um Zonen (die durch Geräte repräsentiert werden) auf der Grundlage ihres Schwierigkeitsgrades auszuwählen.
# Die Verwendung eines Arrays erleichtert die Änderung von Schwierigkeitsstufen: Wir können weitere
# Level hinzufügen, ihre Granularität erhöhen/verringern oder ihre Reihenfolge ändern, ohne den Code zu verändern.
# Erstelle einen tagged_zone_selector für jeden Schwierigkeitsgrad-Tag, damit alle Geräte mit demselben Tag (d.h. demselben Schwierigkeitsgrad)
# im selben Auswahlpool landen.
LevelTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}}
set PickupZones = für (ZoneTag : LevelTags):
NewZone := tagged_zone_selector{}
NewZone.InitZones(ZoneTag)
NewZone
StartGameOnPlayerEntersVehicle<private>()<suspends> : void =
VehiclePlayer := VehicleSpawner.AgentEntersVehicleEvent.Await()
Logger.Print("Spieler hat das Fahrzeug bestiegen")
set MaybePlayer = option{player[VehiclePlayer]}
StartGame()
HandlePlayerExitsVehicle<private>(VehiclePlayer : agent) : void =
Logger.Print("Spieler ist ausgestiegen. Spieler wird dem Fahrzeug neu zugeordnet")
VehicleSpawner.AssignDriver(VehiclePlayer)
StartGame<private>()<suspends> : void =
Logger.Print("Spiel wird versucht zu starten...")
<# Wir konstruieren einen neuen countdown_timer, der ab InitialCountdownTime herunterzählt, sobald er gestartet wurde.
Erstelle außerdem einen neuen score_manager, der den Punktestand des Spielers und das Abhol-Level festhält.
Der countdown_timer und score_manager benötigen einen Spieler, dem sie ihre UI zeigen.
Wir sollten jetzt einen gültigen Spieler haben: derjenige, der in das Fahrzeug eingestiegen ist und damit den Spielstart ausgelöst hat. #>
if (ValidPlayer := MaybePlayer?):
Logger.Print("Gültiger Spieler, Spiel wird gestartet...")
set ScoreManager = MakeScoreManager(ValidPlayer, ScoreManagerDevice)
ScoreManager.AddScoreManagerToUI()
set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer)
CountdownTimer.StartCountdown()
# Wir warten auf das Ende des Countdowns.
# Gleichzeitig führen wir auch die Abhol- und Lieferschleife aus, die das Kernstück des Spiels darstellt.
race:
HandleCountdownEnd(ValidPlayer)
PickupDeliveryLoop()
else:
Logger.Print("Kein gültiger Spieler gefunden. Spielstart wird abgebrochen", ?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(){}
<# Die Deaktivierung des MapIndicator aufschieben, so dass das Beenden der PickupDeliveryLoop immer mit der Deaktivierung des Markers endet.
Der Aufschub wird auch ausgeführt, wenn die PickupDeliveryLoop abgebrochen wird. #>
defer:
if (ValidPlayer := MaybePlayer?):
PickupMarker.MapIndicator.DeactivateObjectivePulse(ValidPlayer)
PickupMarker.MapIndicator.Disable()
PickupMarker.MapIndicator.Enable()
loop:
var PickupLevel : int = 0
var IsFirstPickup : logic = true
<# Jedes Mal, wenn die Schleife neu startet, sollten wir die Abhol-Level-UI über die Punktanpassung zurücksetzen.
Das Abhol-Level in der Benutzeroberfläche beginnt bei 1 (nicht bei 0). Einige Spieler werden verwirrt sein, wenn er bei 0 beginnt.
Der Index beginnt bei 0, also ist PickupLevel=0 Level 1 in der 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)
<# Dies ist der einzige Aufschub, den wir für jede aktivierte PickupZone benötigen. Es wird entweder die erste PickupZone am Ende jeder äußeren Schleife deaktiviert,
oder es wird eine spätere PickupZone deaktiviert. Das liegt daran, dass der Ausdruck am Ende ausgewertet wird, wenn die Variable PickupZone an eine neuere Zone gebunden wurde. #>
defer:
PickupZone.DeactivateZone()
PickupZone.ZoneCompletedEvent.Await()
Logger.Print("Abgeholt", ?Level:=log_level.Normal)
<# Wir entfernen Pizzas aus dem Inventar des Spielers, um zu vermeiden, dass sie gestapelt werden und zu Boden fallen, sobald der Stapel voll ist. #>
if (RemovingPlayer := MaybePlayer?):
PizzaRemover.Remove(RemovingPlayer)
# Nach der ersten Abholung können wir die Lieferzone aktivieren.
if (IsFirstPickup?):
set IsFirstPickup = false
FirstPickupZoneCompletedEvent.Signal()
if (PickupPoints := PointsForPickupLevel[PickupLevel]):
ScoreManager.UpdatePendingScore(PickupPoints)
# Aktualisiere den Abhol-Level und die Punktanpassung.
if (PickupLevel < MaxPickupLevel):
set PickupLevel += 1
ScoreManager.UpdatePickupLevel(PickupLevel + 1)
else:
Logger.Print("Kann die nächste auszuwählende PickupZone nicht finden.", ?Level := log_level.Error)
return # Fehler aus dem PickupDeliveryLoop
block:
FirstPickupZoneCompletedEvent.Await()
if (DeliveryZone := DeliveryZoneSelector.SelectNext[]):
DeliveryZone.ActivateZone()
# Wir verschieben die Deaktivierung der Zone, so dass das Abbrechen von PickupDeliveryLoop auch die Deaktivierung einer aktiven Lieferzone zur Folge hat.
defer:
Logger.Print("Lieferzone wird deaktiviert.", ?Level:=log_level.Normal)
DeliveryZone.DeactivateZone()
DeliveryZone.ZoneCompletedEvent.Await()
Logger.Print("Abgeliefert", ?Level:=log_level.Normal)
PointsCommitted := ScoreManager.AddPendingScoreToTotalScore()
BonusTime : float = DeliveryBonusSeconds * PointsCommitted
CountdownTimer.AddRemainingTime(BonusTime)
else:
Logger.Print("Nächste auszuwählende Lieferzone kann nicht gefunden werden.", ?Level:=log_level.Error)
return # Fehler aus dem 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 }
<# Eine Zone ist ein Bereich der Karte (dargestellt durch ein Gerät), der aktiviert/deaktiviert werden kann und der Events bereitstellt,
um zu signalisieren, dass die Zone "abgeschlossen" wurde (kann bis zur nächsten Aktivierung nicht mehr abgeschlossen werden).
Zone "Abgeschlossen" hängt vom Gerätetyp (ActivatorDevice) für die Zone ab.
Empfohlene Verwendung: 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()
<# Aktiviert die Zone.
Hier solltest du Geräte und alle visuellen Indikatoren für die Zone aktivieren. #>
ActivateZone<public>() : void =
# Die Basiszone kann Zonen handhaben, die als Gegenstands-Spawnpunkte oder Eroberungsgebiete definiert sind.
# Versuche, auf jeden Typ zu casten, um zu sehen, mit welchem wir es zu tun haben.
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}) }
<# Deaktiviert die Zone.
Hier solltest du Geräte und alle visuellen Indikatoren für die Zone deaktivieren. #>
DeactivateZone<public>() : void =
if (CaptureArea := capture_area_device[ActivatorDevice]):
CaptureArea.Disable()
else if (ItemSpawner := item_spawner_device[ActivatorDevice]):
ItemSpawner.Disable()
ZoneDeactivatedEvent.Signal()
<# Dieses Event ist notwendig, um die WaitForZoneCompleted-Coroutine zu beenden, wenn die Zone deaktiviert wird, ohne dass sie abgeschlossen wurde. #>
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
# Der tagged_zone_selector erstellt Zonen auf der Grundlage von Triggern, die mit dem an InitZones übergebenen Tag gekennzeichnet sind.
tagged_zone_selector<public> := class:
var Zones<protected> : []base_zone = array{}
InitZones<public>(ZoneTag : tag) : void =
<# Beim Erstellen eines Zonenselektors alle verfügbaren Zonen finden
und zwischenspeichern, damit wir nicht jedesmal Zeit mit der Suche nach gekennzeichneten Geräten verschwenden,
wenn die nächste Zone ausgewählt wird. #>
ZoneDevices := GetCreativeObjectsWithTag(ZoneTag)
set Zones = für (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:
<# Da wir den Canvas während der Lebensdauer der Punkteanpassung nicht neu erstellen werden, machen wir das einmal,
wenn ein Objekt dieses Typs erstellt wird. #>
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()
<# Fügt PendingScore zu TotalGameScore hinzu und setzt PendingScore auf 0 zurück.
Gibt die Gesamtzahl der hinzugefügten Abholpunkte zurück. #>
AddPendingScoreToTotalScore<public>() : int =
set TotalGameScore += PendingScore
defer:
set PendingScore = 0
UpdateUI()
PendingScore
<# Fügt die angegebene Anzahl von Punkten zu den ausstehenden Punkten hinzu. #>
UpdatePendingScore<public>(Points : int) : void =
set PendingScore += Points
UpdateUI()
UpdatePickupLevel<public>(Level : int) : void =
set PickupLevel = Level
UpdateUI()
<# Vergibt den Punktestand an den Spieler mit dem Punkteanpassung-Gerät, indem es aktiviert wird. #>
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 = "Abholpunkt-Level: {InLevel}"
PendingScoreText<private><localizes>(InPoints : int) : message = "Ausstehende Punkte: {InPoints}"
TotalGameScoreText<private><localizes>(InPoints : int) : message = "Gesamtpunkte: {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))
Auf eigene Faust
Wenn du diese Anleitung fertiggestellt hast, weißt du, wie du das komplette Time-Trial-Spiel Pizza Pursuit in Verse erstellen kannst.
Probiere mit dem, was du gelernt hast, Folgendes aus:
- Füge weitere Abhol-Zonen-Level hinzu.
- Füge verschiedene Arten von Lieferzonen hinzu. Erweitere die Klasse
base_zoneso, dass der Spieler ein anderes Gerät, z.B. einen Knopf, aktivieren muss, um die Zone abzuschließen. - Du kannst den Spieler auch vom Kart absteigen und einen kurzen Hindernisparcours absolvieren lassen, um die Pizza auszuliefern.
- Aktiviere mehrere Zonen zur gleichen Zeit.
- Moduliere die Kriterien für die Zonenauswahl in Abhängigkeit von der Entfernung zum Spieler.