Skip to content

mateuseap/pixelhub

Repository files navigation

👾 PixelHub

A virtual space you can actually walk around in.
2D pixel world · Proximity chat · Self-hosted

CI Publish Images version license stars visitors

PixelHub: a walkable 2D pixel world with proximity chat

Why PixelHub?

Video calls put everyone in a grid; real rooms let you drift between conversations. PixelHub is a Gather-style space: walk a 2D pixel world with your avatar, and only the people near you hear you. It runs on your own server, fits a 1 vCPU VPS, and every conversation stays yours.

  • Proximity is the interface. Walk up to someone to talk, walk away to leave.
  • Server-authoritative. Movement and chat delivery are validated server-side.
  • No assets to license. Every texture is generated programmatically.
  • Self-hosted. One Docker image pair, deployable to any Kubernetes or compose setup.

Features

🗺 2D World 40x30 tile map with collision, rendered by Phaser 3
🚶 Real-time Movement Server simulation at 20 ticks/s, client prediction + interpolation
💬 Proximity Chat Messages reach only players within 5 tiles, filtered server-side
🫧 Speech Bubbles See who is talking above their avatar
🛡 Input Validation Name/message limits, sanitized movement, 5 msg/5s rate limit
🎙 Proximity Voice Self-hosted LiveKit SFU, audio-only Opus, volume falls off with distance
🐳 Docker-first Multi-stage images, nginx WebSocket proxy, GHCR publishing

Architecture

Three packages: a Phaser 3 client, an authoritative Colyseus server, and a shared pure-TypeScript core (map, collision, proximity, validation) that both sides import. The browser reaches the server through the client's nginx proxy; voice runs peer-to-SFU through self-hosted LiveKit.

flowchart LR
    browser(["browser: Phaser 3 client"])
    nginx["nginx client pod<br/>static assets + /colyseus proxy"]
    server["Colyseus server :2567<br/>authoritative state, 20 ticks/s"]
    shared["@pixelhub/shared<br/>map, collision, proximity, validation"]
    livekit["LiveKit SFU<br/>audio-only, by avatar distance"]

    browser -->|HTTPS static| nginx
    browser <-->|/colyseus WebSocket| nginx
    nginx <--> server
    server -.->|imports| shared
    browser -.->|imports| shared
    browser <-->|WebRTC audio| livekit
    server -->|mints access tokens| livekit
Loading

Movement and chat are validated server-side at 20 ticks per second; the client predicts and interpolates. Chat and voice are proximity-scoped in @pixelhub/shared, so who you hear and read is a pure function of avatar distance. See docs/architecture for the full design.

Quick Start

Requirements: Node >= 20 and pnpm >= 8 (corepack enable sets up pnpm).

git clone https://github.com/mateuseap/pixelhub && cd pixelhub
pnpm install
pnpm dev        # server on :2567 + Vite client on http://localhost:5173

Production-like stack (nginx client on http://localhost:8080):

docker compose -f docker-compose.dev.yml up --build

Other useful commands:

pnpm -r build   # build shared, server, and client
pnpm test       # shared unit + server room + client (jsdom) tests (vitest)
pnpm typecheck  # typecheck all packages

The client always talks to the server via the same-origin /colyseus path. Vite proxies it in dev, nginx proxies it in Docker/Kubernetes.

Voice chat

Voice is opt-in per deployment. Without configuration the world runs silently and shows no voice UI.

Env var (server) Meaning
LIVEKIT_URL Public wss URL of the LiveKit server
LIVEKIT_API_KEY API key id used to sign access tokens
LIVEKIT_API_SECRET API secret used to sign access tokens

When set, the server sends each player a scoped LiveKit token after join. The client shows an "Enable voice" button (microphone permission is requested only then), publishes audio-only, and subscribes exclusively to players within the proximity radius, with playback volume falling off linearly with distance. Speaking players get a green ring under their avatar. The choice persists: after a page refresh the client reconnects to voice by itself (the browser only re-asks for the microphone if permission was granted one time only).

Stack

Layer Technology
Rendering Phaser 3, Vite, TypeScript
Multiplayer Colyseus 0.15 (authoritative rooms, schema state sync)
Shared logic Pure TypeScript package (map, collision, proximity, validation)
Audio LiveKit, self-hosted, audio-only
DevOps Docker, Nginx, GitHub Actions, GHCR
Deployment homelab GitOps cluster

Roadmap

  • M0 design: spec and architecture (docs/)
  • M1 walkable world: map renders, avatars move and see each other
  • M2 proximity text chat
  • M3 proximity audio (LiveKit, audio-only)
  • M4 deploy: apps/pixelhub/ in homelab, TLS, monitoring
  • M5+ later: video, screen share, map editor, private zones

Documentation

Doc Description
System Overview Architecture diagram, the three packages, component map
System Design Tick loop, state sync, prediction, proximity pipeline, LiveKit token flow
ADRs Stack choice, server-authoritative movement, proximity model, audio-only voice
Development Setup Local dev, workspace layout, the Vite /colyseus proxy, conventions
Deployment Guide GHCR images, homelab manifests, LiveKit ports, env vars, Nginx proxy
Testing Test layout, the client LiveKit fake, coverage, how to run
Security Server authority, chat filtering, rate limits, token least-privilege, secrets
References Curated study links for the whole stack

Contributing

Read CONTRIBUTING.md before opening a PR. Branch from main, use Conventional Commits, keep the test suite green, and assign @mateuseap for review.

Learn more

New to any part of the stack? docs/references.md is a curated set of official study links for Phaser 3, Colyseus, LiveKit and WebRTC, pnpm workspaces, Vite, Vitest, TypeScript, WebSockets, and Docker, so you can learn every layer of PixelHub from primary sources.

License

MIT, see LICENSE.

About

Gather.town-style 2D virtual space: walkable pixel world with proximity chat and spatial audio. Phaser + Colyseus + LiveKit.

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages