Saltar al contenido principal

Vinculación de Ciclo de Vida con Auto-Cancel

Uno de los errores más comunes en el código async de Unity es lanzar una tarea desde un MonoBehaviour y luego olvidar cancelarla cuando el objeto es destruido. La tarea continúa ejecutándose, intenta acceder a objetos de Unity destruidos y lanza MissingReferenceException — o peor, corrompe silenciosamente el estado.

ValkarnTasks elimina esta clase de errores a través de un generador de código fuente Roslyn que conecta automáticamente los métodos async al tiempo de vida de destrucción del objeto.

El Problema

Sin ninguna infraestructura, cada método async en un MonoBehaviour requiere que el desarrollador pase un CancellationToken manualmente:

// Enfoque manual — fácil de olvidar, tedioso de mantener
public class EnemyAI : MonoBehaviour
{
CancellationTokenSource _cts;

void OnEnable()
{
_cts = new CancellationTokenSource();
RunPatrolLoop(_cts.Token).Forget();
}

void OnDestroy()
{
_cts?.Cancel();
_cts?.Dispose();
}

async ValkarnTask RunPatrolLoop(CancellationToken ct)
{
while (true)
{
await ValkarnTask.Delay(2000, ct);
MoveToNextWaypoint();
}
}
}

A medida que crece el número de métodos async, también lo hace el código repetitivo. Omitir OnDestroy — o desechar en el orden incorrecto — causa las fugas descritas anteriormente.

El Enfoque Generado

Declara tu clase como partial y ValkarnTasks se encarga del resto:

// Después — declarar partial y el generador hace el cableado
public partial class EnemyAI : MonoBehaviour
{
void OnEnable()
{
RunPatrolLoop().Forget();
}

async ValkarnTask RunPatrolLoop()
{
while (true)
{
await ValkarnTask.Delay(2000, __ValkarnTaskLifetimeToken);
MoveToNextWaypoint();
}
}
}

Sin CancellationTokenSource, sin OnDestroy, sin disposición. El token generado se cancela automáticamente cuando Unity destruye el objeto.

Cómo Funciona el Generador de Código Fuente

El generador (LifecycleBindingGenerator) es un generador incremental de Roslyn que se ejecuta en tiempo de compilación. Su pipeline tiene tres etapas.

Etapa 1 — Filtro de sintaxis

El generador examina cada declaración de clase en tu proyecto. Una clase se considera candidata si:

  • Se declara con la palabra clave partial.
  • Tiene una lista de clase base (es decir, hereda de algo).

Este filtro es puramente sintáctico y muy rápido. No se ejecuta análisis semántico en esta etapa.

Etapa 2 — Transformación semántica

Para cada clase candidata, el generador usa el modelo semántico de Roslyn para:

  1. Confirmar que la clase deriva de UnityEngine.MonoBehaviour (recorre la cadena de herencia completa).
  2. Enumerar todos los miembros. Para cada miembro, verifica si es:
    • Un método async.
    • Devuelve UnaPartidaMas.Valkarn.Tasks.ValkarnTask (o ValkarnTask<T>).
    • No tiene [NoAutoCancel].
  3. Si no se encuentran métodos calificados, la clase se omite silenciosamente — no se genera nada.
  4. Solo se procesa la primera declaración parcial. Si una clase está dividida en varios archivos, el generador emite código una vez, vinculado a la primera declaración, para evitar miembros duplicados.

Etapa 3 — Emisión de código

Para cada clase que supera las etapas 1 y 2, el generador escribe un nuevo archivo .g.cs. El código generado para una clase llamada EnemyAI en el espacio de nombres Game.Enemies se ve así:

// <auto-generated/>
#nullable disable
using System.Threading;

namespace Game.Enemies
{
partial class EnemyAI
{
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
CancellationTokenSource __valkarnTaskLifetimeCts;

/// <summary>
/// Token de cancelación que se activa cuando este MonoBehaviour es destruido.
/// Generado automáticamente por el generador de código fuente de ValkarnTask.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
protected CancellationToken __ValkarnTaskLifetimeToken
{
get
{
if (__valkarnTaskLifetimeCts == null)
__valkarnTaskLifetimeCts =
CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
return __valkarnTaskLifetimeCts.Token;
}
}
}
}

Detalles clave:

  • El CancellationTokenSource es diferido — asignado solo la primera vez que se accede a __ValkarnTaskLifetimeToken.
  • Está vinculado al destroyCancellationToken integrado de Unity (MonoBehaviour.destroyCancellationToken, disponible desde Unity 2022). Cuando Unity destruye el objeto, destroyCancellationToken se dispara, lo que se propaga en cascada a __valkarnTaskLifetimeCts, que cancela __ValkarnTaskLifetimeToken.
  • Tanto el campo como la propiedad están marcados como EditorBrowsable(Never) para que no contaminen IntelliSense para los usuarios de la clase.
  • La propiedad es protected, por lo que las subclases también pueden usar el mismo token.

El Atributo [NoAutoCancel]

