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

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.

Junto con el contenido anterior.

paleta retráctil

Como acto final de nuestro juego, alejemos la imagen cada vez que anotamos. Esto crea una desventaja basada en lo cerca que está un jugador de ganar. Convierte su alcance actual en un campo privado y hace que sus valores mínimo y máximo sean configurables, ambos establecidos en 4 de forma predeterminada. Se introdujo un método para reemplazar la extensión actual, que también ajusta la escala local del objeto del juego para que coincida.**Paddle**``SetExtents

[SerializeField, Min(0f)]
float

minExtents = 4f,
maxExtents = 4f,
speed = 10f,
maxTargetingBias = 0.75f;

…

float extents, targetingBias;

…

void SetExtents (float newExtents)
{
extents = newExtents;
Vector3 s = transform.localScale;
s.x = 2f * newExtents;
transform.localScale = s;
}

Establezca el rango al final del golpe según lo cerca que esté de ganar el golpe. Esto se hace interpolando desde el rango máximo al rango mínimo según la nueva puntuación dividida por la puntuación requerida para ganar menos 1. Agregue el parámetro requerido para esto, que puede ser cualquier valor mayor que 1 de forma predeterminada cuando se establece en cero.SetScore

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

…

void SetScore (int newScore, float pointsToWin = 1000f)
{
score = newScore;
scoreText.SetText("{0}", newScore);
SetExtents(Mathf.Lerp(maxExtents, minExtents, newScore / (pointsToWin - 1f)));
}

También debemos resetear la puntuación cuando la raqueta se despierte para que su tamaño inicial sea el correcto en la fase de preparación del juego.

void Awake ()
{
SetScore(0);
}

Usemos al menos 1 para la raqueta inferior y 3,5 para la raqueta superior para que a la IA le resulte más fácil golpear la pelota.

cámara activa

Con la jugabilidad completa, veamos si podemos hacer que la presentación del juego sea más interesante. Una forma sencilla de dar la sensación de un fuerte impacto cuando la pelota golpea algo es agitar la cámara. Simula cómo se sienten los jugadores cuando la pelota golpea los lados de la arena. Para aumentar aún más la inmersión, cambiamos de vista de arriba hacia abajo a vista en perspectiva. Establezca la posición de la cámara en (0, 20, -19) y su rotación X en 50.

Muestra la arena desde la perspectiva del jugador.

empujando y empujando

Para controlar el comportamiento de la cámara, cree un tipo de componente. La cámara se puede mover en el plano XZ a un pulso determinado, o en la dimensión Y. Esto se logra dándole a la cámara una velocidad 3D que se aplica después de que se completan todos los fotogramas de empujones y empujones.**LivelyCamera**``LateUpdate

Empujar se realiza a través de un método público que aumenta la velocidad Y en una fuerza configurable, establecida en 40 de forma predeterminada. Empujar se realiza a través de un método público con un parámetro de pulso 2D que se agrega a la velocidad, escalado por un factor de fuerza de empuje configurable, establecido en 1 de forma predeterminada.JostleY``PushXZ

using UnityEngine;

public class LivelyCamera : MonoBehaviour
{
[SerializeField, Min(0f)]
float
jostleStrength = 40f,
pushtrength = 1f;

Vector3 velocity;

public void JostleY () => velocity.y += jostleStrength;

public void PushXZ (Vector2 impulse)
{
velocity.x += pushStrength * impulse.x;
velocity.z += pushStrength * impulse.y;
}

void LateUpdate ()
{
transform.localPosition += velocity * Time.deltaTime;
}
}

Agregue este componente a la cámara principal, luego proporcione un campo de configuración y conéctelo a la cámara.**Game**

[SerializeField]
LivelyCamera livelyCamera;

Cuando se detecta un rebote en , se convoca utilizando la velocidad de la pelota como pulso antes de ejecutar el rebote.BounceXIfNeeded``PushXZ

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

Haga lo mismo antes de ejecutar el retorno Y. Además, empuja la cámara mientras anotas.BounceY

void BounceY (float boundary, Paddle defender, Paddle attacker)
{
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;
livelyCamera.PushXZ(ball.Velocity);
ball.BounceY(boundary);

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

resortes y amortiguadores

La cámara ahora se mueve, pero su velocidad se mantiene, por lo que la arena desaparece rápidamente de la vista. Para retroceder utilizamos un mecanismo de resorte simple para mantenerlo en su posición original. Le otorga una fuerza de resorte configurable con una configuración predeterminada de 100 y una fuerza de amortiguación predeterminada de 10. También dale una posición de anclaje y configúralo en la posición de la cámara cuando se despierte.**LivelyCamera**

[SerializeField, Min(0f)]
float
springStrength = 100f,
dampingStrength = 10f,
jostleStrength = 40f,
pushStrength = 1f;

Vector3 anchorPosition, velocity;

void Awake () => anchorPosition = transform.localPosition;

Implementamos el resorte usando el desplazamiento actual de la cámara como aceleración, escalada por la fuerza del resorte. También ralentizamos el movimiento mediante una aceleración negativa igual a la velocidad actual proporcional a la fuerza de amortiguación.

void LateUpdate ()
{
Vector3 displacement = anchorPosition - transform.localPosition;
Vector3 acceleration = springStrength * displacement - dampingStrength * velocity;
velocity += acceleration * Time.deltaTime;
transform.localPosition += velocity * Time.deltaTime;
}

Cámara vívida con alta velocidad de fotogramas.

Tiempo máximo delta

Nuestra sencilla regla de resorte sólo funciona bien si la velocidad de fotogramas es lo suficientemente alta. Se resiste a empujar y tirar de la cámara hacia su punto de anclaje, pero puede causar un sobrepaso y moverse un poco antes de detenerse. Sin embargo, si la velocidad de fotogramas es demasiado baja, el exceso puede terminar exagerando su impulso y puede salirse de control, acelerando en lugar de desacelerarse. Este problema se puede demostrar forzando una velocidad de fotogramas muy baja mediante el método add. Se debe volver a poner a cero más tarde para eliminar el límite, ya que esta configuración es permanente.Application.targetFrameRate = 5;``Awake

Cuando la velocidad de cuadros es lo suficientemente alta, este problema no ocurre. Por lo tanto, podemos evitarlo aplicando incrementos de tiempo más pequeños. Esto lo podemos hacer moviendo la cámara. Sin embargo, dado que esto impone incrementos de tiempo precisos, se producirá un micro tartamudeo ya que es posible que la cámara no actualice la misma cantidad de veces por cuadro, lo cual es muy notable ya que afecta el movimiento de toda la vista. Además, limita la velocidad de fotogramas efectiva del movimiento de la cámara.FixedUpdate

Una solución simple es imponer un incremento de tiempo máximo, pero no un incremento de tiempo mínimo. Agrega un valor máximo configurable para esto, establecido en una sexagésima de segundo de forma predeterminada. Luego mueva el código a un nuevo método que tenga un delta de tiempo como parámetro. Llame con el incremento máximo tantas veces como quepan los incrementos del fotograma actual, luego llame nuevamente para los incrementos restantes.**LivelyCamera**``LateUpdate``TimeStep``LateUpdate``TimeStep

[SerializeField, Min(0f)]
float
springStrength = 100f,
dampingStrength = 10f,
jostleStrength = 40f,
pushStrength = 1f,
maxDeltaTime = 1f / 60f;

…

void LateUpdate ()
{
float dt = Time.deltaTime;
while (dt > maxDeltaTime)
{
TimeStep(maxDeltaTime);
dt -= maxDeltaTime;
}
TimeStep(dt);
}

void TimeStep (float dt)
{
Vector3 displacement = anchorPosition - transform.localPosition;
Vector3 acceleration = springStrength * displacement - dampingStrength * velocity;
velocity += acceleration * dt;
transform.localPosition += velocity * dt;
}

Visión

El último paso es mejorar ligeramente el apartado visual de nuestro juego. Elegiremos un aspecto simple de neón brillante.

Comience oscureciendo todo y simplemente apagando las luces direccionales. También configure la cámara con un fondo de color sólido. Luego habilite las opciones de la cámara y configúrela en FXAA.

Mantener intacta la iluminación ambiental a pesar de que el fondo negro no tiene sentido, por lo que todavía hay algo de iluminación para hacer visible la arena.

为什么使用 FXAA 而不是 MSAA?
我们将使用高强度HDR颜色。URP的FXAA可以处理这些问题,但其MSAA不能。

bola brillante

La pelota será nuestra única fuente de luz. Dale una luz secundaria, establece su color en amarillo, la intensidad en 20 y el rango en 100.

Efecto visual oscuro usando una bola como fuente de luz.

La pelota debería brillar. Cree un mapa de sombreador apagado utilizando las propiedades de color HDR. Utilice esto para crear un material con un color amarillo de alta intensidad y asígnelo a la bola prefabricada.

Para que luzca brillante, debemos aplicar un efecto de floración posterior. Cree un volumen global de la siguiente manera. Selecciónelo y cree un nuevo perfil de volumen para él. Para agregar anulaciones, configúrelo en 1 y habilítelo. Además, se agregó una anulación para el conjunto a ACES.

Ajuste el recurso URP para que esté configurado en HDR. También podemos configurarlo en Esto evita que Unity actualice innecesariamente los datos del volumen en cada cuadro, ya que nunca los cambiamos.

bola brillante

rebotar partículas

La pelota ahora parece un cubo de alta energía. Para reforzar esta idea, hagamos saltar una chispa cuando rebote. Cree un gráfico de sombreador apagado que utilice el color del vértice multiplicado por el atributo de intensidad. Establece su Tipo de superficie en Transparente y su Modo de fusión en Aditivo para que siempre sea claro. Crea un material de partículas con una fuerza establecida en 10.

Gráfico de sombreado para partículas

                                   粒子的着色器图(上图)

Crea un objeto de juego de sistema de partículas en el origen. Deshabilitado y configurado en rangos constantes 0,5–1, 2–4 y 0,5. Cámbielo a y.

Establezca ambas tasas del módulo en cero.

De forma predeterminada, el módulo está configurado en un cono, mantenemos ese cono pero lo configuramos en 45 y lo configuramos en 0,5.

Con el módulo habilitado, el degradado de color pasa de amarillo a rojo, con su alfa establecido en cero en ambos extremos y en 10% en 255%.

Con el módulo habilitado, la curva lineal disminuye de 1 a cero.

Las restricciones que solo usan cubos también se aplican a las partículas, así que configure el módulo para que use cubos de forma predeterminada. Deje que utilice nuestros materiales granulares.

Este sistema de partículas no es parte de la pelota en sí, pero requiere una referencia configurable desde la cual generar partículas y una opción de configuración para controlar cuántas partículas se generan por rebote, que está establecida en 20 de forma predeterminada.**Ball**

[SerializeField]
ParticleSystem bounceParticleSystem;

[SerializeField]
int bounceParticleEmission = 20;

Cree una forma de manejar la emisión de partículas rebotadas llamando al sistema de partículas rebotadas. El cono de emisión debe colocarse y girarse correctamente, así que proporcione la posición X y Z y la rotación Y como parámetros del método. Utilícelos para ajustar el módulo de forma del sistema.Emit

void EmitBounceParticles (float x, float z, float rotation)
{
ParticleSystem.ShapeModule shape = bounceParticleSystem.shape;
shape.position = new Vector3(x, 0f, z);
shape.rotation = new Vector3(0f, rotation, 0f);
bounceParticleSystem.Emit(bounceParticleEmission);
}

Llame a este método con los parámetros apropiados. Los límites son posiciones en dimensiones correspondientes. La segunda posición se puede encontrar invirtiendo la posición del balón hasta el momento del rebote. La rotación depende del tamaño del rebote y de si los límites son negativos o positivos.BounceX``BounceY

public void BounceX (float boundary)
{
float durationAfterBounce = (position.x - boundary) / velocity.x;
position.x = 2f * boundary - position.x;
velocity.x = -velocity.x;
EmitBounceParticles(
boundary,
position.y - velocity.y * durationAfterBounce,
boundary < 0f ? 90f : 270f
);
}

public void BounceY (float boundary)
{
float durationAfterBounce = (position.y - boundary) / velocity.y;
position.y = 2f * boundary - position.y;
velocity.y = -velocity.y;
EmitBounceParticles(
position.x - velocity.x * durationAfterBounce,
boundary,
boundary < 0f ? 0f : 180f
);
}

Por favor agregue la descripción de la imagen.

                                      反弹粒子

Partículas iniciales

Hagamos volar las chispas cuando aparezca la pelota al inicio del juego. Convierta el sistema de partículas existente en una casa prefabricada y utilice una segunda instancia ubicada en el origen del nuevo sistema de partículas inicial. Auméntelo a 0,5-1,5 para que dure más y configúrelo en una esfera.

Agregue un campo de configuración, así como un campo para la cantidad de partículas que se generarán al inicio, que está establecido en 100 de forma predeterminada. Estas partículas se emiten al iniciar un nuevo juego.**Ball**

[SerializeField]
ParticleSystem bounceParticleSystem, startParticleSystem;

[SerializeField]
int
bounceParticleEmission = 20,
startParticleEmission = 100;

…

public void StartNewGame ()
{
…
startParticleSystem.Emit(startParticleEmission);
}

partícula inicial.

partículas de rastro

El tercer y último efecto de partículas será el rastro que dejará la pelota. Cree otra instancia del sistema prefabricado de partículas, esta vez usándolo y habilitándolo. Configúrelo en 1–1,25 y configúrelo en cero. Cámbielo a una caja que emita desde su volumen. Para que emita partículas cuando se mueve, configúrelo en 2.

Añade también un campo de configuración para que este sistema sincronice su posición con la posición de la pelota al actualizar su visualización. No haremos que el sistema de seguimiento sea hijo de la pelota para que pueda permanecer visible después de que la pelota se desactive al final del juego; de lo contrario, la estela desaparecerá inmediatamente.**Ball**

[SerializeField]
ParticleSystem bounceParticleSystem, startParticleSystem, trailParticleSystem;

…

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

Por favor agregue la descripción de la imagen.

                                           尾迹粒子

Esto funciona, excepto que al final y al comienzo de cada juego, un efecto de trayectoria también seguirá a la pelota a medida que viaja. Podemos evitar esto haciendo dos cosas. Primero tenemos que desactivar el inicio cuando finaliza el juego y activarlo cuando comienza un nuevo juego. Esto se hace configurando las propiedades del módulo emisor, así que agreguemos un método conveniente para esto.

public void StartNewGame ()
{
…
SetTrailEmission(true);
}

public void EndGame ()
{
…
SetTrailEmission(false);
}

…

void SetTrailEmission (bool enabled)
{
ParticleSystem.EmissionModule emission = trailParticleSystem.emission;
emission.enabled = enabled;
}

En segundo lugar, el sistema de partículas recuerda su antigua posición. Para borrarlo y evitar mostrar el rastro teletransportado al comienzo de un nuevo juego, tenemos que llamarlo.Play

SetTrailEmission(true);
trailParticleSystem.Play();

Esto significa que podemos volver a desactivarlo ya que ahora lo estamos reproduciendo explícitamente.

superficie de reacción

La pelota y sus partículas no son los únicos que pueden emitir luz. También hagamos que su superficie reaccione al ser golpeada con un brillo temporal. Crea un mapa de sombreado de iluminación con HDR y propiedades, con un valor predeterminado de −1000.

El color de su emisión depende de cuándo se produjo el último impacto. Estará a todo trapo al golpear y se desvanecerá linealmente al segundo siguiente. Esto se puede hacer restando el tiempo del último golpe del tiempo actual, restando ese tiempo de 1, saturándolo y usándolo para escalar el color de emisión.

Gráfico de sombreado para superficies reactivas

                                   反应表面的着色器图

Utilice este mapa de sombreado para crear un material y utilícelo con la casa prefabricada de sombreado. Utilice el blanco como color base y el blanco de alta intensidad como color emisivo.

Recupere la instancia del material al activarse y actualice su última hora de acceso en caso de éxito. Esto hará que la raqueta brille cuando logre golpear la pelota.**Paddle**``HitBall

static readonly int timeOfLastHitId = Shader.PropertyToID("_TimeOfLastHit");

…

Material paddleMaterial;

void Awake ()
{
paddleMaterial = GetComponent<MeshRenderer>().material;
SetScore(0);
}

…

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

bool success = -1f <= hitFactor && hitFactor <= 1f;
if (success)
{
paddleMaterial.SetFloat(timeOfLastHitId, Time.time);
}
return success;
}

Por favor agregue la descripción de la imagen.

                                             反应桨

Vayamos un paso más allá y dejemos que los bordes de la arena que sirven como goles de los oponentes brillen cuando se anotan. Cree otro material de superficie reactiva con su color establecido en gris medio y utilícelo para la casa prefabricada del borde de la arena. Luego proporciona una referencia configurable para su objetivo, así como un color de objetivo HDR configurable.**Paddle**``MeshRenderer

Cuando Racket se despierta y establece el color de emisión del material en el color objetivo, recupera una instancia de su material objetivo. Establezca el tiempo del último golpe al anotar.

static readonly int
emissionColorId = Shader.PropertyToID("_EmissionColor"),
timeOfLastHitId = Shader.PropertyToID("_TimeOfLastHit");

[SerializeField]
TextMeshPro scoreText;

[SerializeField]
MeshRenderer goalRenderer;

[SerializeField, ColorUsage(true, true)]
Color goalColor = Color.white;

…

Material goalMaterial, paddleMaterial;

void Awake ()
{
goalMaterial = goalRenderer.material;
goalMaterial.SetColor(emissionColorId, goalColor);
paddleMaterial = GetComponent<MeshRenderer>().material;
SetScore(0);
}

…

public bool ScorePoint (int pointsToWin)
{
goalMaterial.SetFloat(timeOfLastHitId, Time.time);
SetScore(score + 1, pointsToWin);
return score >= pointsToWin;
}

Conecte la púa al render correspondiente. Utilice verde de alta intensidad como color del jugador inferior y rojo de alta intensidad como color superior de la IA.

Metas reactivas.

Texto en color

Terminamos coloreando el texto y haciéndolo brillar. Comience configurando el color del material de fuente predeterminado del texto prefabricado en amarillo de alta intensidad.

texto brillante

                                        发光的文本

Usaremos los colores de las porterías para mostrar el puntaje, pero con un giro. Comenzaremos con negro desde cero, por lo que la fracción no será visible inicialmente sobre el fondo negro. Una vez que el color de la puntuación sea igual a la puntuación ganadora, alcanzarán su máxima fuerza.

En este caso, la instancia del material se recupera a través de las propiedades del texto, cuya propiedad de sombreador Color de superficie se denomina.fontMaterial

static readonly int
emissionColorId = Shader.PropertyToID("_EmissionColor"),
faceColorId = Shader.PropertyToID("_FaceColor"),
timeOfLastHitId = Shader.PropertyToID("_TimeOfLastHit");

…

Material goalMaterial, paddleMaterial, scoreMaterial;

void Awake ()
{
goalMaterial = goalRenderer.material;
goalMaterial.SetColor(emissionColorId, goalColor);
paddleMaterial = GetComponent<MeshRenderer>().material;
scoreMaterial = scoreText.fontMaterial;
SetScore(0);
}

…

void SetScore (int newScore, float pointsToWin = 1000f)
{
score = newScore;
scoreText.SetText("{0}", newScore);
scoreMaterial.SetColor(faceColorId, goalColor * (newScore / pointsToWin));
SetExtents(Mathf.Lerp(maxExtents, minExtents, newScore / (pointsToWin - 1f)));
}
                                       改变颜色的分数

Tenga en cuenta que la configuración predeterminada facilita derrotar a la IA una vez que descubre su debilidad. Esto facilita el desarrollo, pero debe ajustarse para proporcionar el nivel deseado de desafío.
(fin)

Supongo que te gusta

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