[Minijuego de Unity] Caso de desarrollo de juegos: Unity crea minijuegos sin obstáculos (Parte 1)

cuadrado de bateo

clon de ping pong

  • Usa cubos para construir la arena, la raqueta y la pelota.
  • Mueve la pelota y la raqueta.
  • Golpea la pelota y anota carreras.
  • Deja que la cámara sienta el impacto.
  • Le da al juego un aspecto de neón abstracto.

Este es el primero de una serie de tutoriales sobre el juego básico. En él, crearemos un clon simple de Pong.

Este tutorial se realizó con Unity 2021.3.16f1.

Golpea la pelota cuadrada con la raqueta en la arena cuadrada.

Esta serie cubrirá la creación de un juego base simple para mostrar cómo puedes convertir una idea en un juego funcional mínimo en un corto período de tiempo. Los juegos serán clones, por lo que no tendremos que inventar una nueva idea desde cero, pero nos desviaremos del estándar de alguna manera.

Además de mantenerlo simple, nos limitaremos a establecer una restricción de diseño para esta serie: solo podemos representar el texto predeterminado del cubo y del espacio mundial, y eso es todo. Además, no incluyo el sonido.

Esta serie supone que ha completado al menos la serie Básica y series adicionales de su elección para que esté familiarizado con el trabajo y la programación en Unity. No mostraré cada paso en detalle como en otras series, asumiendo que puedes crear los objetos del juego tú mismo y conectar cosas en el inspector sin tomar capturas de pantalla. Tampoco explicaré las matemáticas básicas ni las leyes del movimiento.

escena del juego

El juego que clonaremos en este tutorial es Pong, que es una representación muy abstracta del tenis de mesa o tenis. Puedes clonar la idea de Pong, pero no le pongas un nombre similar para que no te pidan que la elimines. Así que llamamos a nuestro juego Batting Square porque es todo cuadrado.

Comenzaremos con la escena de ejemplo predeterminada para un nuevo proyecto 3D, aunque renombrado. Los únicos paquetes que usaremos son el paquete de integración del editor de su elección (en mi caso) y sus dependencias. Las dependencias de estos paquetes son Burst, Core RP Library, Custom NUnit, Mathematics, Seacher, Shader Graph, Test Framework y Unity UI.

Cree un recurso de renderizado/URP (usar renderizador universal) y utilícelo con la configuración de gráficos/canalización de renderizado programable y el recurso calidad/canalización de renderizado en la configuración del proyecto.

arena

Crea un cubo predeterminado y conviértelo en una casa prefabricada. Elimine su colisionador ya que no dependeremos del sistema de física. Utilice cuatro de estos escalados a 20 unidades en una dimensión para formar los límites de un área cuadrada de 20×20 alrededor del origen en el plano XZ. Como los cubos tienen una unidad de espesor, cada cubo se mueve 10,5 unidades desde el origen en la dirección apropiada.
(La transferencia de la imagen del enlace externo falló. El sitio de origen puede tener un mecanismo anti-leeching. Se recomienda guardar la imagen y cargarla directamente (img-f0XUScMH-1682677243478) (nulo)]

Para hacerlo un poco más interesante, agregue cuatro instancias más de la casa prefabricada ampliadas al tamaño 2 y úselas para rellenar las esquinas del borde. Establezca su coordenada Y en 0,5 para que todos los fondos estén alineados. También ajuste la cámara principal para que muestre una vista de arriba hacia abajo de toda la arena.
Por favor agregue la descripción de la imagen.

La arena requiere dos remos (tablas). Cree otro cubo predeterminado y conviértalo en una nueva casa prefabricada, nuevamente sin colisionador. Establece su escala en (8, 1.1, 1.1) y dale un material blanco. Agregue dos instancias de este a la escena para que se superpongan en el medio de los bordes superior e inferior, como se muestra arriba.
Por favor agregue la descripción de la imagen.

