Fixpontos számolás N-Gage-en: miért nincs float a játékciklusban
Közzétéve:
Az N-Gage és az N-Gage QD egy 104 MHz-es ARM920T processzoron fut, amelyben nincs lebegőpontos egység. Ez a C++ kódban nem látszik: a float és a double ugyanúgy lefordul, csak éppen minden szorzásból egy könyvtári függvényhívás lesz, amely egész műveletekkel utánozza a lebegőpontos számolást. Egy játékciklusban, ahol képkockánként több száz pozíciót és sebességet kell frissíteni, ez gyorsan elviszi a képkockasebességet.
A megoldás régi, a konzolos és PC-s játékfejlesztők a kilencvenes években is ezt használták: a fixpontos számolás.
A 16.16-os formátum
Egy 32 bites egész felső 16 bitje az egészrész, az alsó 16 bitje a törtrész. Az 1.0 tehát 65536, a 0.5 pedig 32768.
typedef TInt TFix; // 16.16 fixpontos szám
const TInt KFixShift = 16;
const TFix KFixOne = 1 << KFixShift;
inline TFix IntToFix(TInt a) { return a << KFixShift; }
inline TInt FixToInt(TFix a) { return a >> KFixShift; }
// Összeadás és kivonás: a sima egész művelet pont jó.
// Szorzás: az eredmény 32.32 lenne, ami nem fér el 32 biten.
inline TFix FixMulFast(TFix a, TFix b)
{
return (a >> 8) * (b >> 8); // mindkettőből 8 bit pontosságot dobunk el
}
A FixMulFast a játékokban bevett trükk. Mindkét tényezőt 8 bittel jobbra tolja, így a szorzat visszakerül a 16.16-os skálára, és a köztes eredmény 32 biten marad, amíg a két érték szorzata abszolút értékben 32 768 alatt van. Ára a pontosság: a törtrészből csak 8 bit marad. Képernyőkoordinátákhoz és sebességhez ez bőven elég, egy fizikai szimulációhoz már nem.
Szinusz táblából
A forgatáshoz szinusz és koszinusz kell. Futás közben ezeket nem számoljuk, hanem induláskor egyszer, egy táblába. A szög sem fokban van, hanem 256 lépésben egy teljes kör:
TFix iSinTable[256]; // induláskor egyszer töltjük fel
inline TFix FixSin(TInt aAngle) { return iSinTable[aAngle & 255]; }
inline TFix FixCos(TInt aAngle) { return iSinTable[(aAngle + 64) & 255]; }
// mozgás a szög irányába
iX += FixMulFast(FixCos(iAngle), iSpeed);
iY += FixMulFast(FixSin(iAngle), iSpeed);
Az & 255 miatt a körbefordulás ingyen van: a 256-os szög újra 0 lesz, a -1 pedig 255. Nincs szükség if-re és modulóra sem. A táblát egyszer, induláskor töltjük fel lebegőpontos számolással, és ez a betöltőképernyő alatt észre sem vehető.
Hol bukik el?
- Osztás. Az ARM9-ben nincs hardveres osztás sem, tehát az egész osztás is lassú. Ahol lehet, szorozzunk reciprokkal, vagy toljunk biteket kettő hatványaival.
- Túlcsordulás. Két 200-as érték szorzata a gyors változatban már túlcsordul. A pályakoordinátákat érdemes kisebb egységben tárolni, vagy pontosabb, lassabb szorzást használni ott, ahol kell.
- Felhalmozódó hiba. Ha a pozíciót ezer képkockán át apró sebességekkel növeled, a 8 bites törtrész pontatlansága összeadódik. Ilyenkor a pozíciót tárold teljes 16.16-os pontossággal, és csak a sebességszorzást számold gyorsan.
Egy mérés a saját projektemből: ugyanaz a részecskeeffekt float-tal 9–10, fixpontosan 25 képkocka/másodperccel futott a QD-n. Az emulátorban mindkettő 60 fölött volt. Ezért kell mindig a készüléken mérni.