Computación Gráfica

Empanada Rush: experiencia desarrollando un videojuego 2D en Unity

Empanada Rush es un proyecto desarrollado en Unity, explicando paso a paso las etapas de diseño, programación e implementación. El objetivo principal fue desarrollar un juego entretenido e interactivo, aplicando conceptos fundamentales de programación, diseño de escenarios, físicas y lógica de videojuegos.

Creación del proyecto en Unity

Se creo un nuevo proyecto en Unity con la plantilla 2D.
En Edit → Project Settings → Physics 2D, ajusta la gravedad a -20 para que el salto se sienta más ágil como en Pony Express.
Se creo esta estructura de carpetas en Assets/:

Creación del Canvas y pantalla inicial en Unity

Para la creación de la pantalla inicial de Empanada Rush, se utilizó el sistema de interfaz gráfica de Unity mediante un Canvas, el cual permite agregar botones, textos e imágenes dentro del videojuego.

El primer paso fue crear un objeto Canvas dentro de la escena principal.

  1. En la jerarquía de Unity seleccionar:
    • Click derecho → UI → Canvas
  2. Unity automáticamente crea:
    • Canvas
    • EventSystem
  3. Configurar el Canvas en modo:
    • Screen Space – Overlay

El Canvas será el contenedor principal de todos los elementos visuales del menú.

Dentro del Canvas se agregaron diferentes componentes UI para construir la pantalla inicial.

  • Instrucciones
  • Seleccionar Personaje
  • Empezar Partida
  • Salir

Cada botón fue configurado con diferentes acciones utilizando el componente On Click() de Unity.

Como resultado se obtuvo una pantalla inicial completamente funcional, organizada y visualmente atractiva, permitiendo al jugador navegar fácilmente entre las diferentes opciones del menú principal de Empanada Rush.

Selección de Personajes

Se crearon tres botones de personaje (Estudiante 1, Estudiante 2, Profesor) con sus sprites correspondientes y los textos que muestre las dificultades al pasar el cursor sobre cada botón.

El script CharacterSelector.cs almacena la elección en un objeto persistente:

public class CharacterSelector : MonoBehaviour
{
    public static int SelectedCharacter; 

    public void SelectCharacter(int index)
    {
        SelectedCharacter = index;
        // Muestra preview y dificultad
    }

    public void StartGame()
    {
        SceneManager.LoadScene("GameScene");
    }
}

Se creo l script DifficultyManager.cs. La dificultad controla velocidad del scroll, frecuencia de obstáculos y número de vidas:

public class DifficultyManager : MonoBehaviour
{
    public static DifficultyConfig Current;

    void Awake()
    {
        Current = CharacterSelector.SelectedCharacter switch
        {
            0 => new DifficultyConfig { speed = 4f, obstacleRate = 2.5f, lives = 3 },  
            1 => new DifficultyConfig { speed = 6f, obstacleRate = 1.8f, lives = 2 },  
            2 => new DifficultyConfig { speed = 9f, obstacleRate = 1.0f, lives = 1 },  
            _ => throw new System.Exception("Personaje no válido")
        };
    }
}

[System.Serializable]
public class DifficultyConfig
{
    public float speed;
    public float obstacleRate;
    public int lives;
}

Resultado

Escenario y personaje jugable

Se creo un GameObject Player con: Sprite Renderer, Rigidbody2D (gravedad activa), BoxCollider2D y el script PlayerController.cs

public class PlayerController : MonoBehaviour
{
    private Rigidbody2D rb;
    private bool isGrounded;
    private int jumpsRemaining;

