Quando le cose non funzionano come previsto nel codice Verse, a volte è difficile capire cosa sia andato storto. Ad esempio, puoi incontrare:
- Errori di runtime.
- Il codice viene eseguito nell'ordine sbagliato.
- Processi che richiedono più tempo del dovuto.
Ognuno di questi può causare il comportamento del codice in modi imprevisti e creare problemi nell'esperienza. L'atto di diagnosticare i problemi nel codice è chiamato debugging e ci sono diverse soluzioni che puoi utilizzare per correggere e ottimizzare il tuo codice.
Errori di runtime Verse
Il codice Verse viene analizzato sia mentre lo scrivi nel server del linguaggio sia quando lo compili dall'editor o da Visual Studio Code. Tuttavia, questa analisi semantica da sola non è in grado di individuare tutti i possibili problemi che si possono incontrare. Quando il codice viene eseguito in fase di esecuzione, è possibile che vengano attivati errori di runtime. Questi causeranno l'interruzione dell'esecuzione di tutto il codice Verse successivo, il che potrebbe rendere non riproducibile la tua esperienza.
Ad esempio, supponiamo di avere del codice Verse che ha eseguito quanto segue:
# Ha lo specificatore di sospensione, quindi può essere chiamato in un'espressione loop.
SuspendsFunction()<suspends>:void={}
# Chiama SuspendFunction per sempre senza interruzioni o ritorni,
# causando un errore di runtime a causa di un loop infinito.
CausesInfiniteLoop()<suspends>:void=
loop:
SuspendsFunction()
La funzione CausesInfiniteLoop()
non causerebbe alcun errore nel compilatore Verse e il tuo programma verrebbe compilato correttamente. Tuttavia, se chiami CausesInfiniteLoop()
in fase di esecuzione, verrà eseguito un loop infinito e quindi si attiverà un errore di runtime.
Per controllare gli errori di runtime che si sono verificati nella tua esperienza, vai al Portale di servizio contenuti. Qui puoi vedere un elenco di tutti i tuoi progetti, sia pubblicati che non. Per ogni progetto, hai accesso a una scheda Verse che elenca le categorie di errori di runtime che si sono verificati in un progetto. Puoi anche controllare lo stack di chiamate Verse in cui è stato segnalato l'errore che fornisce maggiori dettagli su cosa potrebbe essere andato storto. Le segnalazioni di errore vengono archiviate per un massimo di 30 giorni.

