Cuando las cosas no funcionan como esperas en tu código de Verse, a veces es difícil entender qué ha salido mal. Por ejemplo, puedes encontrarte con:
- Errores en tiempo de ejecución.
- Código que se ejecuta en el orden incorrecto.
- Procesos que tardan más de lo debido.
Todo lo anterior puede hacer que tu código se comporte de formas inesperadas y crear problemas de experiencia. El acto de diagnosticar problemas en el código se llama depurar y existen varias soluciones que puedes utilizar para corregir y optimizar tu código.
Errores en tiempo de ejecución de Verse
Tu código de Verse se analiza tanto cuando lo escribes en el servidor de lenguaje como cuando lo compilas desde el editor o Visual Studio Code. Sin embargo, este análisis semántico por sí solo no puede detectar todos los posibles problemas que puedes encontrar. Cuando el código se ejecuta en tiempo de ejecución, puedes activar errores en tiempo de ejecución. Esto hará que todo el resto del código de Verse deje de ejecutarse, lo que puede hacer que tu experiencia no se pueda reproducir.
A modo de ejemplo, supón que tienes código de Verse que hace lo siguiente:
# Tiene el especificador `suspends`, por lo que puede ser llamado en una expresión de bucle.
SuspendsFunction()<suspends>:void={}
# Llama a la función SuspendFunction para siempre sin interrumpir ni devolver nada,
# provocando un error en tiempo de ejecución debido a un bucle infinito.
CausesInfiniteLoop()<suspends>:void=
loop:
SuspendsFunction()
La función CausesInfiniteLoop()
no provocaría ningún error en el compilador de Verse y tu programa se compilaría correctamente. Sin embargo, si llamas a CausesInfiniteLoop()
en tiempo de ejecución, ejecutará un bucle infinito y, por tanto, activará un error en tiempo de ejecución.
Para inspeccionar los errores en tiempo de ejecución que se han producido en tu experiencia, ve al portal de servicios de contenido. Allí puedes ver una lista de todos tus proyectos, tanto publicados como no publicados. Para cada proyecto, tienes acceso a una pestaña de Verse que enumera las categorías de errores en tiempo de ejecución que se han producido en un proyecto. También puedes consultar la pila de llamadas de Verse donde se notificó ese error para ver más detalles sobre lo que pudo haber salido mal. Los informes de errores se almacenan hasta durante 30 días.

