Files

9.4 KiB

GunCircle.io — Technical Specification

Overview

A browser-based multiplayer .io arena shooter (diep.io-style). Continuous FFA — no matches, no timers. Players join, spawn immediately, fight, earn XP, level up, choose class branches, die and respawn.

Architecture

  • Monorepo: packages/shared, packages/server, packages/client
  • Client: TypeScript + Vite + HTML5 Canvas 2D (PC only, WASD + mouse)
  • Server: Node.js + TypeScript + Colyseus (uWebSockets transport)
  • Physics: Custom circle-circle + spatial hash grid (no external physics)
  • Network: Client-side prediction + server reconciliation + entity interpolation

Shared Package (packages/shared/)

Already implemented. Contains:

  • schema.ts — Colyseus Schema classes (Player, Bullet, XPOrb, Obstacle, LeaderboardEntry, RoomState)
  • types.ts — TypeScript interfaces (GunConfig, ClassBranch, PlayerInput, Skin, etc.)
  • constants.ts — Game constants (TICK_RATE, ARENA, COLORS, STATS, XP_LEVELS, etc.)
  • math.ts — Vector math, spatial hash grid, scalar utilities
  • config/guns.json — 5 gun definitions (pistol, rifle, shotgun, sniper, SMG)
  • config/branches.json — 19 class branch definitions

Server (packages/server/src/)

Entry Point: index.ts

Sets up Colyseus server with uWebSockets transport, listens on port 3000, registers ArenaRoom.

Room: room.tsArenaRoom extends Room<RoomState>

  • maxClients: 50
  • Game loop: setInterval at TICK_RATE (60Hz), increments state.tick
  • On join: create Player, spawn at random position, assign base gun (pistol), send gun configs
  • On leave: mark player dead, drop 50% XP as orbs after 60s room destroy timer
  • On message: deserialize PlayerInput, validate, apply

Game Loop: game-loop.ts

Per tick (order matters):

  1. Process player inputs (movement, shooting, upgrades)
  2. Update bullet positions (linear velocity)
  3. Update player positions (velocity + friction)
  4. Detect collisions (bullets vs players, players vs obstacles, players vs XP orbs)
  5. Apply damage, handle deaths
  6. Update recoil recovery (lerp recoilOffset toward 0)
  7. Update reload timers
  8. Regenerate HP
  9. Update leaderboard
  10. Broadcast state (Colyseus handles delta compression)

Physics: physics.ts

  • SpatialHashGrid for O(1) queries
  • circleCollisionResolve() for player-player and player-obstacle
  • lineCircleIntersect() for bullet hit detection
  • Arena bounds clamping

Player Manager: player-manager.ts

  • spawnPlayer(): random position, base stats, apply class bonuses
  • applyInput(): validate speed, update velocity, handle shooting
  • handleShoot(): apply recoil, spawn bullet(s), apply kickback, consume ammo
  • takeDamage(): apply damage, check crit, check death
  • onDeath(): drop XP orbs, reset level/stats, schedule respawn
  • respawnPlayer(): new random position, level 1, base gun
  • applyUpgrade(): validate stat choice, apply bonus
  • checkLevelUp(): check XP thresholds, award upgrade points, prompt branch choice

Collision: collision.ts

  • checkBulletPlayerCollisions(): spatial hash query, damage on overlap
  • checkPlayerOrbCollisions(): collect orbs within pickup radius
  • checkPlayerObstacleCollisions(): resolve overlap, apply slow zone effect
  • checkPlayerPlayerCollisions(): soft collision (push apart)

Gun System: gun-system.ts

  • Load config/guns.json at startup
  • getGunConfig(id) / getGunConfigByIndex(idx)
  • calculateRecoil(): random sign * recoilOffset * (1 - recoilStatBonus)
  • calculateKickback(): -aimVector * kickbackForce
  • calculateBulletSpawn(): barrel tip position at aimAngle + recoilOffset

Class Branch System: branch-system.ts

  • Load config/branches.json at startup
  • getAvailableBranches(level, currentBranch) — filters by levelRequired and parentId
  • applyBranchBonuses(player, branchId) — applies passive stat multipliers
  • getUnlockedGunCategories(branchIds) — union of categories

Validation: validation.ts

  • validateMovement(): speed <= max * (1 + moveSpeedStat)
  • validateAimAngle(): change <= max_turn_rate per tick
  • validateFireRate(): timeSinceLastShot >= cooldown / (1 + reloadStat)
  • validateRecoil(): server-calculated vs client-reported within tolerance

Client (packages/client/src/)

Entry Point: main.ts

  • Wait for DOM ready
  • Show menu, get player name
  • On PLAY click: connect to Colyseus room, hide menu, start game loop

Renderer: renderer.ts

Canvas 2D rendering engine:

  • render(): called every requestAnimationFrame
  • Clear canvas → save → apply camera transform → render world → restore → render HUD

