Cyber Monday Sale Save up to 40% off unlimited courses, tutorials and creative assets. Cyber Monday Sale! Save Now
Advertisement
  1. Game Development
  2. Programming

Hoe maak 2D Custom Physics Machine: Kern Masjien

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called How to Create a Custom Physics Engine.
How to Create a Custom 2D Physics Engine: The Basics and Impulse Resolution
How to Create a Custom 2D Physics Engine: Friction, Scene and Jump Table

Afrikaans (Afrikaans) translation by Aisyah Arrafah (you can also view the original English article)

In hierdie deel van my reeks om 'n spesiale 2D fisika-enjin vir jou spel te skep, sal ons meer funksies byvoeg tot die impulsresolusie wat ons in die eerste deel kry. In die besonder, ons sal kyk na integrasie, tydsberekening, die gebruik van modulêre ontwerpe vir ons kode, en breëfase botsingsopsporing.


Inleiding

In die laaste pos in hierdie reeks het ek die onderwerp van impuls resolusie behandel. Lees dit eers, as jou dit nog nie het nie!

Kom ons duik in die onderwerpe wat in hierdie artikel behandel word. Hierdie onderwerpe is almal helfte-ordentlike fisika enjin behoeftes, dus nou is dit 'n goeie tyd om meer funksies te bou bo die kernbesluite van die vorige artikel.


Integration

Integrasie is heeltemal maklik om te implementeer, en daar is baie gebiede op die internet wat goeie inligting verskaf vir herhaalbare integrasie. Hierdie afdeling sal meestal wys hoe om die korrekte integrasie funksie te implementeer, en wys na verskeie verskillende plekke om meer te lees indien nodig.

Eerstens moet dit bekend wees wat die versnelling eintlik is. Newton's Second Law lui soos volg:

\[Equation 1:\\
F = ma\]

Dit verklaar dat die som van alle kragte wat op 'n voorwerp optree, gelyk is aan die massa van daardie voorwerp m vermenigvuldig met sy versnelling a m in kilogram, a in meter/sek, en F is in newton

Herrangskik hierdie vergelyking 'n bietjie om op te los a resultate:

[Vergelyking 2:
a = \frac{F}{m}\\
\therefore\\
a = F * \frac{1}{m}\]

Die volgende stap behels die gebruik van versnelling om 'n voorwerp van een plek na 'n ander te skuif. Aangesien die spel in afsonderlike afsonderlike rame vertoon word in 'n illusie-like animasie, moet die ligging van elke posisie in hierdie aparte stappe bereken word. Vir 'n meer in-diepte dekking van hierdie vergelyking, sien asseblief: Erin Catto's Integration Demo from GDC 2009 en Hannu's addition to symplectic Euler for more stability in low FPS environments.

Euler eksplisiete (uitgesproke "oiler") integrasie word in die volgende fragment vertoon, waar x die posisie is en v die snelheid is. Let asseblief daarop dat 1/m * F versnelling is, soos hierbo verduidelik:

dt verwys hier na delta tyd. Δ is die simbool vir delta, en kan letterlik gelees word as "verandering in", of geskryf as Δt. So wanneer jou dit sien, kan dt as "verandering in tyd" gelees word. dv sal "verandering in snelheid" wees.

Dit sal werk, en word gewoonlik as beginpunt gebruik. Maar, dit het numeriese onakkuraathede wat ons kan ontslae raak sonder enige ekstra moeite. Hier is wat bekend staan ​​as Simlektiese Euler:

Let daarop dat al wat ek gedoen het die volgorde van die twee reëls kode herrangskik - sien "> die voormelde artikel van Hannu.

Hierdie pos verduidelik die numeriese onakkuraathede van Euler Explicit, maar word gewaarsku dat dit RK4 begin insluit, wat ek persoonlik nie aanbeveel nie: gafferongames.com: Euler Inaccuracy.

Hierdie eenvoudige vergelyking is alles wat ons nodig het om alle voorwerpe met lineêre spoed en versnelling te beweeg.


Timestepping

