> Gerstner-hullámok GPU-n és CPU-n egyformán, egy égboltfüggvény, ötperces nap-éj ciklus és apró WebSocket-relé: a portfóliós játék technikája.
>
> Web page: https://balazscsorba.com/hu/blog/multiplayer-sailing-game-threejs · Language: Magyar · Also available in: [English](https://balazscsorba.com/blog/multiplayer-sailing-game-threejs.md) · [Deutsch](https://balazscsorba.com/de/blog/multiplayer-sailing-game-threejs.md)
> Author: Balázs Csorba · Published: 2026-10-05 · Keywords: three.js játék, three.js többjátékos, gerstner hullámok, webgl óceán shader, nappal éjszaka ciklus three.js, websocket relé, böngészős játékfejlesztés, glsl shader

[Blog](https://balazscsorba.com/hu/blog)/[Webfejlesztés](https://balazscsorba.com/hu/blog/category/web)

# Többjátékos 3D vitorlázós játék tiszta three.js-szel

Gerstner-hullámok GPU-n és CPU-n egyformán, egy égboltfüggvény, ötperces nap-éj ciklus és apró WebSocket-relé: a portfóliós játék technikája.

[Balázs Csorba](https://balazscsorba.com/hu/about)·2026\. október 5.·8 perc olvasás

-   three.js
-   WebGL
-   GLSL
-   WebSockets
-   Game development

![Többjátékos vitorlázós játék three.js-szel: két párhuzamos hullámos sín egymás fölött, felettük kétfelé osztott kör.](https://balazscsorba.com/images/blog/multiplayer-sailing-game-threejs/dusk.webp?v=08d19ae5fb)

## A lényeg röviden

-   Az óceán hat Gerstner-hullámból áll; ugyanazok a paraméterek jutnak a vertex shaderbe és egy CPU-függvénybe, így a hajók azon a hullámon úsznak, amelyet látsz.
-   Az égbolt, a víz tükröződése és a köd ugyanazt a GLSL-függvényt hívja (skyColor(dir)), ettől lett a nappal–éjszaka ciklus szinte ingyen.
-   A nap magasságából számolt két görbe (dayAmount, duskAmount) keveri a színeket, a fényeket és a ködöt, JS-ben és GLSL-ben egyaránt.
-   A többjátékos mód egy függőségek nélküli, kb. 280 soros WebSocket-relén fut; a legrégebben csatlakozott böngésző a host, az futtatja a játéklogikát.
-   Az adaptív pixelarány, az instanced meshek és a képernyőn kívüli szüneteltetés átlagos laptopon is folyamatossá teszi.

Ezen az oldalon

1.  [Hullámok, amelyeken a hajó tényleg úszik](https://balazscsorba.com/#waves)
2.  [Egyetlen égbolt-függvény mindenhez](https://balazscsorba.com/#sky)
3.  [Egy ötperces nap](https://balazscsorba.com/#day-night)
4.  [Többjátékos mód egy kb. 280 soros relével](https://balazscsorba.com/#multiplayer)
5.  [60 fps egy laptopon](https://balazscsorba.com/#performance)
6.  [Az apróságok, amelyektől késznek érződik](https://balazscsorba.com/#details)
7.  [Próbáld ki](https://balazscsorba.com/#try-it)
8.  [Források](https://balazscsorba.com/#sources)

Cikk meghallgatása

0:00

A portfóliómban van egy játék. A [Dragon Voyage](https://balazscsorba.com/hu/game) 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](https://developer.nvidia.com/gpugems/gpugems/part-i-natural-effects/chapter-1-effective-water-simulation-physical-models) ö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:

TypeScript

```
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.

![A dzsunka hátulról, mélykék, hullámzó tengeren délben, két oldalon zöld dombok és fehér felhők.](https://balazscsorba.com/images/blog/multiplayer-sailing-game-threejs/noon.webp?v=1ef0093f4a)

Dél: a hajótest ugyanazon a hat hullámon lovagol, amit a shader rajzol.

## 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:

TypeScript

```
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 kikötőváros délben: kivilágított házsorok, egy pagoda és fenyves dombok egy napsütötte öböl mögött.](https://balazscsorba.com/images/blog/multiplayer-sailing-game-threejs/town-noon.webp?v=b51d60f5ff)

Harbour Town délben, szemből megvilágítva.

![Ugyanez a kikötőváros éjjel: kivilágított ablakok és lampionok tükröződnek a sötét vízen.](https://balazscsorba.com/images/blog/multiplayer-sailing-game-threejs/town-night.webp?v=9624959406)

Ugyanaz a hely éjjel: a lampionok és az ablakok veszik át a fényt.

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.

![A dzsunka éjjel, sötét tengeren, lampionok világítanak az árbocon, előttük fényoszlopok emelkednek a vízből.](https://balazscsorba.com/images/blog/multiplayer-sailing-game-threejs/night.webp?v=5305bd7830)

Éjszaka az öbölben: az árboc lampionjai megvilágítják a vizet a hajótest körü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.

![A dzsunka hajnalban, nyugodt, szürkéskék tengeren, halvány ég alatt.](https://balazscsorba.com/images/blog/multiplayer-sailing-game-threejs/dawn.webp?v=4eaa3c22a8)

Hajnal, nagyjából az ötperces nap felénél.

## Próbáld ki

Vitorlát bonts a [balazscsorba.com/hu/game](https://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](https://balazscsorba.com/hu/blog/coding-agent-skills-workflow). Gyakorlatiasabb példa a [Home Assistant app, amely a villanyautót a legolcsóbb órákban tölti](https://balazscsorba.com/hu/blog/home-assistant-ev-charging-energy-manager).

## Források

1.  [GPU Gems, Chapter 1: Effective Water Simulation from Physical Models](https://developer.nvidia.com/gpugems/gpugems/part-i-natural-effects/chapter-1-effective-water-simulation-physical-models)
2.  [RFC 6455: The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455)

## Gyakori kérdések

Hogyan úszik egy hajó egy three.js óceán-shaderen?

A hullámparamétereket egy tömbben kell tartani, és kétszer felhasználni: shader-uniformként a látható felülethez, és egy CPU-függvényben, amely bármely pontban visszaadja a magasságot és a normált. Ezt a függvényt minden képkockában lekérdezzük a hajótest alatt. Gerstner-hullámoknál előtte néhány iterációval ki kell egyenlíteni az oldalirányú elmozdulást.

Mik azok a Gerstner-hullámok?

A Gerstner- (trochoid) hullámok a felszíni pontokat körpályán mozgatják, nem csak fel-le. Ettől élesek a hullámhegyek és laposak a völgyek. Néhány különböző hullámhosszú, meredekségű és irányú hullám összege olcsó és meggyőző óceán valós idejű grafikához.

Kell-e játékszerver egy kis többjátékos böngészős játékhoz?

Teljes szerver nem. Egy kis WebSocket-relé nyilvántarthatja a játékosokat, kijelölhet egy böngészőt hostnak, továbbíthatja az üzeneteket, és eltárolhatja a host legutóbbi világállapotát. A játéklogikát a host futtatja – ez egy laza kooperatív játékhoz jó, versenyjátékhoz nem, ahol a kliensekben nem lehet megbízni.

Írta: Csorba Balázs

Senior fullstack és AI mérnök Stájerországban – több mint 10 év Vue, Nuxt, Node.js és PHP, ma AI-ügynököknek építek eszközöket.

[Vue.js és Nuxt fejlesztés →](https://balazscsorba.com/hu/expertise/vue-nuxt-developer)[Rólam →](https://balazscsorba.com/hu/about)

## További cikkek

-   [Schema.org strukturált adatok B2B webshopokhoz: bejelentkezés mögötti árak, egységek és változatok](https://balazscsorba.com/hu/blog/schema-org-structured-data-b2b-shop)
-   [AI-asszisztens a B2B webshop ügyfélszolgálatához: eszközök, jogosultságok és átadás embernek](https://balazscsorba.com/hu/blog/ai-assistant-b2b-shop-customer-support)
-   [AI-csevegő UI Vue-ban és Nuxtban: akadálymentes streaming](https://balazscsorba.com/hu/blog/vue-ai-chat-ui-streaming-accessibility)
-   [LLM-fordítási pipeline i18n-hez: glosszárium, helyőrzők és review](https://balazscsorba.com/hu/blog/llm-translation-pipeline-i18n)

## Pont erre van szükséged?

Írj a projektedről vagy a pozícióról – szívesen hallok felőled.

[Lépj velem kapcsolatba](mailto:contact@balazscsorba.com) [Kapcsolódjunk LinkedInen](https://www.linkedin.com/in/balazs-csorba)