    [SerializeField] private float jumpForce = 12f;
    [SerializeField] private LayerMask groundLayer;

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        // El estudiante 1 puede hacer doble salto (modo fácil)
        jumpsRemaining = CharacterSelector.SelectedCharacter == 0 ? 2 : 1;
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space) || Input.GetMouseButtonDown(0))
        {
            if (isGrounded || jumpsRemaining > 0)
            {
                rb.velocity = new Vector2(rb.velocity.x, jumpForce);
                jumpsRemaining--;
            }
        }
    }

    void OnCollisionEnter2D(Collision2D col)
    {
        if (((1 << col.gameObject.layer) & groundLayer) != 0)
        {
            isGrounded = true;
            jumpsRemaining = CharacterSelector.SelectedCharacter == 0 ? 2 : 1;
        }
    }

    void OnCollisionExit2D(Collision2D col)
    {
        if (((1 << col.gameObject.layer) & groundLayer) != 0)
            isGrounded = false;
    }
}

Se creo un script ScrollManager.cs y asígnalo a todos los elementos de fondo y al suelo

public class ScrollManager : MonoBehaviour
{
    private float scrollSpeed;

    void Start() => scrollSpeed = DifficultyManager.Current.speed;

    void Update()
    {
        transform.Translate(Vector2.left * scrollSpeed * Time.deltaTime);

        // Repositiona el segmento cuando sale de pantalla (suelo en loop)
        if (transform.position.x < -20f)
            transform.position += new Vector3(40f, 0f, 0f);
    }
}

Generador de obstáculos y empanadas

Se creo un ObstacleSpawner vacío en la escena. Este GameObject instancia prefabs de obstáculos y empanadas a intervalos definidos por la dificultad

public class ObstacleSpawner : MonoBehaviour
{
    [SerializeField] private GameObject[] obstaclePrefabs;
    [SerializeField] private GameObject empanada;
    private float timer;
    private float spawnRate;

    void Start() => spawnRate = DifficultyManager.Current.obstacleRate;

    void Update()
    {
        timer += Time.deltaTime;
        if (timer >= spawnRate)
        {
            timer = 0f;
            SpawnObstacle();

            // Empanada con probabilidad del 40%
            if (Random.value < 0.4f)
                SpawnCollectible();
        }
    }

    void SpawnObstacle()
    {
        var prefab = obstaclePrefabs[Random.Range(0, obstaclePrefabs.Length)];
        var pos = new Vector3(12f, 0.5f, 0f);
        var obj = Instantiate(prefab, pos, Quaternion.identity);
        obj.AddComponent<ScrollManager>(); // Se destruye fuera de pantalla
        Destroy(obj, 8f);
    }

    void SpawnCollectible()
    {
        var pos = new Vector3(12f, Random.Range(1f, 3f), 0f);
        var obj = Instantiate(empanada, pos, Quaternion.identity);
        obj.AddComponent<ScrollManager>();
        Destroy(obj, 8f);
    }
}

En el prefab del Player, se añade este script de colisiones

public class CollisionHandler : MonoBehaviour
{
    private int lives;
    private int empanadaCount = 0;

    void Start()
    {
        lives = DifficultyManager.Current.lives;
        UIManager.Instance.UpdateLives(lives);
    }

    void OnTriggerEnter2D(Collider2D col)
    {
        if (col.CompareTag("Empanada"))
        {
            empanadaCount++;
            UIManager.Instance.UpdateScore(empanadaCount);
            Destroy(col.gameObject);
        }

        if (col.CompareTag("Obstacle"))
        {
            lives--;
            UIManager.Instance.UpdateLives(lives);
            if (lives <= 0)
                GameManager.Instance.GameOver();
        }

        if (col.CompareTag("Goal"))
            GameManager.Instance.Win();
    }
}

Una vez configurados todos los botones de selección de personaje y la generación aleatoria de obstáculos, se creó el Canvas de victoria y de Game Over, el cual utiliza los siguientes scripts:

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using TMPro;
using Image = UnityEngine.UI.Image;

public class UIManager : MonoBehaviour
{
    public static UIManager Instance;

    [Header("HUDs de vida por personaje")]
    public GameObject hudYhonatan;
    public GameObject hudLaura;
    public GameObject hudCarlos;

    [Header("Textos de vidas")]
    public TextMeshProUGUI textoVidasY;
    public TextMeshProUGUI textoVidasL;
    public TextMeshProUGUI textoVidasC;

