spoint

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

spoint

spoint

You are helping a developer work with the spoint multiplayer game server SDK.
你正在协助开发者使用spoint多人游戏服务器SDK。

Setup (First Time)

首次设置

When no
apps/
directory exists in the current working directory, scaffold it:
bash
bunx spoint scaffold
bunx spoint
This copies the default apps (world config, tps-game, environment, etc.) into
./apps/
and starts the server. The engine (src/, client/) always comes from the npm package - never from the user's local folder.
当当前工作目录中不存在
apps/
目录时,执行以下命令搭建框架:
bash
bunx spoint scaffold
bunx spoint
这会将默认应用(世界配置、tps-game、环境等)复制到
./apps/
目录并启动服务器。引擎(src/、client/)始终来自npm包,而非用户的本地文件夹。

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.

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-spawner
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-spawner

App Structure

应用结构

Apps live in
apps/<name>/index.js
and export a default object:
js
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.js
中,并导出一个默认对象:
js
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
apps/world/index.js
to configure port, tickRate, gravity, movement, entities, and scene. The
entities
array auto-spawns apps on start:
js
export default {
  port: 3001,
  tickRate: 128,
  entities: [
    { id: 'my-thing', position: [0, 0, 0], app: 'my-app' }
  ]
}
编辑
apps/world/index.js
以配置端口、tickRate、重力、移动、实体和场景。
entities
数组会在启动时自动生成应用:
js
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)
creates a new entity and returns it. You can mutate the returned object:
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 null
Rotation is quaternion
[x, y, z, w]
. For Y-axis rotation of angle θ:
[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
旋转采用四元数
[x, y, z, w]
。绕Y轴旋转θ角的公式为:
[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
ent.position
directly updates the snapshot sent to all clients.
生成的实体不会自动由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.position
会更新发送给所有客户端的快照。

GLB Shader Stall Prevention

GLB着色器卡顿预防

The engine automatically calls
renderer.compileAsync(object, camera)
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
loadEntityModel
and
loadQueuedModels
. VRM players use a separate one-time warmup path.
引擎会在将任何GLB或 procedural mesh 添加到场景后立即调用
renderer.compileAsync(object, camera)
。这可以防止动态加载的实体(环境模型、物理 crate、能量 crate、智能对象、拖放模型)首次绘制时的GPU卡顿。应用代码无需执行任何操作——预热由
loadEntityModel
loadQueuedModels
处理。VRM玩家使用单独的一次性预热路径。

Key Facts

关键注意事项

  • Engine files (src/, client/) come from the npm package — never edit them
  • Only
    apps/
    is local to the user's project (their CWD)
  • ctx.state
    survives hot reload; timers and bus subscriptions do not
  • 128 TPS server, 60Hz client input, exponential lerp interpolation
  • Client app modules cannot use
    import
    or
    globalThis
    — evaluated via
    new Function()
    . All dependencies come from
    engineCtx
    .
  • Hot reload: server tears down + restarts app; client receives new module and calls
    teardown()
    then
    setup()
    automatically
  • 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.* events
js
ctx.bus.on('combat.hit', (data) => {})
ctx.bus.emit('combat.hit', { damage: 10 })
// 通配符:'combat.*'会捕获所有combat.*事件

Debugging

调试

  • Server:
    globalThis.__DEBUG__.server
    in Node REPL
  • Client:
    window.debug
    in browser console (exposes scene, camera, renderer, client)
  • 服务器:在Node REPL中使用
    globalThis.__DEBUG__.server
  • 客户端:在浏览器控制台中使用
    window.debug
    (可访问场景、相机、渲染器、客户端)