Opprette dynamiske 2D-vanneffekter i enhet

I denne opplæringen skal vi simulere en dynamisk 2D vannkilde ved hjelp av enkel fysikk. Vi vil bruke en blanding av en linjeleverandør, maskereverdere, triggere og partikler for å skape vår effekt. Det endelige resultatet kommer med bølger og sprut, klar til å legge til i ditt neste spill. En enhetskode (Unity3D) er inkludert, men du bør kunne implementere noe lignende ved å bruke de samme prinsippene i hvilken som helst spillmotor.

Relaterte innlegg
  • Lag en splash med dynamiske 2D vann effekter
  • Slik oppretter du en tilpasset 2D-fysikkmotor: Grunnleggende og impulsoppløsning
  • Tilføye turbulens til et partikkelsystem

Sluttresultat

Her skal vi ende opp med. Du trenger Unity-nettleser-pluginet for å prøve det.

Klikk for å lage et nytt objekt å slippe inn i vannet.

Sette opp vår vannforvalter

I sin veiledning demonstrerte Michael Hoffman hvordan vi kan modellere overflaten av vann med en rekke fjærer.

Vi skal gjøre toppen av vannet vårt ved hjelp av en av enhetens linjeleverandører, og bruk så mange noder at det ser ut som en kontinuerlig bølge.


Vi må imidlertid holde øye med posisjonene, hastighetene og akselerasjonene til hver knute, skjønt. For å gjøre det, skal vi bruke arrays. Så i toppen av vår klasse legger vi til disse variablene:

float [] xpositions; float [] ypositions; float [] hastigheter; float [] akselerasjoner; LineRenderer Body;

De LineRenderer vil lagre alle våre noder og skissere vår vannkilde. Vi trenger fortsatt vannet selv, skjønt; Vi lager dette med Meshes. Vi kommer til å trenge objekter for å holde disse masker også.

GameObject [] meshobjects; Mesh [] meshes;

Vi skal også trenge colliders slik at ting kan samhandle med vannet vårt:

GameObject [] colliders;

Og vi lagrer alle våre konstanter også:

 const float springconstant = 0,02f; const float demping = 0,04f; const float spread = 0,05f; const float z = -1f;

Disse konstantene er de samme som Michael diskuterte, med unntak av z-Dette er vår z-offset for vannet vårt. Vi skal bruke -1 for dette slik at det blir vist foran objektene våre. (Det kan hende du vil endre dette, avhengig av hva du vil se foran og bak av det, du må bruke z-koordinaten for å finne ut hvor sprites sitter i forhold til det.)

Deretter skal vi holde fast på noen verdier:

 float baseheight; flyte til venstre; float bunn;

Disse er bare dimensjonene av vannet.

Vi kommer til å trenge noen offentlige variabler vi kan sette i redaktøren også. For det første skal partikkelsystemet vi bruker for våre sprut:

offentlig GameObject splash:

Deretter vil materialet vi bruker til vår linjeleverandør (i tilfelle du vil bruke skriptet på nytt for syre, lava, kjemikalier eller noe annet):

offentlig Materialematte:

I tillegg, den typen maske vi skal bruke til hoveddelen av vann:

offentlig GameObject watermesh:

Alt kommer til å være basert på prefabs, som alle er inkludert i kildefilene.

Vi vil ha et spillobjekt som kan holde alle disse dataene, fungere som en leder, og gyte vår vannkilde til spesifikasjon. For å gjøre det, skriver vi en funksjon som heter SpawnWater ().

Denne funksjonen tar innspill på venstre side, bredden, toppen og bunnen av vannkroppen.

offentlig tomrom SpawnWater (float Venstre, float Bredde, float Top, float Bottom) 

(Selv om dette virker inkonsekvent, virker det i interesse for hurtignivådesign når du bygger fra venstre til høyre).


Opprette noder

Nå skal vi finne ut hvor mange noder vi trenger:

int edgecount = Mathf.RoundToInt (bredde) * 5; int nodecount = edgecount + 1;

Vi skal bruke fem per bredde for å gi oss glatt bevegelse som ikke er for krevende. (Du kan variere dette for å balansere effektiviteten mot glatthet.) Dette gir oss alle våre linjer, da trenger vi + 1 for den ekstra noden på slutten.