    [Header("Contadores de empanadas por personaje")]
    public GameObject contadorYhonatan; 
    public GameObject contadorLaura;    
    public GameObject contadorCarlos;   
    public TextMeshProUGUI textoEmpY;
    public TextMeshProUGUI textoEmpL;
    public TextMeshProUGUI textoEmpC;

    [Header("Pantallas Game Over por personaje")]
    public GameObject gameOverYhonatan;
    public GameObject gameOverLaura;
    public GameObject gameOverCarlos;

    [Header("Pantallas Victoria por personaje")]
    public GameObject victoriaYhonatan;
    public GameObject victoriaLaura;
    public GameObject victoriaCarlos;

    private int vidasActuales;
    private int empanadaActual = 0;
    private int metaEmpanadas;
    private int personaje;

    void Awake()
    {
        if (Instance == null) Instance = this;
        else Destroy(gameObject);
    }

    void Start()
    {
        personaje = MenuManager.PersonajeSeleccionado;
        vidasActuales = DifficultyManager.vidas;

        switch (personaje)
        {
            case 0: metaEmpanadas = 20; break;
            case 1: metaEmpanadas = 30; break;
            case 2: metaEmpanadas = 45; break;
            default: metaEmpanadas = 20; break;
        }

        MostrarHudCorrecto();
        ActualizarTextoVidas();
        ActualizarEmpanadas();
        OcultarTodasLasPantallas();
    }

    void MostrarHudCorrecto()
    {
        if (hudYhonatan != null) hudYhonatan.SetActive(personaje == 0);
        if (hudLaura != null) hudLaura.SetActive(personaje == 1);
        if (hudCarlos != null) hudCarlos.SetActive(personaje == 2);

        if (contadorYhonatan != null) contadorYhonatan.SetActive(personaje == 0);
        if (contadorLaura != null) contadorLaura.SetActive(personaje == 1);
        if (contadorCarlos != null) contadorCarlos.SetActive(personaje == 2);
    }

    void OcultarTodasLasPantallas()
    {
        if (gameOverYhonatan != null) gameOverYhonatan.SetActive(false);
        if (gameOverLaura != null) gameOverLaura.SetActive(false);
        if (gameOverCarlos != null) gameOverCarlos.SetActive(false);
        if (victoriaYhonatan != null) victoriaYhonatan.SetActive(false);
        if (victoriaLaura != null) victoriaLaura.SetActive(false);
        if (victoriaCarlos != null) victoriaCarlos.SetActive(false);
    }

    

    public void PerderVida()
    {
        vidasActuales--;
        vidasActuales = Mathf.Max(0, vidasActuales);
        ActualizarTextoVidas();
        if (vidasActuales <= 0) MostrarGameOver();
    }

    void ActualizarTextoVidas()
    {
        if (textoVidasY != null && personaje == 0) textoVidasY.text = vidasActuales.ToString();
        if (textoVidasL != null && personaje == 1) textoVidasL.text = vidasActuales.ToString();
        if (textoVidasC != null && personaje == 2) textoVidasC.text = vidasActuales.ToString();
    }

    public void ActualizarVidas(int cantidad)
    {
        vidasActuales = cantidad;
        ActualizarTextoVidas();
    }

    

    public void RecogerEmpanada()
    {
        empanadaActual++;
        ActualizarEmpanadas();
        if (empanadaActual >= metaEmpanadas) MostrarVictoria();
    }

    void ActualizarEmpanadas()
    {
        
        
        string texto = empanadaActual.ToString();
        if (textoEmpY != null && personaje == 0) textoEmpY.text = texto;
        if (textoEmpL != null && personaje == 1) textoEmpL.text = texto;
        if (textoEmpC != null && personaje == 2) textoEmpC.text = texto;
    }

    public void ActualizarEmpanadas(int cantidad, int meta)
    {
        empanadaActual = cantidad;
        metaEmpanadas = meta;
        ActualizarEmpanadas();
    }

