Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 175 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,109 @@ A real-time multiplayer platformer game built with HTML5 Canvas, Socket.IO, and
- **Spatial grid collision** — O(1) platform lookup for server-side physics
- **Particle system** — pooled, zero-allocation particle effects on the client
- **Configurable tick rate** — server tick rate is tunable via environment variable
- **Horizontally scalable** — game-server pods self-register with the server-manager; the gateway routes each room to the least-loaded server

---

## 🗂️ Architecture
## 🏛️ Architecture

### Old Architecture

All clients connected directly to a single monolithic game server. Simple, but impossible to scale.

![Old Architecture](./archRef/ArchOld.png)

### New Architecture

A layered system with a **Gateway**, a **Server Manager (orchestrator)**, and a fleet of **game-server pods**.

![New Architecture](./archRef/ArchNew.png)

#### How a room is created

1. **Client → Gateway** (`GET /createRoom`): The client asks the gateway to allocate a room.
2. **Gateway → Server Manager** (`GET /assign`): The gateway asks the orchestrator for the least-loaded game-server. The server-manager maintains a min-heap sorted by active connections and returns the best server's `hostIp` and internal port.
3. **Gateway → Client**: Returns `{ hostIp, port, roomID }`. The gateway stores the room-code → server mapping in memory (in-process map; Redis migration is a TODO).
4. **Client → Game Server** (WebSocket `wss://server.boxgame.shadyggs.xyz:<port>`): The client opens a Socket.IO connection directly to the assigned game-server pod.

> The client connects **directly** to the game-server pod over WebSocket — it never proxies game traffic through the gateway. The gateway is only involved during matchmaking.

#### TLS & port mapping (production)

Game-server pods use `hostNetwork` and listen on **internal** ports **30000–31000** on the VM. The browser requires `wss://` (mixed-content rules). A **host-level Nginx stream** block terminates TLS on **external** ports **20000–21000** and forwards plaintext to the matching internal port (`external + 10 000 = internal`).

```
Browser → wss://server.boxgame.shadyggs.xyz:20426 (TLS)
→ Nginx stream :20426
→ 127.0.0.1:30426 (game-server pod, plaintext)
```

Port blocks are generated by `scripts/gen-stream-ports.sh` and included by `nginx-gameserver-stream.conf`.

---

## 🗂️ Repository Structure

```
BoxGame/
├── client/ # Static frontend (HTML + Canvas + Socket.IO client)
│ ├── index.html
│ ├── index.js # Lobby, socket connection, HUD logic
│ ├── game.js # Canvas rendering, particles, input handling
│ └── styles.css
├── server/ # Game server (Node.js + Express + Socket.IO)
│ ├── server.js # Physics loop, room management, socket events
├── client/ # Static frontend (HTML5 Canvas + Socket.IO)
│ ├── index.html # Entry point / lobby UI
│ ├── index.js # Lobby logic, room creation/joining, HUD
│ ├── game.js # Canvas renderer, particle system, input handling
│ ├── styles.css # Lobby & game styles
│ ├── icon.png # App icon / banner
│ └── heart.png # UI asset (lives / branding)
├── server/ # Game server (Node.js + Socket.IO)
│ ├── server.ts # Physics loop, room management, socket events,
│ │ # self-registers with server-manager on startup
│ ├── config.yaml # Example k8s Pod spec for a game-server pod
│ ├── package.json
│ └── Dockerfile
├── gateway/ # Matchmaking / room-allocation service (Bun + TypeScript)
│ ├── index.ts # Express app; exposes /createRoom and /add
│ ├── routes/
│ │ ├── createRoom.ts # Allocates a room: queries server-manager, maps code→server
│ │ ├── addToRoom.ts # Adds a client to an existing room
│ │ └── removeFromRoom.ts # Removes a client from a room
│ ├── helper/
│ │ └── generateRoom.ts # Generates random room codes
│ ├── types/
│ │ └── server.ts # Shared Server type
│ ├── package.json
│ ├── tsconfig.json
│ └── Dockerfile
└── gateway/ # Optional auth/routing gateway (Bun + TypeScript + Redis)
├── index.ts
└── routes/
├── addToRoom.ts
└── removeFromRoom.ts
├── server-manager/ # Orchestrator / load-balancer (Bun + TypeScript)
│ ├── index.ts # Express app; exposes /assign and /register
│ ├── routes/
│ │ ├── assignServer.ts # Pops best server from heap, increments connections, returns it
│ │ └── handleRegister.ts # Game-server pods POST here on startup to join the pool
│ ├── helper/
│ │ └── heap.ts # Min-heap sorted by active connections (O(log n) assign)
│ ├── types/
│ │ └── server.ts # Shared Server type { hostIp, port, connections }
│ ├── package.json
│ ├── tsconfig.json
│ └── Dockerfile
├── scripts/
│ └── gen-stream-ports.sh # Generates Nginx stream server blocks for ports 20000–21000
├── archRef/
│ ├── ArchOld.png # Monolithic architecture diagram
│ └── ArchNew.png # Distributed architecture diagram
├── nginx-gameserver-stream.conf # Host-level Nginx stream config (TLS termination)
├── k8s.yaml # Production Kubernetes manifests (all services)
├── k8s.local.yaml # Local/dev Kubernetes manifests
├── .env # Root-level env vars
├── CONTRIBUTING.md
└── package-lock.json
```

### How it works

1. A player opens the client, enters a name, and **creates or joins** a room via the gateway/server.
2. The game server runs a fixed-interval **physics loop** at the configured tick rate (default 120 Hz).
3. Each tick the server broadcasts a **delta-state** diff to clients; a full sync is sent every ~2 seconds.
4. The client renders at `requestAnimationFrame` speed, interpolating the received state.