Det første vi skal gjøre er å gjøre vår kropp av vann med LineRenderer komponent:

 Body = gameObject.AddComponent(); Body.material = mat; Body.material.renderQueue = 1000; Body.SetVertexCount (nodecount); Body.SetWidth (0.1f, 0.1f);

Hva vi har gjort her, er å velge vårt materiale, og sett det til å gjengjøre over vannet ved å velge sin posisjon i gjengekøen. Vi har angitt riktig antall noder, og sett bredden på linjen til 0.1.

Du kan variere dette avhengig av hvor tykt du vil ha din linje. Du har kanskje lagt merke til det SetWidth () tar to parametere; Dette er bredden ved starten og slutten av linjen. Vi ønsker at bredden skal være konstant.

Nå som vi har gjort våre noder, vil vi initialisere alle våre toppvariabler:

 xpositions = ny float [nodecount]; ypositions = ny float [nodecount]; hastigheter = ny flyt [nodecount]; akselerasjoner = ny flyt [nodecount]; meshobjects = ny GameObject [edgecount]; masker = nytt Mesh [edgecount]; colliders = new GameObject [edgecount]; basehøyde = topp; bunn = bunn; venstre = venstre;

Så nå har vi alle våre arrays, og vi holder fast på våre data.

Nå å faktisk sette verdiene av våre arrays. Vi starter med noder:

 for (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)); 

Her setter vi alle y-stillingene til toppen av vannet, og legger deretter trinnvis alle knutepunktene side ved side. Våre hastigheter og akselerasjoner er null først, da vannet fortsatt er.

Vi avslutter sløyfen ved å sette hver knute i vår LineRenderer (Kropp) til riktig posisjon.


Opprette masker

Her er hvor det blir vanskelig.

Vi har vår linje, men vi har ikke selve vannet. Og måten vi kan gjøre dette, er å bruke Meshes. Vi starter med å lage disse:

for (int i = 0; i < edgecount; i++)  meshes[i] = new Mesh();

Nå lagrer Meshes en rekke variabler. Den første variabelen er ganske enkel: den inneholder alle hjørner (eller hjørner).


Diagrammet viser hva vi vil ha våre maskinsegmenter til å se ut. For det første segmentet er toppunktene uthevet. Vi ønsker fire totalt.

 Vector3 [] Vertices = ny Vector3 [4]; Vertikaler [0] = ny Vector3 (xpositions [i], ypositions [i], z); Vertikaler [1] = ny Vector3 (xpositions [i + 1], ypositions [i + 1], z); Vertikaler [2] = ny Vector3 (xpositions [i], bunn, z); Vertikaler [3] = ny Vector3 (xpositions [i + 1], bunn, z);

Nå, som du kan se her, vertex 0 er øverst til venstre, 1 er øverst til høyre, 2 er nederst til venstre, og 3 er øverst til høyre. Vi må huske det for senere.

Den andre egenskapen som meshes trenger er UVs. Meshes har teksturer, og UVs velger hvilken del av teksturen vi ønsker å ta tak i. I dette tilfellet vil vi bare ha de øverste venstre, øverste høyre, nederste venstre og nederste høyre hjørner av tekstur.

 Vector2 [] UVs = ny Vector2 [4]; UVs [0] = ny Vector2 (0, 1); UVs [1] = ny Vector2 (1, 1); UVs [2] = ny Vector2 (0, 0); UVs [3] = ny Vector2 (1, 0);

Nå trenger vi disse tallene fra før igjen. Meshes består av trekanter, og vi vet at noen firefargede kan være laget av to trekanter, så nå må vi fortelle nettverket hvordan det skal tegne de trekantene.


Se på hjørnene med merkordre merket. Triangel EN kobler knuter 0, 1 og 3; Triangel B kobler knuter 3, 2 og 0. Derfor ønsker vi å lage en matrise som inneholder seks heltall, som reflekterer akkurat det:

int [] tris = ny int [6] 0, 1, 3, 3, 2, 0;

Dette skaper vår firkant. Nå setter vi nettverksverdiene.

 meshes [i] .vertices = Vertices; masker [i] .uv = UVs; meshes [i] .triangles = tris;

Nå har vi våre masker, men vi har ikke Game Objects å gjengi dem i scenen. Så vi skal lage dem fra vår watermesh prefab som inneholder en Mesh Renderer og Mesh Filter.

 meshobjects [i] = Instantiate (watermesh, Vector3.zero, Quaternion.identity) som GameObject; meshobjects [i] .GetComponent() .mesh = masker [i]; meshobjects [i] .transform.parent = transform;

