Adventi naptár

Czirkos Zoltán · 2021.08.24.

Fizikai motor – biliárd

Írjunk programot, amely biliárdgolyók mozgását szimulálja! Legyen a pályán 15 színes golyó, és egy 16-odik fehér, amelyet az egérrel meg lehet lökni. A lökés erejét és irányát egy kattintás után az egér elmozdítása határozza meg; a lökés az egérgomb elengedésekor történjen.

1. A felhasználói felület

A program grafikai része az SDL-lel és az SDL_gfx-szel végtelenül egyszerű. Van ezekben ugyanis egy filledCircleRGBA() nevű függvény, amely egy kiszínezett kört rajzol. Ez jó lesz golyónak a felülnézetben mutatott asztalhoz.

Az egér koordinátáit és az egérgombokat le tudjuk kérdezni az SDL_MOUSEMOTION, illetve SDL_MOUSEBUTTONDOWN és UP események adatain keresztül. Az egérkattintást egy apró állapotgéppel kell kezelni, amelynek két állapota lehet: nincs „megfogva” a fehér golyó, vagy meg van fogva. Ha a kattintás a fehér golyón történik, akkor megfogjuk, amúgy pedig nem történik semmi. Ha a gomb elengedése úgy történt, hogy előtte történt megfogás is, akkor pedig a fehér golyó sebességet kap:

2. A fizikai motor

Az előző napi játékokban a mozgás nem épült fizikai törvényekre; egyszerűen úgy lett meghatározva az elemek (fák stb.) mozgása a képernyőn, hogy az nagyjából úgy nézzen ki, ahogyan azt a való világból megszoktuk. Izgalmasabb és valószerűbb játékokat tudunk úgy csinálni, ha a valódi fizikai törvényeket használva számoljuk ki a játék szereplőinek mozgását. Ennek előnye egyébként az is, hogy általában néhány egyszerű fizikai törvényt kell a programban megvalósítanunk, és azokból a megszokott, természetes mozgások automatikusan kiadódnak.

Minden golyónak van egy helyzete a képernyőn és egy sebessége. A helyzetük megváltozása az időben a sebességüktől függ. A sebességük megváltozása pedig a gyorsulásoktól. Gyorsulás a rájuk ható erő hatására keletkezhet: például ha egy golyó nekinyomódik a falnak, akkor a fal kifejt egy rá merőleges erőt, amely hatására a golyó visszafordul.

A golyók adatai a lentiek. (A golyókra ható erők változói is a struktúrában vannak. Ez tervezési szempontból nem helyes, hiszen ez nem a golyók tulajdonsága, hanem a körülményekből adódik, de így egyszerűbb a kód.)

typedef struct Golyo {
    double x, y;        /* helyzet */
    double vx, vy;      /* sebesség */
    double fx, fy;      /* erő */
    int c;              /* szín */
} Golyo;

Ha ismerjük a golyóra ható F erőt, abból a gyorsulás számítható Newton II. törvénye, az F=ma alapján. A sebességet elvileg a gyorsulás integrálásával, a helyzetet pedig a sebesség integrálásával tudjuk meghatározni. Ez azonban nem ilyen egyszerű – ha egy kicsit is elmozdul egy golyó, akkor megváltoznak rögtön a rá ható erők. Az integrált emiatt csak közelítőleg tudjuk kiszámolni. Ha feltesszük, hogy egy rövid delta_t időtartamon belül az erők állandók maradnak, akkor az integrálokat szorzással közelíthetjük (ez Euler módszere):

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 */

Minél kisebbre választjuk a delta_t értéket, annál pontosabb lesz a számítás. Az integrál értékére Euler módszere egy viszonylag rossz becslést ad. Első körben azonban ez jó lesz. Már csak az erőket kell meghatároznunk.

3. A golyókra ható erők

A golyókra a következő erők hatnak:

  • Ha a golyó falnak ütközik, akkor a fal eltaszítja magától.
  • Ha két golyó egymásnak ütközik, akkor azok is taszítják egymást.
  • Ha egy golyó gurul, akkor súrlódási erő hat rá. Ez az az erő, amitől végül a golyók megállnak.

A súrlódási erő