Aangesien speletjies met diskrete tydsintervalle vertoon word, moet daar 'n manier wees om die tyd tussen hierdie stappe op 'n beheerde wyse te manipuleer. Het jou al ooit 'n speletjie gesien wat teen verskillende snelhede sal hardloop, afhangende van watter rekenaar dit gespeel word? Dit is 'n voorbeeld van 'n spel wat teen 'n spoed hardloop, afhangende van die rekenaar se vermoë om die spel te hardloop.

Ons het 'n manier nodig om te verseker dat ons fisiese enjin slegs loop wanneer 'n sekere hoeveelheid tyd verby is. Op hierdie manier is die dt wat in berekeninge gebruik word altyd presies dieselfde nommer. Deur presies dieselfde dt waarde in jou kode te gebruik, sal jou fisika-enjin deterministies wees, en staan ​​bekend as 'n vaste tydstip. Dit is 'n goeie ding.

'N deterministiese fisika-enjin is een wat altyd dieselfde ding sal doen elke keer as dit uitgevoer word mits dieselfde insette gegee word. Dit is noodsaaklik vir baie soorte speletjies waar die spel baie fyn ingestel moet wees op die fisiese enjin se gedrag. Dit is ook noodsaaklik vir die ontfouting van jou fisika-enjin, aangesien die gedrag van jou enjin konsekwent moet wees om bugs te bepaal.

Laat ons eers die eenvoudigste weergawe van vaste tydstip bespreek. Hier is 'n voorbeeld:

Dit wag, maak die spel tot genoeg tyd om fisika op te dateer. Die verloop van tyd is aangeteken, en diskrete dt-grootte stukkies tyd word uit die akkumulator geneem en deur die fisika verwerk. Dit verseker dat dieselfde waarde aan fisika geslaag word, ongeag wat, en dat die waarde wat geslaag is aan fisika, 'n akkurate voorstelling is van die werklike tyd wat in die werklike lewe toegereken word. Stompe van dt word uit die akkumulator verwyder totdat die akkumulator kleiner is as 'n dt stukkie.

Daar is 'n paar probleme wat hier vasgestel kan word. Die eerste behels hoe lank dit neem om die fisika-opdatering werklik te doen: Wat as die fisika opdatering te lank neem en die akkumulator hoër en hoër gaan elke wedstrydlus? Dit word die spiraal van die dood genoem. As dit nie reggestel is nie, sal u masjien onmiddellik stop as jou fisika nie vinnig genoeg kan hardloop nie.

Om dit op te los, moet die enjin net minder fisika-opdaterings uitvoer as die akkumulator te hoog word. 'N Eenvoudige manier om dit te doen sou wees om die akkumulator onder 'n paar arbitrêre waarde te klamp.

Nou, as die spel wat hierdie lus loop ooit om enige rede enige vorm van verlating ervaar, sal die fisika hom nie in die doodspiraal dompel nie. Die wedstryd sal eenvoudig net soos wat dit pas, 'n bietjie stadiger hardloop.

Die volgende ding om dit reg te stel is redelik klein in vergelyking met die doodspiraal. Hierdie lus neem dt stukke van die akkumulator totdat die akkumulator kleiner is as dt. Dit is pret, maar daar is nog 'n bietjie oorblywende tyd in die akkumulator. Dit is 'n probleem.

Gestel die akkumulator word gelaat met 1/5 van 'n dt stuk elke raam. Op die sesde raam sal die akkumulator genoeg tyd hê om nog een fisika-opdatering uit te voer as al die ander rame. Dit sal 'n raam elke sekonde genereer of 'n soort diskrete sprong wat effens groter is in die tyd, en baie sigbaar in jou spel.

Om dit op te los, is die gebruik van lineêre interpolasie nodig. As dit skrikwekkend is, moenie bekommerd wees nie - die implementering daarvan sal vertoon word. As jou die implementering wil verstaan is daar baie hulpbronne aanlyn vir lineêre interpolasie.

Hierdeur kan ons interpreter (benader) waar ons tussen twee verskillende tydintervalle kan wees. Dit kan gebruik word om die toestand van 'n spel tussen twee verskillende fisika opdaterings te gee.