Lo último que necesitamos es una pelota, también será un cubo. Crea otro cubo prefabricado para esto (en caso de que decidas agregar más bolas más adelante) y dale un material amarillo. Coloque su instancia en el centro de la escena.

Por favor agregue la descripción de la imagen.

componentes

Si bien nuestro juego es lo suficientemente simple como para controlar todo con un solo script, dividiremos lógicamente la funcionalidad para que el código sea más fácil de entender. Crearemos los tipos de componentes obvios ahora y los completaremos más adelante.

Primero, tenemos una bola en movimiento, así que cree una clase de extensión y agréguela como componente a la bola prefabricada.**Ball**``MonoBehaviour

using UnityEngine;

public class Ball : MonoBehaviour {}

En segundo lugar, tenemos raquetas que intentarán golpear la pelota, así que cree un tipo de componente para ellas y agréguelo a la estructura prefabricada de la raqueta.**Paddle**

using UnityEngine;

public class Paddle : MonoBehaviour {}

En tercer lugar, necesitamos un componente que controle el ciclo del juego y se comunique con la pelota y la raqueta. Simplemente nómbralo, dale los campos de configuración para conectar la pelota y las dos raquetas, conéctalo a un objeto de juego vacío en la escena y conéctalo.**Game**

using UnityEngine;

public class Game : MonoBehaviour
{
[SerializeField]
Ball ball;

[SerializeField]
Paddle bottomPaddle, topPaddle;
}

controlar la pelota

El objetivo del juego es controlar el balón. La pelota se mueve en línea recta por la arena hasta que golpea algo. Cada jugador intenta colocar su raqueta de manera que golpee la pelota y la haga rebotar hacia el otro lado.

posición y velocidad

Para moverse, es necesario rastrear su posición y velocidad. Dado que este es en realidad un juego 2D, usaremos campos para esto, donde la dimensión 2D Y representa la dimensión 3D Z. Comenzamos con velocidades X e Y constantes, configurables mediante campos flotantes serializables separados. Utilizo 8 y 10 como valores predeterminados.**Ball**``Vector2

public class Ball : MonoBehaviour
{
[SerializeField, Min(0f)]
float
constantXSpeed = 8f,
constantYSpeed = 10f;

Vector2 position, velocity;
}

Probablemente terminemos haciendo varios ajustes a la posición y velocidad de la pelota en cada actualización, así que no lo configuremos todo el tiempo. En su lugar, cree un método público para esto.Transform.localPosition``UpdateVisualization

public void UpdateVisualization () =>
transform.localPosition = new Vector3(position.x, 0f, position.y);

En lugar de hacer que la pelota no se mueva por sí sola, haremos que realice un movimiento estándar mediante métodos públicos.Move

public void Move () => position += velocity * Time.deltaTime;

También proporcionamos un método público, listo para nuevos juegos. La pelota comienza en el centro de la arena, actualizando su visualización para que coincida, usando la velocidad configurada. Dado que la raqueta inferior será controlada por el jugador, establezca el componente Y de la velocidad en negativo para que se mueva primero hacia el jugador.StartNewGame

public void StartNewGame ()
{
position = Vector2.zero;
UpdateVisualization();
velocity = new Vector2(constantXSpeed, -constantYSpeed);
}

Ahora puedes controlar el balón. Al menos, cuando despierta la pelota, debería comenzar un nuevo juego, cuando se actualiza, la pelota debería moverse, y luego actualizar su visualización.**Game**

void Awake () => ball.StartNewGame();

void Update ()
{
ball.Move();
ball.UpdateVisualization();
}

más allá de los límites

En este punto, tenemos una bola que comienza a moverse después de entrar en el modo de juego, y continúa hacia adelante, cruzando el borde inferior y desapareciendo de la vista. Los límites de la arena no se conocen directamente y lo mantendremos así. En su lugar, le agregaremos dos métodos públicos que fuerzan el rebote en una sola dimensión dados los límites. Simplemente asumimos que devolver la solicitud es apropiado.**Ball**