Aplica [NoAutoCancel] a cualquier método async ValkarnTask cuando intencionalmente quieres que continúe ejecutándose más allá del tiempo de vida del objeto. Escenarios comunes:

  • Un método que guarda datos en disco y debe completarse incluso si el objeto que lo activó es destruido.
  • Un método que gestiona un recurso compartido propiedad de un sistema diferente.
  • Efectos de transición que intencionalmente sobreviven al objeto que los inició.
public partial class SaveManager : MonoBehaviour
{
// Este método SÍ será auto-cancelado al destruir
async ValkarnTask AutoSaveLoop()
{
while (true)
{
await ValkarnTask.Delay(30_000, __ValkarnTaskLifetimeToken);
await WriteSaveToDisk();
}
}

// Este método NO será auto-cancelado — debe terminar de escribir
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk(CancellationToken ct = default)
{
await FileSystem.WriteAsync(_saveData, ct);
}
}

[NoAutoCancel] es un atributo a nivel de método. El generador simplemente excluye ese método de su conteo de métodos calificados. Si todos los métodos en una clase llevan [NoAutoCancel], el generador no emite nada para esa clase.

Analizador: TT014 — NoAutoCancel sin parámetro CancellationToken

Un analizador complementario (NoAutoCancelAnalyzer) reporta el diagnóstico TT014 cuando aplicas [NoAutoCancel] a un método que no tiene parámetro CancellationToken. Si no hay parámetro de token, el método no tiene forma de observar la cancelación — lo que significa que [NoAutoCancel] está presente pero no tiene efecto práctico. Esto generalmente significa que olvidaste añadir el token:

// TT014: [NoAutoCancel] aplicado pero el método no tiene parámetro CancellationToken
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk() // <-- falta el parámetro ct
{
await FileSystem.WriteAsync(_saveData);
}

Corrección añadiendo un parámetro CancellationToken:

[NoAutoCancel]
async ValkarnTask WriteSaveToDisk(CancellationToken ct = default)
{
await FileSystem.WriteAsync(_saveData, ct);
}

El Atributo [FireAndForget]

[FireAndForget] es un atributo separado y complementario que marca un método async como intencionalmente no esperado. Sirve dos propósitos:

  1. Suprime las advertencias VTASKS-TASK002 y VTASKS-TASK013, que se activan cuando los llamadores no await un valor de retorno ValkarnTask.
  2. Señala la intención — los futuros lectores del código saben que el descarte es deliberado.

El generador de código fuente envuelve los métodos [FireAndForget] para asegurar que cualquier excepción no observada se publique a través del manejador de excepciones no observadas de ValkarnTasks en lugar de perderse silenciosamente.

public partial class SpawnManager : MonoBehaviour
{
void OnPlayerDied()
{
// Sin advertencia, la intención es clara
ShowDeathScreenAsync();
}

[FireAndForget]
async ValkarnTask ShowDeathScreenAsync()
{
await ValkarnTask.Delay(500, __ValkarnTaskLifetimeToken);
_deathScreen.SetActive(true);
}
}

[FireAndForget] y [NoAutoCancel] son independientes y pueden combinarse:

[FireAndForget]
[NoAutoCancel]
async ValkarnTask PlayGlobalMusicAsync(CancellationToken ct = default) { ... }

Analizador: TT010 — Auto-Cancel Activo

El AutoCancelInfoAnalyzer reporta un diagnóstico informativo TT010 en cada método async ValkarnTask en un MonoBehaviour que será auto-cancelado (es decir, no tiene [NoAutoCancel]). Esto no es un error o advertencia — es transparencia intencional para que los desarrolladores puedan ver de un vistazo qué métodos están vinculados al ciclo de vida.

Puedes suprimir TT010 por método con [NoAutoCancel], o deshabilitarlo en todo el proyecto vía .editorconfig si prefieres no verlo.

Limitaciones

La clase debe declararse partial. El generador de código fuente no puede añadir miembros a una clase no parcial. Si tu MonoBehaviour no es partial, el generador lo omite silenciosamente y no se crea ningún vínculo. Los atributos [NoAutoCancel] y [FireAndForget] siguen funcionando como documentación y para los analizadores, pero __ValkarnTaskLifetimeToken no estará disponible.

Clases anidadas. Si un MonoBehaviour se declara como una clase anidada dentro de otra clase, tanto las declaraciones de clase exterior como interior deben ser partial. Roslyn requiere que todos los tipos envolventes sean parciales para que los miembros generados compilen correctamente.

Clases base. La propiedad __ValkarnTaskLifetimeToken generada es protected. Las subclases heredan automáticamente el acceso a ella. El generador se ejecuta para cada clase en la jerarquía de forma independiente; si tanto una clase base como una clase derivada son partial MonoBehaviours con métodos async, cada una obtiene su propia parcial generada, pero comparten el mismo token subyacente porque destroyCancellationToken se hereda de la base MonoBehaviour.

Herencia múltiple. C# no admite herencia múltiple de clases. Un MonoBehaviour solo puede tener una clase base, por lo que no hay ambigüedad sobre qué destroyCancellationToken vincular.

ScriptableObjects. El generador actualmente apunta solo a MonoBehaviour. ScriptableObject no tiene un equivalente de destroyCancellationToken en la API de Unity, por lo que la generación de auto-cancel no está disponible para ellos.