10. Gestionando nuestro Código

Hoy vamos a ver algunas técnicas para poder Gestionar mejor nuestros Códigos en C# y Unity.

Para crear juegos profesionales en Unity, no basta con que “funcione”: tu código debe ser moldeable, legible y eficiente en tiempo de ejecución. En este tutorial cubriremos dos aspectos esenciales:

  1. Separación lógica usando el patrón MVC
  2. Gestión de memoria y rendimiento, profundizando en el Garbage Collector y el Object Pooling
Tutorial de Unity Nivel: Intermedio.

10.1 Separación Lógica con MVC

La tentación de mezclar UI, lógica de datos y comportamiento en un único MonoBehaviour lleva rápidamente al “spaghetti code”. Patrones como MVC nos ayudan a organizar responsabilidades.

Patrón MVC (Modelo–Vista–Controlador)

Modelo (Model)

  • Representa los datos y la lógica de negocio (por ejemplo, estadísticas del jugador).
  • No conoce la UI ni la representación gráfica.

Vista (View)

  • Se encarga de la representación visual (UI, sprites, animaciones).
  • Expone métodos para actualizarse, pero no contiene lógica de datos.

Controlador (Controller)

  • Actúa como puente: recibe eventos de la Vista (inputs), actualiza el Modelo y ordena a la Vista que se refresque.

Ejemplo: Barra de vida del jugador

PlayerModel.cs
public class PlayerModel {
    public int VidaMaxima { get; } = 100;
    public int VidaActual { get; private set; }

    public PlayerModel() {
        VidaActual = VidaMaxima;
    }

    public void RecibirDaño(int dmg) {
        VidaActual = Mathf.Clamp(VidaActual - dmg, 0, VidaMaxima);
    }
}
PlayerView.cs
using UnityEngine;
using UnityEngine.UI;

public class PlayerView : MonoBehaviour {
    [SerializeField] private Slider sliderVida;

    public void ActualizarBarra(int actual, int maximo) {
        sliderVida.value = (float)actual / maximo;
    }
}
PlayerController.cs
using UnityEngine;

public class PlayerController : MonoBehaviour {
    private PlayerModel model;
    [SerializeField] private PlayerView view;

    void Start() {
        model = new PlayerModel();
        view.ActualizarBarra(model.VidaActual, model.VidaMaxima);
    }

    void Update() {
        if (Input.GetKeyDown(KeyCode.Space)) {
            model.RecibirDaño(10);
            view.ActualizarBarra(model.VidaActual, model.VidaMaxima);
        }
    }
}

Beneficios: cada clase tiene una única responsabilidad. El modelo puede testearse sin Unity; la vista sin lógica; el controlador orquesta todo.

10.2 Gestión de Memoria y Rendimiento

En un juego real, instanciar y luego destruir decenas de objetos por segundo (balas, partículas, enemigos) puede disparar el Garbage Collector (GC) de .NET y provocar micro-stutters (un problema de calidad en los videojuegos que se manifiesta como pequeños retrasos irregulares en la reproducción de los cuadros, creando la sensación de que el juego va a tirones). Ahora veamos cómo mitigar eso.

Garbage Collector en .NET

El Garbage Collector (GC) en .NET es un componente esencial que administra automáticamente la memoria, liberando objetos que ya no están en uso para evitar fugas de memoria y mejorar el rendimiento de las aplicaciones

  • Pros: te olvidas del delete.
  • Contras: cuando aparece, puede pausar tu juego brevemente.

Buenas prácticas para minimizar GC:

  • Evita crear objetos temporales en Update() (por ejemplo, strings, listas, enumerable LINQ).
  • Prefiere estructuras reutilizables (arrays fijos, StringBuilder).
  • Usa struct (tipos por valor) para datos pequeños y sin boxing.

Object Pooling

La agrupación de objetos (Object Pooling) es un patrón de diseño que reutiliza instancias de objetos ya creadas en lugar de crear nuevas instancias cada vez que se necesitan; esto reduce la carga de la CPU. En otras palabras; en lugar de Instantiate() y Destroy(), se crea un pool (contenedor) de instancias desactivadas para reutilizarlas.

Ejemplo: Pool de balas

Problema común: Instantiate() + Destroy()