    public void ActualizarDistancia(float actual, float total) { }


    void MostrarGameOver()
    {
        Time.timeScale = 0f;

        if (AudioManager.Instance != null)
            AudioManager.Instance.ReproducirMusicaGameOver();

        switch (personaje)
        {
            case 0: if (gameOverYhonatan != null) gameOverYhonatan.SetActive(true); break;
            case 1: if (gameOverLaura != null) gameOverLaura.SetActive(true); break;
            case 2: if (gameOverCarlos != null) gameOverCarlos.SetActive(true); break;
        }
    }



    void MostrarVictoria()
    {
        Time.timeScale = 0f;

        if (AudioManager.Instance != null)
            AudioManager.Instance.ReproducirMusicaVictoria();

        switch (personaje)
        {
            case 0: if (victoriaYhonatan != null) victoriaYhonatan.SetActive(true); break;
            case 1: if (victoriaLaura != null) victoriaLaura.SetActive(true); break;
            case 2: if (victoriaCarlos != null) victoriaCarlos.SetActive(true); break;
        }
    }

    

    public void Reintentar()
    {
        Time.timeScale = 1f;
        SceneManager.LoadScene("GameScene");
    }

    public void IrAlMenu()
    {
        Time.timeScale = 1f;
        SceneManager.LoadScene("Portada");
    }
}


Posteriormente, se insertaron los sonidos para cada acción del juego: inicio, transcurso de la partida, daño al personaje, recolección de empanadas, Game Over y victoria.

using UnityEngine;

public class AutoDestruir : MonoBehaviour
{
    private Camera cam;

    void Start()
    {
        cam = Camera.main;
    }

    void Update()
    {
        if (cam == null) return;

        float bordeIzquierdoCamara = cam.transform.position.x
            - cam.orthographicSize * cam.aspect;

        // Margen de 4 unidades extra para asegurarse que salió completamente
        if (transform.position.x < bordeIzquierdoCamara - 4f)
        {
            Destroy(gameObject);
        }
    }
}
using UnityEngine;

public class AudioGameScene : MonoBehaviour
{
    void Start()
    {
        if (AudioManager.Instance != null)
            AudioManager.Instance.ReproducirMusicaJuego();
    }
}

Publicación del Juego

Se publico el juego ene la plataforma de https://itch.io/

Crear una cuenta
– Ingresa a itch.io.
– Haz clic en Register.
– Crea una cuenta con usuario, correo y contraseña.

Exportar el juego desde Unity
-Abre el proyecto en Unity.
-Ve a File → Build Settings.
-Selecciona la plataforma:
-Web
-Presiona Build.
-Selecciona una carpeta donde se guardará el ejecutable del juego.

Se debe asegurar que todas las escenas dentro de Unity estén seleccionadas y organizadas en el orden correcto dentro de Build Settings, para garantizar el funcionamiento adecuado de las transiciones y la carga del juego.

Luego de exportar el juego, se debe comprimir la carpeta generada.

Crear un nuevo proyecto en itch.io
-Haz clic en Create new project.
-Completa la información:
-Title: nombre del juego.
-Short description: descripción corta.

  1. En la sección Uploads, haz clic en Upload files.
  2. Selecciona el archivo .zip del juego.
  3. Espera a que termine la carga.

Y a jugar!

Referencias

YouTube. (2024). SUBIR JUEGO de Unity a WEB (itch.io) [Video]. YouTube. YouTube Video

Unity Technologies. (s.f.). Unity Hub. Unity. Recuperado el 22 de mayo de 2026, de Unity Hub Official Site

CraftPix. (s.f.). Sprites. CraftPix. Recuperado el 22 de mayo de 2026, de CraftPix Sprites

Créditos

Autor: Yhonatan Estyben Garcia Becerra / Laura Alejandra Castañeda Reina

Editor: Carlos Iván Pinzón Romero

Código: UCG-10

Universidad: Universidad Central