游戏 NPC:守卫该不该动手
新酷实时循环玩家在逼近,同伴还在路上 —— 先算谁先到,再决定站还是跑
一个守卫 NPC 的每回合决策。每个 NPC 每回合都要判断一次,一张地图上几百个 NPC —— 这类决策的单价必须低到可以随便调用。
为什么要快 回合制还能等,实时战斗不能。每个 NPC 每回合一次调用,几百个 NPC 同时在场,慢一点就是全场卡住。
跑起来
每一跳都是一次真请求 —— 没有预录,暂停就真的停了。世界状态在你的浏览器里, 中途可以随手改(下雨、前车急刹、玩家拔刀),模型立刻就看见了。
等你点开始
这一跳算出来的数
- 玩家距离
- 18 m
- 同伴距离
- 40 m
- 玩家冲到算的
- —
- 同伴赶到算的
- —
- 差算的
- —
标「算的」那几行是本跳算出来的 —— 页面画的和决策依据的是同一个数。
改完直接「走一步」—— 世界状态在浏览器里,模型每一跳都是无状态的一次调用。
这一跳的接线
① 游戏服务端
状态一直在你这边,模型不持有任何东西
合法动作由你的代码生成,模型只在里面挑
一段话 + 选项
② 判断层 · Jett
3 个问题一次前向答完,不是问 3 次
回来的是选项 + 把握程度,不用再解析文字
② 数值 · Jett(与判断同一次请求)
需要的那个数随判断一起回来
精确值,带单位,可以逐位核对
答案 + 那个数
③ 游戏服务端
执行动作,世界更新
然后进入下一个 tick
往返 3 问 · 1 个数值问题
接进你的系统
下面就是这一页真正发出去的东西。把 state 换成你的场景、criteria 换成你的动作集, 它就是你的了。
① 判断层 —— 一次请求,3 个问题一起问
POST /v1/systemone
{
"model": "memorylake-c1",
"state": "你是城门口的守卫,血量 80/100。一个玩家在 18 米外,正以 6 米/秒的速度朝你来。他手里的武器还在鞘里。现在是白天。你以前没见过这个玩家。最近的同伴在 40 米外,赶过来的速度是 5 …",
"questions": {
"action": {
"type": "choice",
"instructions": "这一回合你做什么?",
"criteria": {
"approach": "上前盘问",
"guard": "拔刀戒备,原地不动",
"retreat": "后撤拉开距离",
"wave": "挥手放行",
"other": "以上都不合适"
}
},
"flee": {
"type": "noul",
"instructions": "是否应该直接逃跑?"
},
"trust": {
"type": "score",
"instructions": "你对这个玩家的信任有多高?",
"criteria": [
"很低",
"一般",
"高"
]
}
}
}回来的是 {answers: {key: {value, probs}}} ——不用再写一个解析器去猜模型想说什么, 也不用为「它这次多说了一句」写兜底。
③ 循环 —— 你要写的就这几行
while (running) {
const state = describe(world) // 你的状态 → 一段话
const legal = legalActions(world) // 合法动作:你的 if,不是模型的记性
const ans = await systemone(state, legal)
world = apply(world, ans.action.value, ans.margin.value)
}模型不持有任何状态 —— 每一跳都是无状态调用。 这意味着它可以并发、可以重放、可以随时换掉。