Se produce un rebote cuando se cruza un determinado límite, lo que significa que la pelota ahora está fuera del límite. Esto debe corregirse reflejando su trayectoria. La posición final es simplemente igual a los límites menos el doble de la posición actual. Además, la velocidad en esta dimensión cambia. Estos rebotes son perfectos por lo que la posición y velocidad en la otra dimensión no se ven afectadas. Crear e implementar métodos para este propósito.BounceX``BounceY

public void BounceX (float boundary)
{
position.x = 2f * boundary - position.x;
velocity.x = -velocity.x;
}

public void BounceY (float boundary)
{
position.y = 2f * boundary - position.y;
velocity.y = -velocity.y;
}

El rebote adecuado ocurre cuando el borde de la pelota toca el límite en lugar de su centro. Por lo tanto, necesitamos saber el tamaño de la bola, y para ello agregaremos un campo de configuración expresado como un rango, que por defecto está establecido en 0,5, coincidiendo con la unidad del cubo.

[SerializeField, Min(0f)]
float
constantXSpeed = 8f,
constantYSpeed = 10f,
extents = 0.5f;

La pelota en sí no determina cuándo rebota, por lo que su alcance y ubicación deben ser de acceso público. Agregue una propiedad getter para esto.

public float Extents => extents;

public Vector2 Position => position;

**Game**También necesitas saber la extensión de la arena, que puede ser cualquier rectángulo centrado en el origen. Especifique un campo de configuración para esto, que está configurado en 10×10 de forma predeterminada.Vector2

[SerializeField, Min(0f)]
Vector2 arenaExtents = new Vector2(10f, 10f);

Primero examinamos la dimensión Y. Crea un método para esto. El alcance a comprobar es igual al alcance Y de la arena menos el alcance de la pelota. Si la pelota está por debajo del rango negativo o por encima del rango positivo, debería rebotar desde el límite apropiado. Este método se llama entre mover la pelota y actualizar su visualización.BounceYIfNeeded

void Update ()
{
ball.Move();
BounceYIfNeeded();
ball.UpdateVisualization();
}

void BounceYIfNeeded ()
{
float yExtents = arenaExtents.y - ball.Extents;
if (ball.Position.y < -yExtents)
{
ball.BounceY(-yExtents);
}
else if (ball.Position.y > yExtents)
{
ball.BounceY(yExtents);
}
}

La pelota ahora rebota en los bordes superior e inferior. Para rebotar desde los bordes izquierdo y derecho simultáneamente, cree un método de la misma manera, pero para la dimensión X y en .BounceXIfNeeded``BounceYIfNeeded

void Update ()
{
ball.Move();
BounceYIfNeeded();
BounceXIfNeeded();
ball.UpdateVisualization();
}

void BounceXIfNeeded ()
{
float xExtents = arenaExtents.x - ball.Extents;
if (ball.Position.x < -xExtents)
{
ball.BounceX(-xExtents);
}
else if (ball.Position.x > xExtents)
{
ball.BounceX(xExtents);
}
}

La pelota ahora está contenida en la arena, rebota en el borde y nunca escapa.

paleta en movimiento

También necesitamos saber el alcance y la velocidad de las paletas, así que agregue sus campos de configuración, que están configurados en 4 y 10 por defecto.**Paddle**

public class Paddle : MonoBehaviour
{
[SerializeField, Min(0f)]
float
extents = 4f,
speed = 10f;
}

**Paddle**También obtiene un método público, esta vez tomando parámetros para el alcance objetivo y de arena, ambos en la dimensión X. Deje que obtenga su posición inicialmente, fije la coordenada X para que la paleta no pueda moverse más de lo que debería y luego establezca su posición.Move