Tieni presente che si tratta di una nuova funzionalità in fase di sviluppo e il suo funzionamento potrebbe cambiare nelle versioni future di UEFN e Verse.
Profilazione del codice lento
Se il codice viene eseguito più lentamente del previsto, puoi testarlo utilizzando l'espressione profile. L'espressione del profilo indica il tempo necessario per l'esecuzione di una particolare parte di codice e può aiutarti a identificare i blocchi di codice lenti e a ottimizzarli. Ad esempio, supponiamo di voler scoprire se un array contiene un determinato numero e restituire l'indice dove appare. Puoi farlo scorrendo l'array e controllando se il numero corrisponde a quello che stavi cercando.
# Array di numeri di test.
TestNumbers:[]int = array{1,2,3,4,5}
# Viene eseguito quando il dispositivo viene avviato in un gioco in esecuzione
OnBegin<override>()<suspends>:void=
# Trova se il numero esiste nell'array TestNumbers eseguendo un'iterazione
# attraverso ogni elemento e controllando se corrisponde.
for:
Index -> Number:TestNumbers
Numero = 4
do:
Stampa("Trovato il numero all'indice {Index}!")
Tuttavia, questo codice è inefficiente poiché deve controllare ogni numero dell'array per trovare una corrispondenza. Ciò si traduce in una complessità temporale inefficiente, poiché anche se trova l'elemento, continua a controllare il resto dell'elenco. Invece, puoi utilizzare la funzione Find[]
per verificare se l'array contiene il numero che stai cercando e restituirlo. Poiché Find[]
restituisce immediatamente quando trova l'elemento, verrà eseguito più velocemente quanto prima l'elemento si trova nell'elenco. Se utilizzi un'espressione profile
per testare entrambe le parti di codice, noterai che in questo caso il codice che utilizza la funzione Find[]
comporta un tempo di esecuzione inferiore.
# Viene eseguito quando il dispositivo viene avviato in un gioco in esecuzione
OnBegin<override>()<suspends>:void=
# Trova se il numero esiste nell'array TestNumbers eseguendo un'iterazione
# attraverso ogni elemento e controllando se corrisponde.
profile("Trovare un numero controllando ogni elemento dell'array"):
for:
Index -> Number:TestNumbers
Numero = 4
do:
Print("Trovato il numero all'indice {Index}!")
# Trova se il numero esiste utilizzando la funzione Find[].
profile("Trova un numero utilizzando la funzione Find[]"):
if:
FoundIndex := TestNumbers.Find[4]
then:
Print("Trovato il numero all'indice {FoundIndex}!")
else:
Print("Impossibile trovare il numero!")
Queste piccole differenze nel tempo di esecuzione si amplificano con l'aumentare del numero di elementi da iterare. Ogni espressione eseguita durante l'iterazione di un ampio elenco aumenta la complessità del tempo, soprattutto quando i tuoi array crescono fino a raggiungere centinaia o addirittura migliaia di elementi. Man mano che scali le tue esperienze a un numero sempre maggiore di giocatori, utilizza l'espressione profile
per trovare e affrontare le aree chiave di rallentamento.
Logger e output di registrazione
Per impostazione predefinita, quando chiami Print()
nel codice Verse per stampare un messaggio, quel messaggio viene scritto in un registro Print
dedicato. I messaggi stampati vengono visualizzati sullo schermo nel gioco, nel registro di gioco e nel Registro di output in UEFN.
Quando stampi un messaggio utilizzando la funzione Print(), il messaggio viene scritto nel Registro di output, nella scheda Registro del gioco e nella schermata del gioco.
Tuttavia, molte volte potresti non volere che i messaggi vengano visualizzati sullo schermo nel gioco. Puoi utilizzare i messaggi per tenere traccia degli eventi che accadono, ad esempio quando si verifica un evento o è trascorso un determinato periodo di tempo, o per segnalare quando qualcosa va storto nel codice. Più messaggi durante il gameplay possono distrarre, soprattutto se non forniscono informazioni rilevanti per il giocatore.
Per risolvere questo problema, puoi utilizzare un logger. Un logger è una classe speciale che ti permette di stampare i messaggi direttamente nella scheda Registro di output e Registro senza visualizzarli sullo schermo.
Logger
Per creare un logger, devi prima creare un canale di registro. Ogni logger stampa i messaggi nel registro di output, ma può essere difficile distinguere quale messaggio proviene da quale logger. I canali di registro aggiungono il nome del canale di registro all'inizio del messaggio, rendendo più facile vedere quale logger ha inviato il messaggio. I canali di registro sono dichiarati nell'ambito del modulo, mentre i logger sono dichiarati all'interno di classi o funzioni. Di seguito è riportato un esempio di dichiarazione di un canale di registro nell'ambito del modulo, quindi dichiarazione e chiamata di un logger all'interno di un dispositivo Verse.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# Un canale di registro per la classe debugging_tester.
# I canali di registro dichiarati nell'ambito del modulo possono essere utilizzati da qualsiasi classe.
debugging_tester_log := class(log_channel){}
# Dispositivo di Fortnite Creativo sviluppato con Verse che può essere inserito in un livello
debugging_tester := class(creative_device):
# Un logger locale della classe debugging_tester.
Logger:log = log{Channel := debugging_tester_log}
# Viene eseguito quando il dispositivo viene avviato in un gioco in esecuzione
OnBegin<override>()<suspends>:void=
Print("Questo è il canale di stampa che parla!")
Logger.Print("E questo è il tester del debug che parla!")
Quando stampi un messaggio utilizzando la funzione Print() di un logger, quel messaggio viene scritto nel Registro di output e nella scheda Registro del gioco.
Livelli del registro
Oltre ai canali, puoi anche specificare un livello di registro predefinito su cui il logger esegue la stampa. Ci sono cinque livelli, ognuno con le proprie proprietà:
Livello registro | Stampa su | Proprietà speciali |
---|---|---|
Debug | Registro in gioco | N/D |
Prolisso | Registro in gioco | N/D |
Normale | Registro in gioco, Registro output | N/D |
Avviso | Registro in gioco, Registro output | Il colore del testo è giallo |
Errore | Registro in gioco, Registro output | Il colore del testo è rosso |
Quando crei un logger, per impostazione predefinita viene impostato il livello di registro Normal
. Puoi modificare il livello di un logger quando crei il logger o specificare un livello di registro su cui stampare quando chiami Print()
.
# Un logger locale della classe debugging_tester. Per impostazione predefinita, viene stampato
# a log_level.Normal.
Logger:log = log{Channel := debugging_tester_log}
# Un logger con log_level.Debug come canale di registro predefinito.
DebugLogger:log = log{Channel := debugging_tester_log, DefaultLevel := log_level.Debug}
# Viene eseguito quando il dispositivo viene avviato in un gioco in esecuzione
OnBegin<override>()<suspends>:void=
# Logger stamperà per impostazione predefinita nel canale di registro Normale, mentre DebugLogger
# verrà stampato per impostazione predefinita nel canale del registro di debug. Qualsiasi logger può stampare a qualsiasi livello
# specificando l'argomento ?Level quando si chiama Print()
Logger.Print("Questo messaggio viene stampato sul canale di registro Normale!")
DebugLogger.Print("E questo messaggio viene stampato sul canale del registro di debug!")
Logger.Print("Questo può anche stampare sul canale di debug!", ?Level := log_level.Debug)
Nell'esempio precedente, Logger
è impostato automaticamente sul canale di registro Normal
, mentre DebugLogger
è impostato sul canale di registro Debug
. Qualsiasi logger può stampare su qualsiasi livello di registro specificando log_level
quando chiama Print()
.
Risultati dell'utilizzo di un logger per la stampa su diversi livelli di registro. Tieni presente che log_level.Debug e log_level.Verbose non vengono stampati nel registro di gioco, ma solo nel Registro di output di UEFN.
Stampa dello stack di chiamate
Lo stack di chiamate tiene traccia dell'elenco delle chiamate di funzione che hanno portato all'ambito corrente. È come un insieme impilato di istruzioni che il codice utilizza per sapere dove deve tornare una volta terminata l'esecuzione della routine corrente. Puoi stampare lo stack di chiamate da qualsiasi logger utilizzando la funzione PrintCallStack()
. Ad esempio, prendi il seguente codice:
# Un logger locale della classe debugging_tester. Per impostazione predefinita, viene stampato
# a log_level.Normal.
Logger:log = log{Channel := debugging_tester_log}
# Viene eseguito quando il dispositivo viene avviato in un gioco in esecuzione
OnBegin<override>()<suspends>:void=
# Passa alla prima funzione e stampa lo stack di chiamate dopo alcuni livelli.
LevelOne()
# Chiama LevelTwo() per andare un livello più in profondità.
LevelOne():void=
LevelTwo()
# Chiama LevelThree() per andare un livello più in profondità.
LevelTwo():void=
LevelThree()
# Stampa lo stack di chiamate che stampa la sequenza
# di chiamate di funzione che hanno portato a questo punto.
LevelThree():void=
Logger.PrintCallStack()
Il codice in OnBegin()
sopra richiama LevelOne()
per passare alla prima funzione. Quindi LevelOne()
chiama LevelTwo()
, quindi LevelThree()
che chiama Logger.PrintCallStack()
per stampare lo stack di chiamate corrente. La chiamata più recente sarà in cima allo stack, quindi LevelThree()
verrà stampato per prima. Quindi LevelTwo()
, LevelOne()
e OnBegin()
, in quest'ordine.
Quando qualcosa va storto nel codice, la stampa dello stack di chiamate è utile per sapere esattamente quali chiamate hanno portato a quel punto. In questo modo è più facile vedere la struttura del codice durante l'esecuzione e fornisce un modo per isolare le singole tracce dello stack nei progetti ad alta densità di codice.
Visualizzazione dei dati di gioco con il disegno di debug
Un altro modo per eseguire il debug di diverse funzionalità delle tue esperienze è utilizzare l'API Disegno di debug. Questa API può creare forme di debug per visualizzare i dati di gioco. Alcuni esempi includono:
- La linea di visuale di una guardia.
- La distanza alla quale e un traslatore scenografico sposterà un oggetto.
- La distanza di attenuazione di un riproduttore audio.
Puoi utilizzare queste forme di debug per ottimizzare la tua esperienza senza esporre questi dati in un'esperienza pubblicata. Per maggiori informazioni, consulta Disegno di debug in Verse.
Ottimizzazione e temporizzazione con la concorrenza
La concorrenza è il cuore del linguaggio di programmazione Verse ed è un potente strumento per migliorare le tue esperienze. Con la concorrenza, puoi fare in modo che un dispositivo Verse esegua più operazioni contemporaneamente. Ciò rende possibile scrivere un codice più flessibile e compatto e risparmiare sul numero di dispositivi utilizzati nel livello. La concorrenza è un ottimo strumento per l'ottimizzazione e trovare modi per utilizzare il codice asincrono per gestire più attività contemporaneamente è un ottimo modo per accelerare l'esecuzione dei programmi e affrontare i problemi relativi alla temporizzazione.
Creazione di contesti asincroni con generazione
L'espressione spawn
avvia un'espressione asincrona da qualsiasi contesto permettendo l'esecuzione immediata delle espressioni seguenti. In questo modo è possibile eseguire più attività contemporaneamente, dallo stesso dispositivo, senza dover creare nuovi file Verse per ciascuna. Ad esempio, considera uno scenario in cui hai del codice che monitora la salute di ogni giocatore ogni secondo. Se la salute di un giocatore scende al di sotto di un certo numero, devi curarlo di una piccola quantità. Successivamente eseguirai del codice che gestisce un'altra attività. Un dispositivo che implementa questo codice potrebbe essere simile al seguente:
# Dispositivo di Fortnite Creativo sviluppato con Verse che può essere inserito in un livello
healing_device := class(creative_device):
# Viene eseguito quando il dispositivo viene avviato in un gioco in esecuzione
OnBegin<override>()<suspends>:void=
AllPlayers:[]agent = GetPlayspace().GetPlayers()
# Ogni secondo, controlla ogni giocatore. Se il giocatore ha meno della metà della salute,
# lo guarisce di una piccola quantità.
loop:
for:
Player:Players
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP <= HPThreshold
do:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
Print("Questo è il resto del codice!")
Tuttavia, poiché questo loop viene eseguito all'infinito e non si interrompe mai, il codice che lo segue non verrà mai eseguito. Questo è un progetto limitante poiché questo dispositivo è bloccato eseguendo solo l'espressione loop. Per permettere al dispositivo di fare più cose contemporaneamente ed eseguire codice contemporaneamente, puoi spostare il codice loop
in una funzione asincrona e generarla durante OnBegin()
.
# Viene eseguito quando il dispositivo viene avviato in un gioco in esecuzione
OnBegin<override>()<suspends>:void=
# Utilizza l'espressione spawn per eseguire HealMonitor() in modo asincrono.
spawn{HealMonitor()}
# Il codice successivo viene eseguito immediatamente.
Print("Questo codice continua mentre l'espressione generata viene eseguita")
HealMonitor(Players:[]agent)<suspends>:void=
# Ogni secondo, controlla ogni giocatore. Se il giocatore ha meno della metà della salute,
# lo guarisce di una piccola quantità.
loop:
for:
Player:Players
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP <= HPThreshold
do:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
Questo è un miglioramento, in quanto il dispositivo ora può eseguire altro codice mentre è in esecuzione la funzione HealMonitor()
. Tuttavia, la funzione deve ancora eseguire il loop di ogni giocatore e possibili problemi di temporizzazione potrebbero verificarsi con più giocatori presenti nell'esperienza. Ad esempio, cosa accadrebbe se volessi assegnare a ogni giocatore un punteggio in base ai suoi HP o controllare se è in possesso di un oggetto? L'aggiunta di una logica aggiuntiva per ogni giocatore nell'espressione for
aumenta la complessità temporale di questa funzione e, con un numero sufficiente di giocatori, un giocatore potrebbe non guarire in tempo se subisce danni a causa di problemi di temporizzazione.
Invece di scorrere ogni giocatore e controllarli individualmente, puoi ottimizzare ulteriormente questo codice generando un'istanza della funzione per giocatore. Ciò significa che una singola funzione può monitorare un singolo giocatore, assicurando che il codice non debba controllare ogni singolo giocatore prima di tornare a quello che ha bisogno di essere curato. L'utilizzo di espressioni di concorrenza come spawn
a proprio vantaggio può rendere il codice più efficiente e flessibile e liberare il resto del codice base per gestire altre attività.
# Viene eseguito quando il dispositivo viene avviato in un gioco in esecuzione
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# Genera un'istanza della funzione HealMonitor() per ogni giocatore.
for:
Player:AllPlayers
do:
# Utilizza l'espressione spawn per eseguire HealMonitorPerPlayer() in modo asincrono.
spawn{HealMonitorPerPlayer(Player)}
# Il codice successivo viene eseguito immediatamente.
Print("Questo codice continua mentre l'espressione generata viene eseguita")
HealMonitorPerPlayer(Player:agent)<suspends>:void=
if:
Character := Player.GetFortCharacter[]
then:
# Ogni secondo, controlla il giocatore monitorato. Se il giocatore ha meno della metà della salute,
# lo guarisce di una piccola quantità.
loop:
PlayerHP := Character.GetHealth()
if:
PlayerHP <= HPThreshold
then:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
L'utilizzo dell'espressione spawn
all'interno di un'espressione loop
può causare comportamenti indesiderati se gestita in modo improprio. Ad esempio, poiché HealMonitorPerPlayer()
non termina mai, questo codice continuerà a generare una quantità infinita di funzioni asincrone fino a quando non si verifica un errore di runtime.
# Genera un'istanza della funzione HealMonitor() per ogni giocatore, in loop all'infinito.
# Ciò causerà un errore di runtime quando il numero di funzioni asincrone aumenta all'infinito.
loop:
for:
Player:AllPlayers
do:
spawn{HealMonitorPerPlayer(Player)}
Sleep(0.0)
Controllo della temporizzazione con gli eventi
Sincronizzare correttamente ogni parte del codice può essere difficile, soprattutto in esperienze multigiocatore di grandi dimensioni con molti script in esecuzione contemporaneamente. Parti diverse del codice possono fare affidamento su altre funzioni o script eseguiti in un ordine prestabilito e ciò può creare problemi di temporizzazione tra loro senza controlli severi. Ad esempio, considera la seguente funzione che esegue il conto alla rovescia per un certo periodo di tempo, quindi assegna un punteggio al giocatore che le ha passato se i suoi HP sono superiori alla soglia.
CountdownScore(Player:agent)<suspends>:void=
# Attendi un certo lasso di tempo, quindi assegna un punteggio a ogni giocatore i cui HP sono superiori alla soglia.
Sleep(CountdownTime)
if:
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP >= HPThreshold
then:
ScoreManager.Activate(Player)
Poiché questa funzione ha il modificatore <suspends>
, puoi eseguirne un'istanza in modo asincrono per giocatore utilizzando spawn()
. Tuttavia, devi garantire che qualsiasi altro codice che si basa su questa funzione venga sempre eseguito dopo il suo completamento. E se volessi stampare ogni giocatore che ha segnato un punteggio al termine di CountdownScore()
? Puoi farlo in OnBegin()
chiamando Sleep()
per attendere la stessa quantità di tempo necessaria per l'esecuzione di CountdownScore()
, ma ciò potrebbe creare problemi di temporizzazione quando il gioco è in esecuzione e introduce una nuova variabile che devi aggiornare costantemente se vuoi apportare modifiche al tuo codice. Invece, puoi creare eventi personalizzati e chiamare Await()
su di essi per controllare rigorosamente l'ordine degli eventi nel tuo codice.
# Evento personalizzato per segnalare quando termina il conto alla rovescia.
CountdownCompleteEvent:event() = event(){}
# Viene eseguito quando il dispositivo viene avviato in un gioco in esecuzione
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# Genera una funzione CountdownScore per ogni giocatore
for:
Player:AllPlayers
do:
spawn{CountdownScore(Player)}
# Attende che venga segnalato il CountdownCompletedEvent.
CountdownCompleteEvent.Await()
# Se il giocatore ha un punteggio, stampalo nel registro.
for:
Player:AllPlayers
CurrentScore := ScoreManager.GetCurrentScore(Player)
CurrentScore > 0
do:
Print("Questo giocatore ha un punteggio!")
CountdownScore(Player:agent)<suspends>:void=
# Attendi un certo lasso di tempo, quindi assegna un punteggio a ogni giocatore i cui HP sono superiori alla soglia.
Sleep(CountdownTime)
if:
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP >= HPThreshold
then:
ScoreManager.Activate(Player)
# Segnala l'evento per far procedere qualsiasi codice in attesa.
CountdownCompleteEvent.Signal()
Poiché questo codice ora attende la segnalazione di CountdownCompletedEvent()
, è garantito che controlli il punteggio di ogni giocatore solo al termine dell'esecuzione di CountdownScore()
. Molti dispositivi hanno eventi integrati che puoi chiamare Await()
per controllare la temporizzazione del tuo codice e, sfruttandoli con i tuoi eventi personalizzati, puoi creare loop di gioco complessi con diverse parti mobili. Ad esempio, il modello Verse Starter utilizza diversi eventi personalizzati per controllare il movimento dei personaggi, aggiornare la UI e gestire l'intero loop di gioco da un tabellone all'altro.
Gestione di più espressioni con Sync, Race e Rush
Le espressioni sync, race e rush ti permettono di eseguire più espressioni asincrone contemporaneamente mentre esegui funzioni diverse quando quelle le espressioni terminano l'esecuzione. Sfruttando ciascuno di questi, puoi controllare rigorosamente la durata di ciascuna delle tue espressioni asincrone, risultando in un codice più dinamico in grado di gestire più situazioni diverse.
Ad esempio, prendi l'espressione rush
. Questa espressione esegue più espressioni asincrone contemporaneamente, ma restituisce solo il valore dell'espressione che termina per prima. Supponiamo di avere un minigioco in cui i team devono completare un compito, in cui il team che finisce per primo riceve un potenziamento che gli permette di interferire con gli altri giocatori mentre finiscono. Potresti scrivere una logica di temporizzazione complicata per tenere traccia del momento in cui ogni team completa l'attività oppure puoi utilizzare l'espressione rush
. Poiché l'espressione restituisce il valore della prima espressione asincrona da terminare, restituirà il team vincitore, permettendo al codice che gestisce gli altri team di continuare a essere eseguito.
WinningTeam := rush:
# Tutte e tre le funzioni asincrone si avviano contemporaneamente.
RushToFinish(TeamOne)
RushToFinish(TeamTwo)
RushToFinish(TeamThree)
# L'espressione successiva viene chiamata immediatamente al termine di una delle funzioni asincrone.
GrantPowerup(WinnerTeam)
L'espressione race
segue le stesse regole, tranne per il fatto che quando un'espressione asincrona viene completata, le altre vengono annullate. Ciò ti permette di controllare rigorosamente la durata di più espressioni asincrone contemporaneamente e puoi anche combinarla con l'espressione sleep()
per limitare il tempo di esecuzione dell'espressione. Considera l'esempio rush
, tranne per il fatto che questa volta vuoi che il minigioco termini immediatamente quando un team vince. Vuoi anche aggiungere un timer in modo che il minigioco non continui all'infinito. L'espressione race
ti permette di fare entrambe le cose, senza dover utilizzare eventi o altri strumenti di concorrenza per sapere quando annullare le espressioni che perdono la gara.
WinningTeam := race:
# Tutte e quattro le funzioni asincrone si avviano contemporaneamente.
RaceToFinish(TeamOne)
RaceToFinish(TeamTwo)
RaceToFinish(TeamThree)
Sleep(TimeLimit)
# L'espressione successiva viene chiamata immediatamente al termine di una delle funzioni asincrone. Qualsiasi altra funzione asincrona viene annullata.
GrantPowerup(WinnerTeam)
Infine, l'espressione sync
ti permette di aspettare fino al termine dell'esecuzione di più espressioni, garantendo che ognuna di esse venga completata prima di procedere. Poiché l'espressione sync
restituisce una tupla contenente i risultati di ciascuna delle espressioni asincrone, puoi terminare l'esecuzione di tutte le espressioni e valutare i dati di ciascuna singolarmente. Tornando all'esempio del minigioco, supponiamo invece che tu voglia assegnare potenziamenti a ogni team in base a come si è comportato nel minigioco. È qui che entra in gioco l'espressione sync
.
TeamResults := sync:
# Tutte e tre le funzioni asincrone si avviano contemporaneamente.
WaitForFinish(TeamOne)
WaitForFinish(TeamTwo)
WaitForFinish(TeamThree)
# L'espressione successiva viene chiamata solo quando tutte le espressioni asincrone sono state completate.
GrantPowerups(TeamResults)
Se vuoi eseguire un'espressione asincrona su più elementi di un array, puoi utilizzare la comoda ArraySync()
per garantire che siano tutti sincronizzati.
Ognuna di queste espressioni di concorrenza è di per sé uno strumento potente e, imparando a combinarle e a utilizzarle insieme, puoi scrivere codice per gestire qualsiasi situazione. Considera questo esempio tratto dal modello Gara a circuito con Verse Persistence, che combina più espressioni di concorrenza non solo per riprodurre un'introduzione per ciascun giocatore prima della gara, ma lo annullano anche se il giocatore abbandona durante l'introduzione. Questo esempio evidenzia come puoi utilizzare la concorrenza in diversi modi e compilare codice resiliente che reagisce dinamicamente a eventi diversi.
# Attendi l'inizio della presentazione di ogni giocatore e visualizza le sue informazioni.
# Se esce, annulla l'attesa.
WaitForPlayerIntro(Player:agent, StartOrder:int)<suspends>:void=
var IntroCounter:int = 0
race:
# Attendi che questo giocatore termini la gara e poi registra il fine gara.
loop:
sync:
block:
StartPlayerIntroEvent.TriggeredEvent.Await()
if (IntroCounter = StartOrder):
PlayerLeaderboard.UpdatePopupUI(Player, PopupDialog)
EndPlayerIntroEvent.TriggeredEvent.Await()
if (IntroCounter = StartOrder):
break
set IntroCounter += 1
# Attendi che questo giocatore esca dal gioco.
loop:
LeavingPlayer := GetPlayspace().PlayerRemovedEvent().Await()
if:
LeavingPlayer = Player
then:
break