InfoC adventi naptár
Fizikai motor – World of Goo, „A ragacsok világa”
Csábító lenne a mai alkalommal a tegnapi, biliárdos programot matematikailag továbbfejleszteni: az Euler integrátort lecserélni Runge–Kutta integrátorra… Azonban nem ez lesz a mai naptárbejegyzésben. Helyette World of Goo-vá alakítjuk át a programot.
1 A fizikai motor
Mit is csinált a tegnapi program? Golyók mozogtak benne a képernyőn, közben egymással és a fallal ütköztek. Minden időszeletben kiszámolta a program a golyókra ható erőket (amelyek ütközések által keletkeztek). Aztán azok alapján a gyorsulásokat, azokból pedig a sebességeket, amikből végül a helyzeteket:
Golyo g; g.x += g.vx*delta_t; g.y += g.vy*delta_t; g.vx += (g.fx/m)*delta_t; /* ax*delta_t */ g.vy += (g.fy/m)*delta_t; /* ay*delta_t */
A golyókra a következő erők hatottak:
- Ha a golyó falnak ütközött, akkor a fal eltaszította magától.
- Ha két golyó egymásnak ütközott, akkor azok is taszították egymást.
- Ha egy golyó gurult, akkor súrlódási erő hatott rá.
Leglényegesebb két golyó ütközése volt. Ezt egy rugóval modelleztük. Ha a két golyó középpontjának távolsága kisebb volt, mint a sugaraik összege, akkor összenyomódtak – és ilyenkor egy közéjük képzelt erős rugó taszította el őket egymástól:
/* golyók távolsága */
dx=x1-x2;
dy=y1-y2;
tav=sqrt(dx*dx+dy*dy);
/* rugóerő */
if (tav<2*golyo_r) {
l=2*golyo_r-tav;
f=golyo_d*l;
fx+=dx/tav*f; /* egységvektor*f */
fy+=dy/tav*f;
}
2 Rugók létrejötte és megszűnése
Ezt a programot nagyon könnyen át tudjuk úgy alakítani, hogy a World of Goo-hoz hasonló játékot kapjunk. Először is, a zöld hátteret le kell cserélni feketére. :) Na jó, szóval a lényeg az, hogy két új erőt kell szimulálni:
- a gravitációt,
- rugókat is kell létrehozni a golyók között, amelyeknek az erejét figyelembe kell venni.
A letölthető program működése a következő:
- A tegnap bemutatott fizikai motorral szimulálja a golyók mozgását. A golyók ütközéskor taszítják egymást, és hat rájuk a gravitáció is.
- Az egérgombbal meg lehet fogni egy golyót (kék), aztán „fog és vidd” (drag and drop)
módszerrel áttenni máshova.
- Ha másik golyókhoz közel tesszük le, akkor a kellően közeli golyók és a letett golyók közé egy rugót hoz létre a program.
- Amikor megfogunk egy golyót, akkor viszont kitöröljük azokat a rugókat, amelyekkel eddig más golyókhoz volt kötve.
- A rugók hossza adott: erőt fejtenek ki akkor is, ha széthúzzuk, és akkor is,
ha összenyomjuk őket. Vagyis ezek sokkal inkább úgy működnek, mint az igazi
rugók, szemben a golyók ütközésekor közéjük képzelt fiktív rugókkal:
double f=rugo_d*(tav-rugohossz), /* erő */ fx=dx/tav*f, /* x és y komponensek */ fy=dy/tav*f; golyo[g1].fx-=fx; golyo[g1].fy-=fy; golyo[g2].fx+=fx; golyo[g2].fy+=fy; - Ha egy rugó túl hosszúra nyúlik (a természetes hosszának duplájára), akkor elszakad.
- Van a pályán fix golyó is (piros), amelynek nem változik a helyzete. Erre fel lehet lógatni az építményünket.
A golyókat a program egy tömbben tárolja (golyo[]), mivel azok száma
nem változik a futás során. Változik viszont a rugóknak a száma, ezért ahhoz egy láncolt
lista kell. Mivel gyakran kell beszúrni és törölni is a listába, egyszerűbb egy strázsás
listát választani. (Nagy úr a lustaság.) A rugókhoz elég csak két
tömbindexet eltárolni, hogy melyik két golyót kötik össze:
typedef struct Rugo {
int g1, g2; /* ket tombinex - mely golyokat koti ossze */
struct Rugo *prev, *next; /* duplan lancolt listahoz */
} Rugo;
3 Az egér kezelése
A játék futását alapvetően az idő vezérli, de a szimulációba be tudunk avatkozni az egérrel. Az egérgombnak nem az állapotát, hanem annak változását kell érzékelnünk:
- Ha előzőleg nem volt lenyomva a gomb, de az aktuális pillanatban igen, akkor egy kattintást érzékeltünk. Ilyenkor kell megkeresni az egérmutatóhoz közeli golyót, mert azt szeretné a játékos megfogni (drag).
- Ha előzőleg le volt nyomva, de most nincs, akkor ez egy elengedés. Ilyenkor szeretné a játékos letenni a golyót (drop).
Figyelni kell egyébként azért itt nem csak az állapotváltozásra, hanem az állapotra magára is. Ugyanis ha kattintáskor a játékos megfogott egy golyót, akkor az egérgomb nyomvatartásakor húzza azt. Ilyenkor a golyó koordinátáját folyamatosan módosítani kell az egérmutató koordinátája alapján.
Ezeket a műveleteket a programban az eseménykezelő ciklus vezérli.
Ez látja a golyók tömbjét (golyo, mérete golyok), a rugók
listáját, és a megfogott golyó indexét: megfogott.
Az utóbbi változhat, például kattintáskor a „nincs a kezünkben semmi” jelentésű
-1-es értéket leváltja egy golyo[] tömbbeli index:
case SDL_MOUSEBUTTONDOWN: /* egér kattintás */
mouse_x = ev.button.x;
mouse_y = ev.button.y;
for (i=0; i<golyok && megfogott==-1; ++i) {
double dx=golyo[i].x-mouse_x;
double dy=golyo[i].y-mouse_y;
if (dx*dx+dy*dy <= golyoelkap*golyoelkap) { /* ha elég közel volt az egérhez */
megfogott=i;
if (!golyo[i].fix) { /* ha nem fix, kiszakitjuk */
Rugo *iter=rugo.eleje->next;
while (iter!=rugo.vege) {
Rugo *iternext=iter->next;
if (iter->g1==i || iter->g2==i)
rugolista_torol(iter);
iter=iternext;
}
}
}
}
break;
Elengedéskor pedig az új rugók létrehozásán túl végül visszakerül a változóba
a -1:
case SDL_MOUSEBUTTONUP: /* egér elengedés */
mouse_x = ev.button.x;
mouse_y = ev.button.y;
for (i=0; i<golyok; ++i) {
if (i==megfogott) continue;
double dx=golyo[i].x-golyo[megfogott].x;
double dy=golyo[i].y-golyo[megfogott].y;
if (dx*dx+dy*dy <= rugoelkap*rugoelkap)
rugolista_hozzaad(&rugo, i, megfogott);
}
megfogott=-1;
break;
Az egérgomb nyomvatartásakor a golyó cipelése egyszerű, egyszerűen kihagyjuk a mozgatásból:
for (i=0; i<golyok; i++) {
if (golyo[i].fix || i==megfogott) continue;
golyo[i].x+=golyo[i].vx * delta_t;
golyo[i].y+=golyo[i].vy * delta_t;
golyo[i].vx+=golyo[i].fx/golyo_m * delta_t;
golyo[i].vy+=golyo[i].fy/golyo_m * delta_t;
}
Végülis ennyi az egész. Minden más szinte ugyanúgy van, mint a tegnapi programban. Még a súrlódás is. Valamilyen fékező erőnek kell lennie, amitől a rezgések csillapodnak. Bár elvileg súrlódás a levegőben nincs, csak más törvényszerűség szerint létrejövő közegellenállás, de a program az előbbivel számol.
4 A program
A program letölthető innen: advent17-wog.c. Kicsit szépítgetni kellett, hogy beleférjen 300 sorba, de éppen belefér.