Met lineêre interpolasie kan die lewering van 'n enjin op 'n ander tempo as die fisiese enjin loop. Dit laat 'n grasieuse hantering van die oorblywende akkumulator van die fisika opdaterings toe.

Hier is 'n volledige voorbeeld:

Hier kan alle voorwerpe binne die spel getrek word op wisselende oomblikke tussen diskrete fisika-tydstappe. Dit sal grasieus met alle foute en oorblywende akkumulasie hanteer. Dit is eintlik 'n bietjie agtergrond van die fisika wat nou opgelos is, maar wanneer 'n wedstryd hardloop word al die bewegings perfek gemasker deur interpolasie.

Spelers sal nooit weet dat die vertoning baie min agter die fisika is nie, want spelers sal net weet wat hulle sien, en wat hulle sal sien is 'n baie gladde oorgang van een raam na 'n ander.

Jou mag dalk wonder, "hoekom interpolere ons nie van die huidige posisie na die volgende nie?". Ek het dit probeer en dit vereis om te 'raai' waar die voorwerp in die toekoms sal wees. Dikwels, voorwerpe in 'n fisiese masjien maak 'n skielike verandering in beweging, soos wanneer 'n botsing en wanneer 'n beweging skielik verander, word die voorwerp teleporteer weens onakkurate interpolasie in die toekoms.


Modulêre Ontwerp

Daar is 'n paar dinge wat elke fisika-voorwerp gaan nodig hê. Die spesifieke dinge wat elke fisika-voorwerp nodig het, kan egter effens van voorwerp na voorwerp verander. 'N slim manier om al hierdie data te organiseer is nodig, en dit sal aanvaar word dat die minder hoeveelheid kode geskryf moet word om so 'n organisasie te bereik soos dit verlang word. In hierdie geval sal modulêre ontwerp baie nuttig wees.

Modulêre ontwerp klink waarskynlik 'n bietjie pretensieus of oor-ingewikkeld, maar dit maak sin en is redelik eenvoudig. In hierdie konteks beteken "modulêre ontwerp" net dat ons 'n fisika-voorwerp in afsonderlike stukke wil breek, sodat ons dit kan koppel of loskoppel, maar ons vind dit fiks.

Agentskap

'N Fisika-liggaam is 'n voorwerp wat al die inligting oor 'n gegewe fisika voorwerp bevat. Dit sal die vorm (s) wat die voorwerp verteenwoordig, massa-data, transformasie (posisie, rotasie), snelheid, wringkrag, ensovoorts, berg. Hier is hoe ons liggaam lyk:

Dit is 'n goeie beginpunt vir die ontwerp van 'n fisiese liggaamstruktuur. Daar is 'n paar intelligente besluite wat hier gemaak word, wat geneig is tot 'n sterk kode organisasie.

Die eerste ding om daarop te let is dat 'n vorm deur 'n wyser op die liggaam gevind word. Dit verteenwoordig 'n losse verhouding tussen die liggaam en sy vorm. 'N Liggaam kan enige vorm bevat, en die vorm van 'n liggaam kan omgeruil word. Trouens, die liggaam kan deur verskillende vorms voorgestel word, en so 'n liggaam sal as 'n "saamgestelde" bekend staan, aangesien dit uit verskillende vorms sal bestaan. (Ek gaan nie komposiete in hierdie handleiding dek nie.)

Body and Shape interface.
Liggaam en Vorm Koppelvlak.

Die vorm self is verantwoordelik vir die berekening van grensvorme die berekening van massa deur digtheid, en lewering.

Die mass_data is 'n klein data struktuur om massa-verwante inligting te bevat:

Dit is lekker om alle massa- en intertiaverwante waardes in 'n enkele struktuur op te slaan. Die massa moet nooit met die hand bepaal word nie - massa moet altyd deur die vorm self bereken word. Massa is 'n taamlik intuïtiewe soort waarde, en die opstel van die hand sal baie aanpassingstyd neem. Dit is gedefinieer as:

\[ Equation 3:\\Mass = density * volume\]

