In questo tutorial, simuleremo un corpo d'acqua 2D dinamico usando la fisica semplice. Utilizzeremo una combinazione di un renderizzatore di linee, di renderizzatori di mesh, di trigger e di particelle per creare il nostro effetto. Il risultato finale è completo di onde e spruzzi, pronto per essere aggiunto al tuo prossimo gioco. È inclusa una sorgente demo Unity (Unity3D), ma dovresti essere in grado di implementare qualcosa di simile usando gli stessi principi in qualsiasi motore di gioco.
Post correlatiEcco cosa finiremo con. Avrai bisogno del plugin del browser Unity per provarlo.
Fai clic per creare un nuovo oggetto da far cadere nell'acqua.Nel suo tutorial, Michael Hoffman ha dimostrato come possiamo modellare la superficie dell'acqua con una fila di molle.
Stiamo andando a rendere la parte superiore della nostra acqua usando uno dei renderizzatori di linea di Unity, e usiamo così tanti nodi che appare come un'onda continua.
Dovremo tenere traccia delle posizioni, velocità e accelerazioni di ogni nodo, però. Per fare ciò, useremo gli array. Quindi in cima alla nostra classe aggiungeremo queste variabili:
float [] xpositions; float [] ypositions; velocità float []; float [] accelerazioni; LineRenderer Body;
Il LineRenderer
memorizzerà tutti i nostri nodi e delineerà il nostro corpo d'acqua. Tuttavia, abbiamo ancora bisogno dell'acqua; lo creeremo con maglie
. Avremo bisogno di oggetti per contenere anche queste maglie.
GameObject [] meshobjects; Mesh [] meshes;
Avremo anche bisogno di collisori affinché le cose possano interagire con la nostra acqua:
Collisori GameObject [];
E memorizzeremo anche tutte le nostre costanti:
const float springconstant = 0.02f; const float damping = 0.04f; const float spread = 0.05f; const float z = -1f;
Queste costanti sono dello stesso tipo di quelle di Michael, ad eccezione di z
-questo è il nostro z-offset per la nostra acqua. Noi useremo -1
per questo in modo che venga visualizzato di fronte ai nostri oggetti. (Potresti voler cambiare questo a seconda di ciò che vuoi apparire davanti e dietro di esso, dovrai usare la coordinata z per determinare dove gli sprite siedono relativamente ad esso.)
Successivamente, manterremo alcuni valori:
galleggiare baseheight; fluttuare a sinistra; fluttuare il fondo;
Queste sono solo le dimensioni dell'acqua.
Avremo bisogno di alcune variabili pubbliche che possiamo impostare nell'editor anche. Innanzitutto, il sistema di particelle che useremo per i nostri splash:
splash pubblico di GameObject:
Successivamente, il materiale che utilizzeremo per il nostro renderizzatore di riga (nel caso in cui si desideri riutilizzare lo script per acido, lava, prodotti chimici o qualsiasi altra cosa):
materiale pubblico mat:
Inoltre, il tipo di mesh che utilizzeremo per il corpo idrico principale:
watermesh pubblico di GameObject:
Questi saranno tutti basati su prefabbricati, che sono tutti inclusi nei file sorgente.
Vogliamo un oggetto di gioco in grado di contenere tutti questi dati, agire come un gestore e generare il nostro corpo d'acqua in base alle specifiche. Per farlo, scriveremo una funzione chiamata SpawnWater ()
.
Questa funzione prenderà input sul lato sinistro, la larghezza, la parte superiore e la parte inferiore del corpo idrico.
public void SpawnWater (float Left, float Width, float Top, float Bottom)
(Anche se questo sembra incoerente, agisce nell'interesse del design di livello rapido quando si costruisce da sinistra a destra).
Ora scopriremo quanti nodi abbiamo bisogno:
int edgecount = Mathf.RoundToInt (Larghezza) * 5; int nodecount = edgecount + 1;
Useremo cinque per unità di larghezza, per darci un movimento fluido che non è troppo impegnativo. (Puoi variare questo per bilanciare l'efficienza con la levigatezza.) Questo ci dà tutte le nostre linee, quindi abbiamo bisogno del + 1
per il nodo extra alla fine.
La prima cosa che faremo sarà rendere il nostro corpo d'acqua con il LineRenderer
componente:
Corpo = gameObject.AddComponent(); Body.material = mat; Body.material.renderQueue = 1000; Body.SetVertexCount (nodecount); Body.SetWidth (0.1f, 0.1f);
Quello che abbiamo fatto anche qui è selezionare il nostro materiale e impostarlo per il rendering sopra l'acqua, scegliendo la sua posizione nella coda di rendering. Abbiamo impostato il numero corretto di nodi e impostato la larghezza della linea su 0.1
.
Puoi variare a seconda di quanto vuoi la linea. Potresti averlo notato SetWidth ()
prende due parametri; questi sono la larghezza all'inizio e alla fine della linea. Vogliamo quella larghezza per essere costante.
Ora che abbiamo creato i nostri nodi, inizializzeremo tutte le nostre variabili principali:
xpositions = new float [nodecount]; ypositions = new float [nodecount]; velocities = new float [nodecount]; accelerations = new float [nodecount]; meshobjects = new GameObject [edgecount]; meshes = new Mesh [edgecount]; collider = new GameObject [edgecount]; baseheight = Top; fondo = fondo; sinistra = sinistra;
Quindi ora abbiamo tutti i nostri array e stiamo mantenendo i nostri dati.
Ora per impostare effettivamente i valori dei nostri array. Inizieremo con i nodi:
per (int i = 0; i < nodecount; i++) ypositions[i] = Top; xpositions[i] = Left + Width * i / edgecount; accelerations[i] = 0; velocities[i] = 0; Body.SetPosition(i, new Vector3(xpositions[i], ypositions[i], z));
Qui, impostiamo tutte le posizioni y nella parte superiore dell'acqua, quindi aggiungiamo in modo incrementale tutti i nodi uno accanto all'altro. Inizialmente le nostre velocità e accelerazioni sono pari a zero, poiché l'acqua è ferma.
Finiamo il ciclo impostando ogni nodo nel nostro LineRenderer
(Corpo
) alla loro posizione corretta.
Ecco dove diventa complicato.
Abbiamo la nostra linea, ma non abbiamo l'acqua stessa. E il modo in cui possiamo farlo è usare Meshes. Inizieremo creando questi:
per (int i = 0; i < edgecount; i++) meshes[i] = new Mesh();
Ora, Mesh memorizza un gruppo di variabili. La prima variabile è piuttosto semplice: contiene tutti i vertici (o angoli).
Il diagramma mostra come vogliamo che siano i nostri segmenti di mesh. Per il primo segmento, i vertici sono evidenziati. Ne vogliamo quattro in totale.
Vector3 [] Vertices = new Vector3 [4]; Vertices [0] = new Vector3 (xpositions [i], ypositions [i], z); Vertices [1] = new Vector3 (xpositions [i + 1], ypositions [i + 1], z); Vertices [2] = new Vector3 (xpositions [i], bottom, z); Vertices [3] = new Vector3 (xpositions [i + 1], bottom, z);
Ora, come puoi vedere qui, vertice 0
è in alto a sinistra, 1
è in alto a destra, 2
è in basso a sinistra, e 3
è in alto a destra. Dovremo ricordarlo per dopo.
La seconda proprietà che soddisfa le esigenze è UV. Le maglie hanno trame e gli UV scelgono quale parte delle trame vogliamo afferrare. In questo caso, vogliamo solo gli angoli in alto a sinistra, in alto a destra, in basso a sinistra e in basso a destra della nostra trama.
Vector2 [] UV = nuovo Vector2 [4]; UV [0] = new Vector2 (0, 1); UV [1] = nuovo Vector2 (1, 1); UV [2] = new Vector2 (0, 0); UV [3] = new Vector2 (1, 0);
Ora abbiamo bisogno di quei numeri di prima. Le mesh sono composte da triangoli e sappiamo che ogni quadrilatero può essere composto da due triangoli, quindi ora dobbiamo dire alla mesh come dovrebbe disegnare quei triangoli.
Guarda gli angoli con l'ordine del nodo etichettato. Triangolo UN collega i nodi 0
, 1
e 3
; Triangolo B collega i nodi 3
, 2
e 0
. Pertanto, vogliamo creare un array che contenga sei interi, riflettendo esattamente questo:
int [] tris = new int [6] 0, 1, 3, 3, 2, 0;
Questo crea il nostro quadrilatero. Ora impostiamo i valori della mesh.
meshes [i] .vertices = Vertices; meshes [i] .uv = UVs; meshes [i] .triangles = tris;
Ora abbiamo le nostre mesh, ma non abbiamo oggetti di gioco per renderli nella scena. Quindi li creeremo dal nostro watermesh
prefabbricato che contiene un renderizzatore di mesh e un filtro a maglie.
meshobjects [i] = Instantiate (watermesh, Vector3.zero, Quaternion.identity) come GameObject; meshobjects [i] .GetComponent() .mesh = meshes [i]; meshobjects [i] .transform.parent = transform;
Abbiamo impostato la rete e l'abbiamo impostata come figlia del gestore dell'acqua, per sistemare le cose.
Ora vogliamo anche il nostro collisore:
collider [i] = new GameObject (); collider [i] .name = "Trigger"; collider [i] .AddComponent(); collider [i] .transform.parent = transform; collider [i] .transform.position = new Vector3 (Left + Width * (i + 0.5f) / edgecount, Top - 0.5f, 0); collider [i] .transform.localScale = new Vector3 (Width / edgecount, 1, 1); collider [i] .GetComponent () .isTrigger = true; collider [i] .AddComponent ();
Qui, stiamo creando box collider, dando loro un nome in modo che siano un po 'più ordinati nella scena, e rendendoli di nuovo figli del gestore dell'acqua. Impostiamo la loro posizione a metà strada tra i nodi, impostiamo le loro dimensioni e aggiungiamo a WaterDetector
classe per loro.
Ora che abbiamo la nostra mesh, abbiamo bisogno di una funzione per aggiornarla mentre l'acqua si muove:
void UpdateMeshes () for (int i = 0; i < meshes.Length; i++) Vector3[] Vertices = new Vector3[4]; Vertices[0] = new Vector3(xpositions[i], ypositions[i], z); Vertices[1] = new Vector3(xpositions[i+1], ypositions[i+1], z); Vertices[2] = new Vector3(xpositions[i], bottom, z); Vertices[3] = new Vector3(xpositions[i+1], bottom, z); meshes[i].vertices = Vertices;
Potresti notare che questa funzione utilizza solo il codice che abbiamo scritto prima. L'unica differenza è che questa volta non dobbiamo impostare tris e UV, perché rimangono gli stessi.
Il nostro prossimo compito è di far funzionare l'acqua stessa. Useremo FixedUpdate ()
per modificarli tutti in modo incrementale.
void FixedUpdate ()
In primo luogo, combineremo la legge di Hooke con il metodo di Eulero per trovare le nuove posizioni, accelerazioni e velocità.
Quindi, la legge di Hooke è \ (F = kx \), dove \ (F \) è la forza prodotta da una molla (ricorda, stiamo modellando la superficie dell'acqua come una fila di molle), \ (k \) è la costante di primavera e \ (x \) è lo spostamento. Il nostro spostamento sarà semplicemente la posizione y di ogni nodo meno l'altezza di base dei nodi.
Successivamente, aggiungiamo a fattore di smorzamento proporzionale alla velocità della forza per smorzare la forza.
per (int i = 0; i < xpositions.Length ; i++) float force = springconstant * (ypositions[i] - baseheight) + velocities[i]*damping ; accelerations[i] = -force; ypositions[i] += velocities[i]; velocities[i] += accelerations[i]; Body.SetPosition(i, new Vector3(xpositions[i], ypositions[i], z));
Il metodo di Eulero è semplice; aggiungiamo solo l'accelerazione alla velocità e la velocità alla posizione, ogni fotogramma.
Nota: ho appena assunto che la massa di ciascun nodo fosse 1
qui, ma vorrete usare:
accelerazioni [i] = -force / massa;
se vuoi una massa diversa per i tuoi nodi.
Mancia: Per una fisica precisa, utilizzeremmo l'integrazione di Verlet, ma poiché stiamo aggiungendo lo smorzamento, possiamo usare solo il metodo Eulero, che è molto più veloce da calcolare. Generalmente, tuttavia, il metodo Eulero introdurrà in modo esponenziale l'energia cinetica dal nulla nel tuo sistema fisico, quindi non usarla per qualcosa di preciso.
Ora stiamo andando a creare propagazione delle onde. Il seguente codice è adattato dal tutorial di Michael Hoffman.
float [] leftDeltas = new float [xpositions.Length]; float [] rightDeltas = new float [xpositions.Length];
Qui creiamo due array. Per ogni nodo, controlleremo l'altezza del nodo precedente rispetto all'altezza del nodo corrente e inseriremo la differenza leftDeltas
.
Quindi, controlleremo l'altezza del nodo successivo rispetto all'altezza del nodo che stiamo controllando e inseriremo tale differenza rightDeltas
. (Inoltre moltiplicheremo tutti i valori per una costante di spread).
per (int j = 0; j < 8; j++) for (int i = 0; i < xpositions.Length; i++) if (i > 0) leftDeltas [i] = spread * (ypositions [i] - ypositions [i-1]); velocities [i - 1] + = leftDeltas [i]; se io < xpositions.Length - 1) rightDeltas[i] = spread * (ypositions[i] - ypositions[i + 1]); velocities[i + 1] += rightDeltas[i];
Possiamo modificare le velocità in base alla differenza di altezza immediatamente, ma a questo punto dovremmo memorizzare solo le differenze nelle posizioni. Se cambiassimo la posizione del primo nodo direttamente dal pipistrello, per il momento in cui guardavamo il secondo nodo, il primo nodo si sarebbe già spostato, quindi questo rovinerebbe tutti i nostri calcoli.
per (int i = 0; i < xpositions.Length; i++) if (i > 0) ypositions [i-1] + = leftDeltas [i]; se io < xpositions.Length - 1) ypositions[i + 1] += rightDeltas[i];
Quindi, una volta raccolti tutti i dati relativi all'altezza, possiamo applicarli alla fine. Non possiamo guardare a destra del nodo all'estrema destra o alla sinistra del nodo all'estrema sinistra, quindi le condizioni io> 0
e io < xpositions.Length - 1
.
Inoltre, tieni presente che abbiamo contenuto l'intero codice in un ciclo e lo abbiamo eseguito otto volte. Questo perché vogliamo eseguire questo processo a piccole dosi più volte, piuttosto che un grande calcolo, che sarebbe molto meno fluido.
Ora abbiamo l'acqua che scorre e mostra. Quindi, dobbiamo essere in grado di disturbare l'acqua!
Per questo, aggiungiamo una funzione chiamata Splash ()
, che controllerà la posizione x dello splash e la velocità di qualunque cosa lo stia colpendo. Dovrebbe essere pubblico in modo che possiamo chiamarlo dai nostri collisori più tardi.
public void Splash (float xpos, float velocity)
Innanzitutto, dobbiamo assicurarci che la posizione specificata sia effettivamente entro i limiti della nostra acqua:
if (xpos> = xpositions [0] && xpos <= xpositions[xpositions.Length-1])
E poi cambieremo xpos
quindi ci dà la posizione relativa all'inizio del corpo idrico:
xpos - = xpositions [0];
Successivamente, scopriremo quale nodo sta toccando. Possiamo calcolarlo in questo modo:
int index = Mathf.RoundToInt ((xpositions.Length-1) * (xpos / (xpositions [xpositions.Length-1] - xpositions [0])));
Quindi, ecco cosa sta succedendo qui:
xpos
).0.75
.velocità [indice] = velocità;
Ora impostiamo la velocità dell'oggetto che colpisce la nostra acqua alla velocità di quel nodo, in modo che venga trascinata verso il basso dall'oggetto.
Nota: Puoi cambiare questa linea in qualsiasi cosa ti si addica. Ad esempio, potresti aggiungere la velocità alla sua velocità corrente, oppure potresti usare la quantità di moto invece della velocità e dividerla per la massa del tuo nodo.
Ora vogliamo creare un sistema particellare che produca lo splash. L'abbiamo definito prima; si chiama "splash" (abbastanza creativo). Assicurati di non confonderlo con Splash ()
. Quello che userò è incluso nei file sorgente.
Per prima cosa, vogliamo impostare i parametri dello splash per cambiare con la velocità dell'oggetto.
float lifetime = 0.93f + Mathf.Abs (velocity) * 0.07f; splash.GetComponent() .startSpeed = 8 + 2 * Mathf.Pow (Mathf.Abs (velocity), 0.5f); splash.GetComponent () .startSpeed = 9 + 2 * Mathf.Pow (Mathf.Abs (velocity), 0.5f); splash.GetComponent () .startLifetime = lifetime;
Qui, abbiamo preso le nostre particelle, impostiamo la loro vita in modo che non moriranno subito dopo che colpiscono la superficie dell'acqua, e impostiamo la loro velocità in base al quadrato della loro velocità (più una costante, per piccoli spruzzi).
Potresti guardare quel codice e pensare: "Perché ha impostato il startSpeed
due volte? ", e avresti ragione a chiedertelo: il problema è che stiamo usando un sistema particellare (Shuriken, fornito con il progetto) che ha la sua velocità di avvio impostata su" casuale tra due costanti ". non ho molto accesso su Shuriken tramite script, quindi per far funzionare quel comportamento dobbiamo impostare il valore due volte.
Ora aggiungerò una riga che potresti o meno voler omettere dal tuo script:
Vector3 position = new Vector3 (xpositions [index], ypositions [index] -0.35f, 5); Rotazione del quaternione = Quaternion.LookRotation (nuovo Vector3 (xpositions [Mathf.FloorToInt (xpositions.Length / 2)], baseheight + 8, 5) - position);
Le particelle di Shuriken non verranno distrutte quando colpiscono i tuoi oggetti, quindi se vuoi assicurarti che non atterreranno davanti ai tuoi oggetti, puoi prendere due misure:
5
).La seconda riga di codice prende il punto medio delle posizioni, si sposta un po 'verso l'alto e punta l'emettitore di particelle verso di esso. Ho incluso questo comportamento nella demo. Se stai usando una massa d'acqua molto ampia, probabilmente non vuoi questo comportamento. Se la tua acqua è in una piccola piscina all'interno di una stanza, potresti volerlo usare. Quindi, sentiti libero di cancellare quella linea sulla rotazione.
GameObject splish = Instantiate (splash, position, rotation) come GameObject; Distruggi (splish, lifetime + 0.3f);
Ora facciamo il nostro splash e diciamo che morirà un po 'dopo la morte delle particelle. Perché un po 'dopo? Perché il nostro sistema di particelle emette alcune sequenze di particelle, quindi anche se il primo lotto dura solo fino a Time.time + lifetime
, le nostre ultime raffiche saranno ancora un po 'dopo.
Sì! Finalmente abbiamo finito, giusto?
Sbagliato! Abbiamo bisogno di rilevare i nostri oggetti, o questo era tutto per niente!
Ricorda che abbiamo aggiunto lo script a tutti i nostri collisori prima? Quello chiamato WaterDetector
?
Bene, lo faremo ora! Vogliamo solo una funzione al suo interno:
void OnTriggerEnter2D (Collider2D Hit)
utilizzando OnTriggerEnter2D ()
, possiamo specificare cosa succede ogni volta che un corpo rigido 2D entra nel nostro corpo idrico. Se passiamo un parametro di Collider2D
possiamo trovare più informazioni su quell'oggetto.
if (Hit.rigidbody2D! = null)
Vogliamo solo oggetti che contengono a rigidbody2D
.
transform.parent.GetComponent() .Splash (transform.position.x, Hit.rigidbody2D.velocity.y * Hit.rigidbody2D.mass / 40f);
Ora, tutti i nostri collisori sono figli del gestore dell'acqua. Quindi prendiamo semplicemente il acqua
componente dal loro genitore e chiamata Splash ()
, dalla posizione del collisore.
Ricordo ancora, ho detto che potevi o passare velocità o quantità di moto, se volevi che fosse più preciso dal punto di vista fisico? Bene, qui è dove devi passare quello giusto. Se moltiplichi la velocità y dell'oggetto per la sua massa, avrai il suo momento. Se vuoi solo usare la sua velocità, sbarazzati della massa da quella linea.
Alla fine, vorrai chiamare SpawnWater ()
da qualche parte. Facciamolo al momento del lancio:
void Start () SpawnWater (-10,20,0, -10);
E ora abbiamo finito! Ora qualsiasi rigidbody2D
con un collisore che colpisce l'acqua creerà uno splash e le onde si muoveranno correttamente.
Come bonus extra, ho aggiunto alcune righe di codice all'inizio SpawnWater ()
.
gameObject.AddComponent(); gameObject.GetComponent () .center = new Vector2 (Left + Width / 2, (Top + Bottom) / 2); gameObject.GetComponent () .size = new Vector2 (Width, Top - Bottom); gameObject.GetComponent () .isTrigger = true;
Queste linee di codice aggiungeranno un collettore di riquadri all'acqua stessa. Puoi usarlo per far galleggiare le cose nella tua acqua, usando ciò che hai imparato.
Avrai bisogno di fare una funzione chiamata OnTriggerStay2D ()
che prende un parametro di Collider2D Hit
. Quindi, puoi usare una versione modificata della formula della molla che abbiamo usato prima per verificare la massa dell'oggetto, e aggiungere una forza o una velocità al tuo rigidbody2D
per farlo galleggiare nell'acqua.
In questo tutorial, abbiamo implementato una semplice simulazione dell'acqua per l'utilizzo in giochi 2D con codice di fisica semplice e un renderer di linea, renderer mesh, trigger e particelle. Forse aggiungerai corpi ondulati di acqua fluida come ostacolo al tuo prossimo platform, pronto per i tuoi personaggi a tuffarsi o attraversare con cura con pietre miliari galleggianti, o forse potresti usarlo in un gioco di vela o windsurf, o anche in un gioco in cui semplicemente saltare le rocce attraverso l'acqua da una spiaggia assolata. In bocca al lupo!