Package Exports
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (spoint) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Spawnpoint
Multiplayer game server SDK with authoritative physics, hot reload, and app-based world logic.
Quick Start
npm install
npm start
# open http://localhost:3001server.js runs scaffold() first, then boots the server.
Common Commands
node ./bin/create-app.js my-app
node run-bots.js 50 30000 60
node run-profiling.jscreate-app: generateapps/<name>/index.jsfrom a templatebots: start bot harness (count durationMs inputHz)profile: run included 50-bot profiling harness
Architecture
server.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 loopWorld Config
apps/world/index.js exports the world definition:
export default {
port: 3001,
tickRate: 64,
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]
}Creating Apps
Create an app file:
node ./bin/create-app.js my-appOr manually 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) { }
}
}Server-Side ctx API
| 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 |
Client Engine API
| 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 |
Entity Custom Mesh Properties
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 |
EventBus Channels
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.
App Editor Guide
For app-authoring workflow, lifecycle rules, and a complete checklist, see:
Dependencies
jolt-physics- WASM physics enginews- WebSocket serverwebjsx- JSX-like DOM diffing for client UId3-octree- Spatial indexing
License
MIT