Saltar a contenido

Actividad 3.1 · PongExpress

Objetivo: crear tu primer videojuego con Unity 6.3, una versión del clásico Pong para dos jugadores en el mismo teclado, siguiendo los pasos indicados.

Al terminar tendrás un terreno de juego con dos palas (una se mueve con W/S y la otra con las flechas ↑/↓), una bola que rebota y dos metas que detectan los puntos y reinician la jugada.

Antes de empezar

Repasa Unity: instalación y configuración: en esta actividad se usan constantemente la Jerarquía, el Inspector y la ventana de Proyecto, y conviene tener VS Code configurado como editor de scripts.

1. Preparar el proyecto

  1. Abre Unity Hub y crea un proyecto nuevo con la plantilla Universal 2D y la versión 6.3 del editor. Llámalo PongExpress.
  2. Guarda la escena que viene creada por defecto (File › Save As) dentro de Assets/Scenes con el nombre MainScene.
  3. En la pestaña Game, abre el desplegable de resolución (por defecto pone Free Aspect) y selecciona Full HD (1920x1080).
  4. En la Jerarquía, selecciona la cámara (Main Camera). En el Inspector, dentro del componente Camera, despliega Environment y pon el color Background en negro.

    Fondo de la cámara en negro

2. Crear los elementos del juego

  1. Haz clic derecho en una zona vacía de la Jerarquía y elige 2D Object › Sprites › Square. Unity crea un GameObject que dibuja un cuadrado blanco de exactamente 1 × 1 unidades, así que la escala de cada objeto será directamente su tamaño en unidades. Usaremos este cuadrado para todos los elementos del juego: bola, palas, muros, metas y línea central.
  2. Llama Ball al nuevo GameObject. En su componente Transform, pon Position a 0 en X, Y y Z (si no está ya en el centro) y Scale a 0.5 en X y en Y.
  3. Duplica Ball (clic derecho sobre él en la Jerarquía › Duplicate, o Ctrl+D) para crear el resto de elementos, y renombra cada copia:

    • Player1 y Player2: las palas del jugador 1 (izquierda) y del jugador 2 (derecha).
    • TopWall y BottomWall: los muros superior e inferior.
    • Player1Goal y Player2Goal: las metas que defiende cada jugador, detrás de su pala.
    • MiddleLine: la línea divisoria del centro (solo decorativa).

    Modifica la posición y la escala de cada objeto (propiedades Position y Scale de su componente Transform) para que cumpla su función: tienes una tabla con valores de referencia al final de esta sección.

  4. Puede que la línea central se dibuje por encima de la bola cuando se cruzan. Para que la bola quede siempre delante, selecciona Ball y, en su componente Sprite Renderer, pon Order in Layer a 1.

  5. Crea un GameObject vacío (clic derecho en la Jerarquía › Create Empty), llámalo GameField y arrastra dentro MiddleLine, TopWall y BottomWall. Así la Jerarquía queda más ordenada. Comprueba que la posición de GameField es (0, 0, 0); si no, los muros se desplazarán.

Valores de referencia para la posición y la escala de cada GameObject. Puedes ajustarlos a tu gusto. La Z de la posición, siempre a 0; la de la escala no cambia nada en 2D:

GameObject Position (X, Y) Scale (X, Y)
Ball 0, 0 0.5, 0.5
Player1 −8, 0 0.5, 2
Player2 8, 0 0.5, 2
TopWall 0, 5 20, 0.5
BottomWall 0, −5 20, 0.5
Player1Goal −10, 0 0.5, 10
Player2Goal 10, 0 0.5, 10
MiddleLine 0, 0 0.15, 10

Los muros quedan centrados en el borde superior e inferior de la pantalla, así que en la pestaña Game solo se ve la mitad de su grosor. Las metas quedan fuera de la pantalla, justo detrás de cada pala: es normal que no se vean.

Llegados a este punto, el terreno de juego debería verse así en la escena:

Terreno de juego terminado en la ventana de escena

