在线联机原型全集:第 28 章 副本脚本系统
副本脚本系统(Instance Scripting System)
- 类别:副本脚本系统(Instance Scripting System)
- 目标:提供“可编程副本(Programmable Dungeons/Instances)”能力,支持 Lua/JS 沙箱、事件触发器(Triggers)、可热更(Hot Reload)、版本化(Versioning)、可回放(Replay),并与房间/战斗/任务/AI 等子系统解耦。
- 原型代号:
proto-028-instance-script - 依赖模块:
proto-003-board(房间内核) - 推荐语言栈:Go(gopher-lua/yaegi/quickjs-go)、Rust(mlua/rquickjs)、Java(GraalVM Polyglot)
- 协议栈:TCP/UDP + WebSocket(实时)/HTTP(管理与发布) + Cron/DelayQueue(定时)
1. 设计原则(Design Principles)
- 隔离(Isolation):脚本运行在受限沙箱(Sandbox)中,CPU/内存/IO/时钟/随机数受控;只暴露白名单 API。
- 确定性(Determinism):战斗/判定相关逻辑支持“固定步长 + 纯函数输入输出”,用于回放与反作弊;允许“弱非确定(e.g. 掉落表)”通过可播种 RNG 复现。
- 热更 & 版本化:副本脚本以
script_pack(包)为单位发布,支持蓝绿/灰度、向下兼容数据迁移。 - 事件驱动(Event-Driven):统一触发器模型:事件源(Event Source)→ 条件(Condition)→ 行为(Action)→ 效果(Effect)。
- 可观测(Observability):脚本级指标(执行耗时、内存峰值、触发频率、错误栈)、结构化日志、分布式 Trace。
- 安全(Security):禁用危险库、限制 FFI、限制文件/网络访问;审计与签名校验。
2. 核心概念(Core Concepts)
- Instance(副本实例):一次房间/关卡运行期容器,持有状态(地图、怪物、机关、任务进度、时间轴)。
- ScriptPack(脚本包):
manifest.json + /lua or /js + /assets + schema,含版本号、依赖、迁移脚本。 - Trigger(触发器):
on(Event) if Condition then Action的声明式配置或内嵌脚本。 - Sandbox VM(沙箱虚拟机):Lua 或 JS 引擎(QuickJS)包装,注入 Host API(Timer、ECS、Quest、Drop、UI、Path、Rand、State)。
- State Snapshot(状态快照):用于断线重连、回放与回滚(prediction rollback)。
3. 体系结构(Architecture Overview)
+---------------- 管理面 / CI-CD ------------------+
| Script Registry | Pack Signer | Gray Release |
+---------+---------+-------------+---------------+
| Pull
v
+----------------- 运行面(Game) ------------------+
| InstanceManager | InstanceShard(N) |
| - routing | - VM Pool (Lua/JS) |
| - lifecycle | - Trigger Engine |
| | - Event Bus Adapter |
| | - Snapshot/Replay |
+------------------+------------------------------+
^ |
| Metrics/Logs/Trace | Publish Events
| v
+------------------ Infra -------------------------+
| DelayQueue | KV/DB | ObjectStore | PubSub | KMS |
+--------------------------------------------------+
4. 数据模型(Data Model, 简化)
ScriptPack:
id: string # "dungeon-forest"
version: string # "1.3.2"
engine: "lua"|"js"
entry: "main.lua" # 入口
apis: ["ecs","quest","rand","timer","state","drop","ui","path"]
manifest:
min_protocol: 2
compat: [ "1.x" ]
hash: "sha256:..."
sign: "ed25519:..."
Instance:
id: int64
pack_id: string
pack_version: string
seed: int64
state_ref: "kv://instances/{id}"
tick_ms: 50
players: [player_id...]
Trigger:
id: string
event: string # e.g. "OnEnterArea", "OnMobDead", "OnTimer"
condition: string # 脚本或表达式
action: string # 脚本或动作组合
throttle_ms: 200 # 节流
5. 沙箱设计(Lua / JS)
5.1 资源限制
- CPU:执行指令计数器/时间片;超过阈值中断(
debug.sethook/QuickJSJS_SetInterruptHandler)。 - 内存:VM 堆上限(Lua
lua_gc配额,QuickJSJS_NewRuntimememory limit)。 - 时钟:暴露受控
now(),可注入回放时钟。 - 随机数:播种 RNG(
rand.seed(seed),rand.next()),确保可复现。 - IO:禁止文件/网络,提供 Host 代理函数(队列化、限频)。
5.2 Host API(示例)
-- Lua
local ecs = require("host.ecs")
local timer = require("host.timer")
local ev = require("host.event")
local drop = require("host.drop")
local rand = require("host.rand")
local state = require("host.state")
JS 侧同名命名空间:Host.ecs, Host.timer, …
6. 触发器(Triggers)
6.1 声明式配置
triggers:
- id: "gate-open"
event: "OnLeverSwitch"
condition: "state.get('key_count')>=3"
action: |
ecs.openGate("north")
ui.broadcast("Gate to the North is now open!")
- id: "boss-spawn"
event: "OnAreaEnter:boss_room"
condition: "players.count >= 2"
action: |
ecs.spawn("boss_ogre",{pos={x=10,y=0,z=5}})
music.play("boss_theme")
6.2 编程式(Lua 示例)
ev.on("OnMobDead", function(ctx)
if ctx.mob_id == "ogre_minion" then
local k = state.get("minion_dead") or 0
k = k + 1
state.set("minion_dead", k)
if k >= 5 then
ecs.spawn("boss_ogre", {pos=ctx.pos})
ui.broadcast("Boss appears!")
end
end
end)
7. 生命周期(Lifecycle)
- Create:分配 Instance→绑定 ScriptPack→播种 RNG→载入地图与初始状态。
- Warmup:执行
on_init()(生成物件、注册触发器、定时器)。 - Run:固定 Tick 驱动;事件注入→条件评估→行为执行→状态变更→快照。
- Checkpoint/Snapshot:关键帧保存(每 N 秒或关键事件)。
- Close:掉落与奖励结算→持久化→释放 VM/资源。
8. 回放 / 回滚(Replay / Rollback)
- 输入日志:
[tick, event_type, payload_hash]; - 状态快照:
S0 + Δ1..Δn; - 回放:重播事件流 + 同种 seed + 同版脚本包(或兼容回放适配层)。
- 回滚:用于客户端预测冲突,回退至最近快照并重放未确认事件。
9. 热更与版本(Hot Reload & Versioning)
- 包签名:CI 产出
*.spk(脚本包),带哈希与签名。 - 蓝绿:
pack_version=a与b并存;新开副本用b,旧实例继续用a。 - 灰度:按玩家段/区服/时间窗比例路由。
- 迁移脚本:
migrations/1.2.0_to_1.3.0.lua,用于长期副本状态兼容。
10. 管理与发布(Ops)
-
API:
POST /packs上传脚本包POST /packs/{id}/deploy灰度参数POST /instances创建/关闭/查询
-
指标:
vm_cpu_ms,vm_mem,trigger_exec_count,error_rate,p99_latency。 -
日志:结构化(
inst_id,pack_ver,trigger_id,elapsed_ms)。
11. 与子系统的接口(Integration)
- Event Bus:统一事件名空间(
combat.*,room.*,quest.*),触发器订阅模式匹配。 - ECS/AOI:脚本通过 Host API 操作实体(查询、创建、移动、加 Buff)。
- 任务/成就:脚本调用
quest.progress(player, key, delta)。 - 掉落表:
drop.roll(table_id, rand),可播种。 - AI:脚本发 Command 到 AI 子系统,避免重逻辑。
12. 安全清单(Security Checklist)
- 禁
os.*,io.*, 原生 FFI; - 运行上限:单触发器执行 ≤ 5ms(可配置),超时中断并告警;
- 沙箱上下文只读/写受控
state; - 包签名 & 来源校验,审计发布人/变更单;
- VM 级隔离(按实例/按触发器池化可选);
13. Go 参考实现(节选)
13.1 VM 抽象
type VM interface {
Load(pack ScriptPack) error
Call(funcName string, args ...any) (any, error)
Emit(event string, payload map[string]any) error
SetHostAPI(name string, fn any) error
Close() error
}
type VMLimiter struct {
MaxCPU time.Duration
MaxMemBytes int64
}
13.2 QuickJS(JS)示意
// quickjs-go 伪代码
rt := quickjs.NewRuntime(quickjs.WithMemoryLimit(16<<20))
rt.SetInterruptHandler(func() bool { return time.Since(start) > 5*time.Millisecond })
ctx := rt.NewContext()
injectHost(ctx) // 注入 Host.ecs / Host.timer / ...
ctx.Eval(string(pack.Main), quickjs.EvalModule)
13.3 gopher-lua(Lua)示意
L := lua.NewState(lua.Options{SkipOpenLibs: true})
openSafeLibs(L) // 仅打开 string/table/math 等
injectHost(L) // 注册 host.* 函数
if err := L.DoString(mainLua); err != nil { ... }
func onEvent(event string, payload map[string]any) error {
L.GetGlobal("ev_on") // 约定注册函数
// push args...
return L.PCall(nArgs, 0, nil)
}
13.4 触发器执行器
type Trigger struct {
ID string
Event string
Condition string // 脚本或表达式
Action string
Throttle time.Duration
lastAt time.Time
}
func (t *Trigger) TryFire(ctx *ExecCtx, ev Event) {
if time.Since(t.lastAt) < t.Throttle { return }
if ok := ctx.EvalBool(t.Condition, ev); !ok { return }
_ = ctx.Exec(t.Action, ev) // 超时/限流在 ctx 内部
t.lastAt = time.Now()
}
14. 调试与工具(Tooling)
- 脚本单元测试:离线 VM,Mock Host API,
golden tests。 - 场景回放:用录制的事件流批量回放对比 Hash。
- 可视化编辑器:Trigger Graph(节点编辑)、时间轴(Timeline)、波形图(计时/冷却)。
- 火焰图:脚本热点函数与长尾触发器定位。
15. 作者工作流(Authoring Workflow)
- 脚本包模板:
spk init dungeon-forest。 - 本地运行:
spk run --seed 123 --record trace.json。 - CI:lint(静态检查)→ 单测 → 回放对齐 → 签名打包。
- 灰度发布:
spk deploy --percent 10 --regions eu,de。 - 回滚:
spk rollback --to 1.3.1。
16. 示例脚本(Lua & JS)
16.1 Lua:机关 + 波次
function on_init()
state.set("wave", 0)
timer.every(5000, function() spawn_wave() end)
end
function spawn_wave()
local w = (state.get("wave") or 0) + 1
state.set("wave", w)
ui.broadcast("Wave "..w.." incoming")
for i=1,5 do
ecs.spawn("goblin",{pos={x=5*i,y=0,z=10}})
end
if w==3 then ecs.spawn("ogre",{pos={x=20,z=20}}) end
end
ev.on("OnLeverSwitch", function(ctx)
if state.get("wave")>=2 then
ecs.openGate("east")
else
ui.tip(ctx.player, "Defeat more waves to open the gate.")
end
end)
16.2 JS(QuickJS):钥匙收集 + Boss
export function on_init() {
Host.state.set("keys", 0);
Host.ev.on("OnPickup:key", (ctx) => {
const k = (Host.state.get("keys") || 0) + 1;
Host.state.set("keys", k);
Host.ui.broadcast(`Keys: ${k}/3`);
if (k >= 3) {
Host.ecs.spawn("boss_ogre",{pos:{x:12,z:8}});
Host.music.play("boss_theme");
}
});
}
17. 性能与容量规划
- 每实例 VM:
≤ 16 MB堆(可配置); - 触发器 QPS:常态 < 1k/s;
- 池化策略:按实例/按触发器组 VM 复用;
- 限流:
per-trigger,per-player,global三层。
18. 测试用例清单(节选)
- 触发器节流/去抖;
- RNG 重播一致性;
- 热更期间老实例不变、心跳延续;
- 快照/回放哈希对齐;
- 恶意脚本(死循环、内存爆)拦截;
- 掉落表边界概率;
19. 风险与对策
- 脚本抖动:加节流/批量合并(coalesce),行动压缩。
- 非确定性:统一 RNG、禁止系统时钟;外部调用入队。
- 安全合规:最小权限、包签名、审计链。
- 作者误用 API:强类型声明与 Schema 校验、IDE 提示、示例仓库。
20. 落地里程碑(Milestones)
- M1:Lua 沙箱 + 触发器引擎 + 基本 Host API(2 周)。
- M2:QuickJS 支持 + 回放/快照 + 指标(2 周)。
- M3:灰度/蓝绿发布 + 包签名 + 管理面(2 周)。
- M4:可视化编辑器(触发器图)+ 离线单测套件(3 周)。