Vi setter nettverket, og vi satte det til å være barnet til vannforvalteren, for å rydde opp ting.


Skape våre kollisjoner

Nå ønsker vi også vår collider:

 colliders [i] = new GameObject (); colliders [i] .name = "Trigger"; colliders [i] .AddComponent(); colliders [i] .transform.parent = transform; colliders [i] .transform.position = ny Vector3 (venstre + bredde * (i + 0,5f) / edgecount, topp - 0,5f, 0); colliders [i] .transform.localScale = ny Vector3 (Bredde / Edgecount, 1, 1); colliders [i] .GetComponent() .isTrigger = true; colliders [i] .AddComponent();

Her lager vi boksekollider, og gir dem et navn, slik at de er litt tidierere på scenen, og gjør dem til hver av barna til vannføreren igjen. Vi setter sin posisjon til å være halvveis mellom knutepunktene, setter størrelsen deres og legger til en WaterDetector klasse til dem.

Nå som vi har nettverket vårt, trenger vi en funksjon for å oppdatere den når vannet beveger seg:

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;  

Du kan merke at denne funksjonen bare bruker koden vi skrev før. Den eneste forskjellen er at denne gangen vi ikke trenger å sette tris og UV, fordi disse forblir de samme.

Vår neste oppgave er å få vannet til å fungere. Vi skal bruke FixedUpdate () å endre dem alle trinnvis.

void FixedUpdate () 

Gjennomføring av fysikken

For det første skal vi kombinere Hooke's Law med Euler-metoden for å finne de nye posisjonene, akselerasjonene og hastighetene.

Så, Hooke's Law er \ (F = kx \), hvor \ (F \) er kraften produsert av en fjær (husk, vi modellerer overflaten av vannet som en rekke fjærer), \ er våren konstant, og \ (x \) er forskyvningen. Vår forskyvning kommer rett og slett til å være y-posisjonen til hver node minus basen høyde av noder.

Deretter legger vi til en dempningsfaktor proporsjonal med hastigheten av kraften for å dempe kraften.

for (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)); 

Euler-metoden er enkel; Vi legger bare til akselerasjonen til hastigheten og hastigheten til stillingen, hver ramme.

Merk: Jeg antok bare at massen av hver node var 1 her, men du vil bruke:

 akselerasjoner [i] = -force / mass;

hvis du vil ha en annen masse for dine noder.

Tips: For nøyaktig fysikk, vil vi bruke Verlet-integrasjon, men fordi vi legger til demping, kan vi bare bruke Euler-metoden, som er mye raskere å beregne. Vanligvis vil Euler-metoden eksponentielt introdusere kinetisk energi fra ingensteds inn i ditt fysikksystem, så bruk det ikke for noe nøyaktig.

Nå skal vi lage bølgeutbredelse. Følgende kode er tilpasset fra Michael Hoffmans opplæring.

 float [] leftDeltas = new float [xpositions.Length]; float [] rightDeltas = new float [xpositions.Length];

Her oppretter vi to arrays. For hver node skal vi sjekke høyden på den forrige noden mot høyden på gjeldende knutepunkt og gjøre forskjellen i leftDeltas.

Deretter sjekker vi høyden på den påfølgende noden mot høyden på noden vi sjekker, og setter den forskjellen i rightDeltas. (Vi vil også multiplisere alle verdier med en spredningskonstant).

 for (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]); hastigheter [i - 1] + = leftDeltas [i];  hvis jeg < xpositions.Length - 1)  rightDeltas[i] = spread * (ypositions[i] - ypositions[i + 1]); velocities[i + 1] += rightDeltas[i];   

Vi kan endre hastighetene med utgangspunkt i høydedifferansen umiddelbart, men vi bør bare lagre forskjellene i stillinger på dette punktet. Hvis vi endret posisjonen til den første noden rett utenfor flaggermuset, da vi så på den andre noden, vil den første noden allerede ha flyttet, så det vil ødelegge alle våre beregninger.

for (int i = 0; i < xpositions.Length; i++)  if (i > 0) ypositions [i-1] + = leftDeltas [i];  hvis jeg < xpositions.Length - 1)  ypositions[i + 1] += rightDeltas[i];  

Så når vi har samlet alle våre høyde data, kan vi søke det på slutten. Vi kan ikke se til høyre for knutepunktet til høyre eller til venstre for knutepunktet til venstre, derav betingelsene jeg> 0 og Jeg < xpositions.Length - 1.

Vær også oppmerksom på at vi inneholdt denne hele koden i en løkke, og kjørte den åtte ganger. Dette skyldes at vi ønsker å kjøre denne prosessen i små doser flere ganger, i stedet for en stor beregning, som ville være mye mindre væske.


Legge til flekker

Nå har vi vann som flyter, og det viser. Deretter må vi kunne forstyrre vannet!

For dette, la oss legge til en funksjon som heter Sprut(), som vil sjekke stavens x-posisjon, og hastigheten til det som treffer det. Det burde være offentlig, slik at vi kan ringe det fra våre collider senere.

offentlig tomrom Splash (float xpos, flythastighet) 

Først må vi sørge for at den angitte posisjonen faktisk ligger innenfor grensene til vannet vårt:

 hvis (xpos> = xpositions [0] && xpos <= xpositions[xpositions.Length-1]) 

Og da endres vi XPOS så det gir oss stillingen i forhold til starten av vannkroppen:

 xpos - = xpositions [0];

Deretter skal vi finne ut hvilken knute den berører. Vi kan beregne det slik:

int index = Mathf.RoundToInt ((xpositions.Length-1) * (xpos / (xpositions [xpositions.Length-1] - xpositions [0])));

Så her er det som foregår her:

  1. Vi tar plassen i forhold til stillingen til vannets venstre kant (XPOS).
  2. Vi deler dette ved posisjonen til den høyre kanten i forhold til stillingen til venstre kant av vannet.
  3. Dette gir oss en brøkdel som forteller oss hvor springen er. For eksempel vil et skvett tre fjerdedeler av veien langs vannkilden gi en verdi av 0,75.
  4. Vi multipliserer dette ved antall kanter og runde dette nummeret, noe som gir oss knutepunktet vårt sprut var nærmest.
hastigheter [indeks] = hastighet;

Nå setter vi hastigheten på objektet som treffer vannet vårt til nodens hastighet, slik at det blir trukket ned av objektet.

Merk: Du kan endre denne linjen til det som passer deg. For eksempel kan du legge til hastigheten til sin nåværende hastighet, eller du kan bruke momentum i stedet for hastighet og dele ved nodens masse.

Nå ønsker vi å lage et partikkelsystem som vil produsere sprutet. Vi definerte det tidligere; det kalles "splash" (kreativt nok). Pass på at du ikke forveksler det med Sprut(). Den jeg skal bruke er inkludert i kildefilene.

Først vil vi sette parametrene for springen for å forandre seg med objektets hastighet.

 float levetid = 0.93f + Mathf.Abs (hastighet) * 0,07f; splash.GetComponent() .startSpeed ​​= 8 + 2 * MathfPow (Mathf.Abs (hastighet), 0,5f); splash.GetComponent() .startSpeed ​​= 9 + 2 * MathfPow (Mathf.Abs (hastighet), 0,5f); splash.GetComponent() .startLifetime = levetid;

Her har vi tatt partiklene våre, sett deres levetid slik at de ikke vil dø kort tid etter at de har truffet vannoverflaten, og sett hastigheten til å være basert på kvadratet av deres hastighet (pluss en konstant for små sprut).

Du kan se på den koden og tenke, "Hvorfor har han satt startSpeed to ganger? ", og det ville være riktig å lure på. Problemet er at vi bruker et partikkelsystem (Shuriken, forsynt med prosjektet) som har sin starthastighet satt til" tilfeldig mellom to konstanter ". Dessverre, vi har ikke mye tilgang over Shuriken med skript, for å få den atferden til å fungere må vi sette verdien to ganger.

Nå skal jeg legge til en linje som du kanskje eller kanskje ikke vil utelate fra skriptet ditt:

Vector3 posisjon = ny Vector3 (xpositions [index], ypositions [index] -0,35f, 5); Quaternion rotasjon = Quaternion.LookRotation (ny Vector3 (xpositions [Mathf.FloorToInt (xpositions.Length / 2)], baseheight + 8, 5) - posisjon);

