日程与行为树(Routine & Behavior Tree)的调度系统设计
调度系统不是给NPC写日程表,而是让每个虚拟生命在时间洪流中做出可信的取舍——优先级仲裁、时间块切割、打断恢复这三层油门,决定了一个游戏世界是像《模拟人生》那样充满烟火气,还是像假人秀一样僵硬。
为什么调度系统是第一块基石
生活模拟游戏最迷人的瞬间,往往不是大事件,而是那种不经意的日常:清晨,NPC推开房门,对着清晨的阳光伸个懒腰,然后去煮咖啡;上班路上,他停下来跟邻居闲聊两句;下午他在公园长椅上发呆,偶尔翻翻手机。这些微小的行为序列,构成了“虚拟生命”的幻象。而支撑这些幻象的,不是精美的立绘,不是沉浸感的音效,而是隐藏在背后的调度系统——它每分钟都在回答同一个问题:此刻,这个角色应该做什么,为什么做,做完之后又该去往何方。
如果这块基石不合格,游戏世界就会充满违和感:角色会像提线木偶一样在固定时间点瞬移到固定坐标,或者像个无头苍蝇一样在多个需求之间来回横跳。哪怕是《兽人必须死》这种塔防游戏,也懂得给兽人一点“接近目标”的优先级,更别说强调生活逻辑的模拟游戏了。所以,把调度系统放在设计的第一块基石的位置,毫不夸张。
第1层:行为优先级仲裁——谁的声音能被听到?
理念先行:优先级不是排序,而是一种“紧急度”的连续函数。传统做法是给每个行为一个静态权重,比如工作权重10、吃饭权重8、社交权重5,然后挑最高权重的执行。但《模拟人生》的玩家一定经历过这种矛盾:角色都饿瘪了却跑去玩电脑,原因就是玩家的“自由意志”开关加上环境刺激导致电脑优先级更高。这恰恰说明静态权重不够可靠。
真实世界的决策是动态的:饥饿累积到阈值时,吃饭的紧迫感会瞬间压倒其他一切;但如果你正跟好友聊天,即使饿,也可能会说“再聊五分钟”。我把这种东西叫作“需求衰减曲线与情境调制”。具体公式可以写成:
intention = basePriority × urgencyFactor × personalityModifier × environmentModifier
basePriority是行为的基础权重,urgencyFactor来自需求衰减(比如饥饿每小时代价+15%),personalityModifier是角色性格(如“冒险家”降低安全需求权重),environmentModifier由周围对象触发(比如看到汉堡包,吃饭权重额外+30%)。在设计时,我们还要处理“多目标冲突”——比如同时满足饥饿和社交,那就让角色去餐厅吃饭,顺便和朋友聊天,这样能合成一个复合行为。这也是《环世界》的做法:把“吃饭”和“社交”合并为“在餐厅用餐并社交”,一次行动同时填充两个需求。
这里给出一个对比四种游戏的优先级处理:
| 游戏 | 处理方式 | 优势 | 劣势 |
|---|---|---|---|
| 《模拟人生》 | 动态权重+玩家干预 | 可被玩家操控,有戏剧性 | 常常出现“脑残决策” |
| 《环世界》 | 任务队列+紧急度排序 | 系统性强,效率高 | 缺感情色彩 |
| 《星露谷物语》 | 固定日程表 | 可预测性强,容易看出日常感 | 呆板,缺乏应变 |
| 《动物森友会》 | 松散时间循环 | 自由度高 | 缺乏真正的生活节奏 |
我的建议:用动态意图分数,且不要让任何需求永远占据最高优先级。对于无玩家操控的AI角色,要让“突发”和“日常”之间有明确的优先级切换阈值,比如中断阈值高于当前行为优先级时,才允许打断。否则角色就会像《模拟人生》那样被玩家指挥着做饭到一半就跑去看电视。
第2层:时间块调度——给一天切片,但别把角色变成切片面包
很多弱AI游戏只做“当前时间点该做什么”的判断,但高质量模拟会让角色拥有一整天的“计划轮廓”。我们把一天切成若干个时间块,比如“早晨(5-9点)”、“上午(9-12点)”、“午间(12-13点)”、“下午(13-18点)”、“晚间(18-22点)”、“深夜(22-5点)”。每个时间块有默认活动,比如“早晨”对应“洗漱、吃早餐”,“上午”对应“通勤、工作”等。但注意:时间块不是死教训,它只是一个“吸引子”,角色在时间块内有一定的随机性,比如上午可以花30分钟去浇花,之后再去工作——只要不破坏核心行动。
这里可以用到“日程模板”的概念,类似《星露谷物语》中每个NPC的日程表,精确到具体时刻,但必须允许偏差。我建议把日程表定义为“时间点+活动+偏移范围”,比如:“8:00 去厨房,偏移±10分钟”。更进一步,给每个时间块写入“可拆解的目标”,比如“上午的目标是完成2小时工作,可以分几次做”。这样角色就不会在9:30还在床上,而是灵活地安排。
另一个关键是“时间块边界”的处理。如果角色正在做的事情跨时了,比如正在读一本长篇,但到了睡觉时间,该怎么办?最简单的方法是强制打断,但会显得生硬。更好的设计是“收尾状态”:允许角色在完成当前章节或段落后再中断,但会付出一些“迟到”或“体力”代价。这就是“过渡阶段”的来由。在《环世界》里,角色如果在工作半途被打断去吃饭,他们会在心里记住“未完成的工作”,重新规划时间块时优先回去补完。
时间块调度还需要考虑“节奏感”:《动物森友会》的岛屿居民活动时间随意,但每个角色都有独特的“小动作”;《模拟人生》的自由度过高导致角色低效。我的倾向是:用时间块作为“软约束”,同时用“内部动机”(如疲劳度)推动角色退出当前块进入另一个。比如角色在深夜还在工作,疲劳度积累到阈值,自动触发“想睡觉”的意图,从而打断工作。这种基于生理时钟的调度,比单纯的时钟更可信。
第3层:打断与恢复机制——如何在混乱后重建秩序
现实生活充满意外:消防警报响,你必须放下晚餐;孩子哭,你必须放下工作。游戏里的虚拟生命也一样。但难点在于:被打断之后,角色是回到之前状态,还是另起炉灶?如果每次打断都让角色重新做一切,很容易造成“永远无法完成一件事”的挫败感。反之,如果强制角色从不中断,又显得迟钝。
这里需要三个关键设计:
- 状态快照:保存当前行为的进度(例如阅读到第几页、砍树砍到第几刀),以及该行为的“上下文”(比如正在厨房做饭,且有食材在旁边)。这样被打断后可以恢复。
- 恢复意图:在打断结束后,角色要有一个“评估是否恢复”的决策。如果当前需求已变化,比如刚打完架,累了,那就去休息而不是继续读那本书。
- 打断优先级分类:不是所有打断都是平等的。比如“火灾”是最高优先级,必须立即中断;“朋友来拜访”是中优先级,可以延迟几分钟;而“突然想起要写日记”是低优先级,可以稍后处理。
我给出一个打断机制的状态机参考:角色处于“执行行为A”时,外部事件E触发。先判断E的紧急度是否超过当前行为的“打断阈值”。阈值由行为类型决定:睡觉时被打断的阈值较低(容易被吵醒),而专注工作时阈值较高(不容易被分心)。另外还要考虑角色性格:比如“负责”性格相对“随性”不容易放弃手头任务。
实际案例:在《环世界》中,当殖民地发生袭击时,所有角色会立刻放下当前工作,转入战斗或逃跑。战斗结束后,他们会根据需求状态自动回归,可能是继续之前未完成的任务(如果需求仍紧迫),也可能因为心情崩溃而去做别的事。这种“回归又带点变化”的处理,是我见过最真实的。
为了避免“打断-恢复”循环让AI陷入无限循环,可以增加一个“冷却时间”:同一个行为被打断后,短时间内(比如30分钟)不再进入该行为。另外,如果恢复后再次被打断,则继续执行更高优先级的任务。这套机制配合优先级仲裁,才能让角色不仅活,还能“挣扎着活”。
第4层:群体调度性能优化——一屏的NPC,别一次榨干CPU
当你的小镇里有50个NPC,每帧都要做完整的优先级计算,那再好的算法也可能拖垮帧率。真正的模拟游戏用的是“批次更新”和“惰性筛选”。核心原则是:不要把每个AI都当作独立智慧的个体,而是按需分配计算。
常用手段有四种:
- 时间分片:不用每帧更新所有角色,而是把更新频率设为每0.5秒一次,或者把角色分成4批,每帧只评估1/4。
- 需求级联:先按需求最高的前10%角色更新,剩余角色按低频率执行简单的日程表(比如直接按时间块切换行为),只有当附近有玩家或事件时,才把它们提升为“完整模拟”。
- 事件驱动:在角色周围半径内发生特定事件时,将其唤醒并重新评估优先级。没事发生时,让角色停留在“当前行为”的剩余时长里,不做返工。
- 简化行为树:对于次要角色,砍掉行为树的深度,只保留最核心的“需求→行为”映射。玩家通常不会注意到背景角色是否在细节上有多变。
另外,还要注意“CPU突发”问题:当所有NPC同时因为某个全局事件(比如夜晚降临)切换行为时,会造成尖峰。解决方案是给每个NPC一个随机偏移或“异步切换”窗口,比如通知每个NPC在接下来的10分钟内随机时间点开始换班。
《城市:天际线》的市民模拟就是典型的分层:多数市民只有简单的“家-公司”往返,只有被玩家视线关注到的市民才会启用完整的行为模型。这种“玩家视野内的精细化”是优化与体验的最佳平衡。
可调参数与建议区间
以下是一张核心参数配置表,你可以直接拿到你的项目里调整。所有数值基于“每游戏小时”为基准,建议游戏时间比率为1分钟=1游戏小时。
| 参数名 | 含义 | 建议区间 | 备注 |
|---|---|---|---|
| basePriority | 行为基础权重 | 0~100 | 饮食60,工作50,社交40,娱乐30 |
| decayRate | 需求衰减速率(每小时) | 5%~15% | 饥饿最快12%,社交8%,疲劳5% |
| interruptThreshold | 打断阈值(相对当前行为优先级) | 0.8~1.2 | 低于1.0才允许打断 |
| timeBlockLength | 时间块长度 | 30分钟~2小时 | 按“黎明/早晨/上午/午间/下午/傍晚/夜晚”划分 |
| transitionDelay | 时间块切换的过渡时长 | 5~15分钟 | 用于角色移动、缓冲 |
| resumeCooldown | 打断后冷却时间 | 10~30分钟 | 防止同一行为被反复打断 |
| groupUpdateInterval | 群体更新间隔(秒) | 0.2~0.5 | 越小越精确但耗CPU |
在应用时,注意让“需求衰减速率”明显高于“时间块刚性”才有趣:如果角色完全按时间表行动,需求不涨,就会显得机械;反之需求涨得太快,角色会频繁偏离时间表,就像得了多动症。建议先用“低频时间表+高频需求”做原型,再慢慢平衡。
初级路径:五天实现一个日程系统
如果你是独立原型阶段,别急着上行为树。最快的方法是:定义需求(饥饿/社交/疲劳)和简单行为(吃/聊/睡),用优先级数值计算意图分数,然后用一个主循环让所有角色每1游戏小时决策一次。行为可以用状态机表示,切成“吃饭”“工作”等几个状态。时间块就用一个简单的7段数组,每个时间段给一个默认活动,让角色在“默认活动”和“高需求行为”之间取最高分。这种方式在原型期完全够用,比如《星露谷》的早期版本就是这么做的。你只需要把参数调好,就已经能看出生活感了。
进阶路径:当我们谈论行为树,我们在谈论什么
初级方案在复杂多目标、动态优先级时就会手忙脚乱。这时引入行为树(Behavior Tree)是合理的。行为树的好处是把决策分层:根节点是“选择”(Selector),下面挂“紧急事件”、“日常需求”、“自由活动”等子树;叶子节点是具体动作。优先级由子树的执行顺序决定,但你可以用“条件节点”动态调整分支。关键一点:行为树并不解决“优先级仲裁”本身,它只是把仲裁变成树上的条件判断。真正的仲裁还是要依赖你的数值模型。我也建议配合“黑板系统”(Blackboard)存共享变量,比如“当前关心点”,让其他角色也能感知到。
进阶路径还需要处理“迭代更新”的效率问题:行为树每次评估都会消耗栈。你可以给树加“子树缓存”,只有条件改变时重新评估。另外,状态恢复可以用“行为快照”接口,在节点被中断时保存状态,在恢复时重新加载。这些技术是商业模拟游戏的标准做法。
常见坑:调度垄断了做游戏的注意力
坑一:把调度系统当做目的本身。玩家并不在意你的调度算法多精妙,他们只看到角色是不是令人信服。过度设计优先级和打断机制,却忽略了美术表现,往往得到的只是毫无表情的机器人。要让角色有“发呆”和“思考”的中间态,这些不是行为树能给的。
坑二:时间块切得太碎。如果你把时间块定为10分钟,并且强制每个时段都要有明确活动,那角色会显得像机器一样频繁切换,反而失去自然感。建议时间块至少30分钟,并允许偶发“跳过”。
坑三:性能优化牺牲了行为的丰富度。为了性能,你可能让大部分NPC只用简单日程表,但玩家只要近看一秒就会穿帮。折中方案是用“玩家注视”来激活精细模拟,而不是一刀切。
FAQ
Q1:行为树和状态机到底选哪个?如果你是原型,状态机够用;如果游戏需要多目标决策、动态条件组合,行为树更合适。但行为树不等于智能,它只是让代码更易扩展。你也可以混合:用状态机处理细节动作,用行为树做高层决策。
Q2:如何平衡玩家控制与AI自主?玩家控制有两种:直接指挥(像《模拟人生》)和间接影响(像Big Brother)。建议把玩家指令作为高优先级“意图”加入仲裁,但可以设置“逆反阈值”——如果角色极度疲劳,玩家让他去种田可能会被拒绝,这种拒绝反而增加了生气。
Q3:怎么避免角色每天重复做同一件事导致乏味?在时间块之内增加随机性和浮动空间,比如“上午工作”细化成“整理桌子”、“看电脑”、“打印”等子行为,用概率轮盘挑选。另外,利用“心情”或“性格”改变权重,让角色偶尔冲动行事。
Q4:群体调度时,怎么防止NPC们凑到同一位置堵车?在行为树里加“导航成本”和“空间负载”的判断,或者用Flow Field(流场)来导航。但最直接的是给每个行为一个“最大同时执行人数”限制,超过就切换备用行为。像《环世界》的社交活动就会出现多人聚一起,但那是好现象。
Q5:有没有必要给NPC做长期规划?如果要表现“长期目标”比如“成为一个画家”,可以用一个持久状态对日常行为做调制,而不是直接规划未来。这样角色会在美术和音乐之间多花时间,但依然生活在当下。