Ten en cuenta que se trata de una nueva característica que se encuentra en una fase de desarrollo temprana. Su funcionamiento puede cambiar en futuras versiones de UEFN y Verse.
Cómo probar código lento con la expresión profile
Si tu código se ejecuta más lento de lo esperado, puedes probarlo utilizando la expresión profile. La expresión profile
te indica cuánto tarda en ejecutarse un fragmento de código concreto y puede ayudarte a identificar bloques de código lentos y optimizarlos. Por ejemplo, supongamos que deseas averiguar si una matriz contiene un número concreto y devolver el índice donde aparece. Puedes hacerlo recorriendo la matriz y comprobando si el número coincide con el que estabas buscando.
# Una matriz de números de prueba.
TestNumbers:[]int = array{1,2,3,4,5}
# Se ejecuta cuando se inicia el dispositivo en un juego en ejecución
OnBegin<override>()<suspends>:void=
# Encuentra si el número existe en la matriz TestNumbers iterando
# a través de cada elemento y comprobando si coincide.
for:
Index -> Number:TestNumbers
Number = 4
do:
Print("Se ha encontrado el número en el índice {Index}.")
Sin embargo, este código es ineficaz, ya que necesita comprobar cada número de la matriz en busca de una coincidencia. Esto da lugar a una complejidad temporal ineficaz, ya que aunque encuentre el elemento, seguirá comprobando el resto de la lista. En su lugar, puedes utilizar la función Find[]
para comprobar si la matriz contiene el número que estás buscando y devolverlo. Como Find[]
devuelve inmediatamente cuando encuentra el elemento, se ejecutará más rápido cuanto antes esté el elemento en la lista. Si utilizas una expresión profile
para probar ambos fragmentos de código, verás que en este caso el código que utiliza la función Find[]
reduce el tiempo de ejecución.
# Se ejecuta cuando se inicia el dispositivo en un juego en ejecución
OnBegin<override>()<suspends>:void=
# Encuentra si el número existe en la matriz TestNumbers iterando
# a través de cada elemento y comprobando si coincide.
profile("Buscando un número mediante la comprobación de cada elemento de una matriz"):
for:
Index -> Number:TestNumbers
Number = 4
do:
Print("Se ha encontrado el número en el índice {Index}.")
# Encuentra si el número existe utilizando la función Find[].
profile("Buscando un número con la función Find[]"):
if:
FoundIndex := TestNumbers.Find[4]
then:
Print("Se ha encontrado el número en el índice {FoundIndex}.")
else:
Print("Se ha producido un error al buscar el número.")
Estas pequeñas diferencias en el tiempo de ejecución se magnifican cuantos más elementos tengas por recorrer. Cada expresión que ejecutas mientras iteras por una lista grande añade complejidad temporal, especialmente cuando tus matrices crecen hasta cientos o incluso miles de elementos. A medida que amplíes tus experiencias cada vez a más jugadores, utiliza la expresión profile
para encontrar y abordar las zonas clave de ralentización.
Registradores y registro de salida
De forma predeterminada, cuando llamas a Print()
en el código de Verse para imprimir un mensaje, ese mensaje se escribe en un registro Print
dedicado. Los mensajes impresos aparecen en la pantalla durante la partida, en el registro de la partida y en el registro de salida en UEFN.
Cuando imprimes un mensaje con la función Print(), se escribe en el registro de salida, en la pestaña Registro de la partida y en la pantalla del juego.
Sin embargo, hay muchas ocasiones en las que puede que no quieras que aparezcan mensajes en la pantalla durante la partida. Puede que quieras utilizar mensajes para rastrear cuándo suceden cosas, como cuando se activa un evento o ha pasado una determinada cantidad de tiempo, o para señalar cuando algo va mal en tu código. Muchos mensajes durante el juego pueden distraer, sobre todo si no proporcionan información relevante para el jugador.
Para solucionarlo, puedes utilizar un registrador. Un registrador es una clase especial que te permite imprimir mensajes directamente en las pestañas Registro de salida y Registro sin mostrarlos en pantalla.
Registradores
Para compilar un registrador, primero tienes que crear un canal de registro. Cada registrador imprime mensajes en el registro de salida, pero puede ser difícil distinguir qué mensaje procede de cada registrador. Los canales de registro añaden el nombre del canal de registro al inicio del mensaje, lo que facilita ver qué registrador envió el mensaje. Los canales de registro se declaran en el ámbito del módulo, mientras que los registradores se declaran dentro de clases o funciones. A continuación se muestra un ejemplo de declaración de un canal de registro en el ámbito del módulo, y luego una declaración y una llamada a un registrador dentro de un dispositivo Verse.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# Un canal de registro para la clase debugging_tester.
# Los canales de registro declarados en el ámbito del módulo pueden ser utilizados por cualquier clase.
debugging_tester_log := class(log_channel){}
# Dispositivo del modo Creativo creado con Verse que puede colocarse en un nivel.
debugging_tester := class(creative_device):
# Un registrador local de la clase debugging_tester.
Logger:log = log{Channel := debugging_tester_log}
# Se ejecuta cuando se inicia el dispositivo en un juego en ejecución
OnBegin<override>()<suspends>:void=
Print("¡Hola, soy el canal de impresión!")
Logger.Print("¡Hola! Yo soy el probador de depuración.")
Cuando imprimes un mensaje utilizando la función Print() de un registrador, ese mensaje se escribe en el registro de salida y en la pestaña Registro de la partida.
Niveles de registro
Además de los canales, también puedes especificar un nivel de registro predeterminado en el que el registrador imprime. Hay cinco niveles, cada uno con sus propias propiedades:
Nivel de registro | Dónde se imprime | Propiedades especiales |
---|---|---|
Depuración | Registro de la partida | N/A |
Detallado | Registro de la partida | N/A |
Normal | Registro de la partida, Registro de salida | N/A |
Advertencia | Registro de la partida, Registro de salida | El color del texto es amarillo |
Error | Registro de la partida, Registro de salida | El color del texto es rojo |
Cuando creas un registrador, este se establece de forma predeterminada en el nivel de registro Normal
. Puedes cambiar el nivel de un registrador cuando lo creas o especificar un nivel de registro para imprimir al llamar a Print()
# Un registrador local de la clase debugging_tester. Por defecto, esto imprime
# en log_level.Normal.
Logger:log = log{Channel := debugging_tester_log}
# Un registrador con log_level.Debug como canal de registro predeterminado.
DebugLogger:log = log{Channel := debugging_tester_log, DefaultLevel := log_level.Debug}
# Se ejecuta cuando se inicia el dispositivo en un juego en ejecución
OnBegin<override>()<suspends>:void=
# El registrador imprimirá por defecto en el canal de registro Normal, mientras que DebugLogger
# imprimirá por defecto en el canal de registro Depuración. Cualquier registrador puede imprimir en cualquier nivel con solo
# especificar el argumento ?Level al llamar a Print()
Logger.Print("¡Este mensaje se imprime en el canal de registro Normal!")
DebugLogger.Print("¡Y este mensaje se imprime en el canal de registro Depuración!")
Logger.Print("¡Esto también puede imprimirse en el canal Depuración!", ?Level := log_level.Debug)
En el ejemplo anterior, Logger
toma por defecto el canal de registro Normal
, mientras que DebugLogger
toma por defecto el canal de registro Depuración
. Cualquier registrador puede imprimir en cualquier nivel de registro especificando log_level
al llamar a Print()
.
Resultados de utilizar un registrador para imprimir en diferentes niveles de registro. Ten en cuenta que log_level.Debug y log_level.Verbose no se imprimen en el registro de la partida, solo en el registro de salida de UEFN.
Cómo imprimir la pila de llamadas
La pila de llamadas realiza un seguimiento de la lista de llamadas a funciones que han conducido al ámbito actual. Es como un conjunto de instrucciones apiladas que tu código utiliza para saber adónde debe volver una vez que termine de ejecutarse la rutina actual. Puedes imprimir la pila de llamadas desde cualquier registrador utilizando la función PrintCallStack()
. Por ejemplo, toma el siguiente código:
# Un registrador local de la clase debugging_tester. Por defecto, esto imprime
# en log_level.Normal.
Logger:log = log{Channel := debugging_tester_log}
# Se ejecuta cuando se inicia el dispositivo en un juego en ejecución
OnBegin<override>()<suspends>:void=
# Ve a la primera función e imprime la pila de llamadas después de unos niveles.
LevelOne()
# Llama a LevelTwo() para profundizar un nivel.
LevelOne():void=
LevelTwo()
# Llama a LevelThree() para profundizar un nivel.
LevelTwo():void=
LevelThree()
# Imprime la pila de llamadas, que imprime la secuencia
# de llamadas a funciones que han llevado a este punto.
LevelThree():void=
Logger.PrintCallStack()
El código de OnBegin()
anterior llama a LevelOne()
para pasar a la primera función. A continuación, LevelOne()
llama a LevelTwo()
, que llama a LevelThree()
, que llama a Logger.PrintCallStack()
para imprimir la pila de llamadas actual. La llamada más reciente estará en la parte superior de la pila, por lo que LevelThree()
se imprimirá primero. Seguidamente, LevelTwo()
, LevelOne()
y OnBegin()
, en ese orden.
Cuando algo va mal en tu código, imprimir la pila de llamadas es útil para saber exactamente qué llamadas han conducido a ese punto. Esto facilita la visualización de la estructura de tu código mientras se ejecuta y permite aislar rastros de pila individuales en proyectos con mucho código.
Cómo visualizar los datos del juego con el dibujado de depuración
Otra forma de depurar diferentes funciones de tus experiencias es mediante la API de dibujado de depuración. Esta API puede crear formas de depuración para visualizar los datos del juego. Algunos ejemplos son:
- La línea de visión de un guardia.
- La distancia que un colocador de elementos moverá un objeto.
- La distancia de atenuación de un reproductor de audio.
Puedes utilizar estas formas de depuración para afinar tu experiencia sin exponer estos datos en una experiencia publicada. Para obtener más información, consulta Dibujado de depuración en Verse.
Optimización y sincronización con simultaneidad
La simultaneidad es el núcleo del lenguaje de programación Verse y es una potente herramienta para mejorar tus experiencias. Con la simultaneidad, puedes hacer que un dispositivo de Verse ejecute varias operaciones a la vez. Esto permite escribir código más flexible y compacto, y ahorrar en el número de dispositivos utilizados en el nivel. La simultaneidad es una herramienta estupenda para la optimización, y encontrar formas de utilizar código asíncrono para gestionar varias tareas a la vez es una manera fantástica de acelerar la ejecución de tus programas y abordar los problemas relacionados con la sincronización.
Cómo crear contextos asíncronos con Spawn
La expresión spawn
inicia una expresión asíncrona desde cualquier contexto y permite que las siguientes expresiones se ejecuten inmediatamente. Esto permite ejecutar varias tareas al mismo tiempo, desde el mismo dispositivo, sin necesidad de crear nuevos archivos de Verse para cada una. Por ejemplo, imagina una situación en la que tienes algún código que supervisa la salud de cada jugador cada segundo. Si la salud de un jugador desciende por debajo de un determinado número, debes curarlo un poco. A continuación, desea ejecutar algún código después que se encargue de otra tarea. Un dispositivo que implemente este código podría ser algo como esto:
# Dispositivo del modo Creativo creado con Verse que puede colocarse en un nivel.
healing_device := class(creative_device):
# Se ejecuta cuando se inicia el dispositivo en un juego en ejecución
OnBegin<override>()<suspends>:void=
AllPlayers:[]agent = GetPlayspace().GetPlayers()
# Cada segundo, se comprueba cada jugador. Si el jugador tiene menos de la mitad de salud,
# cúralas una pequeña cantidad.
loop:
for:
Player:Players
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP <= HPThreshold
do:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
Print("¡Este es el resto del código!")
Sin embargo, como este bucle se ejecuta eternamente y nunca se interrumpe, el código que le siga no se ejecutará nunca. Este es un diseño limitante, ya que este dispositivo está atascado solo ejecutando la expresión loop
. Para permitir que el dispositivo haga varias cosas a la vez y ejecute código simultáneamente, puedes mover el código loop
a una función asíncrona y generarla durante OnBegin()
.
# Se ejecuta cuando se inicia el dispositivo en un juego en ejecución
OnBegin<override>()<suspends>:void=
# Utiliza la expresión `spawn` para ejecutar HealMonitor() de forma asíncrona.
spawn{HealMonitor()}
# El código después de esto se ejecuta inmediatamente.
Print("Este código continúa mientras se ejecuta la expresión generada")
HealMonitor(Players:[]agent)<suspends>:void=
# Cada segundo, se comprueba cada jugador. Si el jugador tiene menos de la mitad de salud,
# cúralas una pequeña cantidad.
loop:
for:
Player:Players
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP <= HPThreshold
do:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
Se trata de una mejora, ya que el dispositivo ahora puede ejecutar otro código mientras se ejecuta la función HealMonitor()
. Sin embargo, la función sigue teniendo que recorrer en bucle cada jugador, y podrían producirse posibles problemas de sincronización cuantos más jugadores haya en la experiencia. Por ejemplo, ¿qué pasaría si quisieras otorgar a cada jugador una puntuación en función de sus PS o comprobar si lleva un objeto en la mano? Añadir una lógica adicional por jugador en la expresión for
aumenta la complejidad temporal de esta función, y con un número suficiente de jugadores, es posible que un jugador no se cure a tiempo si recibe daño debido a problemas de sincronización.
En lugar de recorrer en bucle cada jugador y comprobarlo individualmente, puedes optimizar aún más este código generando una instancia de la función por jugador. Esto significa que una sola función puede controlar a un solo jugador, asegurando que tu código no tenga que comprobar a todos y cada uno de los jugadores antes de volver al que necesita curarse. Utilizar expresiones de simultaneidad como spawn
a tu favor puede hacer que tu código sea más eficiente y flexible, y libera el resto de tu base de código para manejar otras tareas.
# Se ejecuta cuando se inicia el dispositivo en un juego en ejecución
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# Genera una instancia de la función HealMonitor() para cada jugador.
for:
Player:AllPlayers
do:
# Utiliza la expresión `spawn` para ejecutar HealMonitorPerPlayer() de forma asíncrona.
spawn{HealMonitorPerPlayer(Player)}
# El código después de esto se ejecuta inmediatamente.
Print("Este código continúa mientras se ejecuta la expresión generada")
HealMonitorPerPlayer(Player:agent)<suspends>:void=
if:
Character := Player.GetFortCharacter[]
then:
# Cada segundo, se comprueba el jugador supervisado. Si el jugador tiene menos de la mitad de salud,
# cúralas una pequeña cantidad.
loop:
PlayerHP := Character.GetHealth()
if:
PlayerHP <= HPThreshold
then:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
Utilizar la expresión spawn
dentro de una expresión loop
puede provocar un comportamiento no deseado si se maneja incorrectamente. Por ejemplo, como HealMonitorPerPlayer()
nunca termina, este código seguirá generando una cantidad infinita de funciones asíncronas hasta que se produzca un error en tiempo de ejecución.
# Genera una instancia de la función HealMonitor() para cada jugador en un bucle infinito.
# Esto provocará un error en tiempo de ejecución, ya que el número de funciones asíncronas aumenta infinitamente.
loop:
for:
Player:AllPlayers
do:
spawn{HealMonitorPerPlayer(Player)}
Sleep(0.0)
Cómo controlar la sincronización con eventos
Conseguir que cada parte de tu código se sincronice correctamente puede ser difícil, sobre todo en grandes experiencias multijugador con muchas secuencias de comandos ejecutándose a la vez. Diferentes partes de tu código pueden depender de otras funciones o secuencias de comandos que se ejecuten en un orden establecido y esto puede crear problemas de sincronización entre ellas sin controles estrictos. Por ejemplo, piensa en la siguiente función que cuenta hacia atrás durante un tiempo y luego otorga al jugador pasado una puntuación si sus PS superan el umbral.
CountdownScore(Player:agent)<suspends>:void=
# Espera un tiempo y, a continuación, otorga una puntuación a cada jugador cuyos PS estén por encima del umbral.
Sleep(CountdownTime)
if:
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP >= HPThreshold
then:
ScoreManager.Activate(Player)
Como esta función tiene el modificador <suspends>
, puedes ejecutar una instancia de ella de forma asíncrona por jugador utilizando spawn()
. Sin embargo, tienes que garantizar que cualquier otro código que dependa de esta función se ejecutará siempre después de que se complete. ¿Qué pasa si quieres imprimir cada jugador que marcó después de que termine CountdownScore()
? Podrías hacerlo en OnBegin()
llamando a Sleep()
para esperar el mismo tiempo que tarda en ejecutarse CountdownScore()
, pero esto podría crear problemas de tiempo cuando tu juego se está ejecutando e introduce una nueva variable. Tienes que actualizarlo constantemente si alguna vez quieres hacer cambios en tu código. En su lugar, puedes crear eventos personalizados y llamar a Await()
sobre ellos para controlar estrictamente el orden de los eventos en tu código.
# Evento personalizado para señalar cuándo termina la cuenta atrás.
CountdownCompleteEvent:event() = event(){}
# Se ejecuta cuando se inicia el dispositivo en un juego en ejecución
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# Genera una función CountdownScore para cada jugador
for:
Player:AllPlayers
do:
spawn{CountdownScore(Player)}
# Espera a que se notifique CountdownCompletedEvent.
CountdownCompleteEvent.Await()
# Si el jugador tiene alguna puntuación, imprímela en el registro.
for:
Player:AllPlayers
CurrentScore := ScoreManager.GetCurrentScore(Player)
CurrentScore > 0
do:
Print("¡Este jugador tiene una puntuación!")
CountdownScore(Player:agent)<suspends>:void=
# Espera un tiempo y, a continuación, otorga una puntuación a cada jugador cuyos PS estén por encima del umbral.
Sleep(CountdownTime)
if:
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP >= HPThreshold
then:
ScoreManager.Activate(Player)
# Señala el evento para permitir que el código que lo espera continúe.
CountdownCompleteEvent.Signal()
Como este código ahora espera a que se señale CountdownCompletedEvent()
, se garantiza que comprobará la puntuación de cada jugador solo después de que CountdownScore()
termine de ejecutarse. Muchos dispositivos tienen eventos incorporados en los que puedes llamar a Await()
para controlar la sincronización de tu código. Si los utilizas con tus propios eventos personalizados, puedes crear bucles de juego complejos con varias partes móviles. A modo de ejemplo, la plantilla inicial de Verse utiliza varios eventos personalizados para controlar el movimiento del personaje, actualizar la IU y gestionar el bucle general del juego paso a paso.
Cómo gestionar varias expresiones con Sync, Race y Rush
sync, race y rush permiten ejecutar varias expresiones asíncronas a la vez y, al mismo tiempo, realizar diferentes funciones cuando esas terminan de ejecutarse. Al aprovechar cada una de ellas, puedes controlar estrictamente la duración de cada una de tus expresiones asíncronas, lo que da como resultado un código más dinámico que puede manejar múltiples situaciones diferentes.
Por ejemplo, tomemos la expresión rush
. Esta expresión ejecuta varias expresiones asíncronas simultáneamente, pero solo devuelve el valor de la expresión que termina primero. Supongamos que tienes un minijuego en el que los equipos tienen que completar alguna tarea y el equipo que termine primero recibe un potenciador que le permite interferir con los otros jugadores mientras ellos terminan. Podrías escribir una lógica de sincronización complicada para rastrear cuándo completa cada equipo la tarea, o podrías utilizar la expresión rush
. Como la expresión devuelve el valor de la primera expresión asíncrona para finalizar, devolverá el equipo ganador y permitirá que el código que controla a los otros equipos siga ejecutándose.
WinningTeam := rush:
Las tres funciones asíncronas se inician al mismo tiempo.
RushToFinish(TeamOne)
RushToFinish(TeamTwo)
RushToFinish(TeamThree)
# Se llama a la siguiente expresión inmediatamente cuando se completa alguna de las funciones asíncronas.
GrantPowerup(WinnerTeam)
La expresión race
sigue las mismas reglas, excepto que cuando una expresión asíncrona se completa, las demás expresiones se cancelan. Esto te permite controlar estrictamente el tiempo de vida de varias expresiones asíncronas a la vez, e incluso puedes combinarlo con la expresión sleep()
para limitar la cantidad de tiempo que quieres que se ejecute la expresión. Retomemos el ejemplo de rush
, excepto que esta vez quieres que el minijuego termine inmediatamente cuando un equipo gana. También debes añadir un cronómetro para que el minijuego no se prolongue eternamente. La expresión race
te permite hacer ambas cosas sin necesidad de utilizar eventos u otras herramientas de simultaneidad para saber cuándo cancelar las expresiones que pierden la carrera.
WinningTeam := race:
Las cuatro funciones asíncronas se inician al mismo tiempo.
RaceToFinish(TeamOne)
RaceToFinish(TeamTwo)
RaceToFinish(TeamThree)
Sleep(TimeLimit)
# Se llama a la siguiente expresión inmediatamente cuando se completa alguna de las funciones asíncronas. Cualquier otra función asíncrona se cancela.
GrantPowerup(WinnerTeam)
Por último, la expresión sync
te permite esperar a que terminen de ejecutarse varias expresiones, lo que garantiza que cada una de ellas finalice antes de continuar. Como la expresión sync
devuelve una tupla que contiene los resultados de cada una de las expresiones asíncronas, puedes terminar de ejecutar todas tus expresiones y evaluar los datos de cada una de ellas individualmente. Volviendo al ejemplo del minijuego, supongamos que quieres conceder potenciadores a cada equipo en función de sus resultados en el minijuego. Aquí es donde entra en juego la expresión sync
.
TeamResults := sync:
Las tres funciones asíncronas se inician al mismo tiempo.
WaitForFinish(TeamOne)
WaitForFinish(TeamTwo)
WaitForFinish(TeamThree)
# Solo se llama a la siguiente expresión cuando se completan todas las expresiones asíncronas.
GrantPowerups(TeamResults)
Si quieres ejecutar una expresión asíncrona en varios elementos de una matriz, puedes utilizar la práctica función ArraySync()
para garantizar que todos se sincronicen.
Cada una de estas expresiones de simultaneidad es una potente herramienta en sí misma, y si aprendes a combinarlas y utilizarlas juntas, podrás escribir código para hacer frente a cualquier situación. Considera este ejemplo de la plantilla Juego de carreras con persistencia de Verse, que combina varias expresiones de simultaneidad para no solo reproducir una introducción para cada una, antes de la carrera, sino también cancelarla si el jugador se va durante la introducción. Este ejemplo destaca cómo puedes utilizar la simultaneidad de múltiples formas y compilar código resistente que reaccione dinámicamente a diferentes eventos.
# Espera a que empiece la introducción del jugador y muestra su información.
# Cancela la espera si abandonan.
WaitForPlayerIntro(Player:agent, StartOrder:int)<suspends>:void=
var IntroCounter:int = 0
race:
# Cómo esperar a que este jugador termine la carrera y registrar la llegada.
loop:
sync:
block:
StartPlayerIntroEvent.TriggeredEvent.Await()
if (IntroCounter = StartOrder):
PlayerLeaderboard.UpdatePopupUI(Player, PopupDialog)
EndPlayerIntroEvent.TriggeredEvent.Await()
if (IntroCounter = StartOrder):
break
set IntroCounter += 1
# Cómo esperar a que este jugador abandone la partida.
loop:
LeavingPlayer := GetPlayspace().PlayerRemovedEvent().Await()
if:
LeavingPlayer = Player
then:
break