Render order (world space):

  1. Arena background (solid color + grid lines)
  2. Obstacles (walls = grey rects, crates = brown rects, slow zones = blue tint, cover = green)
  3. XP orbs (small colored squares/circles)
  4. Bullets (filled circles with slight glow, semi-transparent trail circles behind)
  5. Players (circles: blue = self, red = others)
  6. Gun rendering (rectangle barrel + square body, rotated at aimAngle + recoilOffset)
  7. Name tags (above circle, always visible)
  8. HP bars (below name tag, green fill / dark bg)

Gun rendering details:

  • Barrel: rectangle, GUN_BARREL_WIDTH x GUN_BARREL_LENGTH, attached to player center
  • Body: small square GUN_BODY_SIZE behind barrel
  • Rotation: around player center at angle + recoilOffset
  • Recovery: lerp recoilOffset toward 0 each frame

Bullet rendering:

  • Main circle: radius = bulletSize, filled with bulletColor
  • Trail: 3-5 semi-transparent smaller circles behind at velocity * -dt positions
  • Crit: red outline stroke (2px) when isCritical = true

Camera: camera.ts

  • Follow local player with slight lag (lerp at ~0.1 per frame)
  • Clamp to arena bounds + viewport padding
  • Transform: translate(canvas/2 - camX, canvas/2 - camY)

Input: input.ts

  • WASD: track key states, compute moveAngle from active keys
  • Mouse: track position, convert screen → world for aimAngle
  • Left click: isShooting flag
  • Send input at 60Hz (setInterval)

Networking: network.ts

  • Connect to Colyseus server via WebSocket
  • Client-side prediction: immediately move on WASD, apply velocity
  • Server reconciliation: compare predicted pos to server pos, smooth correction
  • Entity interpolation: for other players/bullets, lerp between prev and current state
  • Bullet confirmation: predict spawn locally, server confirms trajectory

HUD: hud.ts

  • HP bar (top-left): green fill, shows current/max
  • XP bar (below HP): yellow fill, shows progress to next level
  • Ammo counter (bottom-right): current/max, reload indicator
  • Level indicator (top-right): large number
  • Leaderboard (top-right below level): sorted by XP, shows top 10
  • Name above local player: white text, centered
  • Upgrade popup: appears on level-up, shows stat buttons + branch choice at milestones

Menu: menu.ts

  • Name input (max 16 chars)
  • PLAY button → connect → hide menu → show HUD

Game Loop: game.ts

  • update(dt): process input, apply prediction, update camera, update HUD
  • render(): call renderer
  • Uses requestAnimationFrame with delta time
  • Interpolate entity positions between server ticks

Networking Protocol

Client → Server (60Hz)

PlayerInput {
  seq: uint32     // frame counter
  moveAngle: float32  // -1 = none
  aimAngle: float32
  isShooting: bool
  upgradeChoice?: uint8
}

Server → Client (60Hz, delta compressed via Colyseus)

Full RoomState with Player, Bullet, XPOrb maps. Colyseus automatically sends only changed fields.

Bandwidth Budget

  • Target: < 4 KB/s per player downstream
  • With 50 players + 100 bullets + 50 orbs, delta should be ~2-3 KB per tick
  • Colyseus Schema uses binary encoding + delta compression

File Structure

packages/server/src/
  index.ts         — server entry point
  room.ts          — ArenaRoom definition
  game-loop.ts     — tick loop
  physics.ts       — spatial hash, collision resolution
  player-manager.ts — spawn, death, respawn, upgrades
  collision.ts     — bullet-player, player-orb, player-obstacle
  gun-system.ts    — gun config loading, recoil calc
  branch-system.ts — class branch loading, bonus application
  validation.ts    — input validation, anti-cheat
  bot-player.ts    — AI bot for testing (optional)

packages/client/src/
  main.ts          — client entry, menu, connect
  renderer.ts      — Canvas 2D rendering
  camera.ts        — camera follow + smoothing
  input.ts         — WASD + mouse input
  network.ts       — Colyseus client, prediction, reconciliation
  hud.ts           — UI overlays (HP, XP, leaderboard, upgrades)
  game.ts          — client game loop (update + render)
  interpolation.ts — entity interpolation between ticks
  asset-loader.ts  — optional image/font loading

Quality Gates

  1. Client: 60 FPS with 50 entities (Chrome, mid-tier laptop)
  2. Server: 60 Hz tick with 50 players (<10ms per tick)
  3. Bandwidth: < 4 KB/s per player downstream
  4. 100ms latency feels playable (prediction hides lag)
  5. No memory leaks over 10-minute session

Visual Style (diep.io-like)

  • Self: blue circle (#3498db)
  • Enemies: red circle (#e74c3c)
  • Background: dark (#1a1a2e) with subtle grid
  • No particles, no screen shake, no muzzle flash
  • Only visual feedback: gun recoil animation
  • Clean, minimal, readable

Skin System (Cosmetic Only)

  • Circle color/pattern overrides
  • Gun appearance overrides
  • Name tag style overrides
  • MVP: simple hardcoded palette, shop UI stub