void Disparar()
{
    GameObject proyectil = Instantiate(prefab, transform.position, Quaternion.identity);
    Destroy(proyectil, 2f); // Esto será recolectado por el GC
}
  • Cada bala genera una nueva instancia en memoria.
  • Destroy() marca el objeto para eliminación, y el GC lo limpia eventualmente.
  • Resultado: micro-lags por recolección frecuente.

Solución: Object Pooling

Bala.cs
using UnityEngine;

public class Bala : MonoBehaviour {
    public float velocidad = 10f;

    void OnEnable() {
        // Opcional: reiniciar efectos
    }

    void Update() {
        transform.Translate(Vector3.forward * velocidad * Time.deltaTime);
    }

    public void Desactivar() {
        gameObject.SetActive(false);
    }
}
PoolBalas.cs
using UnityEngine;
using System.Collections.Generic;

public class PoolBalas : MonoBehaviour {
    [SerializeField] private Bala prefabBala;
    [SerializeField] private int inicial = 20;

    private Queue<Bala> pool = new Queue<Bala>();

    void Start() {
        for (int i = 0; i < inicial; i++) {
            CrearNueva();
        }
    }

    private Bala CrearNueva() {
        Bala b = Instantiate(prefabBala, transform);
        b.gameObject.SetActive(false);
        pool.Enqueue(b);
        return b;
    }

    public Bala Obtener() {
        if (pool.Count == 0) CrearNueva();
        Bala b = pool.Dequeue();
        b.gameObject.SetActive(true);
        return b;
    }

    public void Devolver(Bala b) {
        b.Desactivar();
        pool.Enqueue(b);
    }
}
Disparador.cs
using UnityEngine;

public class Disparador : MonoBehaviour {
    [SerializeField] private PoolBalas pool;
    [SerializeField] private Transform puntoSalida;

    void Update() {
        if (Input.GetButtonDown("Fire1")) {
            Bala b = pool.Obtener();
            b.transform.position = puntoSalida.position;
            Invoke(nameof(DevolverBala), 2f, b);
        }
    }

    void DevolverBala(Bala b) {
        pool.Devolver(b);
    }
}

Resultado: balas que se activan, vuelan y se desactivan sin consumir ni liberar memoria en cada disparo.

10.3 Conclusión

  • Separación lógica con MVC mantiene tu código claro, testeable y colaborativo.
  • Entender el Garbage Collector y usar Object Pooling evita pausas inesperadas y mejora la experiencia de juego.

Implementar ambos enfoques en tu proyecto de Unity te colocará en un nivel de desarrollo profesional (estos temas te ayudan a pensar como un desarrollador profesional.), donde tu código es tan sólido como tu juego es divertido 🙂

Profundizando en el rendimiento

  • Usa Object Pooling para balas, enemigos, partículas, UI.
  • Evita Find(), GetComponent() en Update().
  • Usa FixedUpdate() solo para física.
  • Optimiza Update(), usa Events o Coroutines si puedes.
  • Divide grandes scripts en componentes pequeños.
  • Batching: usa static o dynamic batching y GPU instancing para reducir llamadas Draw.
  • Avoid Expensive Calls: No uses FindObjectOfType o GetComponent en bucles; cachea referencias.
  • Profilling: emplea el Profiler de Unity para identificar picos de GC y cuellos de botella.
  • Jobs & Burst: para cálculos intensivos, considera el Job System y Burst Compiler.

Ejercicios.

Para reforzar lo aprendido, es necesario practicarlo, por ello intenta realizar los siguientes ejercicios:

  1. Siguiendo los ejemplos de este tutorial, crea tu propio Pool de enemigos para instanciarlos, luego desactivarlos al derrotarlos y después volverlos a instanciar en otro lugar.

Este Tutorial de Unity termina aquí. Esperamos que te haya sido de mucha utilidad y que ahora tengas una noción más amplia de las estrategias más comunes y efectivas para desarrollar mejores videojuegos 🙂

¿Te resultó útil este Tutorial?
¡¡Recuerda, los Anuncios nos Ayudan a Mantener este “Gran Sitio” 😀 !!

Comparte el Post
Posted in CSharpUnity.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

This site uses Akismet to reduce spam. Learn how your comment data is processed.