Jak postavit jednoduchou multiplayer 2D shooter hru přes GitHub Pages + Railway + Socket.IO
Postavíme minimální realtime 2D shooter hru. Frontend na GitHub Pages, backend Node.js + Socket.IO na Railway. Nejdřív si ukážeme, jak to funguje, pak jak na to. Čas: 45–60 minut.

Prerekvizity
- Node.js ≥ 20 a npm
- Git a účet na GitHubu
- Účet na Railway (zdarma, přihlášení přes GitHub)
- Editor (VS Code, Cursor, cokoliv)
Proč zrovna tenhle stack
Na realtime multiplayer prototyp je tenhle stack super, protože:
- GitHub Pages je triviální a zdarma na statický frontend
- Railway je bezbolestný způsob, jak hostovat Node server
- Socket.IO řeší realtime komunikaci mezi klientem a serverem bez zbytečné bolesti
- celé to je snadné na pochopení i debug
Není to jediný možný stack. Ale na první multiplayer hru je rozumný.
Nejdřív: jak funguje realtime multiplayer
U běžné webovky často platí:
- frontend něco zobrazí
- klikneš
- něco se uloží
- stránka se aktualizuje
U realtime multiplayer hry to funguje jinak:
- každý hráč má otevřený svůj klient
- klient posílá vstupy na server
- server drží pravdu o stavu hry
- server pravidelně posílá aktuální stav všem hráčům
- klient ten stav jen vykresluje
To je zásadní.
Architektura na jedné obrazovce
┌──────────────┐ input (WASD, myš, click) ┌──────────────┐
│ │ ──────────────────────────▶ │ │
│ KLIENT A │ │ │
│ (browser) │ ◀────────────────────────── │ SERVER │
│ │ state (60× za s) │ (Node.js + │
└──────────────┘ │ Socket.IO) │
│ │
┌──────────────┐ input │ source of │
│ KLIENT B │ ──────────────────────────▶ │ truth │
│ (browser) │ ◀────────────────────────── │ │
│ │ state └──────────────┘
└──────────────┘
Co běží kde
Klient (frontend) typicky řeší vykreslení hry, ovládání hráče, myš/klávesnici, kameru a UI.
Server (backend) typicky řeší, kdo je připojený, kde kdo je, pohyb hráčů, střelbu, kolize, damage, respawn a rozesílání stavu všem klientům.
Source of truth
U multiplayeru musíš mít jasno v tom, co je source of truth.
V tomhle projektu je source of truth server.
To znamená:
- klient si může myslet, že se pohnul
- ale skutečný stav hráče určuje server
- klient jen posílá input a renderuje, co server vrátí
To je důležité nejen kvůli synchronizaci, ale i kvůli tomu, aby sis nevytvořil chaos.
Tick rate — jak často se co děje
V kódu uvidíš dvě frekvence:
- server tick 60 Hz (
setInterval(gameLoop, 1000/60)) — server 60× za sekundu přepočítá stav a rozešle ho klientům - client input rate 30 Hz (
setInterval(sendInput, 1000/30)) — klient 30× za sekundu posílá, které klávesy drží
Je to kompromis: server chceš plynulý, input nechceš, aby klienta / server zahltil. 60/30 je bezpečná výchozí hodnota.
Realtime multiplayer vs. „jen sync přes databázi"
Je dobré chápat rozdíl mezi dvěma typy multiplayeru.
1. Realtime multiplayer
Používáš třeba Socket.IO.
Vhodné pro:
- střílečky
- pohyb postav
- závody
- akční hry
- cokoliv, kde se něco děje každých pár milisekund
Charakteristika:
- server běží pořád
- komunikace je průběžná
- stav se neustále přepočítává
- hráči musí vidět změny skoro okamžitě
2. „Multiplayer" přes databázi / sync
Používáš třeba Firebase nebo jinou databázi.
Vhodné pro:
- tahové hry
- sdílené mapy
- jednoduché lobby
- scoreboards
- aplikace, kde nevadí menší zpoždění
Charakteristika:
- stav se spíš ukládá než simuluje
- změny jsou méně časté
- nepotřebuješ herní loop na serveru
Na 2D shooter chceš realtime multiplayer.
Jak o tom přemýšlet při vibe codingu
Když necháš AI generovat multiplayer hru, největší chyba je chtít všechno najednou bez struktury.
Správný postup je:
- nejdřív si ujasnit architekturu
- rozdělit projekt na frontend a backend
- přesně definovat, co dělá klient a co server
- nechat AI vytvořit minimální funkční verzi
- teprve potom přidávat další věci
U multiplayeru je skoro vždycky potřeba rozdělit projekt na tyhle části:
- client/ – frontend
- server/ – backend
A uvnitř těch složek mít malé, jasné soubory.
Struktura projektu
Vytvoř si složku projektu takto:
simple-shooter/
client/
index.html
style.css
game.js
server/
package.json
server.js
Co dělá která složka
client/ obsahuje frontend, který poběží na GitHub Pages.
index.html– kostra stránkystyle.css– stylygame.js– logika klienta: render, input, socket připojení
server/ obsahuje Node server, který poběží na Railway.
package.json– Node dependencies a start scriptserver.js– herní server, Socket.IO, stav hráčů, bullets, game loop
Konkrétní projekt: jednoduchá 2D shooter hra
Uděláme minimální top-down hru:
- hráč se pohybuje pomocí WASD
- míří myší
- kliknutím střílí
- ostatní hráči jsou vidět v reálném čase
- střely dávají damage
- při smrti se hráč respawne
Krok 1: vytvoř složky
Ve Windows si otevři terminál v místě, kde chceš projekt mít, a spusť:
mkdir simple-shooter
cd simple-shooter
mkdir client
mkdir server
Krok 2: backend
Soubor server/package.json
Vytvoř v server/ soubor package.json s tímto obsahem:
{
"name": "simple-shooter-server",
"version": "1.0.0",
"description": "Minimal multiplayer shooter server with Socket.IO",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"socket.io": "^4.8.1"
}
}
Soubor server/server.js
const http = require("http");
const { Server } = require("socket.io");
const PORT = process.env.PORT || 3000;
// Po deployi změň FRONTEND_URL na svůj GitHub Pages origin.
// Např. https://tvujucet.github.io
const FRONTEND_URL = process.env.FRONTEND_URL || "http://127.0.0.1:5500";
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Shooter server is running");
});
const io = new Server(server, {
cors: {
origin: FRONTEND_URL,
methods: ["GET", "POST"]
}
});
const WORLD = {
width: 1600,
height: 900
};
const PLAYER_RADIUS = 18;
const BULLET_RADIUS = 5;
const PLAYER_SPEED = 260;
const BULLET_SPEED = 700;
const FIRE_COOLDOWN = 180;
const MAX_HP = 100;
const BULLET_DAMAGE = 25;
const players = {};
const bullets = [];
function randomSpawn() {
return {
x: 100 + Math.random() * (WORLD.width - 200),
y: 100 + Math.random() * (WORLD.height - 200)
};
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function distanceSquared(ax, ay, bx, by) {
const dx = ax - bx;
const dy = ay - by;
return dx * dx + dy * dy;
}
function respawnPlayer(player) {
const spawn = randomSpawn();
player.x = spawn.x;
player.y = spawn.y;
player.hp = MAX_HP;
}
io.on("connection", (socket) => {
const spawn = randomSpawn();
players[socket.id] = {
id: socket.id,
x: spawn.x,
y: spawn.y,
angle: 0,
hp: MAX_HP,
lastShotAt: 0,
input: {
up: false,
down: false,
left: false,
right: false
}
};
socket.emit("init", {
id: socket.id,
world: WORLD
});
socket.on("input", (input) => {
const p = players[socket.id];
if (!p) return;
p.input.up = !!input.up;
p.input.down = !!input.down;
p.input.left = !!input.left;
p.input.right = !!input.right;
if (typeof input.angle === "number") {
p.angle = input.angle;
}
});
socket.on("shoot", ({ angle }) => {
const p = players[socket.id];
if (!p) return;
const now = Date.now();
if (now - p.lastShotAt < FIRE_COOLDOWN) return;
p.lastShotAt = now;
p.angle = angle;
bullets.push({
ownerId: socket.id,
x: p.x + Math.cos(angle) * 24,
y: p.y + Math.sin(angle) * 24,
vx: Math.cos(angle) * BULLET_SPEED,
vy: Math.sin(angle) * BULLET_SPEED,
bornAt: now
});
});
socket.on("disconnect", () => {
delete players[socket.id];
});
});
let lastTime = Date.now();
function gameLoop() {
const now = Date.now();
const dt = (now - lastTime) / 1000;
lastTime = now;
for (const id in players) {
const p = players[id];
let dx = 0;
let dy = 0;
if (p.input.up) dy -= 1;
if (p.input.down) dy += 1;
if (p.input.left) dx -= 1;
if (p.input.right) dx += 1;
const len = Math.hypot(dx, dy) || 1;
dx /= len;
dy /= len;
p.x += dx * PLAYER_SPEED * dt;
p.y += dy * PLAYER_SPEED * dt;
p.x = clamp(p.x, PLAYER_RADIUS, WORLD.width - PLAYER_RADIUS);
p.y = clamp(p.y, PLAYER_RADIUS, WORLD.height - PLAYER_RADIUS);
}
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
b.x += b.vx * dt;
b.y += b.vy * dt;
const tooOld = now - b.bornAt > 1500;
const outside =
b.x < -50 ||
b.y < -50 ||
b.x > WORLD.width + 50 ||
b.y > WORLD.height + 50;
let removed = false;
for (const id in players) {
const p = players[id];
if (id === b.ownerId) continue;
const hitDist = PLAYER_RADIUS + BULLET_RADIUS;
if (distanceSquared(b.x, b.y, p.x, p.y) <= hitDist * hitDist) {
p.hp -= BULLET_DAMAGE;
if (p.hp <= 0) {
respawnPlayer(p);
}
bullets.splice(i, 1);
removed = true;
break;
}
}
if (removed) continue;
if (tooOld || outside) {
bullets.splice(i, 1);
}
}
io.emit("state", {
players,
bullets
});
}
setInterval(gameLoop, 1000 / 60);
server.listen(PORT, "0.0.0.0", () => {
console.log(`Server listening on port ${PORT}`);
console.log(`Allowed frontend origin: ${FRONTEND_URL}`);
});
Krok 3: nainstaluj backend
npm install spouštěj ve složce server, protože tam je package.json.
cd server
npm install
npm start
Tím spustíš backend na http://localhost:3000.
Tip: Po
npm installcommitni ipackage-lock.json. Bez něj ti Railway může při deployi stáhnout jinou verzi Socket.IO, než má klient (CDN fixuje4.8.1).
Krok 4: frontend
Teď vytvoř frontend.
Soubor client/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simple Multiplayer Shooter</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="ui">
<div>WASD to move</div>
<div>Mouse to aim</div>
<div>Click to shoot</div>
<div id="status">Connecting...</div>
</div>
<canvas id="game"></canvas>
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
<script src="game.js"></script>
</body>
</html>
Soubor client/style.css
html, body {
margin: 0;
padding: 0;
background: #111;
overflow: hidden;
font-family: Arial, sans-serif;
color: white;
}
#game {
display: block;
width: 100vw;
height: 100vh;
background: #1b1b1b;
}
#ui {
position: fixed;
left: 12px;
top: 12px;
z-index: 10;
background: rgba(0, 0, 0, 0.45);
padding: 10px 12px;
border-radius: 10px;
line-height: 1.5;
font-size: 14px;
}
Soubor client/game.js
// Po deployi změň na Railway URL.
// Např. https://moje-hra-production.up.railway.app
const SERVER_URL = "http://localhost:3000";
const socket = io(SERVER_URL, {
transports: ["websocket", "polling"]
});
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const statusEl = document.getElementById("status");
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener("resize", resize);
resize();
let myId = null;
let world = { width: 1600, height: 900 };
let state = {
players: {},
bullets: []
};
const input = {
up: false,
down: false,
left: false,
right: false,
angle: 0
};
const mouse = {
x: 0,
y: 0
};
window.addEventListener("keydown", (e) => {
const key = e.key.toLowerCase();
if (key === "w") input.up = true;
if (key === "s") input.down = true;
if (key === "a") input.left = true;
if (key === "d") input.right = true;
});
window.addEventListener("keyup", (e) => {
const key = e.key.toLowerCase();
if (key === "w") input.up = false;
if (key === "s") input.down = false;
if (key === "a") input.left = false;
if (key === "d") input.right = false;
});
canvas.addEventListener("mousemove", (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
});
canvas.addEventListener("mousedown", () => {
socket.emit("shoot", { angle: input.angle });
});
socket.on("connect", () => {
statusEl.textContent = "Connected";
});
socket.on("disconnect", () => {
statusEl.textContent = "Disconnected";
});
socket.on("init", (data) => {
myId = data.id;
world = data.world;
});
socket.on("state", (serverState) => {
state = serverState;
});
function sendInput() {
const me = state.players[myId];
if (me) {
const camera = getCamera(me);
const worldMouseX = mouse.x + camera.x;
const worldMouseY = mouse.y + camera.y;
input.angle = Math.atan2(worldMouseY - me.y, worldMouseX - me.x);
}
socket.emit("input", input);
}
setInterval(sendInput, 1000 / 30);
function getCamera(me) {
const x = me.x - canvas.width / 2;
const y = me.y - canvas.height / 2;
return { x, y };
}
function drawGrid(camera) {
const size = 80;
ctx.strokeStyle = "#2a2a2a";
ctx.lineWidth = 1;
const startX = -((camera.x % size) + size) % size;
const startY = -((camera.y % size) + size) % size;
for (let x = startX; x < canvas.width; x += size) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = startY; y < canvas.height; y += size) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
}
function drawWorldBounds(camera) {
ctx.strokeStyle = "#555";
ctx.lineWidth = 4;
ctx.strokeRect(-camera.x, -camera.y, world.width, world.height);
}
function drawPlayer(player, isMe, camera) {
const x = player.x - camera.x;
const y = player.y - camera.y;
ctx.beginPath();
ctx.arc(x, y, 18, 0, Math.PI * 2);
ctx.fillStyle = isMe ? "#4caf50" : "#2196f3";
ctx.fill();
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(
x + Math.cos(player.angle) * 28,
y + Math.sin(player.angle) * 28
);
ctx.strokeStyle = "#f5f5f5";
ctx.lineWidth = 5;
ctx.stroke();
const hpWidth = 40;
const hp = Math.max(0, player.hp) / 100;
ctx.fillStyle = "#400";
ctx.fillRect(x - hpWidth / 2, y - 32, hpWidth, 6);
ctx.fillStyle = "#f44";
ctx.fillRect(x - hpWidth / 2, y - 32, hpWidth * hp, 6);
}
function drawBullet(bullet, camera) {
const x = bullet.x - camera.x;
const y = bullet.y - camera.y;
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fillStyle = "#ffd54f";
ctx.fill();
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const me = state.players[myId];
const camera = me ? getCamera(me) : { x: 0, y: 0 };
drawGrid(camera);
drawWorldBounds(camera);
for (const bullet of state.bullets) {
drawBullet(bullet, camera);
}
for (const id in state.players) {
drawPlayer(state.players[id], id === myId, camera);
}
requestAnimationFrame(render);
}
render();
Krok 5: jak frontend lokálně pustit
Frontend je statický, takže nepotřebuje vlastní backend. Musíš ho ale servovat přes jednoduchý server, ne otevírat přes file://.
Ve složce client spusť:
npx serve .
(Když npx vyhodí dotaz na instalaci balíčku, potvrď y.)
Pak otevři URL, které ti to vypíše.
Krok 6: lokální test
Teď bys měl mít:
- backend na
http://localhost:3000 - frontend přes
npx serve .
Otevři frontend ve dvou oknech prohlížeče.
Měl bys vidět:
- dva hráče
- pohyb
- střelbu
- synchronizaci
Testování z jiného zařízení
Pokud chceš zkusit hru z telefonu nebo jiného notebooku ve stejné Wi-Fi, spusť frontend tak, aby poslouchal na všech rozhraních:
npx serve . -l 0.0.0.0:5000
Pak se z druhého zařízení připoj přes IP notebooku (např. http://192.168.1.42:5000). V game.js ale musí být SERVER_URL dostupný z druhého zařízení — pro lokální test to znamená stejný trik i pro backend (pustit ho na 0.0.0.0:3000, což už server.listen dělá), a v SERVER_URL použít IP notebooku místo localhost.
Krok 7: deploy backendu na Railway
Push serveru na GitHub
Na GitHubu si vytvoř nový repozitář, třeba simple-shooter-server. GitHub ti hned po vytvoření ukáže blok s git příkazy („…or push an existing repository from the command line") — ten zkopíruj a spusť ve složce server. Bude to zhruba:
git init
git remote add origin https://github.com/TVOJ_UCET/simple-shooter-server.git
git branch -M main
Pak první commit a push:
git add .
git commit -m "Initial multiplayer shooter server"
git push -u origin main
Od téhle chvíle ti stačí na každou další změnu:
git add .
git commit -m "Popis změn"
git push
Railway
- otevři Railway (přihlášený přes GitHub)
- vytvoř nový projekt
- zvol deploy z GitHub repa a vyber server repo
- nastav start command na
npm start
Po deployi dostaneš URL, např. https://simple-shooter-server-production.up.railway.app.
Pozor na free tier: Railway zdarma má měsíční limit hodin a server po nečinnosti uspává. Pro osobní hraní to stačí, ale první připojení po delší pauze může trvat pár sekund, než se server probudí.
Krok 8: deploy frontendu na GitHub Pages
Stejným způsobem jako server: na GitHubu vytvoř druhý repo simple-shooter-client, zkopíruj si z něj git příkazy a spusť je ve složce client. Pak:
git add .
git commit -m "Initial multiplayer shooter client"
git push -u origin main
V GitHub repu nastav Pages:
- otevři Settings → Pages
- Deploy from a branch
- branch:
main, folder:/ (root)
Pak dostaneš frontend URL, např. https://tvujucet.github.io/simple-shooter-client/.
Krok 9: propojení frontendu a backendu
Teď musíš změnit dvě věci.
1. V client/game.js
Najdi:
const SERVER_URL = "http://localhost:3000";
A změň na Railway URL:
const SERVER_URL = "https://simple-shooter-server-production.up.railway.app";
Pak frontend znovu commitni a pushni.
2. Na Railway nastav FRONTEND_URL
V Railway u serveru přidej proměnnou:
FRONTEND_URL=https://tvujucet.github.io
Pozor: origin, ne celá URL stránky — bez cesty za doménou a bez lomítka na konci.
Pak server redeployni.
Co dělají jednotlivé soubory v praxi
Tohle je důležité, protože přesně tahle struktura ti pomůže i u jiných multiplayer her.
client/index.html
Jen kostra stránky: canvas, UI box, načtení Socket.IO klienta, načtení game.js.
client/style.css
Jen vzhled: fullscreen canvas, jednoduché UI.
client/game.js
Mozek klienta:
- připojí se na socket server
- sbírá input
- posílá input serveru
- přijímá stav světa
- renderuje hráče a střely
server/package.json
Říká Node, jak se projekt jmenuje, jaké balíčky nainstalovat a co spustit přes npm start.
server/server.js
Mozek serveru:
- sleduje připojené hráče
- ukládá jejich input
- počítá pohyb
- vytváří bullets
- řeší damage
- rozesílá stav světa všem
Limity tohoto přístupu
Ať víš, na co tenhle minimální setup narazí, kdybys s ním chtěl jít dál:
- Trhaný pohyb ostatních hráčů. Ostatní hráči se na klientu jen teleportují do pozice, kterou server naposledy poslal. Bez interpolace to vypadá škubavě. Další logický krok: interpolace mezi dvěma posledními stavy.
- Pocitová latence u vlastního hráče. Klient si nepředpovídá pohyb, čeká na server. Na dobré lince tě to nepraští, na špatné ano. Další krok: client-side prediction + server reconciliation.
- Žádný anti-cheat. Server je sice authoritative, ale klient může lhát o úhlu střely. Pro hru mezi kámošema v pohodě, pro veřejnou hru ne.
- Žádný persistent state. Restartem serveru zmizí všechno. Až budeš chtít score přes sessions, potřebuješ databázi.
Jak tenhle princip použít na jakoukoliv multiplayer hru
Skoro každá základní realtime multiplayer hra půjde rozložit stejně:
Klient
- input
- kamera
- render
- UI
Server
- entities
- pravidla hry
- fyzika / pohyb
- kolize
- synchronizace
Sdílený pattern
- klient pošle vstup
- server přepočítá stav
- server pošle stav zpět
- klient vykreslí
Ať děláš shooter, závodní hru, coop sbírací hru nebo survival arénu — ten pattern bude skoro stejný. Změní se hlavně typ entit, pravidla, způsob kolizí a win condition.
Master prompt pro AI
Tady je univerzální prompt, kterým si můžeš nechat AI vést celý proces krok za krokem.
Chci, aby ses choval jako praktický technický průvodce a provedl mě krok za krokem sestavením velmi jednoduché realtime multiplayer browser hry na přesně tomhle stacku:
- Frontend: čisté HTML / CSS / JavaScript
- Hosting frontendu: GitHub Pages
- Backend: Node.js + Socket.IO
- Hosting backendu: Railway
Mým cílem není jen získat kód, ale opravdu pochopit, jak ten setup funguje.
Důležitý kontext:
- Už si umím vibecodit jednoduché webovky.
- Chci pochopit hlavně tu multiplayer část.
- Chci, aby vysvětlení bylo praktické, strukturované a přívětivé pro začátečníky, speciálně co se týče multiplayer architektury.
- Předpokládej, že jsem na Windows.
- Používej čistý JavaScript, žádný frontend framework, pokud to není absolutně nutné.
- Drž projekt co nejjednodušší.
Chci, abys mě provedl stavbou minimální multiplayer 2D shooter hry, kde:
- hráči se pohybují pomocí WASD
- hráči míří myší
- hráči střílí
- může se připojit víc hráčů
- server je source of truth
- hráči respawnou po smrti
Postupuj přesně v tomhle pořadí:
1. Nejdřív vysvětli architekturu na vysoké úrovni:
- co běží na frontendu
- co běží na backendu
- jak spolu frontend a backend komunikují
- co znamená „server je source of truth"
- proč je tenhle stack dobrá jednoduchá volba
2. Pak ukaž kompletní strukturu složek projektu.
3. Pak vysvětli, k čemu každý soubor je, ještě než mi dáš kód.
4. Pak mi dej kód soubor po souboru, vždycky jeden soubor, a u každého:
- přesnou cestu k souboru
- kompletní kód
- krátké vysvětlení, co daný soubor dělá
5. Pak mi dej přesné Windows terminálové příkazy na:
- vytvoření složek
- inicializaci backendu
- spuštění `npm install` na správném místě
- spuštění serveru
- lokální servování frontendu
6. Pak vysvětli, jak to lokálně otestovat ve dvou oknech prohlížeče.
7. Pak přesně vysvětli, jak deploynout:
- backend na Railway
- frontend na GitHub Pages
8. Pak přesně vysvětli, co za URL se musí po deployi změnit:
- kam přijde Railway URL
- kam přijde GitHub Pages origin
- co znamená `FRONTEND_URL`
- proč na tom záleží
9. Pak mi dej krátkou troubleshooting sekci pro nejčastější chyby při setupu:
- špatná složka pro `npm install`
- špatná GitHub Pages URL / origin
- frontend otevřený přes `file://` místo lokálního serveru
- CORS / connection problémy
- Railway port problémy
10. Nakonec krátce vysvětli, jak se tahle stejná struktura dá zobecnit skoro na jakoukoliv jednoduchou realtime multiplayer hru.
Důležité pokyny ke stylu:
- Buď stručný, praktický a systematický.
- Nepřeskakuj kroky.
- Nepředpokládej, že znám detaily backend deploymentu.
- Nenahazuj všechno najednou bez struktury.
- Preferuj nejjednodušší funkční verzi před nejpokročilejší.
- Kde se začátečníci typicky zaseknou, upozorni na to jasně.
- Používej kód a příkazy, které jdou přímo copy-paste.
- Nepoužívej zbytečné abstrakce.
- Nepoužívej TypeScript, pokud si o něj neřeknu.
- Nepoužívej frontend framework, pokud si o něj neřeknu.
Než začneš psát kód, krátce mi řekni celkový plán v 5–8 bodech.
Co dělat jako další krok
Jakmile tohle funguje, můžeš přidávat další věci:
- jména hráčů
- score a kill counter
- překážky
- různé zbraně
- respawn delay
- lobby a rooms
- mapu
- animace zásahu
- client-side interpolaci pro plynulejší pohyb ostatních hráčů
První cíl je ale jednoduchý:
dostat funkční realtime multiplayer loop od inputu přes server až po render.
Jakmile tohle chápeš, multiplayer už přestává být magie a stává se z něj jen další typ architektury.
Shrnutí
Na první multiplayer browser game nepotřebuješ nic překomplikovaného. Stačí ti pochopit:
- klient není source of truth
- server drží stav hry
- klient posílá input
- server vrací stav
- frontend a backend mají být oddělené
- statický frontend může běžet na GitHub Pages
- realtime server může běžet na Railway
- Socket.IO je rozumný nástroj pro první prototyp
A pak si to rozsekat na malé soubory a malé kroky.
Pokud si to postavíš, pošli mi screenshot nebo odkaz — zajímá mě, jak se ti to povedlo rozšířit. A když narazíš na něco, co v návodu chybí, dej vědět, ať to doplním.