public void Move (float target, float arenaExtents)
{
Vector3 p = transform.localPosition;
float limit = arenaExtents - extents;
p.x = Mathf.Clamp(p.x, -limit, limit);
transform.localPosition = p;
}

Se supone que la raqueta la controla el jugador, pero hay dos tipos de jugadores: inteligencia artificial y humanos. Comencemos implementando un controlador AI simple creando un método que obtenga la posición X y el objetivo y devuelva la nueva X. Si está a la izquierda del objetivo, simplemente se mueve hacia la derecha a máxima velocidad hasta que coincida con el objetivo; de lo contrario, se mueve hacia la izquierda de la misma manera. Es una estúpida IA ​​reactiva sin predicciones, su dificultad sólo está determinada por su velocidad.AdjustByAI

float AdjustByAI (float x, float target)
{
if (x < target)
{
return Mathf.Min(x + speed * Time.deltaTime, target);
}
return Mathf.Max(x - speed * Time.deltaTime, target);
}

Para los jugadores humanos, creamos un método que no requiere un objetivo y simplemente se mueve hacia la izquierda o hacia la derecha según las teclas de flecha presionadas. Si presionas ambos al mismo tiempo, no se moverá.AdjustByPlayer

float AdjustByPlayer (float x)
{
bool goRight = Input.GetKey(KeyCode.RightArrow);
bool goLeft = Input.GetKey(KeyCode.LeftArrow);
if (goRight && !goLeft)
{
return x + speed * Time.deltaTime;
}
else if (goLeft && !goRight)
{
return x - speed * Time.deltaTime;
}
return x;
}

Ahora agregue un interruptor de palanca para determinar si la raqueta está controlada por la IA y llame al método apropiado para ajustar la coordenada X de la posición.Move

[SerializeField]
bool isAI;

…

public void Move (float target, float arenaExtents)
{
Vector3 p = transform.localPosition;
p.x = isAI ? AdjustByAI(p.x, target) : AdjustByPlayer(p.x);
float limit = arenaExtents - extents;
p.x = Mathf.Clamp(p.x, -limit, limit);
transform.localPosition = p;
}

Mueve ambas paletas al principio.**Game**.Update

void Update ()
{
bottomPaddle.Move(ball.Position.x, arenaExtents.x);
topPaddle.Move(ball.Position.x, arenaExtents.x);
ball.Move();
BounceYIfNeeded();
BounceXIfNeeded();
ball.UpdateVisualization();
}

La paleta ahora responde a las teclas de flecha o se mueve por sí sola. Activa la IA del remo superior y reduce su velocidad a 5 para que sea fácilmente derrotado. Ten en cuenta que puedes habilitar o deshabilitar la IA en cualquier momento mientras juegas.

jugar juegos

Ahora que tenemos una pelota y una raqueta funcionales, podemos crear un juego jugable. Los jugadores intentan mover sus paletas para hacer rebotar la pelota hacia el otro lado de la arena. Si no lo hacen, sus oponentes marcarán.

guata

Agregue un método que devuelva si golpea la pelota en su ubicación actual, dada su posición X y su rango. Esto lo podemos comprobar restando la posición de la raqueta a la pelota y dividiéndola por el alcance de la raqueta más la pelota. El resultado es un coeficiente de golpe, en algún lugar del rango -1-1 si la raqueta golpea la pelota con éxito.HitBall``**Paddle**

public bool HitBall (float ballX, float ballExtents)
{
float hitFactor =
(ballX - transform.localPosition.x) /
(extents + ballExtents);
return -1f <= hitFactor && hitFactor <= 1f;
}

El factor de golpe en sí también es útil porque describe la posición de impacto de la pelota en relación con el centro y el alcance de la raqueta. En el tenis de mesa, esto determina el ángulo con el que la pelota rebota en la raqueta. Así que hagámoslo disponible a través del parámetro de salida.