Wanneer 'n ontwerper 'n vorm wil hê wat meer "massief" of "swaar" is, moet hulle die digtheid van 'n vorm verander. Hierdie digtheid kan gebruik word om die massa van 'n vorm wat sy volume gee, te bereken. Dit is die regte manier om oor die situasie te gaan, aangesien digtheid nie deur volume geraak word nie en nooit verander sal word gedurende die duur van die spel nie (tensy dit spesifiek met spesiale kode ondersteun word).

Enkele voorbeelde van vorms soos AABB en Circle kan gevind word by the previous tutorial in this series.

Materiaal

Al hierdie bespreking van massa en digtheid lei tot die vraag: Waar lê die digtheidwaarde? Dit woon binne die Materiaal struktuur:

Sodra die waardes van die materiaal ingestel is, kan hierdie materiaal oorgedra word na die vorm van 'n liggaam sodat die liggaam die massa kan bereken.

Die laaste ding wat noemenswaardig is die gravity_scale. Skaalgravitasie vir verskillende voorwerpe word so dikwels vereis om die spel te tweak dat dit die beste is om net 'n waarde in elke liggaam spesifiek vir hierdie taak in te sluit.

Sommige nuttige materiaal instellings vir algemene materiaal tipes kan gebruik word om 'n Materiaal voorwerp uit 'n opsommingswaarde te konstrueer:

Krag

Daar is nog een ding om oor te praat in die liggaam sbou. Daar is 'n data-lid genoem krag. Hierdie waarde begin by nul aan die begin van elke fisika opdatering. Ander invloede in die fisika enjin (soos swaartekrag) sal Vec2 vektore in hierdie krag data lid voeg. Voor integrasie sal al hierdie kragte gebruik word om die versnelling van die liggaam te bereken, en gebruik tydens integrasie. Na hierdie integrasie force lid of weggelaat.

Dit laat 'n sekere hoeveelheid krag toe om op 'n voorwerp op te tree wanneer hulle wil, en geen bykomende kode moet geskryf word wanneer 'n nuwe kragsoort op die voorwerp toegepas word nie.

Kom ons neem 'n voorbeeld. Sê ons het 'n klein sirkel wat 'n baie swaar voorwerp verteenwoordig. Hierdie klein sirkel vlieg in die spel, en dit is so swaar dat dit 'n ander voorwerp daarheen trek. Hier is 'n rowwe pseudokode om dit te demonstreer:

Die funksie ApplyForcePullOn() kan dalk 'n klein krag toepas om die liggaam na die HeavyObject te trek, net as die liggaam naby genoeg is.

Two objects pulled towards a larger one. The pull force is dependent upon distance.
Twee voorwerpe getrek na 'n groter een wat hulle verplaas het. Twee voorwerpe getrek na 'n groter een wat hulle verplaas het.

Dit maak nie saak hoeveel kragte word by die krag van 'n liggaam gevoeg nie, aangesien hulle almal 'n enkele opsommende kragvektor vir daardie liggaam sal optel. Dit beteken dat die twee kragte wat op dieselfde liggaam optree potensieel mekaar kan kanselleer.


Breë Fase

In 'n vorige artikel in hierdie reeks botsingsdeteksie roetines is ingestel. Hierdie roetine is eintlik geskei van wat bekend staan ​​as die "smal fase". Die verskille tussen breë fase en smal fase kan redelik maklik met 'n Google-soektog ondersoek word.

