Többjátékos 3D vitorlázós játék tiszta three.js-szel
Gerstner-hullámok, amelyeket a GPU és a CPU ugyanúgy számol, egyetlen égbolt-függvény az égre, a vízre és a ködre, ötperces nap-éj ciklus és egy apró WebSocket-relé – a portfólióm játéka mögötti technika.
Balázs Csorba··8 perc olvasás
- three.js
- WebGL
- GLSL
- WebSockets
- Game development

A portfóliómban van egy játék. A Dragon Voyage egy kis vitorlázós játék: egy sárkányorrú dzsunkát kormányzol egy lampionokkal kivilágított kikötőben, közvetlenül az oldalon. Öt küldetés van (lampionok meggyújtása, verseny a kapukon át, hajótöröttek mentése, egy kalózflotta legyőzése, a zászlóshajó elsüllyesztése), oldalsortüzes csaták, kereskedelem két kikötő között és egy ötperces nap-éj ciklus. Aki épp nyitva tartja az oldalt, mind ugyanabban a kikötőben vitorlázik.
Játékmotor nincs: csak three.js és nagyjából 9700 sor TypeScript, GLSL és Vue. Ezeket a részeket volt a legérdekesebb megépíteni.
Hullámok, amelyeken a hajó tényleg úszik
A legtöbb three.js-es óceándemó a vertex shaderben mozgatja a vizet, és ennyi. Egy játéknak ennél több kell: a hajónak azon a hullámon kell ülnie, amit látsz, bólintania kell vele, és be kell dőlnie a hullámvölgybe.
Az óceán hat Gerstner-hullám összege. Mindegyiket egy hullámhossz, egy meredekség és a széliránytól való eltérés határozza meg, a sebessége pedig a mélyvízi diszperzióból adódik (c = √(g/k)). Ez a tömb az egyetlen igazságforrás:
const SPEC: [number, number, number][] = [
// wavelength (m), steepness, direction offset (rad)
[46, 0.085, 0.0],
[27, 0.1, 0.38],
[15.5, 0.12, -0.52],
// …three shorter waves
]Ugyanezek a számok uniformként a vertex shaderbe és egy CPU-s függvénybe is bekerülnek, amely bármely pontban megadja a magasságot és a normálist. A hajó minden képkockában lekérdezi a hajótest alatt. Ugyanígy a kalózhajók, a többi játékos hajója, a hajótöröttek tutajai és az ágyúgolyók csobbanásai.
Egy csapda: a Gerstner-hullámok oldalirányban is elmozdítják a pontokat, így a (x, z) feletti felszíni pont nem a (x, z)-ből indult. Néhány fixpontos iteráció kiegyenlíti ezt az elcsúszást, mielőtt kiolvassuk a magasságot. Ez olcsó, és ettől nem csúszkál láthatóan a hajó a hullámokon.

Egyetlen égbolt-függvény mindenhez
Az égbolt kupolája, a víz tükröződése és a köd minden mesh-en ugyanazt a GLSL-függvényt hívja: skyColor(dir). Szövegdarabként kerül be minden shaderbe, a ködnél onBeforeCompile segítségével a three.js beépített anyagaiba is.
Ez a legtöbbet érő döntés az egész rendererben:
- A tükröződés mindig illik a fölötte lévő éghez. Nincs cube map, amit szinkronban kellene tartani.
- A távoli hegyek a mögöttük lévő ég színébe olvadnak, nem egy lapos ködszürkébe.
- Ha az ég változik, minden követi, ezért lett a nap-éj ciklus szinte ingyen.
Egy ötperces nap
A kikötő egyetlen alkonyat köré épült. A teljes ciklushoz a napnak mozognia kellett, és mindennek, ami „alkonyatra” épített, a nap állását kellett figyelnie helyette.
A nap és a hold együtt forog egy tengely körül, amely 35°-kal dől a horizonthoz képest, így a déli nap magasan a nyugati égen áll: a város előtt, nem mögötte. Az első változatban a kikötő egész nap ellenfényben volt, és a város úgy nézett ki, mint egy kartondíszlet.
A nap magasságából számolt két érték vezérel mindent:
export const dayAmount = (sunY: number) => smoothstep(-0.06, 0.3, sunY)
export const duskAmount = (sunY: number) => Math.exp(-(((sunY + 0.02) / 0.13) ** 2)) Minden színnek három értéke van (éjszaka, nappal és alkonyat): a dayAmount szerint keveredik éjszakából nappalba, aztán a duskAmount szerint az alkonyat felé. Ugyanezek a görbék GLSL-ben is megvannak, így az ég, a víz, a félgömb fény, a tone mapping expozíciója, a köd sűrűsége és még a párafelhők árnyalata is egyezik.