public bool HitBall (float ballX, float ballExtents, out float hitFactor)
{
hitFactor =
(ballX - transform.localPosition.x) /
(extents + ballExtents);
return -1f <= hitFactor && hitFactor <= 1f;
}

Si la pelota cambia de velocidad después de ser golpeada por la raqueta, entonces nuestro simple código de rebote no es suficiente. Debemos retroceder el tiempo hasta el momento en que ocurrió el rebote, determinar la nueva velocidad y avanzar el tiempo hasta el momento actual.

En, cambie el nombre y agregue un configurable, establecido en 20 de forma predeterminada. Luego cree un método que anule su método actual, dada una posición inicial y un coeficiente de velocidad. La nueva velocidad se convierte en la velocidad máxima escalada por el factor y luego se determina la nueva posición con el incremento de tiempo dado.**Ball**``constantXSpeed``startSpeed``maxXSpeed``SetXPositionAndSpeed

[SerializeField, Min(0f)]
float
maxXSpeed = 20f,
startXSpeed = 8f,
constantYSpeed = 10f,
extents = 0.5f;

…

public void StartNewGame ()
{
position = Vector2.zero;
UpdateVisualization();
velocity = new Vector2(startXSpeed, -constantYSpeed);
}

public void SetXPositionAndSpeed (float start, float speedFactor, float deltaTime)
{
velocity.x = maxXSpeed * speedFactor;
position.x = start + velocity.x * deltaTime;
}

Para encontrar el momento exacto del rebote, debes conocer la velocidad de la pelota, así que agrégale una propiedad de captador público.

public Vector2 Velocity => velocity;

**Game**Ahora hay más trabajo por hacer al rebotar en la dimensión Y. Entonces, en lugar de llamarlo directamente, primero llamaremos a un nuevo método con los parámetros de la paleta defensora.ball.Bounce``**Game**.BounceY

void BounceYIfNeeded ()
{
float yExtents = arenaExtents.y - ball.Extents;
if (ball.Position.y < -yExtents)
{
BounceY(-yExtents, bottomPaddle);
}
else if (ball.Position.y > yExtents)
{
BounceY(yExtents, topPaddle);
}
}

void BounceY (float boundary, Paddle defender)
{
ball.BounceY(boundary);
}

Lo primero que hay que hacer es determinar cuándo se produjo el rebote. Esto se encuentra restando los límites de la posición Y de la pelota y dividiéndolo por la velocidad Y de la pelota. Tenga en cuenta que ignoramos que la raqueta sea un poco más gruesa que el borde, ya que esto es solo una cosa visual para evitar batallas Z al renderizar.BounceY

float durationAfterBounce = (ball.Position.y - boundary) / ball.Velocity.y;

ball.BounceY(boundary);

A continuación, calcule la posición X de la pelota cuando se produce el rebote.

float durationAfterBounce = (ball.Position.y - boundary) / ball.Velocity.y;
float bounceX = ball.Position.x - ball.Velocity.x * durationAfterBounce;

Después de eso realizamos el rebote Y original y luego verificamos si la paleta defensiva golpea la pelota. Si es así, establezca la posición X y la velocidad de la pelota según la posición X del rebote, el factor de golpe y cuándo ocurre.

ball.BounceY(boundary);

if (defender.HitBall(bounceX, ball.Extents, out float hitFactor))
{
ball.SetXPositionAndSpeed(bounceX, hitFactor, durationAfterBounce);
}

En este punto, debemos considerar la posibilidad de que se produzca un rebote en ambas dimensiones. En este caso, la posición X del rebote puede acabar fuera de la pista. Esto se puede evitar realizando primero un rebote X, pero sólo si es necesario. Para admitir este cambio, la posición X que verifica se proporciona a través de un parámetro.BounceXIfNeeded

void Update ()
{
…
BounceXIfNeeded(ball.Position.x);
ball.UpdateVisualization();
}