3. Añadir la física

  1. Añade un componente Box Collider 2D a Ball, Player1, Player2, TopWall, BottomWall, Player1Goal y Player2Goal. No se lo añadas a la cámara, a la luz (Global Light 2D), a MiddleLine ni a GameField: la bola tiene que atravesar la línea central.

    Para añadir un componente: en el Inspector, baja hasta el final, pulsa Add Component, escribe Box Collider 2D y selecciónalo en la lista. Puedes seleccionar varios GameObjects a la vez (con Ctrl pulsado) y añadirles el componente a todos de golpe.

    Búsqueda del componente Box Collider 2D

  2. Marca la casilla Is Trigger del Box Collider 2D solo en las dos metas (Player1Goal y Player2Goal). Un trigger no hace rebotar a la bola: la deja pasar y avisa de que algo lo ha atravesado, que es justo lo que necesitamos para detectar los puntos.

    Is Trigger activado en el Box Collider 2D

  3. Añade un componente Rigidbody 2D a Ball, Player1 y Player2. En los tres, pon Gravity Scale a 0 (si no, caerían).

    Componente Rigidbody 2D con Gravity Scale a 0

  4. En el Rigidbody 2D de Ball, despliega Constraints y marca Freeze Rotation Z, para que la bola no gire al chocar.

    Constraints de la bola: rotación Z fija

  5. En el Rigidbody 2D de Player1 y Player2, marca Freeze Position X y Freeze Rotation Z: las palas solo pueden moverse arriba y abajo, y la bola no podrá empujarlas hacia los lados.

    Constraints de las palas: posición X y rotación Z fijas

  6. Para que la bola rebote sin perder velocidad:

    1. Crea una carpeta Materials dentro de Assets.
    2. Dentro de ella, crea un material físico (Assets › Create › 2D › Physics Material 2D, o clic derecho › Create › 2D › Physics Material 2D) y llámalo Bounce.
    3. Configúralo con Friction a 0 (sin rozamiento) y Bounciness a 1 (rebota sin perder energía).
    4. Arrastra Bounce a la propiedad Material del Rigidbody 2D de Ball.

    Material físico Bounce: fricción 0 y rebote 1

4. Mover las palas

  1. Dentro de Assets, crea una carpeta Scripts. Dentro de ella, crea un script (clic derecho › Create › Scripting › MonoBehaviour Script) y llámalo Paddle. Escribe el nombre nada más crearlo: Unity usa ese nombre para la clase, y el nombre del fichero y el de la clase deben coincidir.
  2. Ábrelo con doble clic y complétalo partiendo de este esqueleto:

    Paddle.cs
    using UnityEngine;
    using UnityEngine.InputSystem;
    
    public class Paddle : MonoBehaviour
    {
        // Variables públicas: ¿es el jugador 1?, velocidad y Rigidbody 2D
    
        void Update()
        {
            // 1. Calcula el movimiento: 1 (subir), -1 (bajar) o 0 (quieto)
            // 2. Cambia la velocidad vertical del Rigidbody 2D
        }
    }
    

    Indicaciones:

    • Declara tres variables públicas: isPlayer1 (bool), que indica si la pala es la del jugador 1; speed (float), la velocidad de la pala; y rb (Rigidbody2D), el componente que la mueve. Las dos palas usan el mismo script: isPlayer1 es lo que las distingue.
    • using UnityEngine.InputSystem; permite leer el teclado. Keyboard.current es el teclado, y Keyboard.current[Key.W].isPressed vale true mientras la tecla W está pulsada. Las flechas son Key.UpArrow y Key.DownArrow.
    • El jugador 1 se mueve con W (subir) y S (bajar), y el jugador 2, con las flechas ↑ y ↓. Usa un if-else que compruebe isPlayer1 para leer unas teclas u otras.
    • Guarda el movimiento en una variable movement de tipo float: 1 si se pulsa la tecla de subir, -1 si se pulsa la de bajar y 0 si no se pulsa ninguna (o las dos a la vez).
    • En lugar de mover la pala cambiando su posición, le damos una velocidad a su Rigidbody 2D: así es el motor de física quien la mueve, y se detiene sola al chocar con los muros. Se hace así, manteniendo la velocidad en X que ya tenía y cambiando solo la Y:

      rb.linearVelocity = new Vector2(rb.linearVelocity.x, movement * speed);
      
    Pista

    Empieza el Update con float movement = 0;. Después, dentro de cada rama del if (isPlayer1), usa dos if independientes (sin else): si la tecla de subir está pulsada, suma 1 a movement; si lo está la de bajar, réstale 1. Si se pulsan las dos, el resultado es 0 y la pala no se mueve.

  3. Añade el script Paddle como componente de Player1 y de Player2 (arrástralo desde la ventana de Proyecto hasta cada GameObject de la Jerarquía) y configura sus variables públicas en el Inspector:

    Variable Player1 Player2
    isPlayer1 Marcada Sin marcar
    Speed 5 5
    Rb El Rigidbody 2D de Player1 El Rigidbody 2D de Player2

    Para rellenar Rb, arrastra el GameObject (o su componente Rigidbody 2D) hasta el hueco de la variable.

  4. Pulsa Play y prueba el juego: deberías poder mover las dos palas con W/S y con las flechas ↑/↓, y las palas no deberían atravesar los muros. Vuelve a pulsar Play para salir.

