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:

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 letölthető program működése a következő:

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:

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.