Qué vas a construir#
Pong: dos palas que suben y bajan, una pelota que rebota entre ellas y en los bordes, puntos cuando alguien falla, y un ganador al llegar a cierta puntuación. Es el clásico perfecto para tu primer juego terminado: pequeño, pero toca físicas 2D, colisiones, input, UI y lógica de juego.
Con haber leído GameObjects y Componentes, Colliders y triggers y algo de movimiento 2D te sobra. No hacen falta gráficos: usaremos cuadrados blancos.
Paso 1 — Proyecto y escena#
- Crea un proyecto 2D (plantilla Universal 2D). Fondo negro: en la Main Camera, pon Background negro y Projection en Orthographic (ya viene así en 2D).
- No necesitas sprites: crearás las palas y la pelota como cuadrados.
GameObject → 2D Object → Sprites → Squarete da un cuadrado blanco.
Paso 2 — Las palas#
- Crea un Square, renómbralo PalaIzquierda. Escálalo estrecho y alto (por ejemplo Scale
0.3, 2, 1) y colócalo a la izquierda (Position X = -8). - Añádele un Box Collider 2D (Add Component).
- Duplícala (
Ctrl+D), renómbrala PalaDerecha y ponla a la derecha (Position X = 8).
Ahora el script de movimiento. Cada pala sube/baja con sus teclas y no se sale de la pantalla:
using UnityEngine;
public class Pala : MonoBehaviour
{
[SerializeField] private float velocidad = 9f;
[SerializeField] private KeyCode teclaArriba = KeyCode.W;
[SerializeField] private KeyCode teclaAbajo = KeyCode.S;
[SerializeField] private float limiteY = 4f; // hasta dónde puede subir/bajar
void Update()
{
float dir = 0f;
if (Input.GetKey(teclaArriba)) dir += 1f;
if (Input.GetKey(teclaAbajo)) dir -= 1f;
Vector3 p = transform.position;
p.y = Mathf.Clamp(p.y + dir * velocidad * Time.deltaTime, -limiteY, limiteY);
transform.position = p;
}
}Añade el script a las dos palas. En la PalaDerecha, cambia sus teclas a UpArrow y DownArrow desde el Inspector. Dale a Play: ya se mueven.
El limiteY depende del tamaño de tu cámara (su Orthographic Size, por defecto 5). Ajústalo para que la pala no se salga por arriba/abajo. Exponerlo en el Inspector te deja afinarlo sin tocar código.
Paso 3 — Los bordes#
La pelota debe rebotar en el techo y el suelo (pero no en los laterales: ahí se marca gol). Crea dos Square finos y largos arriba y abajo de la pantalla, renómbralos BordeSuperior y BordeInferior, escálalos anchos (Scale 20, 0.5, 1) y dales un Box Collider 2D. Puedes desactivar su Sprite Renderer si no quieres verlos.
Paso 4 — La pelota#
- Crea un Square, renómbralo Pelota, escálalo pequeño (
0.4, 0.4, 1), céntralo (0, 0). - Añádele Box Collider 2D y Rigidbody2D.
- En el Rigidbody2D: Gravity Scale = 0 (no cae), Collision Detection = Continuous (para que no atraviese las palas a alta velocidad) y Freeze Rotation Z activado.
El script de la pelota la lanza al empezar y mantiene su velocidad constante tras cada rebote:
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class Pelota : MonoBehaviour
{
[SerializeField] private float velocidad = 8f;
private Rigidbody2D rb;
private Vector2 posicionInicial;
void Awake()
{
rb = GetComponent<Rigidbody2D>();
posicionInicial = transform.position;
}
void Start() => Lanzar();
void Lanzar()
{
transform.position = posicionInicial;
// dirección horizontal aleatoria (izq/der) y un poco de vertical
float x = Random.value < 0.5f ? -1f : 1f;
float y = Random.Range(-0.6f, 0.6f);
rb.linearVelocity = new Vector2(x, y).normalized * velocidad;
}
void OnCollisionEnter2D(Collision2D col)
{
// Tras rebotar, forzar velocidad constante (evita que se frene o acelere)
rb.linearVelocity = rb.linearVelocity.normalized * velocidad;
}
public void Reiniciar() => Lanzar();
}Al rebotar, las físicas pueden dejar la pelota más lenta o desviada. rb.linearVelocity = rb.linearVelocity.normalized * velocidad conserva la dirección del rebote pero fija el módulo a la velocidad deseada, así la pelota nunca se para ni se dispara. Es el truco que hace que Pong se sienta bien.
Dale a Play: la pelota rebota en las palas y los bordes. Si atraviesa algo, revisa que todos tengan Collider y que la pelota use Continuous.
Paso 5 — Rebote más vivo (opcional pero recomendable)#
Un Pong plano aburre. Haz que el punto donde golpea la pala cambie el ángulo (golpear con el borde de la pala manda la pelota más inclinada). Sustituye el OnCollisionEnter2D de la pelota por:
void OnCollisionEnter2D(Collision2D col)
{
if (col.gameObject.CompareTag("Pala"))
{
// diferencia de altura entre la pelota y el centro de la pala
float offset = transform.position.y - col.transform.position.y;
float alturaPala = col.collider.bounds.size.y;
float y = offset / alturaPala; // -0.5 (abajo) .. 0.5 (arriba)
float x = Mathf.Sign(rb.linearVelocity.x); // sigue en su dirección horizontal
Vector2 dir = new Vector2(x, y).normalized;
rb.linearVelocity = dir * velocidad;
}
else
{
rb.linearVelocity = rb.linearVelocity.normalized * velocidad;
}
}Ponles la Tag "Pala" a las dos palas (crea la tag en Inspector → Tag → Add Tag).
Paso 6 — Goles y puntuación#
Las zonas de gol son triggers a izquierda y derecha. Crea dos Square finos justo detrás de cada pala (fuera de la pantalla), renómbralos GolIzquierda y GolDerecha, dales un Box Collider 2D con Is Trigger activado, y desactiva su Sprite Renderer.
El gestor de puntuación:
using UnityEngine;
using TMPro;
public class GestorPong : MonoBehaviour
{
[SerializeField] private Pelota pelota;
[SerializeField] private TextMeshProUGUI marcador;
[SerializeField] private int puntosParaGanar = 5;
private int puntosIzq, puntosDer;
void Start() => Actualizar();
public void PuntoIzquierda() { puntosIzq++; Punto(); }
public void PuntoDerecha() { puntosDer++; Punto(); }
void Punto()
{
Actualizar();
if (puntosIzq >= puntosParaGanar || puntosDer >= puntosParaGanar)
Ganar();
else
pelota.Reiniciar(); // saca de nuevo
}
void Actualizar() => marcador.text = puntosIzq + " - " + puntosDer;
void Ganar()
{
string quien = puntosIzq > puntosDer ? "Izquierda" : "Derecha";
marcador.text = "¡Gana " + quien + "!";
pelota.gameObject.SetActive(false);
}
}Y un pequeño script en cada zona de gol que avisa al gestor:
using UnityEngine;
public class ZonaGol : MonoBehaviour
{
[SerializeField] private GestorPong gestor;
[SerializeField] private bool esGolIzquierda; // true = punto para la derecha
void OnTriggerEnter2D(Collider2D other)
{
if (!other.CompareTag("Pelota")) return;
if (esGolIzquierda) gestor.PuntoDerecha();
else gestor.PuntoIzquierda();
}
}Ponle la Tag "Pelota" a la pelota. En la zona izquierda marca esGolIzquierda = true (si la pelota entra por la izquierda, el punto es para la derecha).
Paso 7 — El marcador en pantalla#
Crea la UI: GameObject → UI → Text - TextMeshPro (acepta importar TMP Essentials). Colócalo arriba y centrado con sus anclas, tamaño grande. Arrástralo al campo marcador del GestorPong. Conecta también la pelota y, en cada ZonaGol, el gestor.
Dale a Play: tienes Pong completo. Se marca, se reinicia la pelota y hay ganador.
Paso 8 — Pulido (el toque final)#
- Sonido: un "bip" al rebotar y otro al marcar (ver Audio).
- Línea central punteada (decoración con squares).
- Menú y reinicio: un botón "Jugar de nuevo" (ver botones).
- Aumentar velocidad poco a poco en cada rebote para tensión.
- Modo 1 jugador: una IA sencilla que mueve la pala derecha hacia la Y de la pelota (con un poco de retardo para que sea vencible).
Consigue primero un Pong jugable (pasos 1–7) antes de añadir sonidos o IA. Un juego pequeño terminado enseña diez veces más que uno enorme a medias. Este es, probablemente, el primer juego que puedes decir que has acabado.
Lo que has aprendido#
Montando Pong has usado, de verdad y todo junto: GameObjects y componentes, Rigidbody2D y física sin gravedad, colliders y triggers, Tags, input, TextMeshPro, y lógica de juego (puntuación, victoria, reinicio). Justo los cimientos sobre los que se construye cualquier juego 2D.
Resumen#
Pong = dos palas (input + clamp), una pelota (Rigidbody2D sin gravedad, velocidad constante re-normalizada al rebotar, ángulo según dónde golpea la pala), bordes con collider, zonas de gol como triggers y un gestor que puntúa, reinicia y decide el ganador, mostrando el marcador con TextMeshPro. Termínalo y luego púlelo. ¡Tu primer juego acabado!