Atualmente, o mecanismo de coroutines no Unity está um pouco desatualizado, com alternativas assíncronas como Unitask e ETTask ganhando destaque, o que resolve problemas como exceções em coroutines.
A Unity também está desenvolvendo seu próprio esquema assíncrono, mas para projetos que ainda usam coroutines, é essencial explorar soluções nessa área.
É sabido que coroutines no Unity não fornecem rastreamento de pilha completo, pois são compiladas em uma máquina de estados IL, resultando em perda de informações de pilha.
Se ocorer um erro em uma coroutine com múltiplas funções yield aninhadas, as informações de pilha podem estar incompletas.
public class ExemploClasse : MonoBehaviour {
private void Iniciar() {
StartCoroutine(A());
}
private IEnumerator A() {
yield return B();
}
private IEnumerator B() {
yield return C();
yield return null;
}
private IEnumerator C() {
yield return null;
<strong>Debug.Log(</strong><strong>"C"</strong><strong>);</strong>
}
}
Saída (rastreamento de pilha perdido):
C
UnityEngine.Debug:Log (object)
<strong>ExemploClasse</strong><strong>/<c>d__3:MoveNext () (at Assets/ExemploClasse.cs:31</c></strong><strong>)</strong>
UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)
Para resolver isso de forma eficaz, uma abordagem é encapsular o MoveNext() ou usar Unitask, mas isso pode ser complexo.
No entanto, após investigação, foram encontradas viáveis alternativas.
- Uso da Classe StackTrace para Rastreamento
A classe StackTrace permite obter informações sobre a pilha de execução atual. Com GetFrame, é possível acessar detalhes de cada nível de chamada.
public class ExemploClasse : MonoBehaviour {
private void Iniciar() {
Metodo1();
}
private void Metodo1() {
Metodo2();
}
private void Metodo2() {
<strong>var pilha = new System.Diagnostics.StackTrace(true);</strong>
var quadro = pilha.GetFrame(0);
Debug.Log(quadro.GetMethod().Nome);
quadro = pilha.GetFrame(1);
Debug.Log(quadro.GetMethod().Nome);
quadro = pilha.GetFrame(2);
Debug.Log(quadro.GetMethod().Nome);
//Saída:
//Metodo2
//Metodo1
//Iniciar
}
}
Entretanto, em coroutines, após a compilação em máquina de estados, esse código não fornece informações de pilha.
1 public class ExemploClasse : MonoBehaviour {
2 private void Iniciar() {
3 StartCoroutine(A());
4 }
5 private IEnumerator A() {
6 yield return null;
7 yield return B();
8 }
9 private IEnumerator B() {
10 yield return null;
11 <strong>var pilha = new StackTrace(true);
</strong>12 for (int i = 0; i < pilha.QuantidadeQuadros; ++i)
13 UnityEngine.Debug.Log(<strong>pilha.GetFrame(i).ObterNumeroLinhaArquivo().ToString()</strong>);
14 //saída
15 //<strong>11
</strong>16 //<strong>0</strong>
17 }
18 }
No entanto, em chamadas de funções comuns fora de yield, é possível obter informações completas.
1 public class ExemploClasse : MonoBehaviour
2 {
3 private StringBuilder construtor = new StringBuilder(1024);
4
5 private void Iniciar() {
6 StartCoroutine(A());
7 }
8 private IEnumerator A() {
9 <strong>StackTrace pilha = new StackTrace(true);
</strong>10 construtor.AppendLine(pilha.GetFrame(0).ObterNumeroLinhaArquivo().ToString());
11 yield return B();
12 }
13 private IEnumerator B() {
14 <strong>StackTrace pilha = new StackTrace(true);
</strong>15 construtor.AppendLine(pilha.GetFrame(0).ObterNumeroLinhaArquivo().ToString());
16 yield return C();
17 }
18 private IEnumerator C() {
19 <strong>StackTrace pilha = new StackTrace(true);
</strong>20 construtor.AppendLine(pilha.GetFrame(0).ObterNumeroLinhaArquivo().ToString());
21 yield return null;
22 UnityEngine.Debug.Log(construtor.ToString());
23 }
24 }
Saída esperada: 14, 19, 24.
- Encapsulamento do StackTrace
2.1 Blocos Begin/End
Crie uma classe AuxiliarCoroutine para armazenar informações de rastreamento, usando uma array em vez de Stack para facilitar a iteração.
public static class AuxiliarCoroutine
{
private static StackTrace[] pilhaRastreamento;
private static int contadorPilha;
static AuxiliarCoroutine()
{
pilhaRastreamento = new StackTrace[64];
contadorPilha = 0;
}
public static void<strong> IniciarRastreamento</strong>() {
pilhaRastreamento[contadorPilha] = new StackTrace(true);
++contadorPilha;
}
public static void<strong> FinalizarRastreamento</strong>() {
pilhaRastreamento[contadorPilha-1] = null;
--contadorPilha;
}
}
Isso requer envolver cada transição de yield com chamadas Begin/End, o que pode ser verboso.
private void Iniciar() {
StartCoroutine(A());
}
private IEnumerator A() {
<strong>AuxiliarCoroutine.IniciarRastreamento();
</strong>yield return B();
<strong>AuxiliarCoroutine.FinalizarRastreamento();</strong>
}
2.2 Otimização com Métodos de Extensão e Using
Melhore usando métodos de extensão e a sintaxe using para automação.
public static class AuxiliarCoroutine
{
public static IEnumerator Rastreamento(this IEnumerator enumerador)
{
IniciarRastreamento();
return enumerador;
}
}
Agora, o uso é mais limpo:
private void Iniciar() {
StartCoroutine(A());
}
private IEnumerator A() {
yield return B()<strong>.Rastreamento();</strong>
}
private IEnumerator B() {
yield return C()<strong>.Rastreamento();</strong>
}
Para gerenciar a saída, adicione uma estrutura IDisposable.
public struct DescarteAutoRastreamento : IDisposable {
public void Descartar() {
AuxiliarCoroutine.FinalizarRastreamento();
}
}
public static class AuxiliarCoroutine
{
public static DescarteAutoRastreamento PopRastreamento() {
return new DescarteAutoRastreamento();
}
}
Uso com using:
private void Iniciar()
{
StartCoroutine(A());
}
private IEnumerator A()
{
<strong>using var _ = AuxiliarCoroutine.PopRastreamento();
</strong>yield return B()<strong>.Rastreamento();
</strong>//...
}
private IEnumerator B()
{
<strong>using var _ = AuxiliarCoroutine.PopRastreamento();<br></br>
</strong>yield return C()<strong>.Rastreamento();
</strong>//...
}
2.3 Sem a Sintaxe Using
Para evitar a necessidade de Pop, detecte automaticamente a saída comparando métodos no StackTrace.
public static void RastreamentoCompleto()
{
var atual = new StackTrace(true);
var quadroAtual = atual.GetFrame(2);
<strong>for (int i = contadorPilha - 1; i >= 0; --i)
{
var quadroAnterior = pilhaRastreamento[i].GetFrame(2);
if (quadroAnterior.GetMethod().GetHashCode() == quadroAtual.GetMethod().GetHashCode())
{
for (int j = i; j < contadorPilha; ++j)
pilhaRastreamento[j] = null;
contadorPilha = i;
break</strong><strong>;
}
}</strong>
pilhaRastreamento[contadorPilha] = atual;
++contadorPilha;
}
Uso simplificado:
private void Iniciar() {
StartCoroutine(A());
}
private IEnumerator A() {
yield return B()<strong>.Rastreamento();</strong>
}
private IEnumerator B() {
yield return C()<strong>.Rastreamento();</strong>
}
- Saída do Rastreamento
Para exibir o rastreamento completo com links clicáveis no editor Unity.
public static void ExibirRastreamento()
{
var construtor = new StringBuilder(4096);
construtor.AppendLine(" --- Rastreamento de Pilha Auxiliar --- ");
for (int i = 0; i < contadorPilha; ++i)
{
var quadro = pilhaRastreamento[i].GetFrame(2);
construtor.AppendFormat("- {0} (em <a href="\"{1}\"" line="\"{2}\"">{1}:{2}</a>)\n", quadro.GetMethod().Nome, quadro.ObterNomeArquivo(), quadro.ObterNumeroLinhaArquivo());
}
construtor.AppendLine(" --- Rastreamento de Pilha Auxiliar --- ");
UnityEngine.Debug.Log(construtor.ToString());
}
- Código Fonte
Versão Estável (com Pop Manual)
using System;
using System.Collections;
using System.Diagnostics;
using System.Text;
public struct DescarteAutoRastreamento : IDisposable
{
public void Descartar()
{
AuxiliarCoroutine.FinalizarRastreamento();
}
}
public static class AuxiliarCoroutine
{
private static StackTrace[] pilhaRastreamento;
private static int contadorPilha;
static AuxiliarCoroutine()
{
pilhaRastreamento = new StackTrace[64];
contadorPilha = 0;
}
public static DescarteAutoRastreamento PopRastreamento()
{
return new DescarteAutoRastreamento();
}
public static IEnumerator Rastreamento(this IEnumerator enumerador)
{
IniciarRastreamento();
return enumerador;
}
public static void IniciarRastreamento()
{
pilhaRastreamento[contadorPilha] = new StackTrace(true);
++contadorPilha;
}
public static void FinalizarRastreamento()
{
pilhaRastreamento[contadorPilha - 1] = null;
--contadorPilha;
}
public static void ExibirRastreamento()
{
var construtor = new StringBuilder(4096);
construtor.AppendLine(" --- Rastreamento de Pilha Auxiliar --- ");
for (int i = 0; i < contadorPilha; ++i)
{
var quadro = pilhaRastreamento[i].GetFrame(2);
construtor.AppendFormat("- {0} (em <a href="\"{1}\"" line="\"{2}\"">{1}:{2}</a>)\n", quadro.GetMethod().Nome, quadro.ObterNomeArquivo(), quadro.ObterNumeroLinhaArquivo());
}
construtor.AppendLine(" --- Rastreamento de Pilha Auxiliar --- ");
UnityEngine.Debug.Log(construtor.ToString());
}
}
Versão com Detecção Automática (possíveis bugs)
using System;
using System.Collections;
using System.Diagnostics;
using System.Text;
public static class AuxiliarCoroutine
{
private static StackTrace[] pilhaRastreamento;
private static int contadorPilha;
static AuxiliarCoroutine()
{
pilhaRastreamento = new StackTrace[64];
contadorPilha = 0;
}
public static IEnumerator Rastreamento(this IEnumerator enumerador)
{
RastreamentoCompleto();
return enumerador;
}
public static void RastreamentoCompleto()
{
var atual = new StackTrace(true);
var quadroAtual = atual.GetFrame(2);
for (int i = contadorPilha - 1; i >= 0; --i)
{
var quadroAnterior = pilhaRastreamento[i].GetFrame(2);
if (quadroAnterior.GetMethod().GetHashCode() == quadroAtual.GetMethod().GetHashCode())
{
for (int j = i; j < contadorPilha; ++j)
pilhaRastreamento[j] = null;
contadorPilha = i;
break;
}
}
pilhaRastreamento[contadorPilha] = atual;
++contadorPilha;
}
public static void ExibirRastreamento()
{
var construtor = new StringBuilder(4096);
construtor.AppendLine(" --- Rastreamento de Pilha Auxiliar --- ");
for (int i = 0; i < contadorPilha; ++i)
{
var quadro = pilhaRastreamento[i].GetFrame(2);
construtor.AppendFormat("- {0} (em <a href="\"{1}\"" line="\"{2}\"">{1}:{2}</a>)\n", quadro.GetMethod().Nome, quadro.ObterNomeArquivo(), quadro.ObterNumeroLinhaArquivo());
}
construtor.AppendLine(" --- Rastreamento de Pilha Auxiliar --- ");
UnityEngine.Debug.Log(construtor.ToString());
}
}
- Captura de Exceções com Rastreamento Completo
Integrando com um encapsulador para captura de exceções em coroutines.
public class ExemploClasse : MonoBehaviour {
private void Iniciar() {
StartCoroutine(new EnumeratorCapturavel(A(), () => {
AuxiliarCoroutine.ExibirRastreamento();
}));
}
private IEnumerator A() {
yield return B().Rastreamento();
}
private IEnumerator B() {
yield return C().Rastreamento();
}
private IEnumerator C() {
yield return null;throw new System.Exception();
}
}