Los cambios en modo Play no se guardan

Todo lo que modifiques en el Inspector mientras el juego está en marcha se pierde al salir. Comprueba siempre que has parado el juego antes de cambiar algo.

5. Lanzar la bola

  1. En la carpeta Scripts, crea otro script llamado Ball y complétalo partiendo de este esqueleto:

    Ball.cs
    using UnityEngine;
    
    public class Ball : MonoBehaviour
    {
        // Variables públicas: velocidad y Rigidbody 2D
    
        void Start()
        {
            // Lanza la bola al empezar
        }
    
        private void Launch()
        {
            // 1. Elige al azar la dirección en X y en Y: -1 o 1
            // 2. Da a la bola su velocidad inicial
        }
    }
    

    Indicaciones:

    • Declara dos variables públicas: speed (float), la velocidad de la bola, y rb (Rigidbody2D), el componente que la mueve.
    • El método Launch da a la bola su velocidad inicial. Llámalo desde Start.
    • Para elegir al azar entre -1 y 1:

      float x = UnityEngine.Random.Range(0, 2) == 0 ? -1 : 1;
      

      Random.Range(0, 2) devuelve un entero aleatorio que puede ser 0 o 1 (el valor máximo queda excluido), y el operador ternario lo convierte en -1 o 1. Escribimos UnityEngine.Random para usar la clase Random de Unity: .NET tiene otra con el mismo nombre, y si el script tiene también using System;, el compilador no sabría cuál usar.

    • La velocidad de la bola se cambia así:

      rb.linearVelocity = new Vector2(x * speed, y * speed);
      
    Pista

    Necesitas dos números aleatorios, uno para cada eje: calcula y igual que x. Así la bola sale en una de las cuatro diagonales al azar.

  2. Añade el script Ball como componente del GameObject Ball. Pon Speed a 5 y arrastra a Rb el Rigidbody 2D de la bola.

    Componente Ball configurado

  3. Prueba el juego. Ya puedes jugar a Pong, aunque solo un punto: cuando la bola sale por un lado, se pierde.

6. Detectar los puntos

  1. Selecciona Ball y, en la parte superior del Inspector, abre el desplegable Tag y elige Add Tag…. Pulsa +, escribe Ball y guarda. Vuelve a seleccionar Ball y asígnale la nueva etiqueta Ball en el desplegable Tag. La usaremos para saber que lo que ha entrado en la meta es la bola.
  2. En Scripts, crea un script llamado Goal y añádeselo a Player1Goal y a Player2Goal. Debe tener:

    • Una variable pública bool llamada isPlayer1Wall, que marcarás en el Inspector solo en Player1Goal.
    • El método OnTriggerEnter2D, que Unity llama automáticamente cuando otro collider entra en el trigger de la meta. El parámetro collision es el collider que ha entrado:
    Goal.cs
    using UnityEngine;
    
    public class Goal : MonoBehaviour
    {
        public bool isPlayer1Wall;
    
        private void OnTriggerEnter2D(Collider2D collision)
        {
            if (collision.gameObject.CompareTag("Ball"))
            {
                // Aquí: decide quién ha marcado y muéstralo con Debug.Log
            }
        }
    }
    

    Completa el if para que muestre en la Consola qué jugador ha marcado. Ojo: si la bola entra en la meta del jugador 1, el punto es para el jugador 2, y al revés.

  3. Prueba el juego y comprueba en la Consola que cada punto se atribuye al jugador correcto.

Pista

Dentro del if de la etiqueta, añade otro if-else que compruebe isPlayer1Wall. Si vale true, la bola ha entrado en la meta del jugador 1, así que escribe Debug.Log("Punto para el jugador 2");; si no, Debug.Log("Punto para el jugador 1");.

7. Reiniciar la jugada tras cada punto

