Quando as coisas não funcionam como você espera no código Verse, às vezes é difícil entender o que deu errado. Por exemplo, você pode encontrar:
Erros de tempo de execução.
Código sendo executado na ordem errada.
Processos demorando mais do que deveriam.
Qualquer uma desses itens pode fazer com que seu código se comporte de maneiras inesperadas e criar problemas em sua experiência. O ato de diagnóstico de problemas em seu código é chamado de depuração, e existem várias soluções diferentes que você pode usar para corrigir e otimizar seu código.
Erros de tempo de execução em Verse
Seu código Verse é analisado conforme você o escreve no servidor de linguagens e quando o compila a partir do editor ou do Visual Studio Code. No entanto, essa análise semântica sozinha não pode detectar todos os possíveis problemas que você pode encontrar. Quando seu código é executado no tempo de execução, você pode acionar erros de tempo de execução. Isso fará com que todos os outros códigos Verse sejam interrompidos, o que pode tornar sua experiência impossível de jogar.
Por exemplo, suponha que você tenha um código Verse que faz o seguinte:
# 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()A função CausesInfiniteLoop() não causaria erros no compilador Verse, e seu programa seria compilado com sucesso. No entanto, se você chamar CausesInfiniteLoop() no tempo de execução, ela entrará em loop infinito e, assim, acionará um erro de tempo de execução.
Para inspecionar os erros de tempo de execução que ocorreram na sua experiência, navegue até o Portal de Serviço de Conteúdo. Nele, é possível ver uma lista de todos os seus projetos, publicados e não publicados. Para cada projeto, você tem acesso a uma aba Verse que lista as categorias de erros de tempo de execução que ocorreram em um projeto. Você também pode verificar a pilha de chamadas do Verse onde esse erro foi relatado, o que fornece mais detalhes sobre o que pode ter dado errado. Os relatório de erros são armazenados por até 30 dias.
Observe que esta é uma nova funcionalidade que está no início do desenvolvimento, e sua forma de funcionamento pode mudar em versões futuras do UEFN e de Verse.
Usando "profile" para código lento
Se o seu código estiver sendo executado mais lentamente do que o esperado, você poderá testá-lo usando a expressão profile. A expressão profile informa quanto tempo um determinado trecho de código leva para ser executado e pode ajudar a identificar blocos de código lentos e otimizá-los. Por exemplo, suponha que você queira descobrir se uma matriz contém um número específico e retornar o índice onde ele aparece. Você pode fazer isso iterando a matriz e verificando se o número corresponde ao que você está procurando.
# 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
No entanto, esse código é ineficiente, pois precisa verificar cada número da matriz para encontrar uma correspondência. O que resulta em uma complexidade de tempo ineficiente, pois mesmo que encontre o elemento, continuará verificando o restante da lista. Em vez disso, você pode usar a função Find[] para verificar se a matriz contém o número que você está procurando e retorná-lo. Como Find[] retorna imediatamente ao encontrar o elemento, ele será executado mais rapidamente quanto mais cedo o elemento estiver na lista. Se você usar uma expressão profile para testar ambas as partes do código, verá que, nesse caso, o código que usa a função Find[] resulta em um tempo de execução mais baixo.
# 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:
Essas pequenas diferenças no tempo de execução são ampliadas à medida que você tiver mais elementos para iterar. Cada expressão executada durante a iteração por uma grande lista aumenta a complexidade do tempo, especialmente à medida que as matrizes aumentam para centenas ou até milhares de elementos. À medida que você escala suas experiências para mais e mais jogadores, use a expressão profile para encontrar e lidar com as principais áreas de lentidão.
Loggers e registrando a saída
Por padrão, quando você chama Print() em código Verse para imprimir uma mensagem, essa mensagem é gravada em um log Print dedicado. As mensagens impressas aparecem na tela durante o jogo, no log do jogo e no Log de Saída no UEFN.
Quando você gera uma mensagem usando a função Print(), essa mensagem é gravada no Log de Saída, na aba Log do jogo e na tela do jogo.
No entanto, há muitas vezes que você pode não querer que as mensagens apareçam na tela do jogo. Você pode querer usar mensagens para rastrear quando coisas acontecem, como quando um evento é acionado ou um certo período de tempo se passou, ou para sinalizar quando algo dá errado no seu código. Várias mensagens durante o jogo podem causar distração, especialmente se não fornecerem informações relevantes ao jogador.
Para resolver isso, você pode usar um logger. Um logger é uma classe especial que permite imprimir mensagens diretamente no Log de Saída e na aba Log sem exibi-las na tela.
Loggers
Para construir um logger, primeiro você precisa criar um canal de log. Cada logger imprime mensagens no log de saída, mas pode ser difícil discernir qual mensagem vem de qual logger. Os canais de log adicionam o nome do canal de log ao início da mensagem, facilitando a visualização de qual logger envia a mensagem. Canais de logs são declarados no escopo do módulo, enquanto os loggers são declarados dentro de classes ou funções. Veja a seguir um exemplo de declaração de um canal de log no escopo de módulo e, em seguida, declarando e chamando um logger dentro de um dispositivo 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):
Quando você gera uma mensagem usando a função Print() de um logger, essa mensagem é gravada no Log de Saída e na aba Log do jogo.
Níveis de log
Além dos canais, você também pode especificar um nível de log padrão no qual o logger imprime. Existem cinco níveis, cada um com suas próprias propriedades:
| Nível de log | Imprime para | Propriedades especiais |
|---|---|---|
Debug | Log de jogo | N/A |
Verbose | Log de jogo | N/A |
Normal | Log de jogo, Log de Saída | N/A |
Warning | Log de jogo, Log de Saída | A cor do texto é amarela |
Error | Log de jogo, Log de Saída | A cor do texto é vermelha |
Ao criar um logger, o padrão é o nível de log Normal. Você pode alterar o nível de um logger ao criá-lo ou especificar um nível de log para imprimir ao chamar 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=
No exemplo acima, Logger usa como padrão o canal de log Normal, enquanto DebugLogger usa como padrão o canal de log Debug. Qualquer logger pode imprimir em qualquer nível de log especificando log_level ao chamar Print().
Resultados do uso de um logger para imprimir em diferentes níveis de log. Observe que log_level.Debug e log_level.Verbose não são impressos no log do jogo, apenas no Log de Saída do UEFN.
Imprimindo a pilha de chamadas
A pilha de chamadas rastreia a lista de chamadas de funções que levam ao escopo atual. É como um conjunto empilhado de instruções que seu código usa para saber para onde deve retornar quando a rotina atual terminar de ser executada. Você pode imprimir a pilha de chamadas de qualquer logger usando a função PrintCallStack(). Por exemplo, veja o seguinte 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()
O código em OnBegin() acima chama LevelOne() para passar para a primeira função. Em seguida, LevelOne() chama LevelTwo(), que chama LevelThree(), que chama Logger.PrintCallStack() para gerar a pilha de chamadas atual. A chamada mais recente estará no topo da pilha, então LevelThree() será impresso primeiro. Em seguida, LevelTwo(), LevelOne() e OnBegin(), nessa ordem.
Quando algo dá errado em seu código, imprimir a pilha de chamadas é útil para saber exatamente qual chamada levou a esse ponto. Isso facilita a visualização da estrutura do seu código conforme ele é executado e fornece uma maneira de isolar os rastros individuais da pilha em projetos com muito código.
Visualizando dados do jogo com Debug Draw
Outra maneira de depurar diferentes funcionalidades das suas experiências é usar a API Debug Draw. Esta API pode criar configurações de depuração para visualizar os dados do jogo. Alguns exemplos incluem:
A linha de visão de um guarda.
A distância em que um propulsor de adereços moverá um objeto.
A distância de atenuação de um reprodutor de áudio.
Você pode usar essas formas de depuração para ajustar sua experiência sem expor esses dados em uma experiência publicada. Para obter mais informações, confira Debug Draw em Verse.
Otimização e sincronia com simultaneidade
A simultaneidade está no centro da linguagem de programação Verse e é uma ferramenta poderosa para aprimorar suas experiências. Com simultaneidade, você pode fazer com que um dispositivo Verse execute várias operações ao mesmo tempo. Isso torna possível escrever código mais flexível e compacto e economizar no número de dispositivos usados no seu nível. A simultaneidade é uma ótima ferramenta para otimização, e encontrar maneiras de usar código assíncrono para lidar com várias tarefas ao mesmo tempo é uma ótima maneira de acelerar a execução nos seus programas e resolver problemas relacionados ao tempo.
Criando contextos assíncronos com spawn
A expressão spawn inicia uma expressão assíncrona de qualquer contexto, permitindo que as seguintes expressões sejam executadas imediatamente. Assim, você pode executar várias tarefas ao mesmo tempo, no mesmo dispositivo, sem a necessidade de criar novos arquivos Verse para cada uma. Por exemplo, considere um cenário em que você tem um código que monitora a saúde de cada jogador a cada segundo. Se a vida de um jogador cair abaixo de um determinado número, é uma boa ideia curá-lo em uma pequena quantidade. Em seguida, você deseja executar algum código depois que lida com outra tarefa. Um dispositivo que implementa este código pode ser parecido com isto:
# 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.
No entanto, como esse loop é executado indefinidamente e nunca é interrompido, qualquer código que o siga nunca será executado. Esse é um design limitante, pois o dispositivo fica preso apenas executando a expressão loop. Para permitir que o dispositivo faça várias coisas ao mesmo tempo e execute o código simultaneamente, você pode mover o código loop para uma função assíncrona e gerá-lo 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=
Essa é uma melhoria, pois agora o dispositivo pode executar outro código enquanto a função HealMonitor() é executada. No entanto, a função ainda precisa percorrer cada jogador, e possíveis problemas de tempo podem ocorrer com mais jogadores na experiência. Por exemplo, e se você quisesse conceder a cada pontuação do jogador com base em seu PV, ou verificar se ele está portando um item? Adicionar lógica extra por jogador na expressão for aumenta a complexidade de tempo dessa função e, com um número suficiente de jogadores, um jogador pode não ser curado a tempo se sofrer dano devido a problemas de tempo.
Em vez de percorrer cada jogador e verificá-los individualmente, você pode otimizar ainda mais esse código gerando uma instância da função por jogador. Ou seja, uma única função pode monitorar um único jogador, garantindo que seu código não precise verificar todos os jogadores antes de voltar para aquele que precisa de cura. Usar expressões de simultaneidade como spawn a seu lado pode tornar seu código mais eficiente e flexível, além de liberar o restante da base do código para lidar com outras tarefas.
# 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.
O uso da expressão spawn em uma expressão loop pode causar comportamentos indesejados se for tratado de maneira incorreta. Por exemplo, como HealMonitorPerPlayer() nunca termina, esse código continuará a gerar uma quantidade infinita de funções assíncronas até que ocorra um erro de tempo de execução.
# 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)Controlando o tempo com eventos
Sincronizar corretamente todas as partes do código pode ser difícil, especialmente em grandes experiências multijogador com muitos scripts em execução ao mesmo tempo. Diferentes partes do seu código podem depender de outras funções ou scripts sendo executados em uma ordem definida, e isso pode criar problemas de tempo entre eles sem controles rígidos. Por exemplo, considere a seguinte função que faz uma contagem regressiva por um certo período de tempo e, em seguida, concede ao jogador passado uma pontuação para ela se seus PV forem maiores que o limite.
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)Como essa função tem o modificador <suspends>, você pode executar uma instância dela de forma assíncrona por jogador usando spawn(). No entanto, você deve garantir que qualquer outro código que dependa dessa função seja sempre executado após sua conclusão. E se você quiser imprimir cada jogador que marcou após o término de CountdownScore()? Você pode fazer isso em OnBegin() chamando Sleep() para esperar o mesmo tempo que CountdownScore() leva para ser executado, mas isso pode criar problemas de tempo quando o jogo estiver em execução e introduzir uma nova variável que você precisa atualizar constantemente, caso queira fazer alterações no seu código. Em vez disso, você pode criar eventos personalizados e chamar Await() neles para controlar estritamente a ordem dos eventos no seu 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:
Como esse código agora espera que CountdownCompletedEvent() seja sinalizado, é garantido que ele verificará a pontuação de cada jogador somente após CountdownScore() terminar de ser executado. Muitos dispositivos têm eventos integrados nos quais você pode chamar Await() para controlar o tempo do seu código e, ao aproveitá-los com seus próprios eventos personalizados, você pode criar loops de jogo complexos com várias partes móveis. Por exemplo, o Modelo Inicial em Verse usa vários eventos personalizados para controlar o movimento de personagens, atualizar a IU e gerenciar o loop geral do jogo de tabuleiro em tabuleiro.
Como lidar com várias expressões com Sync, Race e Rush
sync, race e rush permitem que você execute várias expressões assíncronas de uma só vez enquanto executa funções diferentes quando essas expressões terminam de ser executadas. Ao aproveitar cada uma delas, você pode controlar estritamente o tempo de vida de cada uma de suas expressões assíncrona, resultando em um código mais dinâmico que pode lidar com várias situações diferentes.
Por exemplo, veja a expressão rush. Essa expressão executa várias expressões assíncronas simultaneamente, mas retorna apenas o valor da expressão que é concluída primeiro. Suponha que você tenha um minijogo em que as equipes precisam concluir uma tarefa, com a equipe que terminar primeiro recebe um potencializador que permite que interfira com os outros jogadores enquanto termina. Você pode escrever uma lógica de tempo complicada para rastrear quando cada equipe conclui a tarefa ou pode usar a expressão rush. Como a expressão retorna o valor da primeira expressão assíncrona a terminar, ela retornará a equipe vencedora, enquanto permite que o código que lida com as outras equipes continue em execução.
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)A expressão race segue as mesmas regras, exceto que, quando uma expressão assíncrona é concluída, as outras expressões são canceladas. Isso permite que você controle estritamente o tempo de vida de várias expressões assíncronas de uma só vez, podendo até mesmo combinar isso com a expressão sleep() para limitar a quantidade de tempo em que deseja que a expressão seja executada. Considere o exemplo rush, com a diferença de que desta vez você quer que o minijogo termine imediatamente quando uma equipe vencer. Você também deseja adicionar um cronômetro para que o minijogo não dure indefinidamente. A expressão race permite fazer as duas coisas, sem precisar usar eventos ou outras ferramentas de simultaneidade para saber quando cancelar as expressões que perdem a corrida.
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, a expressão sync permite esperar até que várias expressões terminem de ser executadas, garantindo que cada uma delas seja concluída antes de continuar. Como a expressão sync retorna uma tupla contendo os resultados de cada uma das expressões assíncronas, você pode terminar de executar todas as suas expressões e avaliar os dados de cada uma delas individualmente. Voltando ao exemplo do minijogo, digamos que você queira conceder poderes a cada equipe com base em seu desempenho no minijogo. É aí que entra a expressão 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)Se você quiser executar uma expressão assíncrona em vários elementos da matriz, você pode usar a prática função ArraySync() para garantir que todas sejam sincronizadas.
Cada uma dessas expressões de simultaneidade é uma ferramenta poderosa por si só e, aprendendo como combiná-las e usá-las juntas, você pode escrever código para lidar com qualquer situação. Considere este exemplo do modelo Speedway Race com persistência em Verse, que combina várias expressões de simultaneidade para não apenas reproduzir uma introdução para cada jogador antes da corrida, mas também cancelá-la se o jogador sair durante a introdução. Este exemplo destaca como você pode usar a simultaneidade de várias maneiras e criar um código flexível que reage dinamicamente 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: