Actividad 3.2 · Pong mejorado

Objetivo: mejorar el Pong de la actividad 3.1 con nuevas mecánicas que te ayuden a consolidar lo aprendido, sobre todo la comunicación entre los scripts de los distintos GameObjects.
A continuación tienes una serie de mejoras. Cuantas más completes, mejor puntuación obtendrás. Antes de ponerte con cada una, piensa en qué script tiene sentido incluir la nueva funcionalidad: si cada script se ocupa de lo suyo, será más sencillo modificar el juego o arreglarlo en el futuro.
Antes de empezar
Parte del proyecto de la Actividad 3.1 · PongExpress
terminado: las palas se mueven, la bola rebota y, tras cada punto, el
GameManager muestra el marcador en la Consola y devuelve la bola y las
palas a su sitio.
Haz una copia de seguridad
Antes de empezar, cierra Unity y haz una copia de la carpeta del proyecto. Si algo se estropea, podrás volver a la versión que funcionaba. Y después, ve mejora a mejora: no empieces la siguiente hasta haber probado que la anterior funciona.
1. Esperar a que se pulse espacio para lanzar la bola
Ahora mismo, en cuanto se marca un punto, la bola vuelve a salir disparada, sin dar tiempo a los jugadores a celebrar el punto (o a lamentarlo). Haz que, tras un punto, la bola espere en el centro hasta que alguien pulse la barra espaciadora.
- En
Ball, quita la llamada aLaunch()del métodoResetPosition: así la bola vuelve al centro y se queda quieta. Haz también público el métodoLaunch(cambiaprivateporpublic), porque ahora lo llamará elGameManager. - En
GameManager, añade una variable privadaboolllamadawaitingForLaunch, que valgatruemientras la bola esté parada esperando el saque. Ponla atrueenResetPositions. -
En el
UpdatedelGameManager, comprueba si la bola está esperando y se acaba de pulsar espacio. Si es así, lanza la bola y ponwaitingForLaunchafalse:void Update() { if (/* la bola espera el saque y se acaba de pulsar espacio */) { // Lanza la bola y recuerda que ya no está esperando } }Para leer el teclado, añade
using UnityEngine.InputSystem;al principio del script.Keyboard.current.spaceKey.wasPressedThisFramevaletruesolo en el fotograma en el que se empieza a pulsar la barra espaciadora. -
Prueba el juego: tras cada punto, la bola debe quedarse en el centro hasta que pulses espacio.
isPressed o wasPressedThisFrame
En la actividad 3.1, las palas usaban isPressed, que vale true
mientras la tecla está pulsada: en cada fotograma, durante todo el
tiempo que la mantengas. Para acciones que deben ocurrir una sola vez por
pulsación, como sacar, usa wasPressedThisFrame, que solo vale true en
el fotograma en el que empieza la pulsación.
Solo con la bola parada
Sin la variable waitingForLaunch, pulsar espacio en mitad de un punto
volvería a lanzar la bola desde donde estuviera. Comprueba que no ocurre.
Pista
Las dos condiciones se unen con &&:
waitingForLaunch && Keyboard.current.spaceKey.wasPressedThisFrame.
Dentro del if, basta con ball.Launch(); y
waitingForLaunch = false;.
Si quieres que el primer saque de la partida también espere al espacio,
quita la llamada a Launch() del Start de Ball e inicializa la
variable a true al declararla: private bool waitingForLaunch = true;.
2. Añadir marcadores visibles
Muestra el marcador de cada jugador en la pantalla, como en la imagen del principio, en lugar de en la Consola.
-
Crea un texto desde el menú GameObject › UI (Canvas), con la opción de texto de TextMesh Pro. Si Unity te pide importar los recursos básicos de TextMesh Pro (TMP Essentials), acepta: sin ellos, el texto no se puede mostrar.
Si la escena no tiene un Canvas (el lienzo sobre el que se dibuja la interfaz), Unity lo crea automáticamente y pone el texto dentro.
-
Llama
Player1ScoreTextal texto. En el Inspector, escribe0en su texto, ajusta el tamaño de letra y el color, y colócalo en la parte superior izquierda de la pantalla. - Duplícalo para crear
Player2ScoreTexty colócalo a la derecha. - En
GameManager, añadeusing TMPro;al principio y una variable pública de tipoTextMeshProUGUIpara cada marcador. En el Inspector, arrastra a cada una su texto. -
Cada vez que cambie el marcador, actualiza los textos. La propiedad
textes el texto que se muestra en pantalla, y es de tipostring:player1ScoreText.text = player1Score.ToString();ToString()convierte el número en texto.
El Canvas se ve enorme en la escena
En la pestaña Scene, el Canvas aparece como un rectángulo gigante, mucho mayor que el terreno de juego. Es normal: se dibuja encima de lo que ve la cámara. Para colocar los textos, fíjate en la pestaña Game.
Pista
Crea en GameManager un método privado UpdateScoreTexts que actualice
los dos textos, y llámalo desde Player1Scores y desde Player2Scores,
justo después de sumar el punto. Así no repites el mismo código en los dos
métodos.
3. Pulsar N para empezar una partida nueva
Cuando la bola esté parada, haz que al pulsar la tecla N (de New game) el marcador vuelva a 0 - 0.
- Sigue la misma estrategia que en la mejora 1: en el
UpdatedelGameManager, comprueba si la bola está esperando el saque y se acaba de pulsar la N (Keyboard.current.nKey). - Al pulsarla, pon a 0 las variables de los puntos y actualiza los textos de los marcadores. La bola no se lanza: seguirá esperando el espacio.
Reinicia todo lo que cambie durante la partida
Si haces también las mejoras 4 y 5, una partida nueva tendrá que ocultar el mensaje del ganador y devolver las palas a su tamaño inicial.
Pista
Puedes escribir un método NewGame en GameManager con todo lo que hay
que reiniciar, y llamarlo desde el Update al pulsar la N. Así, si
más adelante añades algo que reiniciar, sabrás dónde ponerlo.
4. Mostrar un ganador
Gana la partida el primero que llegue a un número de puntos (por ejemplo, 5). Cuando ocurra, debe aparecer en pantalla un mensaje con el ganador.
- Añade a
GameManageruna variable públicaintllamadapointsToWin, para poder cambiar el número de puntos desde el Inspector. - Crea otro texto de TextMesh Pro,
WinnerText, en el centro de la pantalla, y añade aGameManageruna variable pública para él. -
El mensaje debe estar oculto mientras se juega. Para ocultar o mostrar un GameObject desde un script se usa
SetActive:winnerText.gameObject.SetActive(false);winnerText.gameObjectes el GameObject del texto: confalsese oculta y contruese muestra. -
Cuando un jugador llegue a
pointsToWin, muestra el texto con el mensaje adecuado (por ejemplo,¡Gana el jugador 1!). - Mientras se vea el ganador, la barra espaciadora no debe lanzar la bola: solo la N (si has hecho la mejora 3) empezará una partida nueva, que ocultará de nuevo el mensaje.
Pista
En Player1Scores, después de sumar el punto, comprueba
if (player1Score == pointsToWin). Para que el espacio no lance la bola,
puedes usar otra variable bool, por ejemplo gameOver, que se ponga a
true cuando alguien gane y a false en la partida nueva, y añadir
gameOver == false a la condición del saque.
5. Cambiar el tamaño de las palas tras cada punto
Para que la partida sea más emocionante, «castiga» al que marca haciendo su pala un poco más pequeña, y «premia» al que pierde el punto haciendo la suya un poco más grande.
- En
Paddle, añade dos variables públicasfloatpara el tamaño mínimo y el máximo de la pala (por ejemplo,1y3) y un método públicoChangeSizeque reciba cuánto crecer (un número negativo hará que encoja). -
El tamaño de la pala es la escala en Y de su
Transform(transform.localScale). Cámbiala dentro deChangeSizeasí:Vector3 scale = transform.localScale; scale.y = scale.y + amount; transform.localScale = scale; -
Antes de asignar la escala, limita su valor entre el mínimo y el máximo con
Mathf.Clamp(valor, mínimo, máximo), que devuelve el valor si está dentro del intervalo, o el límite más cercano si se sale. - Desde
Player1ScoresyPlayer2Scores, llama aChangeSizede las dos palas: con un número negativo para la del que ha marcado y con uno positivo para la otra.
La escala no se cambia directamente
transform.localScale.y = 2; da un error de compilación: no se puede
modificar solo una coordenada de localScale. Hay que copiar el vector en
una variable, cambiarla y volver a asignarlo entero, como en el código del
paso 2.
Pista
Usa Mathf.Clamp en la segunda línea:
scale.y = Mathf.Clamp(scale.y + amount, minSize, maxSize);.
Para recuperar el tamaño inicial en una partida nueva (mejora 3), guarda
la escala inicial en Start, igual que hiciste con startPosition.
6. Acelerar la bola en cada rebote con una pala
Si ninguno de los dos jugadores falla, la partida puede ser eterna. Haz que la bola vaya más rápida cada vez que rebote en una pala. Al empezar el punto siguiente, debe recuperar su velocidad inicial.
- Asigna a
Player1y aPlayer2la etiqueta Player, que ya viene creada en Unity, para distinguir las palas de los muros. - En
Ball, añade una variable públicafloatcon el multiplicador de la velocidad (por ejemplo,1.1para que cada rebote sea un 10 % más rápido). -
Para detectar el choque, escribe en
Ballel métodoOnCollisionEnter2D, que Unity llama cuando el collider de la bola choca con otro:private void OnCollisionEnter2D(Collision2D collision) { if (collision.gameObject.CompareTag("Player")) { // Aquí: acelera la bola } } -
Para acelerar la bola, multiplica la velocidad de su Rigidbody 2D (
rb.linearVelocity) por el multiplicador. Al multiplicar unVector2por un número se multiplican sus dos coordenadas: la bola mantiene la dirección, pero va más rápida.
OnCollisionEnter2D no es OnTriggerEnter2D
En las metas usamos OnTriggerEnter2D porque su collider es un
trigger: deja pasar a la bola. Las palas son colliders normales, así que
la bola rebota en ellas y Unity llama a OnCollisionEnter2D, cuyo
parámetro es de otro tipo: Collision2D.
Si la bola atraviesa las palas
Cuando la bola va muy rápida, puede atravesar una pala entre un fotograma
y el siguiente. Para evitarlo, en el Rigidbody 2D de Ball, cambia
Collision Detection de Discrete a Continuous.
Pista
La línea que falta es
rb.linearVelocity = rb.linearVelocity * speedMultiplier;.
No cambies la variable speed: como Launch usa speed para dar la
velocidad inicial, la bola recupera su velocidad original en cada saque
sin hacer nada más.
7. Añadir un sonido al marcar un punto
- Crea una carpeta
Soundsdentro deAssetsy arrastra dentro el fichero del sonido que quieras usar. -
Añade un componente Audio Source al GameObject que vaya a reproducir el sonido (por ejemplo,
GameManager). Arrastra el sonido a su primer campo (en la captura, Audio Resource) y desmarca Play On Awake: si no, el sonido se reproducirá al empezar la partida.
-
En el script que deba lanzar el sonido, añade una variable pública de tipo
AudioSourcey arrastra a ella, en el Inspector, el GameObject que tiene el componente. -
Para reproducir el sonido, llama a su método
Play:goalSound.Play();
Cómo hacer que la posición del Audio se note
Si quieres que el sonido se oiga más a la izquierda o más a la derecha según dónde se produce, pon en el Audio Source Spatial Blend a 3D y colócalo en el objeto que debería «emitir» el sonido (por ejemplo, en cada meta).
Pista
El GameManager ya se entera de cada punto: el mejor sitio para llamar a
Play es Player1Scores y Player2Scores (o un método que llamen los
dos).
8. Añadir un sonido al rebotar en una pala
Todo lo necesario para esta mejora está explicado en las mejoras 6 y 7.
Pista
Hay dos formas de hacerlo:
- El sonido en la bola. Pon el Audio Source en la propia bola y
reproduce el sonido en
OnCollisionEnter2D, dentro del mismoifque comprueba la etiqueta Player. Si quieres otro sonido para los muros, añade unelse. - El sonido en lo que golpea la bola. Pon un Audio Source en cada pala
y en cada muro, y crea un script que se añada a todos ellos y que
reproduzca su sonido en
OnCollisionEnter2Dcuando lo que choca tenga la etiquetaBall. Así, cada objeto puede tener su propio sonido sin cambiar el código.
9. Mostrar el aviso «Pulsa espacio para sacar»
Para esta mejora necesitas haber hecho la 1 (sacar con espacio) y, para saber crear textos, la 2. Mientras la bola espera el saque, muestra en pantalla un aviso para que los jugadores sepan qué tienen que hacer. Al sacar, el aviso debe desaparecer.
- Crea un texto de TextMesh Pro llamado
LaunchText, con el mensajePulsa espacio para sacar, y colócalo donde no tape el juego (por ejemplo, en la parte inferior de la pantalla). - Añade a
GameManageruna variable pública para el texto y arrástralo a ella en el Inspector. - Muestra el aviso cada vez que la bola se quede esperando el saque y
ocúltalo al sacar, con
SetActive, como el texto del ganador de la mejora 4.
El primer saque
Si el primer saque de la partida también espera el espacio, deja el texto activo en el editor: así se verá desde el principio. Si no, desactívalo (casilla junto a su nombre en el Inspector) para que no aparezca hasta el primer punto.
Pista
El aviso debe verse exactamente cuando waitingForLaunch vale true.
Por eso, cada vez que cambies el valor de esa variable, puedes escribir
justo después:
launchText.gameObject.SetActive(waitingForLaunch);
Si has hecho la mejora 4, cuando alguien gane oculta el aviso: el espacio ya no saca hasta empezar una partida nueva.
10. Sacar hacia quien ha marcado el punto
Para esta mejora necesitas haber hecho la 1. Ahora la bola sale en una dirección al azar. Haz que, tras cada punto, saque el jugador que lo ha perdido: la bola sale desde el centro hacia el que ha marcado. Así, quien acaba de perder el punto tiene algo de ventaja, y el juego se nivela.
-
Cambia
Launchpara que reciba como parámetro la dirección horizontal de la bola:-1hacia la izquierda (el jugador 1) o1hacia la derecha (el jugador 2). La dirección vertical sigue eligiéndose al azar.public void Launch(float directionX) { // Elige al azar la dirección en Y y da a la bola su velocidad inicial, // usando directionX como dirección en X } -
En
GameManager, añade una variable privadafloatllamadalaunchDirectionXcon la dirección del próximo saque. Dale valor enPlayer1Scoresy enPlayer2Scores, según quién haya marcado el punto. - En el
UpdatedelGameManager, al pulsar espacio, pasa esa dirección aLaunch:ball.Launch(launchDirectionX);. - Prueba el juego: tras cada punto, la bola debe salir siempre hacia el jugador que lo ha marcado.
Todas las llamadas a Launch necesitan el parámetro
Al añadir el parámetro, cualquier llamada a Launch() sin él da un error
de compilación. Si el Start de Ball todavía lanza la bola, tendrá que
elegir antes una dirección al azar y pasársela.
Pista
Si marca el jugador 1, que está a la izquierda, la bola sale hacia él:
en Player1Scores, launchDirectionX = -1;. En Player2Scores, al
revés: launchDirectionX = 1;.
Para el primer saque, que no viene de ningún punto, elige la dirección al
azar en el Start del GameManager, igual que hacías antes en Launch:
launchDirectionX = UnityEngine.Random.Range(0, 2) == 0 ? -1 : 1;