A súrlódás egy gördülési ellenállás. Ennek nagysága attól függ, hogy milyen erősen nyomódik a golyó az asztalhoz. Az pedig attól, hogy mekkora a súlya: Fs = µ·Ft = µ·mg. Az iránya pedig mindig a sebességgel ellentétes. A hozzá tartozó vízszintes (x) és függőleges (y) komponenseket ezért úgy tudjuk meghatározni, ha a sebességvektort normalizáljuk (vagyis elosztjuk a saját hosszával), mivel akkor kapunk egy olyan vektort, amely iránya megegyezik a sebességvektorral, a hosszúsága pedig pontosan egy. Ezt az egységvektort kell komponensenként megszorozni a kapott súrlódási erővel, és persze negatív irányban tekinteni. A zárójelben lévő vx/v és vy/v kifejezések adják az egységvektor komponenseit:

v = sqrt(vx * vx + vy * vy);
fx -= mu * g * m * (vx / v);
fy -= mu * g * m * (vy / v);

A golyók ütközése

Amíg két golyó távol van egymástól, addig nem hatnak egymásra. Azonban ha találkoznak, akkor az összeütközéskor erők lépnek fel. Ilyenkor benyomódnak egy kicsit, még ha nagyon kemények is. Az összepréselődés hatására egy rugalmas erő lép fel. Ezt modellezhetjük úgy, hogy a két golyó közé egy nagyon erős rugót képzelünk.

A rugó erejét Hooke törvényéből, az F=-Dl képletből számíthatjuk ki, ahol l a rugó összenyomódása. A két golyó érintkezésekor, amikor a középpontjuk távolsága r1+r2, ez éppen nulla; ha ennél közelebb kerülnek egymáshoz, akkor kezd el nőni. Az erő irányát a két golyó középpontját összekötő egyenes határozza meg. Itt a számítást az előzőhöz hasonlóan végezhetjük: a két középpont távolságát, mint vektort normalizáljuk. Ebből kapunk egy egységvektort, amely éppen a megfelelő irányú. Ezt kell megszorozni az erővel:

/* 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;
}

Golyó és fal ütközése

Ez hasonlóan képzelhető el, mint két golyó ütközése. Minél jobban belenyomódik egy golyó a falba (vagyis minél közelebb van a középpontja a falhoz a sugarához képest), annál nagyobb erő hat rá. Ezt mind a négy falra külön ki kell számítani. A négy fal négy különböző irányú erőt adhat.

Ebben az esetben nincsen szükség arra, hogy normalizáljuk a vektort, hiszen pl. a bal és a jobb oldalni falnál függőleges, y irányú erő nem lép fel. Csak a vízszintes, x irányú erővel kell foglalkozni, amely az x irányú benyomódásból adódik. A bal oldali falat tekintve:

if (x < golyo_r)
    fx += golyo_d * (golyo_r - x);

4. A program

A program letölthető innen: advent16-biliard.c. Ebben az egyes konstansokat (rugók ereje, nehézségi gyorsulás, golyók tömege) próbálgatással állítottam be, hogy viszonylag jól nézzen ki az eredmény.

A programnak két gyengesége van. Egyik az, hogy nem számolja a golyók perdületét. Azaz a golyók mindenhova forgás nélkül mozognak. Ez azt is jelenti, hogy nem lehet megcsavarni őket.

A másik probléma drámaibb ennél. Az integrál közelítésére alkalmazott Euler-féle módszer ugyanis nem túl pontos, sőt az ismert módszerek közül a legkevésbé pontos. Ha nem szeretnénk, hogy a golyók a képernyőn egymásba olvadjanak (és ez egyébként kihasson a szimuláció pontosságára is), akkor kellően nagyra kell állítani a képzeletbeli rugók erejét meghatározó D rugóállandót (a kódban golyo_d). Ilyenkor azonban nagy erők adódnak, amelyek a helytől erősen függenek, és nem jogos az a közelítés, hogy a delta_t időintervallumon belül az erők, így a gyorsulás állandó. Ha egymás felé tart két golyó, még akár az is előfordulhat, hogy a delta_t időn belül visszafordulnak az ütközés hatására.

Emiatt ha túl nagy erővel lökjük meg a fehér golyót, akkor a szimuláció elszállhat (egyre gyorsul az összes golyó, és végül eltűnnek a képernyőről). A probléma orvoslására csökkenthetjük a delta_t időintervallumot, de jobb ötlet lenne egy másik integrálközelítő módszer használata. Általában a Runge-Kutta módszert szokták alkalmazni. Erről bővebben itt lehet olvasni (példa kóddal): Integration Basics.