Un loop di gioco è un codice che viene eseguito ripetutamente (loop) per rispondere agli input (di solito il giocatore che interagisce con il controller o il mouse), aggiornare lo stato del gioco e fornire un output che mostri al giocatore che ha influito sullo stato del gioco, ad esempio quando si accende una luce premendo un pulsante. Il loop di solito termina quando il gioco raggiunge uno stato di completamento, come ad esempio il raggiungimento di un obiettivo da parte del giocatore o uno stato di errore come l'esaurimento del tempo prima di raggiungere l'obiettivo.
Completando questo passaggio nel tutorial Prova a tempo: A caccia di pizza, imparerai a creare il loop di gioco e a definire gli stati di completamento e di errore del gioco.
Quello che segue è lo pseudocodice per il loop di gioco nella Prova a tempo: A caccia di pizza:
loop:
race:
loop:
SelectNextPickupZone
WaitForPlayerToCompletePickupZone
block:
WaitForFirstPickup
SelectNextDeliveryZone
WaitForPlayerToCompleteDeliveryZone
Questo loop deve terminare quando il timer del conto alla rovescia termina o se si verifica un errore imprevisto nel gioco.
Creazione del loop di gioco di base
Per aggiornare il file game_coordinator_device.verse, attieniti ai seguenti passaggi:
- Crea un nuovo metodo denominato
PickupDeliveryLoop()
con gli specificatoriprivate
esuspends
. Sposta in questo nuovo metodo il loop creato in precedenza inOnBegin()
.OnBegin<override>()<suspends> : void = SetupZones() PickupDeliveryLoop() PickupDeliveryLoop<private>()<suspends> : void = var PickupLevel : int = 0 loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone() PickupZone.ZoneCompletedEvent.Await() PickupZone.DeactivateZone() else: Print("PickupZone successiva da selezionare non trovata") return if (DeliveryZone := DeliveryZoneSelector.SelectNext[]): DeliveryZone.ActivateZone() DeliveryZone.ZoneCompletedEvent.Await() DeliveryZone.DeactivateZone() else: Print("DeliveryZone successiva da selezionare non trovata") return
- Determina il numero massimo di livelli di raccolta dalla lunghezza degli array di tag e aumenta il
PickupLevel
ogni volta che il giocatore completa una zona di raccolta, purché il livello di raccolta non sia superiore al numero massimo di livelli di raccolta.OnBegin<override>()<suspends> : void = SetupZones() PickupDeliveryLoop() 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 var PickupLevel : int = 0 loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone() PickupZone.ZoneCompletedEvent.Await() PickupZone.DeactivateZone() # Aggiorna il livello di raccolta if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 else: Print("PickupZone successiva da selezionare non trovata") return
- La zona di consegna deve attivarsi dopo che il giocatore ha completato il suo primo ritiro, ma il giocatore deve comunque essere in grado di raccogliere gli oggetti se lo desidera prima di recarsi nella zona di consegna. A tal fine, il codice della zona di raccolta e quello della zona di consegna devono coincidere. Questo esempio utilizza l'espressione di concorrenza
race
perché:- Il blocco di consegna deve annullare il loop della zona di raccolta quando il giocatore termina una consegna.
- Il loop della zona di raccolta deve annullare il blocco di consegna se si verifica un problema con il loop di raccolta.
Devi anche eseguire una leggera modifica alla disattivazione della zona. Quando il loop o il blocco di consegna viene annullato,
DeactivateZone()
non deve essere chiamata se lo script stava aspettando il completamento della zona.Poiché la riga di disattivazione della zona non viene mai eseguita, la zona rimane attiva, creando un bug.
-
Per risolvere questo problema, puoi utilizzare l'espressione
defer
. Un'espressionedefer
ritarda l'esecuzione delle espressioni che contiene fino a quando l'ambito in cui appare ildefer
non termina. Undefer
viene eseguito una volta che il controllo del programma viene trasferito fuori dall'ambito, compresa l'uscita normale da un ambito (termine di una funzione), le uscite anticipate (come return o break) o a causa di qualsiasi attività concorrente annullata/espressione asincrona (comerace
). È come mettere in coda operazioni che verranno eseguite alla fine, qualunque cosa accada. Esegui il wrapping di ogni chiamataDeactivateZone
in undefer
e spostala prima del rispettivoZoneCompletedEvent.Await()
.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 var PickupLevel : int = 0 race: loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone() defer: PickupZone.DeactivateZone() PickupZone.ZoneCompletedEvent.Await() # Aggiorna il livello di raccolta if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 else: Print("PickupZone successiva da selezionare non trovata") return block: 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) else: Logger.Print("DeliveryZone successiva da selezionare non trovata.", ?Level:=log_level.Error) return # Errore in uscita dal PickupDeliveryLoop
- L'esempio precedente attiva la zona di consegna nello stesso momento in cui viene attivata la zona di raccolta, ma l'attivazione della zona di consegna deve attendere il completamento della prima raccolta. A tal fine, aggiungi un evento e fai in modo che la zona di consegna attenda l'evento prima di attivarsi.
OnBegin<override>()<suspends> : void = SetupZones() PickupDeliveryLoop() 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(){} var PickupLevel : int = 0 var IsFirstPickup : logic = true race: loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone() defer: PickupZone.DeactivateZone() PickupZone.ZoneCompletedEvent.Await() # Dopo la prima raccolta, possiamo attivare la zona di consegna. if (IsFirstPickup?): set IsFirstPickup = false FirstPickupZoneCompletedEvent.Signal() # Aggiorna il livello di raccolta if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 else: Print("PickupZone successiva da selezionare non trovata") return 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) else: Logger.Print("DeliveryZone successiva da selezionare non trovata.", ?Level := log_level.Error) return # Errore in uscita dal PickupDeliveryLoop
- Esegui il loop di questa espressione race della zona di raccolta/zona di consegna fino al termine del gioco, in modo che il giocatore possa continuare a raccogliere e consegnare oggetti.
OnBegin<override>()<suspends> : void = SetupZones() PickupDeliveryLoop() 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(){} loop: var PickupLevel : int = 0 var IsFirstPickup : logic = true race: loop: if (PickupZone : base_zone = PickupZones[PickupLevel].SelectNext[]): PickupZone.ActivateZone() defer: PickupZone.DeactivateZone() PickupZone.ZoneCompletedEvent.Await() # Aggiorna il livello di raccolta if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 else: Print("PickupZone successiva da selezionare non trovata") return 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) else: Logger.Print("DeliveryZone successiva da selezionare non trovata.", ?Level:=log_level.Error) return # Errore in uscita dal PickupDeliveryLoop
-
Il tuo file game_coordinator_device.verse deve essere simile a:
using { /Verse.org/Simulation } using { /Fortnite.com/Devices } using { /Fortnite.com/Vehicles } using { /Fortnite.com/Characters } using { /Fortnite.com/Playspaces } 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): DeliveryZoneSelector<private> : tagged_zone_selector = tagged_zone_selector{} var PickupZoneSelectors<private> : []tagged_zone_selector = array{} OnBegin<override>()<suspends> : void = SetupZones() PickupDeliveryLoop() SetupZones<private>() : void = DeliveryZoneSelector.InitZones(delivery_zone_tag{}) PickupZoneLevelTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}} set PickupZoneSelectors = for(PickupZoneTag : PickupZoneLevelTags): PickupZone := tagged_zone_selector{} PickupZone.InitZones(PickupZoneTag) PickupZone 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(){} loop: var PickupLevel : int = 0 var IsFirstPickup : logic = true race: loop: if (PickupZone : base_zone = PickupZones[PickupLevel].SelectNext[]): PickupZone.ActivateZone() defer: PickupZone.DeactivateZone() PickupZone.ZoneCompletedEvent.Await() # Aggiorna il livello di raccolta if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 else: Print("PickupZone successiva da selezionare non trovata") return 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) else: Logger.Print("DeliveryZone successiva da selezionare non trovata.", ?Level:=log_level.Error) return # Errore in uscita dal PickupDeliveryLoop
Salva i file di Verse, compila il codice ed esegui il playtest del livello.
Durante il playtest del livello, uno dei dispositivi Generatore oggetti si attiverà all'inizio del gioco e dopo che il giocatore raccoglie un oggetto. Dopo che il giocatore ha raccolto il suo primo oggetto, il dispositivo Generatore oggetti si disattiva e si attiva un dispositivo Area di cattura. Ciò continua fino a quando non termini manualmente il gioco.
Definizione degli stati di completamento ed errore del loop di gioco
Ora che hai creato la base del loop di gioco, devi definire lo stato di completamento e di errore del loop di gioco. Questo gioco deve terminare quando:
- Termina un conto alla rovescia oppure
- Si verifica un problema con il loop di gioco.
Per impostare gli stati di completamento ed errore del gioco, attieniti ai passaggi seguenti:
1 Crea un'istanza della classe countdown_timer
in game_coordinator_device
con lo specificatore private
.
game_coordinator_device<public> := class(creative_device):
@editable
EndGame<public> : end_game_device = end_game_device{}
var CountdownTimer<private> : countdown_timer = countdown_timer{}
- Poiché il costruttore di
countdown_timer
richiede un riferimento al giocatore, aggiungi una variabile opzionale giocatore per memorizzare un riferimento al giocatore in questo gioco a giocatore singolo e creare una funzione chiamataFindPlayer()
per ottenere il riferimento al giocatore. ChiamaFindPlayer()
inOnBegin()
prima di impostare le zone.game_coordinator_device<public> := class(creative_device): @editable EndGame<public> : end_game_device = end_game_device{} var CountdownTimer<private> : countdown_timer = countdown_timer{} var MaybePlayer<private> : ?player = false OnBegin<override>()<suspends> : void = FindPlayer() SetupZones() FindPlayer<private>() : void = # Poiché si tratta di un'esperienza per giocatore singolo, il primo giocatore (a indice 0) # deve essere l'unico disponibile. if (FirstPlayer := GetPlayspace().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)
- Crea una funzione denominata
HandleCountdownEnd()
che attenda la fine del timer del conto alla rovescia e attivi il dispositivo Fine gioco.HandleCountdownEnd<private>(InPlayer : agent)<suspends> : void = CountdownTimer.CountdownEndedEvent.Await() EndGame.Activate(InPlayer)
- Crea una funzione denominata
StartGame()
e chiamala dopoSetupZones()
inOnBegin()
. Questa funzione deve:- Inizializzare il timer del conto alla rovescia.
game_coordinator_device<public> := class(creative_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{} OnBegin<override>()<suspends> : void = FindPlayer() SetupZones() StartGame() 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. Il countdown_timer richiede che un giocatore mostri la propria interfaccia utente. A questo punto, dovresti avere un giocatore valido. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Giocatore valido, avvio della partita...") set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer) CountdownTimer.StartCountdown() else: Logger.Print("Impossibile trovare un giocatore valido. Avvio della partita annullato", ?Level := log_level.Error)
- Utilizza l'espressione
race
per chiamare siaHandleCountdownEnd(ValidPlayer)
chePickupDeliveryLoop()
, in modo che:- Quando il conto alla rovescia termina, il loop di gioco si arresta, oppure
- Se il loop di gioco si ferma, il conto alla rovescia viene annullato.
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. Il countdown_timer richiede che un giocatore mostri la propria interfaccia utente. A questo punto, dovresti avere un giocatore valido. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Giocatore valido, avvio della partita...") 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)
- Inizializzare il timer del conto alla rovescia.
- Il file game_coordinate_device.verse deve essere simile a:
using { /Verse.org/Simulation } using { /Fortnite.com/Devices } using { /Fortnite.com/Vehicles } using { /Fortnite.com/Characters } using { /Fortnite.com/Playspaces } 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): # 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{} DeliveryZoneSelector<private> : tagged_zone_selector = tagged_zone_selector{} var PickupZoneSelectors<private> : []tagged_zone_selector = array{} OnBegin<override>()<suspends> : void = FindPlayer() SetupZones() StartGame() FindPlayer<private>() : void = # Poiché si tratta di un'esperienza per giocatore singolo, il primo giocatore (a indice 0) # deve essere l'unico disponibile. if (FirstPlayer := GetPlayspace().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 = DeliveryZoneSelector.InitZones(delivery_zone_tag{}) PickupZoneLevelTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}} set PickupZoneSelectors = for(PickupZoneTag : PickupZoneLevelTags): PickupZone := tagged_zone_selector{} PickupZone.InitZones(PickupZoneTag) PickupZone 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. Il countdown_timer richiede che un giocatore mostri la propria interfaccia utente. A questo punto, dovresti avere un giocatore valido. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Giocatore valido, avvio della partita...") 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 : agent)<suspends> : void = CountdownTimer.CountdownEndedEvent.Await() 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(){} loop: var PickupLevel : int = 0 var IsFirstPickup : logic = true race: loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone() <# 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) # Dopo la prima raccolta, possiamo attivare la zona di consegna. if (IsFirstPickup?): set IsFirstPickup = false FirstPickupZoneCompletedEvent.Signal() # Aggiorna il livello di raccolta if (PickupLevel < MaxPickupLevel): set PickupLevel += 1 # Logger.Print("PickupLevel aumentato a {PickupLevel}", ?Level := log_level.Normal) 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) else: Logger.Print("DeliveryZone successiva da selezionare non trovata.", ?Level := log_level.Error) return # Errore in uscita dal PickupDeliveryLoop
- Salva i file di Verse, compila il codice ed esegui il playtest del livello.
Quando esegui il playtest del livello, il gioco funziona come nella sezione precedente, ma ora è presente un timer che termina la gioco quando il conto alla rovescia finisce o si verifica un problema nel loop di gioco.