void BounceXIfNeeded (float x)
{
float xExtents = arenaExtents.x - ball.Extents;
if (x < -xExtents)
{
ball.BounceX(-xExtents);
}
else if (x > xExtents)
{
ball.BounceX(xExtents);
}
}

Entonces también podemos llamar en función de dónde alcanza el límite Y. Por lo tanto, sólo nos ocupamos de los rebotes X que ocurren antes de los rebotes Y. A continuación se vuelve a calcular la posición X del rebote, ahora posiblemente basándose en una posición y velocidad diferentes de la pelota.BounceXIfNeeded``BounceY

float durationAfterBounce = (ball.Position.y - boundary) / ball.Velocity.y;
float bounceX = ball.Position.x - ball.Velocity.x * durationAfterBounce;

BounceXIfNeeded(bounceX);
bounceX = ball.Position.x - ball.Velocity.x * durationAfterBounce;
ball.BounceY(boundary);

A continuación, la velocidad de la pelota cambia dependiendo de dónde golpea la raqueta. Su velocidad Y permanece constante, mientras que su velocidad X es variable. Esto quiere decir que pasar de una pala a otra siempre lleva el mismo tiempo, pero puede que se mueva un poco o mucho hacia los lados. La pelota de Pong se comporta de la misma manera.

A diferencia del tenis de mesa, donde cuando la raqueta falla la pelota sigue rebotando en el borde de la pista, en nuestro juego se activa una nueva ronda. Nuestro juego continúa ininterrumpidamente sin interrupción del juego. Hagamos de este comportamiento una peculiaridad única de nuestro juego.

puntaje

Cuando la raqueta defensiva no alcanza la pelota, el oponente anota. Mostraremos las puntuaciones de ambos jugadores en la cancha o en la arena. Crea un objeto de juego de texto para esto, a través de . Esto activará una ventana emergente desde la que seleccionamos opciones.

Convertir texto a prefabricado. Ajuste su ancho a 20, alto a 6, posición Y a −0,5 y rotación X a 90°. Asigne a su componente un texto inicial de 0, un tamaño de fuente de 72 y establezca su alineación en Centro y Medio. Luego cree dos instancias con las posiciones Z −5 y 5.RectTransform``TextMeshPro
Por favor agregue la descripción de la imagen.

Consideramos que un jugador y su raqueta son la misma cosa, por lo que las referencias a su texto de puntuación se rastrearán a través de un campo configurable.**Paddle**``TMPro.TextMeshPro

using TMPro;
using UnityEngine;

public class Paddle : MonoBehaviour
{
[SerializeField]
TextMeshPro scoreText;

…
}

También realizará un seguimiento de su propia tienda. Dale un método privado que reemplace su puntuación actual con una nueva puntuación y actualice su texto para que coincida. Esto se puede hacer llamando al componente de texto con una cadena y una puntuación como parámetros.SetScore``SetText``"{0}"

int score;

…

void SetScore (int newScore)
{
score = newScore;
scoreText.SetText("{0}", newScore);
}

Para comenzar un nuevo juego, introduzca un método público que establezca la puntuación en cero. Además, agregue un método público que incremente la puntuación y devuelva si esto da como resultado que el jugador gane. Sin duda, dale un parámetro de los puntos necesarios para ganar.StartNewGame``ScorePoint

public void StartNewGame ()
{
SetScore(0);
}

public bool ScorePoint (int pointsToWin)
{
SetScore(score + 1);
return score >= pointsToWin;
}

**Game**Ahora también se debe llamar en ambas paletas, así que démosle su propio método para pasar el mensaje, lo cual hace en .StartNewGame``StartNewGame``Awake

void Awake () => StartNewGame();

void StartNewGame ()
{
ball.StartNewGame();
bottomPaddle.StartNewGame();
topPaddle.StartNewGame();
}

