9.4 KiB
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 utilitiesconfig/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.ts — ArenaRoom extends Room<RoomState>
- maxClients: 50
- Game loop:
setIntervalat TICK_RATE (60Hz), incrementsstate.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):
- Process player inputs (movement, shooting, upgrades)
- Update bullet positions (linear velocity)
- Update player positions (velocity + friction)
- Detect collisions (bullets vs players, players vs obstacles, players vs XP orbs)
- Apply damage, handle deaths
- Update recoil recovery (lerp recoilOffset toward 0)
- Update reload timers
- Regenerate HP
- Update leaderboard
- Broadcast state (Colyseus handles delta compression)
Physics: physics.ts
SpatialHashGridfor O(1) queriescircleCollisionResolve()for player-player and player-obstaclelineCircleIntersect()for bullet hit detection- Arena bounds clamping
Player Manager: player-manager.ts
spawnPlayer(): random position, base stats, apply class bonusesapplyInput(): validate speed, update velocity, handle shootinghandleShoot(): apply recoil, spawn bullet(s), apply kickback, consume ammotakeDamage(): apply damage, check crit, check deathonDeath(): drop XP orbs, reset level/stats, schedule respawnrespawnPlayer(): new random position, level 1, base gunapplyUpgrade(): validate stat choice, apply bonuscheckLevelUp(): check XP thresholds, award upgrade points, prompt branch choice
Collision: collision.ts
checkBulletPlayerCollisions(): spatial hash query, damage on overlapcheckPlayerOrbCollisions(): collect orbs within pickup radiuscheckPlayerObstacleCollisions(): resolve overlap, apply slow zone effectcheckPlayerPlayerCollisions(): soft collision (push apart)
Gun System: gun-system.ts
- Load
config/guns.jsonat startup getGunConfig(id)/getGunConfigByIndex(idx)calculateRecoil(): random sign * recoilOffset * (1 - recoilStatBonus)calculateKickback(): -aimVector * kickbackForcecalculateBulletSpawn(): barrel tip position at aimAngle + recoilOffset
Class Branch System: branch-system.ts
- Load
config/branches.jsonat startup getAvailableBranches(level, currentBranch)— filters by levelRequired and parentIdapplyBranchBonuses(player, branchId)— applies passive stat multipliersgetUnlockedGunCategories(branchIds)— union of categories
Validation: validation.ts
validateMovement(): speed <= max * (1 + moveSpeedStat)validateAimAngle(): change <= max_turn_rate per tickvalidateFireRate(): 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):
- Arena background (solid color + grid lines)
- Obstacles (walls = grey rects, crates = brown rects, slow zones = blue tint, cover = green)
- XP orbs (small colored squares/circles)
- Bullets (filled circles with slight glow, semi-transparent trail circles behind)
- Players (circles: blue = self, red = others)
- Gun rendering (rectangle barrel + square body, rotated at aimAngle + recoilOffset)
- Name tags (above circle, always visible)
- HP bars (below name tag, green fill / dark bg)
Gun rendering details:
- Barrel: rectangle,
GUN_BARREL_WIDTHxGUN_BARREL_LENGTH, attached to player center - Body: small square
GUN_BODY_SIZEbehind 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 HUDrender(): 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
- Client: 60 FPS with 50 entities (Chrome, mid-tier laptop)
- Server: 60 Hz tick with 50 players (<10ms per tick)
- Bandwidth: < 4 KB/s per player downstream
- 100ms latency feels playable (prediction hides lag)
- 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