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
- Abre Unity Hub y crea un proyecto nuevo con la plantilla Universal 2D
y la versión 6.3 del editor. Llámalo
PongExpress. - Guarda la escena que viene creada por defecto (File › Save As) dentro
de
Assets/Scenescon el nombreMainScene. - En la pestaña Game, abre el desplegable de resolución (por defecto pone Free Aspect) y selecciona Full HD (1920x1080).
-
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.

2. Crear los elementos del juego
- 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.
- Llama
Ballal nuevo GameObject. En su componente Transform, pon Position a0en X, Y y Z (si no está ya en el centro) y Scale a0.5en X y en Y. -
Duplica
Ball(clic derecho sobre él en la Jerarquía › Duplicate, o Ctrl+D) para crear el resto de elementos, y renombra cada copia:Player1yPlayer2: las palas del jugador 1 (izquierda) y del jugador 2 (derecha).TopWallyBottomWall: los muros superior e inferior.Player1GoalyPlayer2Goal: 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.
-
Puede que la línea central se dibuje por encima de la bola cuando se cruzan. Para que la bola quede siempre delante, selecciona
Bally, en su componente Sprite Renderer, pon Order in Layer a1. - Crea un GameObject vacío (clic derecho en la Jerarquía › Create
Empty), llámalo
GameFieldy arrastra dentroMiddleLine,TopWallyBottomWall. Así la Jerarquía queda más ordenada. Comprueba que la posición deGameFieldes (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:

3. Añadir la física
-
Añade un componente Box Collider 2D a
Ball,Player1,Player2,TopWall,BottomWall,Player1GoalyPlayer2Goal. No se lo añadas a la cámara, a la luz (Global Light 2D), aMiddleLineni aGameField: 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 2Dy selecciónalo en la lista. Puedes seleccionar varios GameObjects a la vez (con Ctrl pulsado) y añadirles el componente a todos de golpe.
-
Marca la casilla Is Trigger del Box Collider 2D solo en las dos metas (
Player1GoalyPlayer2Goal). 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.
-
Añade un componente Rigidbody 2D a
Ball,Player1yPlayer2. En los tres, pon Gravity Scale a0(si no, caerían).
-
En el Rigidbody 2D de
Ball, despliega Constraints y marca Freeze Rotation Z, para que la bola no gire al chocar.
-
En el Rigidbody 2D de
Player1yPlayer2, 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.
-
Para que la bola rebote sin perder velocidad:
- Crea una carpeta
Materialsdentro deAssets. - 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. - Configúralo con Friction a
0(sin rozamiento) y Bounciness a1(rebota sin perder energía). - Arrastra
Bouncea la propiedad Material del Rigidbody 2D deBall.

- Crea una carpeta
4. Mover las palas
- Dentro de
Assets, crea una carpetaScripts. Dentro de ella, crea un script (clic derecho › Create › Scripting › MonoBehaviour Script) y llámaloPaddle. 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. -
Ábrelo con doble clic y complétalo partiendo de este esqueleto:
Paddle.csusing 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; yrb(Rigidbody2D), el componente que la mueve. Las dos palas usan el mismo script:isPlayer1es lo que las distingue. using UnityEngine.InputSystem;permite leer el teclado.Keyboard.currentes el teclado, yKeyboard.current[Key.W].isPressedvaletruemientras la tecla W está pulsada. Las flechas sonKey.UpArrowyKey.DownArrow.- El jugador 1 se mueve con W (subir) y S (bajar), y el jugador 2,
con las flechas ↑ y ↓. Usa un
if-elseque compruebeisPlayer1para leer unas teclas u otras. - Guarda el movimiento en una variable
movementde tipofloat:1si se pulsa la tecla de subir,-1si se pulsa la de bajar y0si 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
Updateconfloat movement = 0;. Después, dentro de cada rama delif (isPlayer1), usa dosifindependientes (sinelse): si la tecla de subir está pulsada, suma 1 amovement; si lo está la de bajar, réstale 1. Si se pulsan las dos, el resultado es 0 y la pala no se mueve. - Declara tres variables públicas:
-
Añade el script
Paddlecomo componente dePlayer1y dePlayer2(arrástralo desde la ventana de Proyecto hasta cada GameObject de la Jerarquía) y configura sus variables públicas en el Inspector:Variable Player1Player2isPlayer1Marcada Sin marcar Speed 55Rb El Rigidbody 2D de Player1El Rigidbody 2D de Player2Para rellenar Rb, arrastra el GameObject (o su componente Rigidbody 2D) hasta el hueco de la variable.
-
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
-
En la carpeta
Scripts, crea otro script llamadoBally complétalo partiendo de este esqueleto:Ball.csusing 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, yrb(Rigidbody2D), el componente que la mueve. - El método
Launchda a la bola su velocidad inicial. Llámalo desdeStart. -
Para elegir al azar entre
-1y1:float x = UnityEngine.Random.Range(0, 2) == 0 ? -1 : 1;Random.Range(0, 2)devuelve un entero aleatorio que puede ser0o1(el valor máximo queda excluido), y el operador ternario lo convierte en-1o1. EscribimosUnityEngine.Randompara usar la claseRandomde Unity: .NET tiene otra con el mismo nombre, y si el script tiene tambiénusing 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
yigual quex. Así la bola sale en una de las cuatro diagonales al azar. - Declara dos variables públicas:
-
Añade el script
Ballcomo componente del GameObjectBall. Pon Speed a5y arrastra a Rb el Rigidbody 2D de la bola.
-
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
- Selecciona
Bally, en la parte superior del Inspector, abre el desplegable Tag y elige Add Tag…. Pulsa +, escribeBally guarda. Vuelve a seleccionarBally asígnale la nueva etiquetaBallen el desplegable Tag. La usaremos para saber que lo que ha entrado en la meta es la bola. -
En
Scripts, crea un script llamadoGoaly añádeselo aPlayer1Goaly aPlayer2Goal. Debe tener:- Una variable pública
boolllamadaisPlayer1Wall, que marcarás en el Inspector solo enPlayer1Goal. - El método
OnTriggerEnter2D, que Unity llama automáticamente cuando otro collider entra en el trigger de la meta. El parámetrocollisiones el collider que ha entrado:
Goal.csusing 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
ifpara 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. - Una variable pública
-
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:
-
Guardar la posición inicial. Añade a
Paddley aBalluna variable privadastartPositiony guarda en ella la posición del objeto enStart. EnPaddletodavía no hay métodoStart: créalo.private Vector3 startPosition; void Start() { startPosition = transform.position; }En
Ball, el métodoStartya existe: añade la líneastartPosition = transform.position;antes de la llamada aLaunch(). -
Volver a la posición inicial. Añade a
Paddley aBallun método públicoResetPositionque 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 aLaunch().No lo llames
ResetResetes 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. -
Crear el GameManager. Crea un GameObject vacío llamado
GameManagery un scriptGameManagerque 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;ypublic Paddle player2Paddle;. En el Inspector, arrastra a cada una el GameObject correspondiente. - Dos variables privadas de tipo
intpara los puntos de cada jugador. - Un método público
Player1Scoresy otroPlayer2Scores. Cada uno suma un punto a su jugador, muestra el marcador conDebug.Log(p. ej.,Marcador: 1 - 0) y llama a un métodoResetPositions. - Un método
ResetPositionsque llame aResetPosition()de la bola y de las dos palas.
- Tres variables públicas para comunicarse con los otros scripts:
-
Avisar al GameManager desde las metas. Añade a
Goaluna variablepublic GameManager gameManager;y, en el Inspector de cada meta, arrastra a ella el GameObjectGameManager. Después, cambia losDebug.Logdel apartado 6 por llamadas agameManager.Player1Scores()o agameManager.Player2Scores(), según quién haya marcado. -
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:
- Desactiva o elimina el GameObject Global Light 2D de la escena. La escena se quedará a oscuras.
- 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:
- Selecciona
Bally añádele un componente Trail Renderer. - 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. - En la curva Width, haz que la estela empiece con grosor
0.25junto a la bola y termine con0, 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. - 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
255y pon la derecha a0: la estela se irá volviendo transparente. - Comprueba que el Order in Layer del Sprite Renderer de
Ballsigue a1(lo pusiste en el apartado 2). La estela tiene0, así que la bola se dibuja por encima de ella. - Prueba el juego. Si quieres una estela más larga o más corta, cambia Time: los segundos que tarda en desaparecer cada tramo.