(In 'n neutedop: ons gebruik breëfase botsingsopsporing om te weet watter voorwerpe miskien bots, en dan noufase botsings opspoor om na te gaan of hulle eintlik is bots.)

Wil 'n paar kode monsters saam met 'n verduideliking van hoe om breë fases te implementeer \(O(n^2)\) berekening van tydkompleksiteit pare.

\(O(n^2)\) beteken basies dat die tyd wat geneem word om elke paar potensiële botsings na te gaan afhang van die vierkant van die aantal voorwerpe. Dit gebruik Big-O notasie.

Aangesien ons met pare voorwerpe werk, sal dit nuttig wees om 'n struktuur soos volg te skep:

'N Breë fase moet 'n klomp moontlike botsings versamel en almal in Pair strukture stoor. Hierdie pare kan dan na 'n ander gedeelte van die enjin (die nou fase) oorgedra word, en dan opgelos word.

Voorbeeld breë fase:

Bogenoemde kode is redelik eenvoudig: Kontroleer elke liggaam teen elke liggaam, en slaan self-tjeks.

Opvul Duplikate

Daar is een probleem in die laaste gedeelte: baie duplikaatpare sal terugbesorg word! Hierdie duplikate moet uit die resultate geskrap word. Sekere vertroudheid met sorteer algoritmes sal hier vereis word as jou nie 'n soort sorteringsbiblioteek beskikbaar het nie. As jou C++ gebruik dan is jou gelukkig:

Nadat alle pare in 'n sekere volgorde gesorteer is, kan aanvaar word dat alle pare in die parehouer alle duplikate langs mekaar sal hê. Plaas alle unieke pare in 'n nuwe houer genaamd uniqePairs, en die taak om duplikate te sorteer, word gedoen.

Die laaste ding om te noem, is die predikaat SortPairs(). Die SortPairs() funksie is wat eintlik vir sortering gebruik word, en dit kan so lyk:

Die terme lhs en rhs kan as 'linkerkant' en "regterkant" gelees word. Hierdie terme word gewoonlik gebruik om na funksieparameters te verwys waarin logiese dinge as die linker en regterkant van sommige vergelykings of algoritmes gesien kan word.

Laag

Lêer verwys na die daad om verskillende voorwerpe te hê wat nooit met mekaar bots nie. Dit is die sleutel omdat die koeël wat afkomstig is van 'n bepaalde voorwerp geen ander spesifieke voorwerp beïnvloed nie. Byvoorbeeld, spelers op een span wil dalk hê dat hul vuurpyle die vyand seermaak maar nie mekaar nie.

Representation of layering; some object collide with one another, some do not.
Lêerverteenwoordiging; Sommige voorwerpe bots met mekaar, sommige doen nie.

Lêer word die beste geïmplementeer met bitmasks - sien A Quick Bitmask How-To for Programmers en the Wikipedia page  vir vinnige erkenning, en die afdeling Filtering the Box2D manual om te sien hoe die masjien bitmask gebruik.

Lêer moet in 'n breë fase gedoen word. Hier sal ek net 'n breë fase voorbeeld plak:

Lêer is baie doeltreffend en baie eenvoudig.


Die Halfspace Kruising

'N halfspace kan gesien word as een kant van die lyn in 2D. Om vas te stel of 'n punt aan die een kant van die lyn of die ander kant is 'n taamlike algemene taak, en moet deeglik verstaan ​​word deur elkeen wat hul eie fisika masjien uitvind. Ongelukkig word hierdie onderwerp nie op enige sin op die internet op 'n sinvolle manier bespreek nie, ten minste van wat ek sien - tot nou toe natuurlik!

Die algemene vergelyking van die lyn in 2D is:

\[Equation 4:\\
Algemene \: form: ax + by + c = 0\\
Normal \: to \: line: \begin{bmatrix}
a \\
b \\
\end{bmatrix}\]

custom-physics-line2d

Let daarop dat, ongeag sy naam, die normale vektor nie genormaliseer moet word nie (dit is, dit behoort nie 'n lengte van 1 te hê nie).

Om te sien of 'n punt op 'n sekere kant van hierdie lyn is, moet ons die punt in die veranderlikes x en y in die vergelyking koppel en die resultaatpunt merk. Die resultaat van 0 beteken die punt is op die lyn en die negatiewe/positiewe kant van die verskillende lyn.

Dis dit! Om hierdie afstand van punt tot lyn te ken is eintlik die gevolg van 'n vorige toets. As die normale vektor nie genormaliseer word nie, dan word die resultaat afgeskaal deur die grootte van die normale vektor.


Gevolgtrekking

Nou kan 'n volledige, maar eenvoudige, fisika enjin heeltemal van nuuts af gebou word. Meer gevorderde onderwerpe soos wrywing, oriëntasie en dinamiese AABB bome kan in die volgende handleiding bespreek word. Vra asseblief 'n vraag of kommentaar hieronder, ek is mal daaroor om dit te lees en te beantwoord!

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.