@gamerstake/game-core
v0.1.0
Published
Reusable multiplayer game engine for GamerStake platform
Maintainers
Readme
@gamerstake/game-core
Reusable multiplayer game engine for the GamerStake platform.
Overview
game-core is a battle-tested, high-performance multiplayer game engine extracted from the GamerStake metaverse. It provides:
- Game-agnostic multiplayer infrastructure (tick loop, networking, state management)
- Pluggable game rules via the
GameRulesinterface - Room/Match lifecycle management (persistent worlds OR match-based games)
- Spatial partitioning with O(1) queries via grid system
- Basic 2D physics (position, velocity, AABB collision)
- Zero-allocation hot paths for optimal performance
Features
Core Systems
- GameServer - Multi-room server management
- Room - Single game instance with tick loop
- GameLoop - Fixed 20 TPS with drift compensation
- Entity System - Base entity class with dirty flag tracking
- Registry - Entity lifecycle management
Spatial & Physics
- Grid - Spatial partitioning for efficient queries
- AABB Collision - Bounding box collision detection
- Movement - Velocity integration and boundary constraints
Networking
- Network Layer - Socket.io abstraction with opcodes
- Snapshot System - Full and delta state synchronization
- InputQueue - Buffered input processing with RingBuffer
Performance
- Zero-allocation buffers - Reused arrays in hot paths
- Dirty flag tracking - Only broadcast changed entities
- RingBuffer - O(1) push/pop for input queues
- Performance metrics - Real-time tick time monitoring
Installation
From Monorepo Workspace
If you're in the GamerStake monorepo:
{
"dependencies": {
"@gamerstake/game-core": "workspace:*"
}
}From NPM Registry
pnpm add @gamerstake/game-coreSee PUBLISHING.md for publishing and distribution options.
Quick Start
1. Implement GameRules
import { GameRules, Room, Entity, MoveCommand } from '@gamerstake/game-core';
class MyGameRules implements GameRules {
onRoomCreated(room: Room): void {
console.log('Room created!');
}
onPlayerJoin(room: Room, player: Entity): void {
console.log(`Player ${player.id} joined`);
// Spawn player at random position
player.setPosition(Math.random() * 1000, Math.random() * 1000);
}
onPlayerLeave(room: Room, playerId: string): void {
console.log(`Player ${playerId} left`);
}
onTick(room: Room, delta: number): void {
// Update all entities
room.getRegistry().forEach((entity) => {
entity.updatePosition(delta);
});
}
onCommand(room: Room, playerId: string, command: Command): void {
const player = room.getRegistry().get(playerId);
if (!player) return;
if (command.type === 'move') {
const moveCmd = command as MoveCommand;
// Set velocity (e.g., 200 units/second)
const speed = 200;
player.setVelocity(moveCmd.dir.x * speed, moveCmd.dir.y * speed);
} else if (command.type === 'stop') {
player.setVelocity(0, 0);
}
}
shouldEndRoom(room: Room): boolean {
// For persistent worlds, return false
// For match-based games, check win condition
return false;
}
}2. Create Server and Room
import { GameServer, Entity } from '@gamerstake/game-core';
import { Server } from 'socket.io';
// Create Socket.io server
const io = new Server(3000);
// Create game server
const gameServer = new GameServer();
gameServer.setServer(io);
// Create a room
const room = gameServer.createRoom('room-1', new MyGameRules(), {
tickRate: 20, // 20 ticks per second
cellSize: 512, // Grid cell size
maxEntities: 1000, // Max entities per room
});
// Handle player connections
io.on('connection', (socket) => {
const playerId = socket.id;
// Create player entity
const player = new Entity(playerId, 0, 0);
room.addPlayer(player);
// Register socket for networking
room.getNetwork().registerSocket(playerId, socket);
// Send initial state
room.sendTo(playerId, {
op: 'S_INIT',
playerId,
...room.getSnapshot(),
});
// Handle player inputs
socket.on('C_MOVE', (data) => {
room.queueInput(playerId, {
seq: data.seq,
type: 'move',
dir: data.dir,
timestamp: Date.now(),
});
});
socket.on('disconnect', () => {
room.removePlayer(playerId);
room.getNetwork().unregisterSocket(playerId);
});
});
console.log('Game server running on port 3000');3. Client Example
import { io } from 'socket.io-client';
const socket = io('http://localhost:3000');
let seq = 0;
socket.on('S_INIT', (data) => {
console.log('Initial state:', data);
});
socket.on('S_UPDATE', (data) => {
console.log('Delta update:', data);
// Update local entities
});
// Send movement input
function move(dirX: number, dirY: number) {
socket.emit('C_MOVE', {
seq: ++seq,
dir: { x: dirX, y: dirY },
timestamp: Date.now(),
});
}
// Example: Move right
move(1, 0);Architecture
GameServer
Room 1
GameLoop (20 TPS)
Registry (Entities)
Grid (Spatial)
InputQueue
Network
GameRules (Your logic)
Room 2
Room N...API Reference
GameServer
const server = new GameServer();
server.setServer(io);
const room = server.createRoom(id, rules, config);
server.destroyRoom(id);
const metrics = server.getMetrics();Room
room.start();
room.stop();
room.addPlayer(entity);
room.removePlayer(playerId);
room.spawnEntity(entity);
room.destroyEntity(entityId);
room.queueInput(playerId, command);
room.broadcast(event);
room.sendTo(playerId, event);
const snapshot = room.getSnapshot();Entity
const entity = new Entity(id, x, y);
entity.setPosition(x, y);
entity.setVelocity(vx, vy);
entity.updatePosition(deltaMs);
entity.markDirty();Grid
const grid = new Grid(cellSize);
grid.addEntity(id, x, y);
grid.removeEntity(id);
grid.moveEntity(id, oldX, oldY, newX, newY);
const nearby = grid.getNearbyEntities(x, y, range);Configuration
interface RoomConfig {
tickRate?: number; // Default: 20 TPS
cellSize?: number; // Default: 512 units
maxInputQueueSize?: number; // Default: 100
maxEntities?: number; // Default: 1000
visibilityRange?: number; // Default: 1 (3x3 cells)
}Performance
Benchmarks
- 20+ TPS with 100+ entities per room
- <40ms average tick time (80% budget)
- O(1) spatial queries via grid partitioning
- Zero allocations in hot paths (tick loop)
Optimizations
- Dirty Flag Tracking - Only broadcast changed entities
- Buffer Reuse - Pre-allocated arrays for queries
- RingBuffer - O(1) input queue operations
- Spatial Grid - Avoid O(n²) collision checks
Testing
Automated Tests
# Run unit tests
pnpm test
# Run with coverage
pnpm test:coverage
# Run in watch mode
pnpm test:watch
# Type check
pnpm typecheck
# Build
pnpm buildManual Testing
To manually test the game engine with a live server:
# 1. Build the package
pnpm build
# 2. Start the test server
node examples/simple-game/server.js
# 3. In another terminal, start a client
node examples/simple-game/client.jsSee examples/simple-game/README.md for detailed manual testing instructions.
Getting Started
Choose your path:
- ** Quick Start** → See QUICK_START.md - Get running in 5 minutes
- ** Full Guide** → See DEVELOPER_GUIDE.md - Complete tutorial
- ** Examples** → See
examples/directory - Working code samples
Examples
The examples/ directory contains:
simple-game/- Manual testing server & client with automated movement tests
Migration from Metaverse Shard
If you're migrating from the metaverse shard:
- Install
@gamerstake/game-core - Implement
GameRuleswith your metaverse logic - Replace direct
GameLoop,Grid,WorldStateusage - Keep metaverse-specific: persistence, zones, gateway
See docs/features/game-core/N2-architecture/game-core-rfc.md for details.
Documentation
For Developers
- Quick Start Guide - Get a multiplayer game running in 5 minutes
- Developer Guide - Comprehensive guide with examples and patterns
- Manual Testing - How to manually test the engine
- Publishing Guide - How to publish and distribute the package
Architecture
License
UNLICENSED - Internal GamerStake package
Contributing
This is an internal package. For issues or feature requests, contact the GamerStake engineering team.