Shuriken-partikler vil ikke bli ødelagt når de treffer gjenstandene dine, så hvis du vil sørge for at de ikke kommer til å lande foran objektene dine, kan du ta to tiltak:

  1. Fest dem i bakgrunnen. (Du kan fortelle dette ved z-posisjonen 5).
  2. Tilt partikkelsystemet for alltid å peke mot midten av vannkroppen din - slik vil partiklene ikke splashes på landet.

Den andre linjen med kode tar posisjonens midtpunkt, beveger seg oppover litt, og peker partikkelemitteren mot den. Jeg har tatt med denne oppførselen i demoen. Hvis du bruker en veldig bred vannkilde, vil du sannsynligvis ikke ha denne oppførselen. Hvis vannet ditt er i et lite basseng inne i et rom, kan du godt bruke det. Så vær så snill å skrap den linjen om rotasjon.

 GameObject splish = Instantiate (splash, posisjon, rotasjon) som GameObject; Destroy (splish, livstid + 0.3f); 

Nå lager vi vårt sprut og forteller det å dø litt etter at partiklene er på grunn av å dø. Hvorfor litt etterpå? Fordi vårt partikkelsystem sender ut noen få sekvensielle partikler, så selv om det første partiet bare varer til Time.time + levetid, våre siste utbrudd vil fortsatt være litt etter det.

Ja! Vi er endelig ferdig, rett?


Kollisjonsdeteksjon

Feil! Vi må oppdage våre objekter, eller dette var alt for ingenting!

Husker vi lagt til det skriptet til alle våre colliders før? Den ene ringte WaterDetector?

Vel, vi skal gjøre det nå! Vi vil bare ha en funksjon i den:

void OnTriggerEnter2D (Collider2D Hit) 

Ved hjelp av OnTriggerEnter2D (), Vi kan spesifisere hva som skjer når en 2D Stiv kropp kommer inn i vår vannkilde. Hvis vi sender en parameter til Collider2D Vi kan finne mer informasjon om det aktuelle objektet.

hvis (Hit.rigidbody2D! = null) 

Vi vil bare ha objekter som inneholder en rigidbody2D.

 transform.parent.GetComponent() .Splash (transform.position.x, Hit.rigidbody2D.velocity.y * Hit.rigidbody2D.mass / 40f); 

Nå er alle våre colliders barn av vannforvalteren. Så vi tar bare tak i Vann komponent fra foreldre og samtale Sprut(), fra posisjonen til collideren.

Husk igjen, jeg sa at du kunne enten passere hastighet eller momentum, hvis du ville ha det mer fysisk nøyaktig? Vel, her er hvor du må passere den rette. Hvis du multipliserer objektets y-hastighet med sin masse, har du momentum. Hvis du bare vil bruke hastigheten, bli kvitt massen fra den linjen.

Til slutt vil du ringe SpawnWater () fra et sted. La oss gjøre det ved lansering:

ugyldig start () SpawnWater (-10,20,0, -10); 

Og nå er vi ferdige! Nå noen rigidbody2D med en collider som treffer vannet vil det skape et sprut, og bølgene vil bevege seg riktig.


Bonusøvelse

Som en ekstra bonus, har jeg lagt til noen linjer med kode til toppen av SpawnWater ().

gameObject.AddComponent(); gameObject.GetComponent() .center = ny Vector2 (venstre + bredde / 2, (topp + bunn) / 2); gameObject.GetComponent() .size = ny Vector2 (bredde, topp - bunn); gameObject.GetComponent() .isTrigger = true;

Disse kodelinjene vil legge til en boksekollider til selve vannet. Du kan bruke dette til å få ting å flyte i vannet ditt, ved å bruke det du har lært.

Du vil ønske å ringe en funksjon OnTriggerStay2D () som tar en parameter av Collider2D Hit. Deretter kan du bruke en modifisert versjon av vårformelen vi brukte før det kontrollerer objektets masse, og legg til en kraft eller hastighet til din rigidbody2D for å få det til å flyte i vannet.


Lag en splash

I denne opplæringen implementerte vi en enkel vann-simulering for bruk i 2D-spill med enkel fysikkode og en linjearterer, maskeringsgjengere, triggere og partikler. Kanskje vil du legge til bølgete kropper av væskevann som et hinder for din neste plattform, klar for at karene dine kan dykke inn eller forsiktig krysse med flytende stepping stones, eller kanskje du kan bruke dette i et seiling eller windsurfing spill, eller til og med et spill hvor du bare hopper over stein over vannet fra en solrik strand. Lykke til!