Hace que el número de puntos otorgados por ganar sea configurable, con un mínimo de 2 y un valor predeterminado de 3. Luego agrégale la raqueta del atacante como tercer parámetro y haz que la llame si el defensor no golpea la pelota. Si esto da como resultado que el atacante gane, se inicia un nuevo juego.BounceY``ScorePoint

[SerializeField, Min(2)]
int pointsToWin = 3;

…

void BounceYIfNeeded ()
{
float yExtents = arenaExtents.y - ball.Extents;
if (ball.Position.y < -yExtents)
{
BounceY(-yExtents, bottomPaddle, topPaddle);
}
else if (ball.Position.y > yExtents)
{
BounceY(yExtents, topPaddle, bottomPaddle);
}
}

void BounceY (float boundary, Paddle defender, Paddle attacker)
{
…

if (defender.HitBall(bounceX, ball.Extents, out float hitFactor))
{
ball.SetXPositionAndSpeed(bounceX, hitFactor, durationAfterBounce);
}
else if (attacker.ScorePoint(pointsToWin))
{
StartNewGame();
}
}

Cuenta regresiva del nuevo juego

En lugar de empezar un nuevo juego inmediatamente, se puede introducir un retraso durante el cual se puede apreciar la puntuación final. También retrasemos el inicio inicial del juego para que los jugadores puedan prepararse. Cree una nueva instancia de texto para mostrar la cuenta regresiva en el centro de la arena, con el tamaño de fuente reducido a 32 como texto inicial.

Por favor agregue la descripción de la imagen.

Proporciona campos de configuración para el texto de la cuenta regresiva y la duración del retraso del nuevo juego, con un valor mínimo de 1 y un valor predeterminado de 3. También dale un campo para realizar un seguimiento de la cuenta regresiva antes del nuevo juego y configúralo con una duración de retraso en lugar de comenzar el nuevo juego inmediatamente.**Game**``Awake

using TMPro;
using UnityEngine;

