În acest tutorial, vom simula un corp dinamic 2D de apă folosind fizica simplă. Vom folosi un amestec de redare de linii, redactori de rețea, declanșatoare și particule pentru a ne crea efectul. Rezultatul final vine complet cu valuri și stropi, gata pentru a adăuga la următorul joc. O sursă demo Unity (Unity3D) este inclusă, dar ar trebui să puteți implementa ceva similar folosind aceleași principii în orice motor de joc.
postări asemănatoareIată ce vom încheia. Veți avea nevoie de pluginul browserului Unity pentru al încerca.
Faceți clic pentru a crea un obiect nou pentru a cădea în apă.În tutorialul său, Michael Hoffman a demonstrat cum putem modela suprafața apei cu un șir de izvoare.
Vom face din partea superioară a apei noastre una din unitățile de redare a liniei Unity și vom folosi atât de multe noduri încât apare ca un val continuu.
Cu toate acestea, va trebui să urmărim pozițiile, vitezele și accelerațiile fiecărui nod. Pentru a face asta, vom folosi matrice. Astfel, în partea de sus a clasei noastre vom adăuga aceste variabile:
float [] xposții; flotați []; viteze de flotare []; float [] accelerații; Corpul LineRenderer;
LineRenderer
vor stoca toate nodurile noastre și vor schița corpul nostru de apă. Totuși, avem nevoie de apă însăși; vom crea acest lucru cu Plasele
. Vom avea nevoie și de obiecte pentru a ține și ele aceste ochiuri.
GameObject [] meshobjects; Ochiuri de plasă [];
De asemenea, vom avea nevoie de agenți de coliziune, astfel încât lucrurile să poată interacționa cu apa noastră:
GameObject [] colizoare;
Și vom stoca și toate constantele noastre:
const float springconstant = 0,02f; const amortizare float = 0,04f; const float spread = 0.05f; const float z = -1f;
Aceste constante sunt aceleași cu cele discutate de Michael, cu excepția lui z
-aceasta este z-compensarea noastră pentru apa noastră. Vom folosi -1
pentru ca aceasta să fie afișată în fața obiectelor noastre. (S-ar putea să doriți să schimbați acest lucru în funcție de ceea ce doriți să apară în față și în spatele acestuia, va trebui să utilizați coordonatele z pentru a determina unde se află spritele relative la acesta.)
Apoi, vom reține câteva valori:
float baseheight; plutește la stânga; fundul plutitor;
Acestea sunt doar dimensiunile apei.
Vom avea nevoie de câteva variabile publice pe care le putem seta și în editor. În primul rând, sistemul de particule pe care îl vom folosi pentru stropirile noastre:
public Splash GameObject:
Apoi, materialul pe care îl vom folosi pentru redarea liniei noastre (în cazul în care doriți să reutilizați scenariul pentru acid, lavă, substanțe chimice sau orice altceva):
materiale mat Materiale:
În plus, felul de plasă pe care o vom folosi pentru corpul principal de apă:
jocul de apa publica GameObject:
Toate acestea se vor baza pe prefabricate, toate incluse în fișierele sursă.
Vrem un obiect de joc care să poată ține toate aceste date, să acționeze ca un manager și să înmulțească corpul nostru de apă ingame în funcție de specificație. Pentru a face asta, vom scrie o funcție numită SpawnWater ()
.
Această funcție va prelua intrările din partea stângă, lățimea, partea superioară și partea de jos a corpului de apă.
public void SpawnWater (plutiți la stânga, flotați Lățime, flotați Sus, plutiți în partea de jos)
(Deși acest lucru pare inconsecvent, acționează în interesul unui design rapid la construirea de la stânga la dreapta).
Acum vom afla câte noduri avem nevoie:
int margincount = Mathf.RoundToInt (Lățime) * 5; int nodecount = margine + 1;
Vom folosi cinci pe lățime de unitate, pentru a ne oferi o mișcare ușoară, care nu este prea exigentă. (Puteți varia acest lucru pentru a echilibra eficiența împotriva netezirii.) Acest lucru ne oferă toate liniile noastre, atunci avem nevoie de + 1
pentru nodul suplimentar de la capăt.
Primul lucru pe care îl vom face este să ne punem corpul de apă cu LineRenderer
componente:
Body = gameObject.AddComponent(); Body.material = mat; Body.material.renderQueue = 1000; Body.SetVertexCount (nodecount); Body.SetWidth (0.1f, 0.1f);
Ceea ce am făcut și aici este selectarea materialului nostru și stabilim ca acesta să se facă peste apă prin alegerea poziției sale în coada de randare. Am setat numărul corect de noduri și l-am setat la lățimea liniei 0.1
.
Puteți alege acest lucru în funcție de grosimea dorită a liniei. S-ar putea să fi observat asta SetWidth ()
ia doi parametri; acestea sunt lățimea la începutul și la sfârșitul liniei. Vrem ca această lățime să fie constantă.
Acum, când am făcut nodurile noastre, vom inițializa toate variabilele noastre de vârf:
xpositions = float nou [nodecount]; ypositions = float nou [nodecount]; velocities = float nou [nodecount]; accelerații = float nou [nodecount]; meshobjects = noul GameObject [edgecount]; ochiurilor de plasă = Mesh nou [edgecount]; colliders = noul GameObject [edgecount]; baseheight = Sus; jos = partea inferioară; stânga = Stânga;
Deci acum avem toate matricele noastre și ne ținem de datele noastre.
Acum, pentru a seta valorile matriceelor noastre. Vom începe cu nodurile:
pentru (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));
Aici, setăm toate pozițiile y să fie la partea superioară a apei și apoi să adăugăm treptat toate nodurile unul lângă altul. Vitezele și accelerațiile noastre sunt inițial zero, deoarece apa este în continuare.
Terminăm buclele prin setarea fiecărui nod în cadrul nostru LineRenderer
(Corp
) în poziția corectă.
Iată în cazul în care devine complicat.
Avem linia noastră, dar nu avem apa însăși. Iar modul în care putem face acest lucru este folosirea Mesei. Vom începe prin crearea acestora:
pentru (int i = 0; i < edgecount; i++) meshes[i] = new Mesh();
Acum, mesh-urile stochează o grămadă de variabile. Prima variabilă este destul de simplă: conține toate vârfurile (sau colțurile).
Diagrama arată ce ne dorim ca segmentele noastre de ochiuri să arate. Pentru primul segment, vârfurile sunt evidențiate. Vrem patru în total.
Vector3 [] Vertex = vector3 nou [4]; Vertices [0] = Vector3 nou (xpositions [i], ypositions [i], z); Vertex [1] = Vector3 nou (xposțiuni [i + 1], ipoziții [i + 1], z); Vertices [2] = Vector3 nou (xpositions [i], partea de jos, z); Vertices [3] = Vector3 nou (xpositions [i + 1], partea de jos, z);
Acum, după cum puteți vedea aici, vertex 0
este partea de sus-stânga, 1
este partea dreaptă sus, 2
este partea de jos din stânga și 3
este partea dreaptă sus. Va trebui să ne amintim că pentru mai târziu.
A doua proprietate care necesită ochiuri este UV-urile. Mesele au texturi, iar UV-urile aleg care parte din texturile pe care dorim sa le apucam. În acest caz, dorim doar colțurile de sus-stânga, sus-dreapta, jos-stânga și din dreapta-jos ale texturii noastre.
Vector2 [] UVs = vectorul nou2 [4]; UVs [0] = vectorul nou2 (0, 1); UVs [1] = Vector2 nou (1, 1); UVs [2] = Vector2 nou (0, 0); UVs [3] = Vector2 nou (1, 0);
Acum avem nevoie de numerele astea din nou înainte. Ochii sunt alcătuiți din triunghiuri și știm că orice patrulater poate fi alcătuit din două triunghiuri, deci acum trebuie să le spunem rețelei cum ar trebui să atragă acele triunghiuri.
Uită-te la colțuri cu ordinea nodului etichetate. Triunghi A conectează nodurile 0
, 1
și 3
; Triunghi B conectează nodurile 3
, 2
și 0
. De aceea, vrem să facem o matrice care să conțină șase numere întregi, reflectând exact faptul că:
int [] tris = int int [6] 0, 1, 3, 3, 2, 0;
Aceasta creează patrulaterul nostru. Acum am setat valorile ochiurilor.
ochiuri [i]. vertices = Vertices; ochiurile [i] .uv = UVs; ochiuri [i] .triangles = tris;
Acum, avem ochiurile noastre, dar nu avem obiecte de joc pentru a le face în scenă. Deci le vom crea de la noi watermesh
prefab care conține un filtru de ochiuri și un filtru de ochiuri.
meshobjects [i] = Instantiate (watermesh, Vector3.zero, Quaternion.identity) ca GameObject; meshobjects [i] .GetComponent() .mesh = ochiuri [i]; meshobjects [i] .transform.parent = transformă;
Am stabilit plasa și am hotărât să fie copilul managerului de apă, pentru a ordona lucrurile.
Acum vrem și colizorul nostru:
agenți de coliziune [i] = new GameObject (); colliders [i] .name = "declanșator"; acceleratoare [i] .AddComponent(); colliders [i] .transform.parent = transformă; colliders [i] .transform.position = Vector3 nou (Left + Width * (i + 0.5f) / edgecount, Top - 0.5f, 0); colliders [i] .transform.localScale = Vector3 nou (Lățime / margine, 1, 1); acceleratoare [i] .GetComponent () .isTrigger = adevărat; acceleratoare [i] .AddComponent ();
Aici facem coliziuni în cutie, oferindu-le un nume, astfel încât să fie un pic mai tîrziu în scenă și să-i facă din nou pe fiecare copil al managerului de apă. Am stabilit poziția lor de a fi la jumătatea distanței dintre noduri, a stabilit dimensiunea acestora și a adăuga a WaterDetector
clasa pentru ei.
Acum, că avem ochiurile noastre, avem nevoie de o funcție pentru ao actualiza pe măsură ce se mișcă apa:
void UpdateMeshes () pentru (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;
S-ar putea să observați că această funcție utilizează doar codul pe care l-am scris mai devreme. Singura diferență este că de data aceasta nu trebuie să setăm tris și UV-urile, deoarece acestea rămân aceleași.
Următoarea noastră sarcină este ca apa să funcționeze singură. Vom folosi FixedUpdate ()
pentru a le modifica incremental.
void FixedUpdate ()
În primul rând, vom combina Legea lui Hooke cu metoda Euler pentru a găsi noile poziții, accelerații și viteze.
Deci, Legea lui Hooke este \ (F = kx \), unde \ (F \) este forța produsă de un izvor (amintiți-vă că modelăm suprafața apei ca un șir de izvoare), \ (k \ este constanta arcului si \ (x \) este deplasarea. Deplasarea noastră pur și simplu va fi poziția y a fiecărui nod minus înălțimea de bază a nodurilor.
Apoi, adăugăm a factorul de amortizare proporțional cu viteza forței de a atenua forța.
pentru (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));
Metoda Euler este simplă; doar adaugam acceleratia la viteza si viteza la pozitie, fiecare cadru.
Notă: Doar am presupus că masa fiecărui nod a fost 1
aici, dar veți dori să utilizați:
accelerații [i] = forța / masa;
dacă doriți o masă diferită pentru nodurile dvs..
Bacsis: Pentru fizica exactă, vom folosi integrarea Verlet, dar pentru că adăugăm amortizarea, putem folosi metoda Euler, care este mult mai rapid de calculat. În general, totuși, metoda Euler va introduce exponențial energia cinetică din nicăieri în sistemul fizic, deci nu o utilizați pentru nimic precis.
Acum o să creăm propagarea valurilor. Următorul cod este adaptat de tutorialul lui Michael Hoffman.
float [] leftDeltas = float nou [xpositions.Length]; float [] rightDeltas = float nou [xpositions.Length];
Aici, creăm două matrice. Pentru fiecare nod vom verifica înălțimea nodului anterior față de înălțimea nodului curent și vom pune diferența leftDeltas
.
Apoi, vom verifica înălțimea nodului ulterior față de înălțimea nodului pe care îl verificăm și vom pune diferența rightDeltas
. (De asemenea, vom multiplica toate valorile cu o constantă de împrăștiere).
pentru (int j = 0; < 8; j++) for (int i = 0; i < xpositions.Length; i++) if (i > 0) leftDeltas [i] = răspândire * (ipoziții [i] - ypositions [i-1]); viteze [i - 1] + = leftDeltas [i]; dacă eu < xpositions.Length - 1) rightDeltas[i] = spread * (ypositions[i] - ypositions[i + 1]); velocities[i + 1] += rightDeltas[i];
Putem schimba vitezele bazate pe diferența de înălțime imediat, dar ar trebui să stocăm numai diferențele de poziții în acest moment. Dacă am schimbat poziția primului nod direct de pe lilieci, până când ne-am uitat la cel de-al doilea nod, primul nod va fi deja mutat, astfel încât vom ruina toate calculele noastre.
pentru (int i = 0; i < xpositions.Length; i++) if (i > 0) ypositions [i-1] + = leftDeltas [i]; dacă eu < xpositions.Length - 1) ypositions[i + 1] += rightDeltas[i];
Deci, odată ce ne-am colectat toate datele privind înălțimea, îl putem aplica la sfârșit. Nu putem privi în dreapta nodului din extrema dreaptă sau din stânga nodului din extrema stângă, de aici condițiile i> 0
și eu < xpositions.Length - 1
.
De asemenea, rețineți că am conținut întregul cod într-o buclă și l-am rulat de opt ori. Acest lucru se datorează faptului că vrem să executăm acest proces în doze mici de mai multe ori, mai degrabă decât un calcul mare, care ar fi mult mai puțin fluid.
Acum avem apă care curge și arată. Apoi, trebuie să fim capabili să deranjăm apa!
Pentru aceasta, să adăugăm o funcție numită Stropi()
, care va verifica poziția x a stropii și viteza oricăror lovituri. Ar trebui să fie public, astfel încât să putem să îl sunăm ulterior de la colizoarele noastre.
public void Splash (float xpos, viteza flotorului)
În primul rând, trebuie să ne asigurăm că poziția specificată este de fapt în limitele apei noastre:
dacă (xpos> = xpositions [0] && xpos <= xpositions[xpositions.Length-1])
Și apoi ne vom schimba xpos
așa că ne dă poziția relativă la începutul corpului de apă:
xpos - = xpoziții [0];
Mai departe, vom afla care nod se atinge. Putem calcula astfel:
int index = Mathf.RoundToInt ((xpositions.Length-1) * (xpos / (xpositions [xpositions.Length-1] - xpositions [0])));
Deci, iată ce se întâmplă aici:
xpos
).0,75
.viteze [index] = viteza;
Acum am stabilit viteza obiectului care a lovit apa noastră la viteza nodului, astfel încât să fie tras în jos de obiect.
Notă: Puteți schimba această linie la ceea ce vă convine. De exemplu, ați putea adăuga viteza la viteza curentă, sau ați putea folosi impuls în loc de viteză și ați putea împărți masa.
Acum vrem să facem un sistem de particule care va produce stropirea. Am definit asta mai devreme; se numeste "splash" (destul de creativ). Asigurați-vă că nu-l confundați cu Stropi()
. Cel pe care îl folosesc este inclus în fișierele sursă.
Mai întâi, vrem să setăm parametrii stropii să se schimbe cu viteza obiectului.
float durata de viață = 0.93f + Mathf.Abs (viteza) * 0.07f; splash.GetComponent() .startSpeed = 8 + 2 * Mathf.Pow (Mathf.Abs (viteza), 0.5f); splash.GetComponent () .startSpeed = 9 + 2 * Mathf.Pow (Mathf.Abs (viteza), 0.5f); splash.GetComponent () .startLifetime = durata de viață;
Aici, ne-am luat particulele noastre, le-am stabilit durata de viata astfel incat sa nu moara la scurt timp dupa ce au atins suprafata apei si au stabilit ca viteza lor sa se bazeze pe pătratul vitezei lor (plus o constantă, pentru stropiri mici).
S-ar putea să te uiți la codul ăsta și să te gândești: "De ce a stabilit-o startSpeed
de două ori? ", și ați fi drept să ne întrebați problema. Problema este că folosim un sistem de particule (Shuriken, prevăzut cu proiectul) care are viteza de pornire setată la" aleator între două constante ". nu aveți prea mult acces la Shuriken de scripturi, așa că pentru a obține acel comportament să funcționeze, trebuie să setăm valoarea de două ori.
Acum voi adăuga o linie pe care ați putea sau nu doriți să omiteți din scenariul dvs.:
Vector3 pozitie = Vector3 nou (xpositions [index], ypositions [index] -0.35f, 5); Quaternion rotation = Quaternion.LookRotation (noua Vector3 (xpositions [Mathf.FloorToInt (xpositions.Length / 2)], baseheight + 8, 5) - pozitie);
Particulele Shuriken nu vor fi distruse atunci când vă ating obiectele, deci dacă doriți să vă asigurați că nu vor ateriza în fața obiectelor dvs., puteți lua două măsuri:
5
).A doua linie de cod ia punctul central al pozițiilor, se mișcă puțin în sus și indică emițătorul de particule spre el. Am inclus acest comportament în demo. Dacă utilizați un corp de apă cu adevărat larg, probabil că nu doriți acest comportament. Dacă apa dvs. se află într-o piscină mică din interiorul unei camere, este posibil să doriți să o utilizați. Deci, nu ezitați să eliminați linia de rotație.
GameObject splish = Instantiate (splash, position, rotation) ca GameObject; Distruge (splish, life + 0.3f);
Acum, ne facem stropirea și îi spunem să moară puțin după ce particulele vor muri. De ce puțin după aceea? Deoarece sistemul nostru de particule trimite câteva explozii secvențiale de particule, deci chiar dacă primul lot durează doar până la Timpul + durata de viață
, ultima noastră explozie va fi în jur de puțin după aceea.
Da! În cele din urmă am terminat, corect?
Gresit! Trebuie să detectăm obiectele noastre, sau asta nu a fost nimic!
Amintiți-vă că am adăugat acest scenariu tuturor agresorilor noștri înainte? Cel numit WaterDetector
?
Ei bine, vom reuși acum! Vrem doar o funcție în ea:
void OnTriggerEnter2D (Hit Collider2D)
Utilizarea OnTriggerEnter2D ()
, putem specifica ce se întâmplă de fiecare dată când un corp rigid 2D intră în corpul nostru de apă. Dacă trecem un parametru de Collider2D
putem găsi mai multe informații despre acest obiect.
dacă (Hit.rigidbody2D! = null)
Vrem doar obiecte care conțin a rigidbody2D
.
transform.parent.GetComponent() .Splash (transform.position.x, Hit.rigidbody2D.velocity.y * Hit.rigidbody2D.mass / 40f);
Acum, toți colizii noștri sunt copii ai managerului de apă. Așa că am apucat-o Apă
componentă de la părintele lor și apel Stropi()
, din poziția colizorului.
Amintiți-vă din nou, am spus că puteți trece viteza sau impulsul dacă ați fi dorit să fie mai corect din punct de vedere fizic? Ei bine, aici trebuie să treci pe cea dreaptă. Dacă înmulțiți viteza Y a obiectului cu masa sa, veți avea impulsul său. Dacă vrei doar să-ți folosești viteza, scapi de masa de pe acea linie.
În cele din urmă, veți dori să sunați SpawnWater ()
de undeva. Să o facem la lansare:
void Start () SpawnWater (-10,20,0, -10);
Și acum am terminat! Acum orice rigidbody2D
cu un collider care lovește apa va crea o stropire, iar valurile se vor mișca corect.
Ca bonus suplimentar, am adăugat câteva linii de cod în partea de sus a paginii SpawnWater ()
.
gameObject.AddComponent(); gameObject.GetComponent () .center = vector2 nou (stânga + lățime / 2, (partea de sus + jos) / 2); gameObject.GetComponent () .size = vector2 nou (lățime, partea de sus); gameObject.GetComponent () .isTrigger = adevărat;
Aceste linii de cod vor adăuga un collider cutie la apa în sine. Puteți folosi acest lucru pentru a face lucrurile să plutească în apă, utilizând ceea ce ați învățat.
Veți dori să faceți o funcție numită OnTriggerStay2D ()
care are un parametru de Collider2D Hit
. Apoi, puteți utiliza o versiune modificată a formulei de primăvară pe care am folosit-o înainte de a verifica masa obiectului și de a adăuga o forță sau o viteză rigidbody2D
pentru a face să plutească în apă.
În acest tutorial am implementat o simulare simplă de apă pentru utilizarea în jocuri 2D cu un cod simplu de fizică și un randător de linie, redactori de rețea, declanșatori și particule. Poate că veți adăuga corpuri ondulate de apă fluidă ca un obstacol pentru platformerul dvs. următor, pregătit pentru ca personajele dvs. să se scufunde sau să treacă cu grijă cu pietre plutitoare, sau poate puteți folosi acest lucru într-un joc de navigație sau windsurfing sau chiar un joc în care pur și simplu sări peste roci peste apă de pe o plajă însorită. Mult noroc!