Multiplayer game server SDK. 128 TPS authoritative server, Jolt Physics WASM, WebSocket transport, hot reload.
npm install
node server.js
# http://localhost:8080server.js Entry point, calls boot()
src/sdk/server.js Creates all subsystems, wires them together
src/sdk/TickHandler.js Per-tick: movement -> physics -> collisions -> app tick -> snapshot
src/sdk/ReloadManager.js File watchers for SDK hot reload
src/apps/AppRuntime.js Entity system, app lifecycle, timers, collision events
src/apps/AppLoader.js Loads apps from apps/ directory, validates, watches for changes
src/physics/World.js Jolt Physics wrapper (bodies, characters, raycasts)
src/netcode/PhysicsIntegration.js CharacterVirtual per player, gravity application
src/connection/ConnectionManager.js WebSocket client management, heartbeat, msgpack encode/decode
src/protocol/msgpack.js Hand-rolled msgpack encoder/decoder
client/app.js Three.js renderer, VRM loading, entity rendering, input loop
apps/world/index.js exports the world definition:
export default {
port: 8080,
tickRate: 128,
gravity: [0, -9.81, 0],
movement: { maxSpeed: 4.0, groundAccel: 10.0, airAccel: 1.0, friction: 6.0, stopSpeed: 2.0, jumpImpulse: 4.0 },
player: { health: 100, capsuleRadius: 0.4, capsuleHalfHeight: 0.9, mass: 120, modelScale: 1.323, feetOffset: 0.212 },
scene: { skyColor: 0x87ceeb, sunColor: 0xffffff, sunIntensity: 1.5, sunPosition: [21, 50, 20] },
camera: { fov: 70, shoulderOffset: 0.35, zoomStages: [0, 1.5, 3, 5, 8], defaultZoomIndex: 2 },
animation: { mixerTimeScale: 1.3, walkTimeScale: 2.0, sprintTimeScale: 0.56, fadeTime: 0.15 },
entities: [
{ id: 'environment', model: './apps/tps-game/schwust.glb', position: [0, 0, 0], app: 'environment' },
{ id: 'game', position: [0, 0, 0], app: 'tps-game' }
],
playerModel: './apps/tps-game/Cleetus.vrm',
spawnPoint: [-35, 3, -65]
}Create apps/<name>/index.js:
export default {
server: {
setup(ctx) {
// Called once on spawn and on hot reload
// ctx.state persists across hot reloads
ctx.state.counter = ctx.state.counter || 0
},
update(ctx, dt) {
// Called every tick (128/sec)
ctx.state.counter += dt
},
teardown(ctx) {
// Called on destroy or before hot reload
},
onMessage(ctx, msg) {
// Receives player_join, player_leave, fire, and custom APP_EVENT messages
if (msg.type === 'player_join') { /* ... */ }
},
onInteract(ctx, player) {
// Called when client sends APP_EVENT with this entity's ID
},
onCollision(ctx, other) {
// Called when this entity's collider overlaps another entity's collider
}
},
client: {
setup(engine) {
// engine.scene, engine.camera, engine.renderer, engine.THREE, engine.client, engine.cam
},
render(ctx) {
// Return visual state. ctx.entity, ctx.state, ctx.h (createElement), ctx.engine, ctx.players
return {
position: ctx.entity.position,
custom: { mesh: 'box', color: 0xff0000, sx: 1, sy: 1, sz: 1 },
ui: ctx.h('div', {}, 'Hello')
}
},
onInput(input, engine) { },
onFrame(dt, engine) { },
onEvent(payload, engine) { },
onMouseDown(e, engine) { },
onMouseUp(e, engine) { }
}
}| Property | Description |
|---|---|
ctx.state |
Persistent state object (survives hot reload) |
ctx.entity |
Entity proxy: .id, .position, .rotation, .scale, .velocity, .custom, .destroy() |
ctx.physics |
.addBoxCollider(size), .addSphereCollider(r), .addCapsuleCollider(r, h), .addTrimeshCollider(), .setDynamic(bool), .addForce(vec3), .setVelocity(vec3) |
ctx.world |
.spawn(id, cfg), .destroy(id), .attach(eid, app), .getEntity(id), .query(filter), .nearby(pos, radius) |
ctx.players |
.getAll(), .getNearest(pos, r), .send(pid, msg), .broadcast(msg), .setPosition(pid, pos) |
ctx.time |
.tick, .deltaTime, .elapsed, .after(sec, fn), .every(sec, fn) |
ctx.bus |
.on(channel, fn), .emit(channel, data), .once(channel, fn), .handover(targetEntityId, state) |
ctx.network |
.broadcast(msg), .sendTo(id, msg) |
ctx.storage |
.get(key), .set(key, val), .delete(key), .list(prefix), .has(key) |
ctx.config |
Entity config passed from world definition |
ctx.raycast(origin, dir, maxDist) |
Physics raycast against world geometry |
| Property | Description |
|---|---|
engine.scene |
THREE.Scene |
engine.camera |
THREE.PerspectiveCamera |
engine.renderer |
THREE.WebGLRenderer |
engine.THREE |
Three.js module |
engine.client |
PhysicsNetworkClient instance |
engine.playerId |
Local player ID |
engine.cam |
Camera controller: .yaw, .pitch, .getAimDirection(pos), .setMode(m), .applyConfig(cfg) |
engine.players |
.getMesh(id), .getState(id), .getAnimator(id), .setExpression(id, name, val) |
engine.createElement |
webjsx createElement for UI |
When an entity has no model, the client builds geometry from entity.custom:
| Field | Description |
|---|---|
mesh |
'box' (default), 'cylinder', 'sphere' |
color |
Hex color (default 0xff8800) |
sx, sy, sz |
Box dimensions |
r |
Radius for sphere/cylinder |
h |
Height for cylinder |
spin |
Y-axis rotation speed (radians/sec) |
hover |
Vertical bob amplitude |
light |
PointLight color |
lightIntensity, lightRange |
PointLight params |
emissive, emissiveIntensity |
Material emissive |
Apps communicate via ctx.bus:
// Publisher
ctx.bus.emit('combat.fire', { shooterId, origin, direction })
// Subscriber (supports wildcard)
ctx.bus.on('combat.*', (event) => {
// event.channel, event.data, event.meta.sourceEntity, event.meta.timestamp
})Scoped subscriptions auto-cleanup on entity destroy/hot reload.
jolt-physics- WASM physics enginews- WebSocket serverwebjsx- JSX-like DOM diffing for client UId3-octree- Spatial indexing
MIT