public class Game : MonoBehaviour
{
…

[SerializeField]
TextMeshPro countdownText;

[SerializeField, Min(1f)]
float newGameDelay = 3f;

float countdownUntilNewGame;

void Awake () => countdownUntilNewGame = newGameDelay;

Seguimos moviendo siempre la raqueta para que los jugadores puedan posicionarse durante la cuenta atrás. Mueve el resto del código a un nuevo método al que solo llamamos cuando la cuenta regresiva es cero o menos. De lo contrario, llamamos a un nuevo método que disminuye la cuenta regresiva y actualiza su texto.Update``UpdateGame``UpdateCountdown

void Update ()
{
bottomPaddle.Move(ball.Position.x, arenaExtents.x);
topPaddle.Move(ball.Position.x, arenaExtents.x);

if (countdownUntilNewGame <= 0f)
{
UpdateGame();
}
else
{
UpdateCountdown();
}
}

void UpdateGame ()
{
ball.Move();
BounceYIfNeeded();
BounceXIfNeeded(ball.Position.x);
ball.UpdateVisualization();
}

void UpdateCountdown ()
{
countdownUntilNewGame -= Time.deltaTime;
countdownText.SetText("{0}", countdownUntilNewGame);
}

Si la cuenta regresiva llega a cero, desactiva el texto de la cuenta regresiva y comienza un nuevo juego; de lo contrario, actualiza el texto. Pero mostremos el segundo completo. Podemos hacer esto poniendo un límite a la cuenta regresiva. Para hacer visible el texto inicial, cámbielo solo si el valor de visualización es menor que el retraso configurado. Si el retraso se establece en un número entero, el texto será visible durante el primer segundo.

countdownUntilNewGame -= Time.deltaTime;
if (countdownUntilNewGame <= 0f)
{
countdownText.gameObject.SetActive(false);
StartNewGame();
}
else
{
float displayValue = Mathf.Ceil(countdownUntilNewGame);
if (displayValue < newGameDelay)
{
countdownText.SetText("{0}", displayValue);
}
}

Escondamos el balón cuando no haya juego. Como es conveniente tener la pelota activa en la escena durante el desarrollo, hemos incluido una forma de desactivarla por sí sola. Luego actívalo nuevamente al final. Además, se ha introducido un método público que establece su posición X en el centro del estado (por lo que la IA moverá su raqueta al medio entre juegos) y se desactiva.**Ball**``Awake``StartNewGame``EndGame

void Awake () => gameObject.SetActive(false);

public void StartNewGame ()
{
position = Vector2.zero;
UpdateVisualization();
velocity = new Vector2(startXSpeed, -constantYSpeed);
gameObject.SetActive(true);
}

public void EndGame ()
{
position.x = 0f;
gameObject.SetActive(false);
}

También proporciona un método para llamar cuando un jugador gana, en lugar de comenzar un nuevo juego inmediatamente. En él, reinicie la cuenta atrás, establezca el texto de la cuenta atrás, actívelo y dígale a la pelota que el juego ha terminado.**Game**``EndGame

void BounceY (float boundary, Paddle defender, Paddle attacker)
{
…

if (defender.HitBall(bounceX, ball.Extents, out float hitFactor))
{
ball.SetXPositionAndSpeed(bounceX, hitFactor, durationAfterBounce);
}
else if (attacker.ScorePoint(pointsToWin))
{
EndGame();
}
}

void EndGame ()
{
countdownUntilNewGame = newGameDelay;
countdownText.SetText("GAME OVER");
countdownText.gameObject.SetActive(true);
ball.EndGame();
}

aleatoriedad

En este punto, tenemos un juego mínimamente funcional, pero hagámoslo más interesante agregando algo de aleatoriedad de dos maneras diferentes. Primero, en lugar de comenzar siempre con la misma velocidad X, establezca de forma predeterminada la velocidad X inicial máxima configurable en 2 y úsela al comienzo de cada juego para aleatorizar su velocidad.**Ball**

[SerializeField, Min(0f)]
float
maxXSpeed = 20f,
maxStartXSpeed = 2f,
constantYSpeed = 10f,
extents = 0.5f;

…

public void StartNewGame ()
{
position = Vector2.zero;
UpdateVisualization();

velocity.x = Random.Range(-maxStartXSpeed, maxStartXSpeed);
velocity.y = -constantYSpeed;
gameObject.SetActive(true);
}

En segundo lugar, dale a la IA un sesgo objetivo para que no siempre intente golpear la pelota en su centro exacto. Para controlar esto, se introduce un sesgo de posicionamiento máximo configurable, que representa una fracción de su rango (similar a un factor de acierto), que está establecido en 0,75 de forma predeterminada. Utilice campos para realizar un seguimiento de su sesgo actual y agregue métodos de aleatorización.**Paddle**``ChangeTargetingBias

[SerializeField, Min(0f)]
float
extents = 4f,
speed = 10f,
maxTargetingBias = 0.75f;

…

float targetingBias;

…

void ChangeTargetingBias () =>
targetingBias = Random.Range(-maxTargetingBias, maxTargetingBias);

La desviación del objetivo cambia con cada nuevo juego y cuando la raqueta intenta golpear la pelota.

public void StartNewGame ()
{
SetScore(0);
ChangeTargetingBias();
}

public bool HitBall (float ballX, float ballExtents, out float hitFactor)
{
ChangeTargetingBias();
…
}

Para aplicar el sesgo, agréguelo al objetivo antes de mover la raqueta.AdjustByAI

float AdjustByAI (float x, float target)
{
target += targetingBias * extents;
…
}

Práctica de desarrollo de juegos pequeños de Unity: crea tu propio mundo de juegos desde cero (Parte 1)
[Juego pequeño de Unity] Caso de desarrollo de juegos: Unity crea juegos pequeños sin obstáculos (Parte 2)

Supongo que te gusta

Origin blog.csdn.net/weixin_72715182/article/details/130430911
Recomendado
Clasificación