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.

Demo multiplayer shooter hry

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:

  1. nejdřív si ujasnit architekturu
  2. rozdělit projekt na frontend a backend
  3. přesně definovat, co dělá klient a co server
  4. nechat AI vytvořit minimální funkční verzi
  5. 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ánky
  • style.css – styly
  • game.js – logika klienta: render, input, socket připojení

server/ obsahuje Node server, který poběží na Railway.

  • package.json – Node dependencies a start script
  • server.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 install commitni i package-lock.json. Bez něj ti Railway může při deployi stáhnout jinou verzi Socket.IO, než má klient (CDN fixuje 4.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

  1. otevři Railway (přihlášený přes GitHub)
  2. vytvoř nový projekt
  3. zvol deploy z GitHub repa a vyber server repo
  4. 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:

  1. otevři Settings → Pages
  2. Deploy from a branch
  3. 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.

← Blog