In quest'ultimo passaggio del tutorial Prova a tempo: A caccia di pizza, troverai il codice completo del gioco e idee per personalizzare ulteriormente l'esperienza di gioco a tua discrezione.
Codice completo
In questo progetto sono presenti più file di Verse:
- countdown_timer.verse: Per il codice completo del file, vedi Timer del conto alla rovescia personalizzato.
- game_coordinator_device.verse: Per il codice completo del file, vedi di seguito.
- objective_marker.verse: Per il codice completo del file, vedi il tutorial Indicatore di obiettivo mobile.
- pickup_delivery_zone.verse:: Per il codice completo del file, vedi di seguito.
- score_manager.verse: Per il codice completo del file, vedi di seguito.
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 }
# Tag delle zone di gioco
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{}
# Per quanto tempo il timer del conto alla rovescia inizierà il conto alla rovescia.
@editable
InitialCountdownTime<public> : float = 30.0
@editable
EndGame<public> : end_game_device = end_game_device{}
# Quanti secondi aggiungere al timer del conto alla rovescia quando viene consegnato un oggetto raccoglibile.
@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
# Esegue la mappatura di quanti punti vale un oggetto raccoglibile, in base al suo livello di raccolta.
PointsForPickupLevel<public> : []int = array{1, 2, 3}
OnBegin<override>()<suspends> : void =
FindPlayer()
SetupZones()
# Dopo essere entrato nel veicolo, il giocatore potrebbe uscirne in qualsiasi momento
# vogliamo rilevarlo ogni volta che accade per farlo rientrare nel veicolo.
VehicleSpawner.AgentExitsVehicleEvent.Subscribe(HandlePlayerExitsVehicle)
# Vogliamo ricevere una notifica solo la prima volta che il giocatore entra nel veicolo per iniziare la partita.
# StartGameOnPlayerEntersVehicle attende l'evento e avvia il loop di gameplay.
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 =
# Poiché si tratta di un'esperienza per giocatore singolo, il primo giocatore (0)
# deve essere l'unico disponibile.
Playspace := Self.GetPlayspace()
if (FirstPlayer := Playspace.GetPlayers()[0]):
set MaybePlayer = option{FirstPlayer}
Logger.Print("Giocatore trovato")
else:
# Registra un errore se non riusciamo a trovare un giocatore.
# Questo non deve accadere perché almeno un giocatore è sempre presente.
Logger.Print("Impossibile trovare un giocatore valido", ?Level := log_level.Error)
SetupZones<private>() : void =
# Esiste un solo tipo di zona di consegna, in quanto non è ridimensionabile in base al livello di difficoltà.
DeliveryZoneSelector.InitZones(delivery_zone_tag{})
# Utilizziamo i tag di gameplay per selezionare le zone (rappresentate dai dispositivi), in base al loro livello di difficoltà.
# L'utilizzo di un array rende più semplice la modifica dei livelli di difficoltà: possiamo aggiungere altri
# livelli, aumentare/diminuire la loro granularità o cambiarne l'ordine senza toccare il codice.
# Crea un tagged_zone_selector per ogni tag di livello di difficoltà, in modo che tutti i dispositivi con lo stesso tag (cioè lo stesso livello di difficoltà)
# finiscano nel medesimo pool di selezione.
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("Il giocatore è entrato nel veicolo")
set MaybePlayer = option{player[VehiclePlayer]}
StartGame()
HandlePlayerExitsVehicle<private>(VehiclePlayer : agent) : void =
Logger.Print("Il giocatore è uscito dal veicolo. Riassegnazione del giocatore al veicolo")
VehicleSpawner.AssignDriver(VehiclePlayer)
StartGame<private>()<suspends> : void =
Logger.Print("Tentativo di avvio della partita...")
<# Costruiamo un nuovo countdown_timer che, una volta avviato, eseguirà il conto alla rovescia a partire da InitialCountdownTime.
Costruisci anche un nuovo score_manager che tenga traccia del punteggio del giocatore e del livello di raccolta.
Gli elementi countdown_timer e score_manager richiedono un giocatore a cui mostrare le relative interfacce utente.
A questo punto, dovremmo avere un giocatore valido: quello che è entrato nel veicolo, attivando l'avvio della partita. #>
if (ValidPlayer := MaybePlayer?):
Logger.Print("Giocatore valido, avvio della partita...")
set ScoreManager = MakeScoreManager(ValidPlayer, ScoreManagerDevice)
ScoreManager.AddScoreManagerToUI()
set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer)
CountdownTimer.StartCountdown()
# Aspettiamo che il conto alla rovescia termini.
# Allo stesso tempo, gestiamo anche il loop di gioco di raccolta e consegna che costituisce il nucleo del gameplay.
race:
HandleCountdownEnd(ValidPlayer)
PickupDeliveryLoop()
else:
Logger.Print("Impossibile trovare un giocatore valido. Avvio della partita annullato", ?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(){}
<# Esegui il rinvio della disabilitazione del MapIndicator, in modo che la chiusura del PickupDeliveryLoop comporti sempre la disabilitazione dell'indicatore.
Defer viene eseguito anche se PickupDeliveryLoop viene annullato. #>
defer:
if (ValidPlayer := MaybePlayer?):
PickupMarker.MapIndicator.DeactivateObjectivePulse(ValidPlayer)
PickupMarker.MapIndicator.Disable()
PickupMarker.MapIndicator.Enable()
loop:
var PickupLevel : int = 0
var IsFirstPickup : logic = true
<# Ogni volta che il loop si riavvia, dobbiamo reimpostare l'interfaccia utente del livello di raccolta attraverso ScoreManager.
Nell'interfaccia utente, il livello di raccolta parte da 1 (non da 0). Alcuni giocatori si confonderanno, se inizia da 0.
L'indice parte da 0, quindi PickupLevel=0 corrisponde al livello 1 nell'interfaccia utente. #>
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)
<# Questo è l'unico rinvio necessario per ogni PickupZone attivata. Disattiverà la prima PickupZone alla fine di ogni loop esterno,
oppure disattiverà qualsiasi PickupZone successiva. Questo avviene perché l'espressione viene valutata alla fine, quando la variabile PickupZone è stata legata a una zona più recente. #>
defer:
PickupZone.DeactivateZone()
PickupZone.ZoneCompletedEvent.Await()
Logger.Print("Raccolto", ?Level:=log_level.Normal)
<# Rimuoviamo gli oggetti raccoglibili pizza dall'inventario del giocatore per evitare di impilarli e farli cadere a terra, una volta che la pila è al completo. #>
if (RemovingPlayer := MaybePlayer?):
PizzaRemover.Remove(RemovingPlayer)
# Dopo la prima raccolta, possiamo attivare la zona di consegna.
if (IsFirstPickup?):
set IsFirstPickup = false
FirstPickupZoneCompletedEvent.Signal()
if (PickupPoints := PointsForPickupLevel[PickupLevel]):
ScoreManager.UpdatePendingScore(PickupPoints)
# Aggiorna il livello di raccolta e ScoreManager.
if (PickupLevel < MaxPickupLevel):
set PickupLevel += 1
ScoreManager.UpdatePickupLevel(PickupLevel + 1)
else:
Logger.Print("PickupZone successiva da selezionare non trovata.", ?Level := log_level.Error)
return # Errore in uscita dal PickupDeliveryLoop
block:
FirstPickupZoneCompletedEvent.Await()
if (DeliveryZone := DeliveryZoneSelector.SelectNext[]):
DeliveryZone.ActivateZone()
# Rinviamo la disattivazione delle zone in modo che l'annullamento di PickupDeliveryLoop finisca per disattivare anche le zone di consegna attive.
defer:
Logger.Print("Disattivazione della zona di consegna.", ?Level:=log_level.Normal)
DeliveryZone.DeactivateZone()
DeliveryZone.ZoneCompletedEvent.Await()
Logger.Print("Consegnato", ?Level:=log_level.Normal)
PointsCommitted := ScoreManager.AddPendingScoreToTotalScore()
BonusTime : float = DeliveryBonusSeconds * PointsCommitted
CountdownTimer.AddRemainingTime(BonusTime)
else:
Logger.Print("DeliveryZone successiva da selezionare non trovata.", ?Level:=log_level.Error)
return # Errore in uscita dal 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 }
<# Una zona è un'area della mappa (rappresentata da un dispositivo) che può essere attivata/disattivata e che fornisce eventi
per segnalare quando la zona è stata "Completata" (non può più essere completata fino all'attivazione successiva).
La zona "Completata" dipende dal tipo di dispositivo (ActivatorDevice) della zona.
Utilizzo consigliato: 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()
<# Attiva la zona.
Devi abilitare i dispositivi e tutti gli indicatori visivi della zona. #>
ActivateZone<public>() : void =
# La zona base può gestire le zone definite come generatori oggetti o aree di cattura.
# Prova a eseguire il cast di ogni tipo per vedere di che zona si tratta.
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}) }
<# Disattiva la zona.
Devi disabilitare i dispositivi e tutti gli indicatori visivi della zona. #>
DeactivateZone<public>() : void =
if (CaptureArea := capture_area_device[ActivatorDevice]):
CaptureArea.Disable()
else if (ItemSpawner := item_spawner_device[ActivatorDevice]):
ItemSpawner.Disable()
ZoneDeactivatedEvent.Signal()
<# Questo evento è necessario per terminare la coroutine WaitForZoneCompleted, se la zona viene disattivata senza essere completata. #>
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
# Il selettore tagged_zone crea zone basate su trigger taggati con il tag passato a InitZones.
tagged_zone_selector<public> := class:
var Zones<protected> : []base_zone = array{}
InitZones<public>(ZoneTag : tag) : void =
<# Al momento della creazione di un selettore di zona, individua tutte le zone disponibili
e memorizzale nella cache per non sprecare tempo nella ricerca di dispositivi taggati
a ogni selezione della zona successiva. #>
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:
<# Poiché non ricreeremo canvas durante la durata di gestione punteggio, esegui questa operazione una volta
ogni volta che viene creato un oggetto di questo tipo. #>
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()
<# Aggiunge il PendingScore al TotalGameScore e reimposta il PendingScore a 0.
Restituisce il numero totale di Punti di raccolta aggiunti. #>
AddPendingScoreToTotalScore<public>() : int =
set TotalGameScore += PendingScore
defer:
set PendingScore = 0
UpdateUI()
PendingScore
<# Aggiunge la quantità di punti indicata ai punti in sospeso. #>
UpdatePendingScore<public>(Points : int) : void =
set PendingScore += Points
UpdateUI()
UpdatePickupLevel<public>(Level : int) : void =
set PickupLevel = Level
UpdateUI()
<# Assegna il punteggio al giocatore con ScoreManagerDevice, eseguendone l'attivazione. #>
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 = "Livello di raccolta: {InLevel}"
PendingScoreText<private><localizes>(InPoints : int) : message = "Punti in sospeso: {InPoints}"
TotalGameScoreText<private><localizes>(InPoints : int) : message = "Punti totali: {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))
In autonomia
Dopo aver completato questa guida, hai imparato a usare Verse per creare il gioco completo della prova a tempo A caccia di pizza.
Utilizzando quanto appreso, prova a fare quanto segue:
- Aggiungi altri livelli di zona di raccolta.
- Aggiungi diversi tipi di zone di consegna. Estendi la classe
base_zone
, in modo che il giocatore debba attivare qualche altro dispositivo, come un pulsante, per completare la zona. - Fai uscire il giocatore dal veicolo e fagli completare un breve percorso a ostacoli a piedi per consegnare la pizza.
- Attiva più zone contemporaneamente.
- Modifica i criteri di selezione della zona in base alla distanza dal giocatore.