A képalapú megvilágítás volt a trükkös rész. A PBR-anyagok környezeti térképe PMREMGenerator-ral sül ki az égből. Ez korábban egyszer, induláskor történt, a mozgó nappal viszont most az égi időben mérve pár másodpercenként újra. Az előző render targetet minden alkalommal fel kell szabadítani, különben folyamatosan nő a GPU-memória.
Többjátékos mód egy kb. 280 soros relével
A többjátékos módnak olcsón hosztolhatónak és unalmasan üzemeltethetőnek kellett lennie. A szerver egy függőségek nélküli WebSocket-relé: RFC 6455 keretezés a node:http fölött, nginx mögött. A játékról szinte semmit sem tud. Amit csinál:
- nyilvántart legfeljebb 24 játékost;
- hostot választ: azt a játékost, aki a legrégebben csatlakozott;
- továbbítja a hajók állapotát és az ágyúlövéseket a játékosok között;
- a játékműveleteket a hosthoz küldi;
- eltárolja a host legutóbbi világ-pillanatképét, hogy az újonnan érkezők – vagy egy újonnan választott host – bekapcsolódhassanak a futó küldetésbe.
A host böngészője futtatja a mérvadó játéklogikát: küldetések, kalózok, ki melyik hajótöröttet mentette ki. Ha a host kilép, a következő leghosszabb ideje csatlakozott játékos veszi át a tárolt pillanatképből. Ez nem egy kompetitív lövöldözős játék, úgyhogy rendben van megbízni egy kliensben – cserébe a szerver szinte semmibe sem kerül.

60 fps egy laptopon
Egy portfólión fut, ezért már az első látogatásnak is gördülékenynek kell lennie, bármin nézi a látogató:
- Adaptív felbontás: a renderer figyeli az átlagos képkockaidőt. Ha kb. 21 ms fölé megy, a pixelarány egy lépcsőt csökken, kb. 14 ms alatt visszanő. A legtöbb laptop magától beáll egy éles képre, senkinek sem kell beállításokhoz nyúlnia.
- Instanced meshek az ismétlődő dolgokhoz: a piac ládái és cölöpei, és minden repülő ágyúgolyó.
- Semmi sem renderelődik, amíg a játék nem látszik, és a ciklus szünetel, ha a fül a háttérben van.
Az apróságok, amelyektől késznek érződik
- Irányítás: billentyűzet, érintéses vezérlés telefonon és gamepad.
- Csökkentett mozgás: a beállítást tiszteletben tartja: nincs kamerarázás, sem kontroller- vagy telefonrezgés.
- Automatikus mentés: a haladás pár másodpercenként és az oldal elhagyásakor a
localStorage-ba kerül. Újratöltés után az „Út folytatása – 3. küldetés az 5-ből” gomb vár. - Nyelvek: angol, német és magyar, mint az oldal többi része.

Próbáld ki
Vitorlát bonts a balazscsorba.com/hu/game oldalon. Ha más is épp az oldalon van, látni fogod a hajóját. Ha pedig kíváncsi vagy a munkám másik felére, írtam arról is, milyen skillekkel jutnak el a kódoló ügynökeim a hibabejelentéstől a pull requestig.