Saltar a contenido

Actividad 3.2 · Pong mejorado

El Pong mejorado, con marcadores y luces

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.

  1. En Ball, quita la llamada a Launch() del método ResetPosition: así la bola vuelve al centro y se queda quieta. Haz también público el método Launch (cambia private por public), porque ahora lo llamará el GameManager.
  2. En GameManager, añade una variable privada bool llamada waitingForLaunch, que valga true mientras la bola esté parada esperando el saque. Ponla a true en ResetPositions.
  3. En el Update del GameManager, comprueba si la bola está esperando y se acaba de pulsar espacio. Si es así, lanza la bola y pon waitingForLaunch a false:

    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.wasPressedThisFrame vale true solo en el fotograma en el que se empieza a pulsar la barra espaciadora.

  4. 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.

  1. 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.

  2. Llama Player1ScoreText al texto. En el Inspector, escribe 0 en su texto, ajusta el tamaño de letra y el color, y colócalo en la parte superior izquierda de la pantalla.

  3. Duplícalo para crear Player2ScoreText y colócalo a la derecha.
  4. En GameManager, añade using TMPro; al principio y una variable pública de tipo TextMeshProUGUI para cada marcador. En el Inspector, arrastra a cada una su texto.
  5. Cada vez que cambie el marcador, actualiza los textos. La propiedad text es el texto que se muestra en pantalla, y es de tipo string:

    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.

  1. Sigue la misma estrategia que en la mejora 1: en el Update del GameManager, comprueba si la bola está esperando el saque y se acaba de pulsar la N (Keyboard.current.nKey).
  2. 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.

  1. Añade a GameManager una variable pública int llamada pointsToWin, para poder cambiar el número de puntos desde el Inspector.
  2. Crea otro texto de TextMesh Pro, WinnerText, en el centro de la pantalla, y añade a GameManager una variable pública para él.
  3. 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.gameObject es el GameObject del texto: con false se oculta y con true se muestra.

  4. Cuando un jugador llegue a pointsToWin, muestra el texto con el mensaje adecuado (por ejemplo, ¡Gana el jugador 1!).

  5. 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.

  1. En Paddle, añade dos variables públicas float para el tamaño mínimo y el máximo de la pala (por ejemplo, 1 y 3) y un método público ChangeSize que reciba cuánto crecer (un número negativo hará que encoja).
  2. El tamaño de la pala es la escala en Y de su Transform (transform.localScale). Cámbiala dentro de ChangeSize así:

    Vector3 scale = transform.localScale;
    scale.y = scale.y + amount;
    transform.localScale = scale;
    
  3. 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.

  4. Desde Player1Scores y Player2Scores, llama a ChangeSize de 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.

  1. Asigna a Player1 y a Player2 la etiqueta Player, que ya viene creada en Unity, para distinguir las palas de los muros.
  2. En Ball, añade una variable pública float con el multiplicador de la velocidad (por ejemplo, 1.1 para que cada rebote sea un 10 % más rápido).
  3. Para detectar el choque, escribe en Ball el método OnCollisionEnter2D, 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
        }
    }
    
  4. Para acelerar la bola, multiplica la velocidad de su Rigidbody 2D (rb.linearVelocity) por el multiplicador. Al multiplicar un Vector2 por 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

  1. Crea una carpeta Sounds dentro de Assets y arrastra dentro el fichero del sonido que quieras usar.
  2. 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.

    Componente Audio Source con el sonido asignado y Play On Awake desmarcado

  3. En el script que deba lanzar el sonido, añade una variable pública de tipo AudioSource y arrastra a ella, en el Inspector, el GameObject que tiene el componente.

  4. 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 mismo if que comprueba la etiqueta Player. Si quieres otro sonido para los muros, añade un else.
  • 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 OnCollisionEnter2D cuando lo que choca tenga la etiqueta Ball. 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.

  1. Crea un texto de TextMesh Pro llamado LaunchText, con el mensaje Pulsa espacio para sacar, y colócalo donde no tape el juego (por ejemplo, en la parte inferior de la pantalla).
  2. Añade a GameManager una variable pública para el texto y arrástralo a ella en el Inspector.
  3. 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.

  1. Cambia Launch para que reciba como parámetro la dirección horizontal de la bola: -1 hacia la izquierda (el jugador 1) o 1 hacia 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
    }
    
  2. En GameManager, añade una variable privada float llamada launchDirectionX con la dirección del próximo saque. Dale valor en Player1Scores y en Player2Scores, según quién haya marcado el punto.

  3. En el Update del GameManager, al pulsar espacio, pasa esa dirección a Launch: ball.Launch(launchDirectionX);.
  4. 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;