Physics and netcode SDK for authoritative-server multiplayer games. Jolt physics, hot reload, app-based world logic, Three.js client.
Demo: https://anentrypoint.github.io/spoint/
npx spoint
# or
npm install && npm start
# open http://localhost:3001node ./bin/create-app.js my-appOr manually create apps/<name>/index.js:
export default {
server: {
setup(ctx) {
ctx.state.counter = ctx.state.counter || 0
},
update(ctx, dt) {
ctx.state.counter += dt
},
onInteract(ctx, player) { },
onCollision(ctx, other) { }
},
client: {
setup(engine) { },
render(ctx) {
return { custom: { mesh: 'box', color: 0xff0000, sx: 1, sy: 1, sz: 1 } }
}
}
}export default {
port: 3001,
tickRate: 64,
gravity: [0, -18, 0],
movement: { maxSpeed: 14, sprintSpeed: 24, groundAccel: 300, airAccel: 30, friction: 5, stopSpeed: 1, jumpImpulse: 5.5 },
player: { health: 100, capsuleRadius: 0.28, capsuleHalfHeight: 0.63, modelScale: 1.323, feetOffset: 0.212 },
scene: { skyColor: 0x54c5eb, sunColor: 0xffffff, sunIntensity: 1.5, sunPosition: [21, 50, 20] },
camera: { fov: 70, shoulderOffset: 0.35, zoomStages: [0, 1.5, 3, 5, 8], defaultZoomIndex: 2 },
entities: [
{ id: 'env', model: './apps/maps/mymap.glb', position: [0, 0, 0], scale: [1, 1, 1], bodyType: 'static' },
{ id: 'game', position: [0, 0, 0], app: 'tps-game' }
],
playerModel: './apps/tps-game/cleetus.vrm',
spawnPoint: [0, 2, 0]
}| Property | Description |
|---|---|
ctx.state |
Persistent state (survives hot reload) |
ctx.entity |
.id, .position, .rotation, .scale, .velocity, .custom, .destroy() |
ctx.physics |
.addBoxCollider(), .addSphereCollider(), .addCapsuleCollider(), .addTrimeshCollider(), .addConvexFromModel() |
ctx.interactable({ prompt, radius }) |
Register E-key interaction |
ctx.world |
.spawn(id, cfg), .destroy(id), .getEntity(id), .query(filter) |
ctx.players |
.getAll(), .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) — wildcard * suffix supported |
ctx.lagCompensator |
.getPlayerStateAtTime(pid, ms) for hit detection |
ctx.raycast(origin, dir, maxDist) |
Physics raycast |
| Property | Description |
|---|---|
engine.scene |
THREE.Scene |
engine.camera |
THREE.PerspectiveCamera |
engine.renderer |
THREE.WebGLRenderer or WebGPURenderer |
engine.THREE |
Three.js module |
engine.client |
Network client (PhysicsNetworkClient or LocalClient) |
engine.playerId |
Local player ID |
engine.cam |
Camera controller |
engine.players |
.getMesh(id), .getState(id), .getAnimator(id) |
Press P to toggle editor mode:
G/R/S— translate / rotate / scale gizmoF— focus camera on selected entityDel— delete selected entity- Drag-and-drop
.glbfiles to place models
Add ?singleplayer to the URL to run without a server. Loads client/singleplayer-world.json.
server.js Entry point
src/sdk/server.js Creates subsystems
src/sdk/TickHandler.js Per-tick: movement → physics → collisions → apps → snapshot
src/physics/World.js Jolt physics wrapper
src/netcode/PhysicsIntegration.js CharacterVirtual per player
src/apps/AppRuntime.js Entity system, app lifecycle
client/app.js Three.js renderer, VRM loading, input loop
client/LocalClient.js Serverless drop-in for singleplayer
npm run botsEnv vars: BOT_COUNT, BOT_DURATION, BOT_HZ, BOT_URL
MIT