Por último, consigue que, cuando se marque un punto, la jugada vuelva a empezar: la bola y las palas regresan a su posición inicial, la bola se lanza de nuevo y la Consola muestra el marcador. Sigue estos pasos:

  1. Guardar la posición inicial. Añade a Paddle y a Ball una variable privada startPosition y guarda en ella la posición del objeto en Start. En Paddle todavía no hay método Start: créalo.

    private Vector3 startPosition;
    
    void Start()
    {
        startPosition = transform.position;
    }
    

    En Ball, el método Start ya existe: añade la línea startPosition = transform.position; antes de la llamada a Launch().

  2. Volver a la posición inicial. Añade a Paddle y a Ball un método público ResetPosition que pare el objeto y lo devuelva a su sitio:

    public void ResetPosition()
    {
        rb.linearVelocity = Vector2.zero;
        transform.position = startPosition;
    }
    

    En Ball, además, este método debe volver a lanzar la bola: añade al final una llamada a Launch().

    No lo llames Reset

    Reset es un nombre reservado de Unity: el editor llama automáticamente a un método con ese nombre al añadir el componente, aunque el juego no esté en marcha.

  3. Crear el GameManager. Crea un GameObject vacío llamado GameManager y un script GameManager que se añada a él. El GameManager es quien lleva el marcador y coordina a los demás objetos. Necesita:

    • Tres variables públicas para comunicarse con los otros scripts: public Ball ball;, public Paddle player1Paddle; y public Paddle player2Paddle;. En el Inspector, arrastra a cada una el GameObject correspondiente.
    • Dos variables privadas de tipo int para los puntos de cada jugador.
    • Un método público Player1Scores y otro Player2Scores. Cada uno suma un punto a su jugador, muestra el marcador con Debug.Log (p. ej., Marcador: 1 - 0) y llama a un método ResetPositions.
    • Un método ResetPositions que llame a ResetPosition() de la bola y de las dos palas.
  4. Avisar al GameManager desde las metas. Añade a Goal una variable public GameManager gameManager; y, en el Inspector de cada meta, arrastra a ella el GameObject GameManager. Después, cambia los Debug.Log del apartado 6 por llamadas a gameManager.Player1Scores() o a gameManager.Player2Scores(), según quién haya marcado.

  5. Prueba el juego: tras cada punto, todo debe volver a su sitio, la bola debe salir en una dirección al azar y la Consola debe mostrar el marcador actualizado.

Pista

Un script puede usar los métodos públicos de otro script si tiene una referencia a él. Por ejemplo, con la variable public Ball ball; ya configurada en el Inspector, el GameManager puede escribir ball.ResetPosition();. Si al marcar aparece en la Consola un error NullReferenceException, es que alguna de esas variables se ha quedado sin rellenar en el Inspector (aparece como None).

El método Player1Scores podría empezar así:

public void Player1Scores()
{
    player1Score += 1;
    // Mostrar el marcador y reiniciar las posiciones
}

Parte opcional 1: efectos de luz

La plantilla Universal 2D usa el renderizador 2D de URP, que permite iluminar la escena con luces 2D:

  1. Desactiva o elimina el GameObject Global Light 2D de la escena. La escena se quedará a oscuras.
  2. Añade una luz como hijo de cada GameObject que quieras que tenga luz propia: clic derecho sobre él en la Jerarquía › Light › Spot Light 2D (o Freeform Light 2D, cuya forma puedes ajustar a la del objeto). Configura a tu gusto su color, su intensidad y su radio.

Parte opcional 2: estela de la bola

El componente Trail Renderer dibuja una estela detrás de un objeto que se mueve:

  1. Selecciona Ball y añádele un componente Trail Renderer.
  2. En Materials, asigna a Element 0 el material Sprite-Unlit-Default, que trae URP. Como no tiene textura, la estela será un trazo continuo, y como no le afectan las luces 2D, se verá aunque hayas apagado la luz global.
  3. En la curva Width, haz que la estela empiece con grosor 0.25 junto a la bola y termine con 0, para que se vaya afinando. Para añadir un punto a la curva, haz doble clic sobre ella; después, arrastra cada punto a su altura.
  4. Haz clic en el degradado Color. Pon en las dos marcas de color (las de abajo) el mismo color que la luz de la bola. En las marcas de alfa (las de arriba, la opacidad), deja la izquierda, que es el extremo junto a la bola, a 255 y pon la derecha a 0: la estela se irá volviendo transparente.
  5. Comprueba que el Order in Layer del Sprite Renderer de Ball sigue a 1 (lo pusiste en el apartado 2). La estela tiene 0, así que la bola se dibuja por encima de ella.
  6. Prueba el juego. Si quieres una estela más larga o más corta, cambia Time: los segundos que tarda en desaparecer cada tramo.