El problema: dependencias escondidas#
Cuando una clase crea o busca por su cuenta lo que necesita, queda atada a ello:
// ❌ Enemigo atado a implementaciones concretas
class Enemigo : MonoBehaviour
{
void Morir()
{
AudioManager.Instance.Reproducir(sonido); // Singleton concreto
FindObjectOfType<PuntuacionUI>().Sumar(10); // búsqueda frágil y lenta
}
}Esto es difícil de probar (no puedes cambiar el AudioManager por uno falso), frágil (si desaparece el Singleton, peta) y opaco (no ves de qué depende sin leer el cuerpo).
La idea: que te den las dependencias#
Inyección de dependencias (DI): en vez de crear/buscar lo que necesitas, lo recibes desde fuera. La clase declara qué necesita (idealmente como interfaz) y otro se lo entrega.
interface IAudio { void Reproducir(AudioClip clip); }
class Enemigo : MonoBehaviour
{
private IAudio audio;
private IPuntuacion puntuacion;
// Recibe sus dependencias (aquí, un inicializador)
public void Init(IAudio audio, IPuntuacion puntuacion)
{
this.audio = audio;
this.puntuacion = puntuacion;
}
void Morir()
{
audio.Reproducir(sonido); // no sabe ni le importa quién lo implementa
puntuacion.Sumar(10);
}
}Ahora Enemigo depende de abstracciones (la letra D de SOLID): en el juego le pasas el audio real; en un test, uno falso.
DI a mano (sin framework)#
En muchos juegos no necesitas nada más que esto: un objeto "compositor" (a veces el propio GameManager de arranque) que crea las implementaciones y las reparte. Es la forma más simple y transparente, y para proyectos pequeños/medianos suele bastar.
Un ScriptableObject es una dependencia inyectable buenísima en Unity: arrastras el mismo asset (un canal de eventos, una config) a varios componentes desde el Inspector. Es "inyección" a mano, visual y sin código de fontanería.
Frameworks de DI: VContainer / Zenject#
Cuando el proyecto crece y repartir dependencias a mano se vuelve tedioso, un contenedor de DI lo automatiza: registras "cuando alguien pida IAudio, dale AudioReal" y el contenedor inyecta todo solo (en constructores, campos o métodos).
- VContainer: moderno, muy rápido y ligero; la recomendación actual para Unity.
- Zenject/Extenject: veterano, muy completo, más pesado.
Un contenedor de DI añade una curva de aprendizaje y "magia" que oscurece el flujo. Para un Pong o un prototipo es sobreingeniería: la DI a mano con interfaces es suficiente y más clara. Introduce el framework cuando el reparto manual sea un dolor real, no antes.
DI vs Singleton#
| Inyección de dependencias | Singleton | |
|---|---|---|
| Acoplamiento | Bajo (dependes de interfaces) | Alto (dependes de la clase concreta global) |
| Testabilidad | Alta (inyectas dobles) | Baja (estado global difícil de aislar) |
| Visibilidad de dependencias | Explícita (las declaras) | Oculta (aparecen dentro de métodos) |
| Coste inicial | Algo más de fontanería | Cero, muy rápido de escribir |
| Riesgo | Sobreingeniería si abusas | Estado global y acoplamiento si abusas |
Errores frecuentes#
- Llamar "DI" a seguir usando Singletons por dentro.
- Inyectar clases concretas en vez de interfaces (pierdes casi toda la ventaja).
- Adoptar Zenject/VContainer en un prototipo (complejidad que no necesitas).
- Inyectar absolutamente todo, hasta lo trivial: acaba en fontanería infinita.
Ponte a prueba#
Resumen#
La inyección de dependencias hace que una clase reciba lo que necesita (como interfaz) en vez de crearlo o buscarlo, reduciendo el acoplamiento y haciéndola testable: es la letra D de SOLID en acción, y la alternativa sana al abuso de Singletons. En la mayoría de juegos, hacerla a mano (un compositor que reparte, o ScriptableObjects por el Inspector) es suficiente; los frameworks como VContainer aportan cuando el proyecto crece. Como siempre: sin sobreingeniar.