Cómo usar Unity para crear efectos dinámicos de agua en 2D
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: p>
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.