---

## 🚀 Getting Started
Expand All @@ -56,9 +128,9 @@ BoxGame/
|------|---------|
| Node.js | ≥ 18 |
| npm | ≥ 9 |
| Bun *(gateway only)* | ≥ 1.1 |
| Redis *(gateway only)* | ≥ 7 |
| Bun *(gateway & server-manager)* | ≥ 1.1 |
| Docker *(optional)* | ≥ 24 |
| kubectl + k8s cluster *(production)* | — |

### 1. Clone the repository

Expand All @@ -67,24 +139,62 @@ git clone https://github.com/your-username/BoxGame.git
cd BoxGame
```

### 2. Start the game server
### 2. Start the server-manager

The server-manager must be running before game servers start, because each server registers itself on startup.

```bash
cd server-manager
bun install
bun run index.ts
# Listens on http://localhost:4689
```

**Environment variables (`server-manager/.env`):**

| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `4689` | HTTP port |

### 3. Start one or more game servers

Each server picks a random port, starts the Socket.IO game loop, and self-registers with the server-manager.

```bash
cd server
npm install
node server.js
# Point it at your server-manager
SERVER_MANAGER_URL=http://localhost:4689 npx ts-node server.ts
```

The server listens on `http://localhost:3000` by default.

**Environment variables (`server/.env`):**

| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `3000` | HTTP port |
| `SERVER_MANAGER_URL` | — | URL of the server-manager |
| `HOST_IP` | — | IP reported to server-manager (clients use this to connect) |
| `PORT_MIN` | `0` | Lower bound for random port selection |
| `PORT_MAX` | `0` | Upper bound (`0` = OS-assigned) |
| `TICK` | `120` | Game tick rate (Hz) |
Comment on lines +174 to 178

### 3. Serve the client
### 4. Start the gateway

```bash
cd gateway
bun install
SERVER_MANAGER_URL=http://localhost:4689 bun run index.ts
# Listens on http://localhost:3000
```

**Environment variables (`gateway/.env`):**

| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `3000` | HTTP port |
| `SERVER_MANAGER_URL` | — | URL of the server-manager |
| `SERVER_HOST_DOMAIN` | `localhost` | Public hostname clients use to reach game servers (production: `server.boxgame.shadyggs.xyz`) |

### 5. Serve the client

The `client/` directory is plain static HTML — serve it with any static file server:

Expand All @@ -98,31 +208,48 @@ python3 -m http.server 8080 --directory client/

Then open `http://localhost:8080` in your browser.

> **Important:** Update the Socket.IO server URL in `client/index.js` to point to your running game server.
> **Important:** Update the gateway URL in `client/index.js` to point to your running gateway.

### 4. (Optional) Start the gateway

The gateway provides a Redis-backed routing layer for assigning players to rooms.
### 6. Docker (individual services)

```bash
cd gateway
bun install
# Game server
cd server && docker build -t boxgame-server . && docker run -p 30000:30000 boxgame-server

# Set environment variables
export REDIS_URL=redis://localhost:6379
export PORT=4000
# Gateway
cd gateway && docker build -t boxgame-gateway . && docker run -p 3000:3000 boxgame-gateway
Comment on lines +216 to +220

bun run index.ts
# Server-manager
cd server-manager && docker build -t boxgame-server-manager . && docker run -p 4689:4689 boxgame-server-manager
```

### 5. Docker (server only)
---

## ☸️ Kubernetes Deployment (Production)

All manifests are in `k8s.yaml`. It provisions two namespaces:

| Namespace | Services |
|-----------|----------|
| `box-game` | `gateway`, `server-manager` |
| `box-game-servers` | dynamically created `game-server` pods |

```bash
cd server
docker build -t boxgame-server .
docker run -p 6000:6000 boxgame-server
kubectl apply -f k8s.yaml
```

Key design decisions:
- **Game-server pods use `hostNetwork: true`** so they bind directly on the VM's network interface and are reachable by the Nginx stream proxy on the host.
- **RBAC**: the `server-manager-sa` ServiceAccount is granted pod create/delete rights in `box-game-servers` so the server-manager can spawn new game-server pods dynamically.
- **Nginx stream** on the host VM (not inside k8s) terminates TLS for `server.boxgame.shadyggs.xyz` on external ports 20000–21000 and forwards to internal pod ports 30000–31000. Generate port blocks with:

```bash
sudo bash scripts/gen-stream-ports.sh
sudo nginx -t && sudo systemctl reload nginx
```

- **Gateway Ingress** (`gateway.boxgame.shadyggs.xyz`) is handled by the nginx ingress controller + cert-manager (Let's Encrypt).

---

## 🎮 Controls
Expand All @@ -137,14 +264,19 @@ docker run -p 6000:6000 boxgame-server

## 🔧 Configuration

### Game constants (`server/server.js`)
### Game constants (`server/server.ts`)

| Constant | Default | Description |
|----------|---------|-------------|
| `gravity` | `0.5` | Downward acceleration per tick |
| `jumpPower` | `-15` | Vertical velocity applied on jump |
| `PLAYER_SPEED` | `3` | Horizontal movement speed |
| `MAX_PLAYERS_PER_ROOM` | `20` | Hard cap per room |
| `tick` | `120` | Physics + broadcast rate (Hz) |

### Server-manager internals

The server-manager uses an in-memory **min-heap** keyed on active connection count. Each `POST /register` from a game-server pod inserts into the heap in O(log n). Each `GET /assign` pops the minimum (least-loaded server), increments its connection count, re-inserts it, and returns the server's `hostIp` / `port` in response headers.

---

Expand Down
Binary file added archRef/ArchNew.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added archRef/ArchOld.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.