Multiplayer Web Game Starter / POC

A starting point for building a web-based multiplayer game, utilizing modern web development technologies.

Multiplayer Web Game Starter / POC

GitHub Repository: https://github.com/martinhjartmyr/multiplayer-web-game-starter

While learning more about Three.js I decided to setup a starting point for building a web-based multiplayer game, utilizing modern web development technologies. The project is implemented in TypeScript, powered by pnpm for package management and Turborepo for efficient monorepo workflows. Code quality is maintained using Prettier and ESLint.

The main idea behind this project is to run the simulation on the server to ensure fair play and prevent cheating. The client is responsible for rendering the visuals, handling user input, and sending it to the server for processing.

Features

  • Multiplayer Support: Multiple players can join the game simultaneously.
  • Physics-Driven Interactions: Server-side collision detection powered by Rapier.
  • Real-Time Synchronization: Player positions and rotations are broadcasted to all connected clients.
  • User Input Handling: The client processes user input and communicates it to the server using WebSockets.

Demo

Players can freely move within the 3D scene, with server-side handling of collision detection and synchronization.

Play

Project Structure

The project consists of two main components:

  • Server (Backend): Handles WebSocket communication, physics simulations, and game state management.
  • Client (Frontend): Renders the 3D environment, processes user input, and sends it to the server via WebSockets.

Server (Backend)

  • Node.js: JavaScript runtime for the server.
  • Hono: Lightweight web framework for WebSocket and API handling.
  • Rapier: High-performance physics engine for collision detection and simulation.

Storing WebSocket Connections and Player Cubes

Data structures to track the active WebSocket connections and player cubes.

const connections: Record<string, WSContext> = {}
const cubes = new Map<string, Cube>()

WebSocket Connection Handling

Define how new WebSocket connections are handled, including generating unique IDs, processing user input, and removing disconnected players.

app.get(
  '/ws',
  upgradeWebSocket(() => {
    return {
      onOpen: (_, ws) => {
        // Handle new WebSocket connections
        // Generate a unique ID for the connection and store it
        // Create a new player cube
      },
      onMessage(event, ws) {
        // Handle incoming WebSocket message
        // Apply user input/forces to the player cube
      },
      onClose: (_, ws) => {
        // Handle WebSocket disconnections
        // Remove the player cube and WebSocket connection
      },
    }
  }),
)

Initializing the Physics Simulation

Sets up the Rapier physics engine with gravity to simulate realistic physics.

RAPIER.init().then(() => {
  const gravity = { x: 0.0, y: -9.81, z: 0.0 }
  world = new RAPIER.World(gravity)
})

Game loop

The loop updates the physics simulation and broadcasts the game state to all connected clients.

function gameLoop() {
  // Update player cubes based on physics simulation
  world.step()
  // Broadcast player positions to all connected clients
  for (const ws of Object.values(connections)) {
    ws.send(JSON.stringify(state))
  }
}
setInterval(gameLoop, 1000 / 60)

Handling User Input

Processes user input received from the WebSocket, applying forces to the player’s cube in the physics simulation.

onMessage(event, ws) {
  const data = JSON.parse(event.data.toString())
  if (data.type === 'move') {
    const cube = cubes.get(ws.connectionId)
    if (cube) {
      const { forward, backward, left, right } = data.controls
      const force = {
        x: (right ? 1 : left ? -1 : 0) * 10.0,
        y: 0.0,
        z: (backward ? 1 : forward ? -1 : 0) * 10.0,
      }
      cube.body.resetForces(true)
      cube.body.addForce(force, true)
    }
  }
}

Client (Frontend)

  • Svelte: Framework for building web applications.
  • Three.js: Library for 3D rendering and animations.

Connecting to the WebSocket Server

Sets up the WebSocket connection to receive game state updates from the server.

const socket = new WebSocket('ws://localhost:3000/ws')

// Handle incoming WebSocket messages
socket.onmessage = (event) => {
  const data = JSON.parse(event.data) as ServerState
  updateScene(data)
}

Updating the 3D Scene

Update the position and rotation of cubes in the 3D scene or creates new cubes when a new player joins.

function updateScene(state: ServerState): void {
  for (const id of state.connectionIds) {
    if (this.cubes.has(id)) {
      // Update existing cubes position
      const cube = this.cubes.get(id)
      if (cube) {
        cube.position.set(
          state.cubes[id].position.x,
          state.cubes[id].position.y,
          state.cubes[id].position.z,
        )
        cube.rotation.set(
          state.cubes[id].rotation.x,
          state.cubes[id].rotation.y,
          state.cubes[id].rotation.z,
        )
      }
    } else {
      // Create new cube
      const cube = createCube(id, state.cubes[id].color)
      this.cubes.set(id, cube)
      this.scene.add(cube)
    }
  }
}

Handling User Input

Capture keyboard input for movement and send the updated controls state to the server.

function onKeyHandler(event: KeyboardEvent) {
  const controlState: ControlsState = {
    forward: false,
    backward: false,
    left: false,
    right: false,
  }
  if (event.key === 'w' || event.key === 'ArrowUp') {
    controlState.forward = event.type === 'keydown'
  }
  if (event.key === 's' || event.key === 'ArrowDown') {
    controlState.backward = event.type === 'keydown'
  }
  if (event.key === 'a' || event.key === 'ArrowLeft') {
    controlState.left = event.type === 'keydown'
  }
  if (event.key === 'd' || event.key === 'ArrowRight') {
    controlState.right = event.type === 'keydown'
  }

  game.controlsState = controlState
}

// Send the updated controls state to the server using the WebSocket
const event: ClientEventMove = {
  type: 'move',
  controls: game.controlsState,
}
socket.send(JSON.stringify(event))

Debug Mode

In debug mode, players can view hitboxes and the physics simulation. The server broadcasts the collision shapes as vertices, providing insight into the world’s physics interactions.

Play

GitHub Repository: https://github.com/martinhjartmyr/multiplayer-web-game-starter

More Articles

Generate Commit Messages with Ollama in Neovim

Generate Commit Messages with Ollama in Neovim

Generate conventional commit messages from staged diffs using Ollama. Run it locally for privacy and offline, or use cloud models for speed.

Adding Umami analytics to the OpenClaw morning brief

Adding Umami analytics to the OpenClaw morning brief

An agent skill that fetches traffic data from Umami, and how it fits into a daily automated briefing.

Setting up Google Calendar sync for OpenClaw

Setting up Google Calendar sync for OpenClaw

How I set up read-only Google Calendar sync for my personal AI assistant running on a home server VM.

Better Clipboard Handling in Claude Code

Better Clipboard Handling in Claude Code

A plugin that makes clipboard operations in Claude Code more reliable and natural to use.

Get notified when Claude Code needs your input

Get notified when Claude Code needs your input

Stop constantly checking your terminal. Set up notifications that alert you when Claude Code is ready for your input.

Auto-format generated Code with Claude Code Hooks

Auto-format generated Code with Claude Code Hooks

How to set up a PostToolUse hook in Claude Code to automatically run prettier after every file edit or write operation.

PWA Web Share Target on Android: The Absolute URL Fix

PWA Web Share Target on Android: The Absolute URL Fix

Getting the Web Share Target API to work on Android PWAs can be frustrating. Your manifest looks correct, but the app never appears in the share sheet. Here is what finally worked.

Standard function keys on external keyboards in MacOS

Standard function keys on external keyboards in MacOS

Configure Karabiner Elements to use standard function keys on external keyboards while keeping media keys on your MacBook.

How to Clear Cloudflare Cache using a webhook

How to Clear Cloudflare Cache using a webhook

Automatically purge Cloudflare cache using webhooks and API tokens.