spoint
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesespoint
spoint
You are helping a developer work with the spoint multiplayer game server SDK.
你正在协助开发者使用spoint多人游戏服务器SDK。
Setup (First Time)
首次设置
When no directory exists in the current working directory, scaffold it:
apps/bash
bunx spoint scaffold
bunx spointThis copies the default apps (world config, tps-game, environment, etc.) into and starts the server. The engine (src/, client/) always comes from the npm package - never from the user's local folder.
./apps/当当前工作目录中不存在目录时,执行以下命令搭建框架:
apps/bash
bunx spoint scaffold
bunx spoint这会将默认应用(世界配置、tps-game、环境等)复制到目录并启动服务器。引擎(src/、client/)始终来自npm包,而非用户的本地文件夹。
./apps/Daily Use
日常使用
bash
bunx spoint # start server (port 3001, 128 TPS)Open http://localhost:3001 in browser. Apps hot-reload on file save — both server AND client receive updated code automatically, no manual refresh needed.
bash
bunx spoint # 启动服务器(端口3001,128 TPS)Creating Apps
创建应用
bash
bunx spoint-create-app my-app
bunx spoint-create-app --template physics my-physics-object
bunx spoint-create-app --template interactive my-button
bunx spoint-create-app --template spawner my-spawnerbash
bunx spoint-create-app my-app
bunx spoint-create-app --template physics my-physics-object
bunx spoint-create-app --template interactive my-button
bunx spoint-create-app --template spawner my-spawnerApp Structure
应用结构
Apps live in and export a default object:
apps/<name>/index.jsjs
export default {
server: {
setup(ctx) {
ctx.entity.custom = { mesh: 'box', color: 0x00ff00 }
ctx.physics.setStatic(true)
ctx.physics.addBoxCollider([0.5, 0.5, 0.5])
},
update(ctx, dt) {},
teardown(ctx) {}
},
client: {
setup(engineCtx) {}, // runs once when app module loads on client
teardown(engineCtx) {}, // called before hot reload replaces the module
onFrame(dt, engineCtx) {}, // called every render frame
onEvent(payload, engineCtx) {}, // called on APP_EVENT from server
render(ctx) { // called ~4x/sec for UI overlay only (not 3D)
return { ui: null }
}
}
}应用存放在中,并导出一个默认对象:
apps/<name>/index.jsjs
export default {
server: {
setup(ctx) {
ctx.entity.custom = { mesh: 'box', color: 0x00ff00 }
ctx.physics.setStatic(true)
ctx.physics.addBoxCollider([0.5, 0.5, 0.5])
},
update(ctx, dt) {},
teardown(ctx) {}
},
client: {
setup(engineCtx) {}, // 在客户端加载应用模块时运行一次
teardown(engineCtx) {}, // 在热重载替换模块前调用
onFrame(dt, engineCtx) {}, // 每个渲染帧调用一次
onEvent(payload, engineCtx) {}, // 收到服务器的APP_EVENT时调用
render(ctx) { // 仅针对UI覆盖层调用(非3D),约每秒4次
return { ui: null }
}
}
}World Config
世界配置
Edit to configure port, tickRate, gravity, movement, entities, and scene. The array auto-spawns apps on start:
apps/world/index.jsentitiesjs
export default {
port: 3001,
tickRate: 128,
entities: [
{ id: 'my-thing', position: [0, 0, 0], app: 'my-app' }
]
}编辑以配置端口、tickRate、重力、移动、实体和场景。数组会在启动时自动生成应用:
apps/world/index.jsentitiesjs
export default {
port: 3001,
tickRate: 128,
entities: [
{ id: 'my-thing', position: [0, 0, 0], app: 'my-app' }
]
}Spawning Entities Dynamically
动态生成实体
ctx.world.spawn(id, config)js
const entity = ctx.world.spawn('my-car', {
position: [10, 5, 0],
rotation: [0, Math.sin(yaw / 2), 0, Math.cos(yaw / 2)], // quaternion [x,y,z,w]
model: './apps/my-app/car.glb'
})
// CRITICAL: scale MUST go in custom.scale — it is not in the snapshot wire format
entity.custom = { scale: [1.5, 1.5, 1.5] }
ctx.world.destroy('my-car')
ctx.world.getEntity('my-car') // returns entity or nullRotation is quaternion . For Y-axis rotation of angle θ: .
[x, y, z, w][0, Math.sin(θ/2), 0, Math.cos(θ/2)]ctx.world.spawn(id, config)js
const entity = ctx.world.spawn('my-car', {
position: [10, 5, 0],
rotation: [0, Math.sin(yaw / 2), 0, Math.cos(yaw / 2)], // 四元数 [x,y,z,w]
model: './apps/my-app/car.glb'
})
// 重要提示:缩放必须设置在custom.scale中——它不在快照传输格式里
entity.custom = { scale: [1.5, 1.5, 1.5] }
ctx.world.destroy('my-car')
ctx.world.getEntity('my-car') // 返回实体或null旋转采用四元数。绕Y轴旋转θ角的公式为:。
[x, y, z, w][0, Math.sin(θ/2), 0, Math.cos(θ/2)]Simulating Gravity for Spawned Entities
为生成的实体模拟重力
Spawned entities are NOT automatically simulated by Jolt. To make them fall, simulate gravity in :
update()js
setup(ctx) {
ctx.state.velocities = {}
},
update(ctx, dt) {
for (const [id, vy] of Object.entries(ctx.state.velocities)) {
const ent = ctx.world.getEntity(id)
if (!ent || ent.position[1] <= GROUND_Y) continue
ctx.state.velocities[id] += -9.81 * dt
ent.position[1] = Math.max(GROUND_Y, ent.position[1] + ctx.state.velocities[id] * dt)
}
}Mutating directly updates the snapshot sent to all clients.
ent.position生成的实体不会自动由Jolt进行模拟。要让它们下落,需在中模拟重力:
update()js
setup(ctx) {
ctx.state.velocities = {}
},
update(ctx, dt) {
for (const [id, vy] of Object.entries(ctx.state.velocities)) {
const ent = ctx.world.getEntity(id)
if (!ent || ent.position[1] <= GROUND_Y) continue
ctx.state.velocities[id] += -9.81 * dt
ent.position[1] = Math.max(GROUND_Y, ent.position[1] + ctx.state.velocities[id] * dt)
}
}直接修改会更新发送给所有客户端的快照。
ent.positionGLB Shader Stall Prevention
GLB着色器卡顿预防
The engine automatically calls immediately after adding any GLB or procedural mesh to the scene. This prevents first-draw GPU stall for dynamically loaded entities (environment models, physics crates, power crates, smart objects, drag-and-drop models). No action is needed from app code — warmup is handled in and . VRM players use a separate one-time warmup path.
renderer.compileAsync(object, camera)loadEntityModelloadQueuedModels引擎会在将任何GLB或 procedural mesh 添加到场景后立即调用。这可以防止动态加载的实体(环境模型、物理 crate、能量 crate、智能对象、拖放模型)首次绘制时的GPU卡顿。应用代码无需执行任何操作——预热由和处理。VRM玩家使用单独的一次性预热路径。
renderer.compileAsync(object, camera)loadEntityModelloadQueuedModelsKey Facts
关键注意事项
- Engine files (src/, client/) come from the npm package — never edit them
- Only is local to the user's project (their CWD)
apps/ - survives hot reload; timers and bus subscriptions do not
ctx.state - 128 TPS server, 60Hz client input, exponential lerp interpolation
- Client app modules cannot use or
import— evaluated viaglobalThis. All dependencies come fromnew Function().engineCtx - Hot reload: server tears down + restarts app; client receives new module and calls then
teardown()automaticallysetup() - AppLoader blocks these strings (app silently fails if found, even in comments): ,
process.exit,child_process,require(,__proto__,Object.prototype,globalThis,eval(import(
- 引擎文件(src/、client/)来自npm包——请勿编辑它们
- 只有目录属于用户项目本地(当前工作目录)
apps/ - 会在热重载后保留;计时器和总线订阅则不会
ctx.state - 服务器为128 TPS,客户端输入为60Hz,采用指数线性插值
- 客户端应用模块不能使用或
import——通过globalThis执行。所有依赖都来自new Function()。engineCtx - 热重载:服务器会销毁并重启应用;客户端会接收新模块并自动调用然后
teardown()setup() - AppLoader会拦截以下字符串(如果发现,应用会静默失败,即使在注释中也不行):,
process.exit,child_process,require(,__proto__,Object.prototype,globalThis,eval(import(
ctx API Reference
ctx API参考
js
ctx.entity.id / .position / .rotation / .scale / .velocity / .custom / .model
ctx.physics.setStatic(true)
ctx.physics.setDynamic(true)
ctx.physics.setMass(kg)
ctx.physics.addBoxCollider([halfX, halfY, halfZ])
ctx.physics.addSphereCollider(radius)
ctx.physics.addCapsuleCollider(radius, height)
ctx.physics.addTrimeshCollider() // uses entity.model as collision mesh
ctx.world.spawn(id, config) // returns entity object
ctx.world.destroy(id)
ctx.world.getEntity(id)
ctx.world.query(filterFn)
ctx.world.nearby(pos, radius)
ctx.world.gravity // [x, y, z]
ctx.players.getAll()
ctx.players.send(playerId, msg)
ctx.players.broadcast(msg)
ctx.players.setPosition(playerId, pos)
ctx.state // persists across hot reloads
ctx.time.tick / .deltaTime / .elapsed
ctx.time.after(seconds, fn)
ctx.time.every(seconds, fn)
ctx.bus.on(channel, fn)
ctx.bus.emit(channel, data)
ctx.network.broadcast(msg)
ctx.network.sendTo(playerId, msg)
ctx.storage.get(key) / .set(key, value) / .delete(key)
ctx.debug.log(...)js
ctx.entity.id / .position / .rotation / .scale / .velocity / .custom / .model
ctx.physics.setStatic(true)
ctx.physics.setDynamic(true)
ctx.physics.setMass(kg)
ctx.physics.addBoxCollider([halfX, halfY, halfZ])
ctx.physics.addSphereCollider(radius)
ctx.physics.addCapsuleCollider(radius, height)
ctx.physics.addTrimeshCollider() // 使用entity.model作为碰撞网格
ctx.world.spawn(id, config) // 返回实体对象
ctx.world.destroy(id)
ctx.world.getEntity(id)
ctx.world.query(filterFn)
ctx.world.nearby(pos, radius)
ctx.world.gravity // [x, y, z]
ctx.players.getAll()
ctx.players.send(playerId, msg)
ctx.players.broadcast(msg)
ctx.players.setPosition(playerId, pos)
ctx.state // 热重载后仍保留
ctx.time.tick / .deltaTime / .elapsed
ctx.time.after(seconds, fn)
ctx.time.every(seconds, fn)
ctx.bus.on(channel, fn)
ctx.bus.emit(channel, data)
ctx.network.broadcast(msg)
ctx.network.sendTo(playerId, msg)
ctx.storage.get(key) / .set(key, value) / .delete(key)
ctx.debug.log(...)Server App Hooks
服务器应用钩子
js
server: {
setup(ctx) {},
update(ctx, dt) {},
teardown(ctx) {},
onMessage(ctx, msg) {}, // msg from client APP_EVENT
onInteract(ctx, player) {}, // player pressed interact near entity
onCollision(ctx, other) {} // entity-entity collision (sphere-based)
}js
server: {
setup(ctx) {},
update(ctx, dt) {},
teardown(ctx) {},
onMessage(ctx, msg) {}, // 来自客户端APP_EVENT的消息
onInteract(ctx, player) {}, // 玩家在实体附近按下交互键
onCollision(ctx, other) {} // 实体间碰撞(基于球体)
}Physics
物理系统
js
// Static ground collider
ctx.physics.setStatic(true)
ctx.physics.addBoxCollider([halfX, halfY, halfZ])
// Dynamic entity (ctx.entity only — spawned entities need manual gravity)
ctx.physics.setDynamic(true)
ctx.physics.setMass(5)js
// 静态地面碰撞体
ctx.physics.setStatic(true)
ctx.physics.addBoxCollider([halfX, halfY, halfZ])
// 动态实体(仅ctx.entity——生成的实体需要手动处理重力)
ctx.physics.setDynamic(true)
ctx.physics.setMass(5)EventBus
事件总线
js
ctx.bus.on('combat.hit', (data) => {})
ctx.bus.emit('combat.hit', { damage: 10 })
// Wildcard: 'combat.*' catches all combat.* eventsjs
ctx.bus.on('combat.hit', (data) => {})
ctx.bus.emit('combat.hit', { damage: 10 })
// 通配符:'combat.*'会捕获所有combat.*事件Debugging
调试
- Server: in Node REPL
globalThis.__DEBUG__.server - Client: in browser console (exposes scene, camera, renderer, client)
window.debug
- 服务器:在Node REPL中使用
globalThis.__DEBUG__.server - 客户端:在浏览器控制台中使用(可访问场景、相机、渲染器、客户端)
window.debug