Cuando las cosas no funcionan como esperas en el código de Verse, a veces, es difícil comprender qué salió mal. Por ejemplo, puedes encontrarte con lo siguiente:
Errores de tiempo de ejecución.
El código se ejecuta en el orden incorrecto.
Procesos que tardan más de lo debido.
Cualquiera de estos inconvenientes pueden hacer que tu código se comporte de manera inesperada y crear problemas en tu experiencia. El proceso de diagnosticar problemas en el código se denomina depuración, y existen varias soluciones que puedes utilizar a fin de corregir y optimizar el código.
Errores de tiempo de ejecución de Verse
Tu código de Verse se analiza mientras lo escribes en el servidor de lenguaje y 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 tu código se ejecuta en tiempo de ejecución, se pueden desencadenar errores de tiempo de ejecución. Esto hará que todo el código de Verse deje de ejecutarse, lo que es posible que haga que tu experiencia sea imposible de reproducir.
Por ejemplo, supongamos que tienes un código de Verse que realiza lo siguiente:
# Has the suspends specifier, so can be called in a loop expression.
SuspendsFunction()<suspends>:void={}
# Calls SuspendFunction forever without breaking or returning,
# causing a runtime error due to an infinite loop.
CausesInfiniteLoop()<suspends>:void=
loop:
SuspendsFunction()La función CausesInfiniteLoop() no genera ningún error en el compilador de Verse, y tu programa se compilará con éxito. Sin embargo, si llamas a CausesInfiniteLoop() en tiempo de ejecución, se ejecutará un bucle infinito y, por lo tanto, se activará un error de tiempo de ejecución.
A fin de inspeccionar los errores de tiempo de ejecución que ocurrieron en tu experiencia, dirígete al Portal del servicio de contenido. Allí encontrarás una lista de todos tus proyectos, tanto los publicados como los que no lo están. Para cada proyecto, tienes acceso a una pestaña de Verse que enumera las categorías de errores de tiempo de ejecución que ocurrieron en un proyecto. También puedes consultar la pila de llamadas de Verse donde se informó ese error, ya que ofrece más detalles sobre lo que pudo haber fallado. Los informes de error se almacenan durante un máximo de 30 días.
Ten en cuenta que esta es una característica nueva en desarrollo, y es posible que su funcionamiento cambie en futuras versiones de UEFN y Verse.
Generación de perfiles de código lento
Si tu código se ejecuta más lento de lo esperado, puedes probarlo con la expresión profile. Profile (expresión) te indica cuánto tarda en ejecutarse un fragmento de código en particular y puede ayudarte a identificar los bloques de código lentos y optimizarlos. Por ejemplo, supongamos que quieres averiguar si una matriz contiene un número determinado y devolver el índice donde aparece. Podrías realizar esto mediante la iteración a través de la matriz y la comprobación de si el número coincide con el que estabas buscando.
# An array of test numbers.
TestNumbers:[]int = array{1,2,3,4,5}
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
# Find if the number exists in the TestNumbers array by iterating
# through each element and checking if it matches.
for:
Index -> Number:TestNumbers
Sin embargo, este código es poco eficaz, ya que necesita comprobar cada número de la matriz en busca de una coincidencia. Como resultado, se observa una complejidad temporal poco eficaz, ya que incluso si encuentra el elemento, continuará comprobando el resto de la lista. En cambio, puedes utilizar la función Find[] a fin de comprobar si la matriz contiene el número que buscas y devolverlo. Como Find[] devuelve de forma inmediata cuando encuentra el elemento, se ejecutará más rápido cuanto antes esté el elemento en la lista. Si usas una expresión profile para probar ambos fragmentos de código, verás que, en este caso, el código que usa la función Find[] reduce el tiempo de ejecución.
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
# Find if the number exists in the TestNumbers array by iterating
# through each element and checking if it matches.
profile("Finding a number by checking each array element"):
for:
Index -> Number:TestNumbers
Number = 4
do:
Estas pequeñas diferencias que se muestran en el tiempo de ejecución aumentan a medida que tengas más elementos para iterar. Cada expresión que ejecutas mientras iteras en una lista grande aumenta la complejidad del tiempo, en especial, a medida que tus matrices crecen a cientos o incluso miles de elementos. A medida que escalas tus experiencias a más y más jugadores, usa la expresión profile para encontrar y abordar las áreas clave de la ralentización.
Registradores y salida de registros
De forma predeterminada, cuando llamas a Print() en el código de Verse con el objetivo de imprimir un mensaje, ese mensaje se escribe en un registro Print específico. Los mensajes impresos aparecen en la pantalla y en el registro en el juego 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 en el juego y en la pantalla en el juego.
Sin embargo, existen muchas ocasiones en las que es posible que no quieras que los mensajes aparezcan en la pantalla en el juego. Es posible que quieras usar algunos mensajes con el objetivo de rastrear cuándo suceden las cosas, como cuando se activa un evento o pasó una determinada cantidad de tiempo, o de indicar cuando algo sale mal en el código. Si hay varios mensajes durante la jugabilidad, pueden distraer, en especial, si no brindan información relevante para el jugador.
Para solucionar esto, puedes utilizar un registrador. Un registrador es una clase especial que te permite imprimir mensajes directamente en el registro de salida y en la pestaña de registro sin mostrarlos en la pantalla.
Registradores
Para compilar un registrador, primero debes crear un canal de registro. Cada registrador imprime mensajes en el registro de salida, pero puede ser difícil distinguir qué mensaje proviene de qué registrador. Los canales de registro añaden el nombre del canal de registro al inicio del mensaje, lo que facilita ver qué registrador lo envió. Los canales de registro se declaran en el ámbito del módulo, mientras que los registradores se declaran dentro de clases o funciones. Lo que aparece a continuación es un ejemplo de cómo declarar un canal de registro en el ámbito de módulo y, luego, cómo declarar y llamar a un registrador dentro de un dispositivo de Verse.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# A log channel for the debugging_tester class.
# Log channels declared at module scope can be used by any class.
debugging_tester_log := class(log_channel){}
# A Verse-authored creative device that can be placed in a level
debugging_tester := class(creative_device):
Cuando imprimes un mensaje con la función Print() de un registrador, se escribe en el registro de salida y en la pestaña Registro en el juego.
Niveles de registro
Además de los canales, también puedes especificar un nivel de registro predeterminado en el que se imprima el registrador. Existen cinco niveles, y cada uno cuenta con sus propiedades, que aparecen a continuación:
| Nivel de registro | Se imprime a… | Propiedades especiales |
|---|---|---|
Depuración | Registro en el juego | N/A |
Detallado | Registro en el juego | N/A |
Normal | Registro en el juego, registro de salida | N/A |
Advertencia | Registro en el juego, registro de salida | El color del texto es amarillo |
Error | Registro en el juego, registro de salida | El color del texto es rojo |
Cuando creas un registrador, se establece de forma predeterminada en el nivel de registro Normal. Puedes cambiar su nivel al momento de su creación o especificar un nivel de registro para imprimir al llamar a Print().
# A logger local to the debugging_tester class. By default, this prints
# to log_level.Normal.
Logger:log = log{Channel := debugging_tester_log}
# A logger with log_level.Debug as the default log channel.
DebugLogger:log = log{Channel := debugging_tester_log, DefaultLevel := log_level.Debug}
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
En el ejemplo anterior, Logger utiliza de forma predeterminada el canal de registro Normal, mientras que DebugLogger utiliza de forma predeterminada el canal de registro Debug. Cualquier registrador puede imprimir en cualquier nivel de registro mediante la especificación de log_level al llamar a Print().
Resultados del uso de 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 en el juego, sino en el registro de salida de UEFN.
Cómo imprimir la pila de llamadas
La pila de llamadas rastrea la lista de llamadas a funciones que condujeron al ámbito actual. Es como un conjunto de instrucciones apiladas que el código utiliza a fin de saber a dónde debe volver una vez que la rutina actual termina de ejecutarse. Puedes imprimir la pila de llamadas desde cualquier registrador con la función PrintCallStack(). Por ejemplo, mira el siguiente código:
# A logger local to the debugging_tester class. By default, this prints
# to log_level.Normal.
Logger:log = log{Channel := debugging_tester_log}
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
# Move into the first function, and print the call stack after a few levels.
LevelOne()
El código anterior en OnBegin() llama a LevelOne() a fin de pasar a la primera función. Luego, LevelOne() llama a LevelTwo(), que llama a LevelThree(), que llama a Logger.PrintCallStack() con el objetivo de imprimir la pila actual de llamadas. La llamada más reciente se encontrará en la parte superior de la pila, por lo que LevelThree() se imprimirá primero. Luego, LevelTwo(), LevelOne() y OnBegin(), en ese orden.
Cuando algo sale mal en el código, es útil imprimir la pila de llamadas a fin de saber con exactitud cuáles llevaron a ese punto. De este modo, es más fácil ver la estructura del código mientras se ejecuta y ofrece una forma de aislar los seguimientos de las pilas individuales en los proyectos que cuenten con un código denso.
Visualización de los datos del juego con trazado de depuración
Otra forma de depurar las diferentes características de tus experiencias es mediante la API de dibujo de depuración. Esta API puede crear formas de depuración para visualizar los datos del juego. Algunos ejemplos son los siguientes:
La línea de visión de un guardia.
La distancia a la que un movedor de utilería moverá un objeto.
La distancia de atenuación de un reproductor de audio.
Puedes utilizar estas formas de depuración con el objetivo de afinar tu experiencia sin exponer estos datos en una experiencia publicada. Para obtener más información, consulta Dibujo de depuración en Verse.
Optimización y sincronización con concurrencia
La simultaneidad es el núcleo del lenguaje de programación de Verse y una herramienta clave para mejorar tus experiencias. Con el uso de la simultaneidad, puedes hacer que un dispositivo de Verse ejecute varias operaciones a la vez. De esta manera, se puede escribir un código más flexible y compacto además de ahorrar en la cantidad de dispositivos que se usan en el nivel. La simultaneidad es una gran herramienta para la optimización, y encontrar formas de utilizar un código asíncrono a fin de manejar varias tareas a la vez es una excelente manera de acelerar la ejecución de tus programas y abordar los problemas relacionados con la sincronización.
Cómo crear contextos asíncronos con la aparición
La expresión spawn inicia una expresión asíncrona desde cualquier contexto y, al mismo tiempo, permite que las siguientes expresiones se ejecuten de inmediato. Esto ofrecerá una manera de ejecutar varias tareas a la vez desde el mismo dispositivo y sin necesidad de crear nuevos archivos de Verse para cada una. Por ejemplo, piensa en un escenario en el que tienes un código que monitorea la vida de los jugadores cada segundo. Si la vida de un jugador cae por debajo de un número determinado, debes curarlo un poco. Luego, ejecutas algún código después de esa situación que se encargue de otra tarea. Un dispositivo que implemente este código podría llegar a verse de la siguiente manera:
# A Verse-authored creative device that can be placed in a level
healing_device := class(creative_device):
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
AllPlayers:[]agent = GetPlayspace().GetPlayers()
# Every second, check each player. If the player has less than half health,
# heal them by a small amount.
Sin embargo, debido a que este bucle se ejecuta para siempre y nunca se rompe, cualquier código que siga nunca se ejecutará. Este es un diseño limitante, ya que este dispositivo está atascado solo en la ejecución de loop (expresión). A fin de permitir que el dispositivo realice varias tareas a la vez y ejecute el código de manera simultánea, puedes mover el código loop a una función asíncrona y generarla durante OnBegin().
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
# Use the spawn expression to run HealMonitor() asynchronously.
spawn{HealMonitor()}
# The code after this executes immediately.
Print("This code keeps going while the spawned expression executes")
HealMonitor(Players:[]agent)<suspends>:void=
Esto constituye una mejora, ya que el dispositivo ahora puede ejecutar otro código mientras se ejecuta la función HealMonitor(). Sin embargo, la función aún tiene que recorrer en bucle a cada jugador, y podrían surgir posibles problemas de sincronización si hay muchos jugadores en la experiencia. Por ejemplo, ¿qué pasaría si le otorgas una puntuación a cada jugador en función de sus PV o si compruebas si están sosteniendo un objeto? Si se añade una lógica extra 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 uno no se cure a tiempo si recibe daños debido a los problemas de sincronización.
En lugar de recorrer en bucle a cada jugador y comprobarlo de forma individual, puedes optimizar aún más este código mediante la generación de una instancia de la función por jugador. Es decir, una sola función puede monitorear a un solo jugador, lo que garantiza que el código no tenga que comprobar a cada uno antes de regresar en bucle a quien se tiene que curar. El uso de expresiones de simultaneidad a tu favor, como spawn, garantiza que tu código sea más eficiente y flexible, y libera el resto de tu código base con el objetivo de que controle otras tareas.
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# Spawn an instance of the HealMonitor() function for each player.
for:
Player:AllPlayers
do:
# Use the spawn expression to run HealMonitorPerPlayer() asynchronously.
El uso de la expresión spawn dentro de una expresión loop puede generar un comportamiento no deseado si se maneja de forma incorrecta. Por ejemplo, como HealMonitorPerPlayer() nunca termina, este código aún generará una cantidad infinita de funciones asíncronas hasta que se produzca un error de tiempo de ejecución.
# Spawn an instance of the HealMonitor() function for each player, looping forever.
# This will cause a runtime error as the number of asynchronous functions infinitely increases.
loop:
for:
Player:AllPlayers
do:
spawn{HealMonitorPerPlayer(Player)}
Sleep(0.0)Cómo controlar la sincronización con eventos
Puede resultar difícil conseguir que cada parte del código se sincronice de manera correcta, en especial, en grandes experiencias de multijugador, ya que cuentan con muchas secuencias de comandos que se ejecutan a la vez. Es posible que las diferentes partes de tu código dependan de otras funciones o secuencias de comandos que se ejecutan en un orden establecido, lo que puede crear problemas de sincronización entre ellas sin algunos controles estrictos. Por ejemplo, observa la siguiente función que realiza una cuenta regresiva durante un tiempo determinado y, luego, otorga al jugador una puntuación si sus PV son mayores que el umbral establecido.
CountdownScore(Player:agent)<suspends>:void=
# Wait for some amount of time, then award each player whose HP is above the threshold some score.
Sleep(CountdownTime)
if:
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP >= HPThreshold
then:
ScoreManager.Activate(Player)Debido a que esta función tiene el modificador <suspends>, puedes ejecutar una instancia de ella de forma asíncrona por jugador mediante spawn(). Sin embargo, tienes que garantizar que todo otro código que dependa de esta función siempre se ejecute después de completarse. ¿Qué sucede si deseas imprimir cada jugador que ganó puntos después de la finalización de CountdownScore()? Podrías realizarlo en OnBegin() llamando a Sleep() a fin de esperar la misma cantidad de tiempo que tarda en ejecutarse CountdownScore(), pero esto podría crear problemas de sincronización cuando el juego está en ejecución e introduce una nueva variable que tienes que actualizar constantemente si desearas realizar cambios en el código. En cambio puedes crear eventos personalizados y llamar a Await() en ellos para controlar de forma estricta el orden de los eventos en tu código.
# Custom event to signal when the countdown finishes.
CountdownCompleteEvent:event() = event(){}
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# Spawn a CountdownScore function for each player
for:
Debido a que este código espera a que se indique CountdownCompletedEvent(), se garantiza que comprobará la puntuación de cada jugador solo después de que CountdownScore() termine de ejecutarse. Muchos dispositivos tienen eventos integrados a los que puedes llamar Await() para controlar la sincronización de tu código, y al aprovecharlos con tus propios eventos personalizados, puedes crear bucles de juego complejos con varias partes móviles. Por ejemplo, en la plantilla de Verse para principiantes, se usan varios eventos personalizados a fin de controlar el movimiento del personaje, actualizar la IU y administrar el bucle general del juego de un tablero a otro.
Cómo manejar varias expresiones con sync, race y rush
Las expresiones sync, race y rush te permiten ejecutar varias expresiones asíncronas a la vez mientras se llevan a cabo diferentes funciones cuando esas terminan de ejecutarse. Al aprovechar cada una, puedes controlar de manera estricta la duración de cada una de las expresiones asíncronas, lo que da como resultado un código más dinámico que maneja varias situaciones diferentes.
Veamos el ejemplo de la expresión rush. Esta expresión ejecuta varias expresiones asíncronas a la vez, pero solo devuelve el valor de la que finaliza primero. Supongamos que tienes un minijuego en el que los equipos tienen que completar alguna tarea, y el equipo que termina primero recibe un potenciador que le permite interferir con los otros jugadores mientras terminan. Podrías escribir una lógica complicada de la sincronización a fin de rastrear cuándo cada equipo completa la tarea o usar la expresión rush. Dado que la expresión devuelve el valor de la primera expresión asíncrona para finalizar, devolverá el equipo ganador mientras el código que controla a los otros continúa ejecutándose.
WinningTeam := rush:
# All three async functions start at the same time.
RushToFinish(TeamOne)
RushToFinish(TeamTwo)
RushToFinish(TeamThree)
# The next expression is called immediately when any of the async functions complete.
GrantPowerup(WinnerTeam)La expresión race sigue las mismas reglas, excepto que, cuando una expresión asíncrona se completa, las demás se cancelan. Esto te permitirá controlar de manera estricta la duración de varias expresiones asíncronas a la vez e, incluso, podrás combinarla con la expresión sleep() a fin de limitar la cantidad de tiempo que quieras que se ejecute la expresión. Vuelve al ejemplo de rush, excepto que esta vez el minijuego termina cuando un equipo gana. También debes añadir un cronómetro a fin de que el minijuego no dure para siempre. La expresión race permite hacer ambas cosas, sin necesidad de utilizar eventos u otras herramientas de concurrencia para saber cuándo cancelar las expresiones que pierden la carrera.
WinningTeam := race:
# All four async functions start at the same time.
RaceToFinish(TeamOne)
RaceToFinish(TeamTwo)
RaceToFinish(TeamThree)
Sleep(TimeLimit)
# The next expression is called immediately when any of the async functions complete. Any other async functions are canceled.
GrantPowerup(WinnerTeam)Finalmente, la expresión sync te permite esperar hasta que varias expresiones terminen de ejecutarse, lo que garantiza que cada una de ellas se complete antes de continuar. Dado que la expresión sync devuelve una tuple 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 por separado. Volvamos al ejemplo del minijuego, y supongamos que le otorgas potenciadores a cada equipo en función de los resultados que obtuvieron en el minijuego. Aquí es donde entra en juego la expresión sync.
TeamResults := sync:
# All three async functions start at the same time.
WaitForFinish(TeamOne)
WaitForFinish(TeamTwo)
WaitForFinish(TeamThree)
# The next expression is called only when all of the async expressions complete.
GrantPowerups(TeamResults)Si quieres ejecutar una expresión asíncrona en varios elementos de matriz, puedes utilizar la función práctica ArraySync() a fin de garantizar que todos se sincronicen.
Cada una de estas expresiones de simultaneidad son una herramienta clave y, si aprendes a combinarlas y usarlas en conjunto, podrás escribir un código para manejar cualquier situación. Ten en cuenta el ejemplo de la Plantilla Carrera en un circuito con persistencia de Verse, que combina varias expresiones de simultaneidad con el objetivo de no solo reproducir una introducción para cada una antes de la carrera, sino también de cancelarla si el jugador se va durante la introducción. En este ejemplo, se destaca cómo puedes usar la simultaneidad de varias maneras y compilar un código resistente que reaccione con dinamismo a diferentes eventos.
# Wait for the player's intro start and display their info.
# Cancel the wait if they leave.
WaitForPlayerIntro(Player:agent, StartOrder:int)<suspends>:void=
var IntroCounter:int = 0
race:
# Waiting for this player to finish the race and then record the finish.
loop:
sync: