Red de conocimiento informático - Material del sitio web - Cómo usar Unity para crear efectos dinámicos de agua en 2D

Cómo usar Unity para crear efectos dinámicos de agua en 2D

En este tutorial usaremos un mecanismo físico simple para simular una masa de agua dinámica en 2D. Usaremos un renderizador lineal, un renderizador de malla, activadores y partículas mixtas para crear este efecto de agua y terminaremos con líneas de agua y salpicaduras que puedes usar en tu próximo juego. La fuente del ejemplo de Unity se incluye aquí, pero deberías poder realizar operaciones similares usando cualquier motor de juego basado en los mismos principios.

Construyendo un administrador de cuerpos de agua

Usaremos el renderizador lineal de Unity para renderizar la superficie del agua y usaremos estos nodos para mostrar ondas continuas.

Unity-water-linerenderer (de desarrollo del juego)

Seguiremos la posición, velocidad y aceleración de cada nodo. Para esto usaremos una matriz. Por lo tanto, las siguientes variables se agregarán al principio de nuestra clase:

float[]x posiciones

float[]y posiciones

float[; ]Velocidad;

Float[]Aceleración;

Texto de LineRenderer;

LineRenderer almacenará todos nuestros nodos y delineará nuestro cuerpo de agua. Todavía necesitamos el cuerpo de agua en sí, que se creará utilizando una malla. Necesitaremos objetos para alojar estas cuadrículas.

objeto del juego[]objetos de malla;

Mesh[]grid;

También necesitamos un colisionador para que los objetos puedan interactuar con el agua:

GameObject[] colisionador;

También almacenamos todas las constantes:

Constante de resorte de flotación constante = 0,02f

Amortiguación flotante constante = 0,04 f

Diferencia de coma flotante constante = 0.05f

Coma flotante constante z =-1f;

Z en estas constantes es lo que establecemos para el cuerpo de agua dado desplazamiento z. Lo marcaremos con -1 para que aparezca frente a nuestro objeto (nota del juego: es posible que necesites ajustarlo al frente o detrás del objeto según tus necesidades, luego debes usar la coordenada Z para determinar la posición de la guía correspondiente).

A continuación, conservaremos algunos valores:

altura de base flotante

Flotante hacia la izquierda

Base flotante;

Estas son las dimensiones del agua.

Necesitaremos algunas variables públicas que se puedan configurar en el editor. Primero, usaremos un sistema de partículas para el renderizado:

Salpicadura de objetos de juego público:

El siguiente es el material que usaremos para el renderizador lineal:

Tapete de material público:

Además, usaremos los siguientes tipos de malla para el cuerpo de agua principal:

Objeto de juego público:

Queremos un objeto de juego. que puede alojar todos estos datos y convertirlo en un administrador para generar los cuerpos de agua en nuestro juego. Para hacer esto, escribiremos la función SpawnWater().

Esta función aceptará entradas para el lado izquierdo, el galope, el vértice y el fondo del cuerpo de agua:

Public void SpawnWater(flotante hacia la izquierda, ancho flotante, flotante superior, flotante inferior)

{

(Aunque esto pueda parecer contradictorio, facilita el diseño de niveles rápido de izquierda a derecha.)

Creación de nodos

Ahora encontraremos Descubra cuántos nodos necesitamos:

int edgecount = Mathf. dot(width) * 5;

int node count = edge count 1;

Utilizaremos 5 nodos por unidad de ancho para generar un movimiento suave (puede cambiar esto para equilibrar la eficiencia y fluidez). Podemos obtener todos los segmentos de línea de esto y luego necesitamos el nodo final 1.

Lo primero que debemos hacer es renderizar el cuerpo de agua usando el componente LineRenderer:

Body = objeto del juego. AddComponent ltLineRenderer gt();

Body.material = mat

material del cuerpo. cola de procesamiento = 1000;

Cuerpo. SetVertexCount(recuento de nodos);

Cuerpo. SetWidth(0.1f, 0.1f);

Lo que tenemos que hacer aquí es seleccionar el material y renderizarlo sobre la superficie del agua seleccionando la posición en la cola de renderizado. Establecemos los datos de nodo correctos y establecemos el ancho del segmento en 0,1.

Puedes cambiar este ancho dependiendo del grosor que quieras que tenga el segmento de línea. Es posible que hayas notado que SetWidth() requiere dos parámetros, que son los anchos de los puntos inicial y final del segmento de línea. Queremos que el ancho siga siendo el mismo.

Ahora que hemos creado el nodo, inicializaremos todas las variables de nivel superior:

x posiciones = new float[node count]

ypositions; = new float [recuento de nodos];

Velocidad = new float [recuento de nodos];

aceleraciones = new float[recuento de nodos];

malla objetos = nuevo objeto de juego[recuento de bordes];

mallas = nueva malla[recuento de bordes];

colliders = nuevo objeto de juego[recuento de bordes];

baseheight = Top

bottom = bottom;

left = left;

Ya tenemos todos los arrays con datos de control.

Ahora necesitamos establecer el valor de la matriz. Comenzaremos con el nodo:

for(int I = 0; iltnodecounti)

{

y posiciones[I] = Top;

x posiciones[I]= Ancho izquierdo * I/recuento de bordes;

Aceleración[I]= 0;

Velocidad[I]= 0;

Cuerpo. SetPosition(i, new vector 3(xpositions[i], ypositions[i], z));

}

Aquí, configuramos todas las posiciones Y sobre el cuerpo de agua, y luego aumente gradualmente todos los nodos juntos. Como el agua está en calma, nuestros valores de velocidad y aceleración son inicialmente cero.

Completaremos este bucle configurando cada nodo en LineRenderer (Cuerpo) en la posición correcta.

Crear la cuadrícula

Esta es la parte complicada.

Tenemos nuestros propios segmentos de línea, pero no la masa de agua en sí. Usaremos una malla para hacerlo de la siguiente manera:

for(int I = 0; iltedgecounti)

{

Mesh[I]= new Mesh( );

Ahora, la malla almacena una serie de variables. La primera variable es muy simple: contiene todos los vértices (o esquinas).

unity-water-Firstmesh (de Game Development)

El diagrama muestra cómo necesitamos que se vean los fragmentos de malla. Marque los vértices en el primer clip. Siempre necesitamos cuatro vértices.

Vector3[] vértice = nuevo vector 3[4];

Vértice [0] = nuevo vector 3 (posición x [i], posición y [i], z);

Vértice[1] = nuevo vector 3(posición x[i 1], posición y[i 1], z);

Vértice[2] = nuevo vector 3(x posición[i], abajo, z);

Vértice[3] = nuevo vector 3(x posición[i 1], abajo, z);

Ahora puedes ver, El vértice 0 está en la esquina superior izquierda, 1 está en la esquina superior derecha, 2 está en la esquina inferior izquierda y 3 está en la esquina inferior derecha. Necesitamos recordar esto en el futuro.

La segunda actuación que requiere la red eléctrica es la luz ultravioleta. La malla tiene una textura y los UV seleccionarán la textura que queremos extraer. En este caso sólo necesitamos las texturas para las esquinas superior izquierda, superior derecha, inferior derecha e inferior derecha.

Vector2[] UVs = nuevo vector 2[4];

UVs[0] = nuevo vector 2(0, 1);

UVs[1); ] = nuevo vector 2 (1, 1);

UVs[2] = nuevo vector 2 (0, 0);

UVs[3] = nuevo vector 2 (1, 0);

Ahora necesitamos estos datos nuevamente. La malla está formada por triángulos y sabemos que cualquier cuadrilátero está formado por dos triángulos, así que ahora necesitamos decirle a la malla cómo dibujar estos triángulos.

unity-water-Tris (de Game Development)

Mira la esquina con la etiqueta de orden de los nodos. El triángulo A conecta los nodos 0, 1 y 3, el triángulo B conecta los nodos 3, 2, 1. Entonces queremos hacer una matriz que contenga seis números enteros:

int[] tris = new int[6] { 0, 1, 3, 3, 2, 0 }

This; crea nuestro cuadrilátero. Ahora necesitamos establecer los valores de la cuadrícula.

Cuadrícula[i]. vértice = vértice;

Malla[i]. uv = UVs

Cuadrícula[i]. Triangles = tris

Ahora tenemos nuestras propias mallas, pero no renderizamos sus objetos de juego en la escena. Por lo tanto, los crearemos a partir de la casa prefabricada de malla de agua, incluido el renderizador de malla y el filtro de pantalla.

meshobjects[i] = instanciar(watermesh, Vector3.zero, quaternion.identity) como GameObject

meshobjects[i]. GetComponent lt filtro de malla gt().mesh = meshs[I];

objetos de malla[I]. hijo de WaterManager.

Crear efectos de colisión

Ahora necesitamos nuestro propio colisionador:

colliders[I]= new game object();

Collider [i]. nombre = " Disparador

Colisionador[i].

AddComponent ltBoxCollider2D gt();

colliders[I]. transform . parent = transform;

colliders[I] transform . f)/recuento de bordes, Top–0.5f, 0);

colisionadores[I]. escala local = nuevo vector 3(Ancho/recuento de bordes, 1, 1);

colisionador [i]. GetComponent ltBoxCollider2D gt().isTrigger = true

Collider[i]. AddComponent ltWaterDetectorgt();

En este punto creamos los colisionadores cuadrados, les dimos un nombre para que se vieran más ordenados en la escena y volvimos a crear el cuerpo de agua. Cada hijo del administrador. Establecemos sus posiciones en los puntos de los dos nodos, establecemos sus tamaños y les agregamos la clase WaterDetector.

Ahora que tenemos nuestra malla, necesitamos una función para actualizarla a medida que fluye el agua:

void UpdateMeshes()

{

for(int I = 0; i lt grid. length; i)

{

Vector3[] vértice = nuevo vector 3[4];

Vértice[0] = nuevo vector 3(posición x[i], posición y[i],z);

Vértice[1] = nuevo vector 3(posiciónx[i 1], posición y [ i 1], z);

Vértice [2] = nuevo vector 3 (posición x [i], abajo, z

Vértice [3] = Nuevo vector 3( posición x[i 1], abajo, z);

Cuadrícula[i]. vertex = vertex;

}

}

Es posible que hayas notado que esta función solo usa el código que escribimos antes. La única diferencia es que esta vez no necesitamos configurar los UV del triángulo ya que siguen siendo los mismos.

Nuestra siguiente tarea es hacer que el agua fluya por sí sola. Usaremos FixUpdate() para ajustarlos incrementalmente.

actualización fija nula()

{

Ejecutar mecanismo físico

Primero, combinamos la ley de Hooke y el método de Euler para encontrar nuevos coordenadas, aceleraciones y velocidades.

La ley de Hooke es F=kx, donde F se refiere a la fuerza generada por el flujo de agua (recuerde, modelamos la superficie del agua como un flujo de agua), k se refiere a la constante del flujo de agua y x es el desplazamiento. Nuestro desplazamiento será la coordenada y de cada nodo menos la altura de la base del nodo.

A continuación, debilitaremos la fuerza añadiendo un factor de amortiguación que sea proporcional a la velocidad de la fuerza.

for(int I = 0; iltx positioning. length; i)

{

Flotabilidad = constante del resorte * (posición [I] – altura de la base) Velocidad[I]*Amortiguación;

Aceleración[I]=-fuerza;

yposiciones[i] = Velocidad[I];

Velocidad[i ] = aceleración[I];

Cuerpo. SetPosition(i, new vector 3(xpositions[i], ypositions[i], z));

}

El método de Euler es muy simple, solo necesitamos agregar Aceleración , agrega velocidad a las coordenadas de cada cuadro.

Nota: Simplemente asumí que la masa de cada nodo es 1, pero es posible que quieras usar:

Aceleración[I]=-fuerza/masa;

Ahora crearemos la propagación de ondas. Los siguientes nodos están adaptados del tutorial de Michael Hoffman:

float[] leftDeltas = new float[xpositions. length];

float[] rightDeltas = new float[xpositions. length];

Aquí crearemos dos matrices. Para cada nodo, verificaremos la altura del nodo anterior y la altura del nodo actual y pondremos la diferencia entre ellas en leftDeltas.

Después, comprobamos la altura del nodo siguiente y la altura del nodo actual y ponemos la diferencia entre ellos en rightDeltas (la multiplicamos por una constante de propagación para aumentar todos los valores).

for(int j = 0; j lt8; j )

{

for(int I = 0; i ltx posicionamiento. longitud; i)

{

if (i gt0)

{

deltas izquierdos[I]= spread *(y posiciones[I]–y posiciones[I-1]);

Velocidad[I-1] = deltas izquierdos[I];

}

if (i ltx posicionamiento. longitud –1)

{

deltas derechos[I]= spread *(y posiciones[I]–y posiciones[I 1]);

Velocidad[ I 1] = deltas derechos[I];

}

}

}

Después de recopilar todos los datos de altura, Finalmente puedo ponerlo en uso. No podemos ver el lado derecho del nodo más a la derecha, ni el lado izquierdo del nodo más a la izquierda, por lo que la condición base es I > 0 y la posición I. Longitud: 1.

Por lo tanto, tenga en cuenta que tenemos un fragmento de código completo en un bucle y lo ejecutamos 8 veces. Esto se debe a que queremos ejecutar este proceso en pequeños períodos de tiempo, en lugar de grandes operaciones que erosionarían la liquidez.

Añadir spray

Ahora que tenemos una masa de agua que fluye, ¡el siguiente paso es hacerla salpicar!

Para hacer esto, necesitamos agregar una función llamada Splash(), que verificará la coordenada x del spray y la velocidad de cualquier objeto que golpee. Hazlo público para que podamos llamarlo más tarde en el colisionador.

Salpicadura de espacio público (xpos flotantes, velocidad de flotación)

{

En primer lugar, debemos asegurarnos de que las coordenadas específicas estén dentro del rango de nuestra agua. cuerpo:

if(xpos gt; = x posiciones[0] & ampxpos lt= xposiciones[xpositions.length-1])

{

Entonces ajustaremos xpos para que en posición relativa al inicio del cuerpo de agua:

xpos-= x posiciones[0];

A continuación, encontraremos los nodos que toca. Podemos calcularlo así:

int index = Mathf. punto redondo((x posiciones.longitud-1)*(xpos/(x posiciones[x posiciones.longitud-1]–x posiciones[0])));

Así es como funciona:

1. Seleccionamos la posición de salpicadura (xpos) respecto al borde izquierdo del cuerpo de agua.

2. Dividiremos la posición derecha respecto al borde izquierdo del cuerpo de agua.

Esto nos permite saber dónde está la salpicadura. Por ejemplo, un rocío ubicado a tres cuartos del camino a través de una masa de agua tendría un valor de 0,75.

Multiplicamos este número por el número de aristas para obtener el nodo más cercano a nuestro chapoteo.

Velocidad [index] = velocidad;

Ahora necesitamos establecer la velocidad del objeto que golpea la superficie del agua para que sea consistente con la velocidad del nodo, de modo que el El nodo será arrastrado profundamente por el objeto.

Sistema de partículas (de Game Development)

Nota: puedes cambiar este segmento de línea según tus necesidades. Por ejemplo, podrías sumar su velocidad a la velocidad actual o usar el impulso en lugar de la velocidad y dividirlo por la masa del nodo.

Ahora queremos crear un sistema de partículas que pueda producir salpicaduras de agua. Lo definimos antes y lo llamamos "splash". Asegúrese de no confundir esto con Splash().

En primer lugar, debemos configurar los parámetros del spray para ajustar la velocidad del objeto:

Float Lifetime = 0.93f Mathf. Abs (velocidad) * 0.07f

Salpicadura. GetComponent ltParticleSystem gt().startSpeed ​​​​= 8 2*Mathf. Pow(Mathf.Abs(velocidad),0.5f);

Splash. GetComponent ltParticleSystem gt().startSpeed ​​​​= 9 2 * Mathf. Pow(Mathf.Abs(velocidad),0.5f);

Splash. GetComponent ltParticleSystem gt().startLifetime = life

Aquí debemos seleccionar las partículas, establecer su vida útil para evitar que desaparezcan rápidamente al golpear el agua y configurarlas de acuerdo con el ángulo correcto de su velocidad. (pequeña salpicadura de agua más una constante).

Es posible que estés mirando el código y pensando: "¿Por qué configurar startSpeed ​​dos veces?". El problema es que estamos usando un sistema de partículas (Shuriken) cuya velocidad inicial se establece en "un número aleatorio entre dos constantes". Desafortunadamente, no tenemos muchas formas de acceder a Shuriken a través de un script, por lo que para obtener este comportamiento debemos establecer este valor dos veces.