你一个人想打英雄本,但组不到人。这时候 .bot add 5 回车,一个满级法师 bot 出现在你面前,自动治疗、自动走位、自动打断施法。
这个 bot 背后是一个 480+ 源文件的模块,跑着一套完全独立的 AI 引擎------检测你的血量低了就治疗、发现附近的怪就攻击、冷却好了就放爆发技能。它不依赖 AzerothCore 自带的 CreatureAI 系统,而是自建了 Trigger → Strategy → Action 的每帧决策循环。
这篇拿这个真实的模块做完整解剖------mod-playerbots ,看到一个模块如何在 Hook 框架上构建出完整的 AI 系统。

mod-playerbots 是什么
简单说:让服务器上自动控制角色(bot)的模块。 玩家可以邀请 bot 组队、下副本、PVP,bot 能自动打怪、做任务、升级、换装备,甚至自动跑世界。整个模块的代码量相当可观------光 src/ 目录下就有 480+ 个源文件 ,PlayerbotAI.cpp 单文件超过 21 万字节。
模块支持三种 bot 模式:
- RandomBot:服务器独立生成、独立登录的角色,遍布世界自动活动(打怪、做任务、升级),可以被玩家邀请入队
- AddClass Bot:从预设账号池快速拉出一个指定职业的 bot(已满级已穿装备),适合快速凑齐一个副本团
- Alt Bot:玩家在自己账号上创建的替代角色,手动登录后当作 bot 使用,适合长期搭档
三种模式共享一套 AI 引擎,区别仅在于如何登录和管理。
七个 Script 钩子:模块的入口
mod-playerbots 通过 7 个 ScriptObject 子类接入 AzerothCore 的 Hook 系统,全部定义在 src/Script/Playerbots.cpp 中:
cpp
class PlayerbotsDatabaseScript : public DatabaseScript // 数据库初始化
class PlayerbotsPlayerScript : public PlayerScript // 玩家登录/更新/经验
class PlayerbotsMiscScript : public MiscScript // 杂项Hook
class PlayerbotsServerScript : public ServerScript // 服务器启动
class PlayerbotsWorldScript : public WorldScript // 世界更新
class PlayerbotsScript : public PlayerbotScript // bot专用Hook
class PlayerBotsBGScript : public BGScript // 战场事件
以 PlayerbotsPlayerScript 为例,它精确声明了自己关心的 10 个 Hook(上一篇讲的 EnabledHooks 机制):
cpp
PlayerbotsPlayerScript() : PlayerScript("PlayerbotsPlayerScript", {
PLAYERHOOK_ON_LOGIN, // 玩家登录时创建PlayerbotMgr
PLAYERHOOK_ON_AFTER_UPDATE, // 每帧更新AI
PLAYERHOOK_ON_BEFORE_CRITERIA_PROGRESS, // 成就进度
PLAYERHOOK_ON_BEFORE_ACHI_COMPLETE, // 成就完成
PLAYERHOOK_CAN_PLAYER_USE_PRIVATE_CHAT, // 私聊过滤
PLAYERHOOK_CAN_PLAYER_USE_GROUP_CHAT, // 队聊过滤
PLAYERHOOK_CAN_PLAYER_USE_GUILD_CHAT, // 会聊过滤
PLAYERHOOK_CAN_PLAYER_USE_CHANNEL_CHAT, // 频道过滤
PLAYERHOOK_ON_GIVE_EXP, // 经验获取
PLAYERHOOK_ON_BEFORE_TELEPORT // 传送前
}) {}
10 个 Hook 编号一次性传入构造函数,ScriptMgr 在分发时只遍历匹配编号的脚本实例。如果某个 bot 模块只关心 ON_LOGIN,其他 9 个 Hook 的分发过程直接跳过------这就是 EnabledHooks 的性能价值。
OnPlayerLogin 做了两件事:给真实玩家创建 PlayerbotMgr(管理器,负责创建/控制 bot),以及通知 RandomPlayerbotMgr(随机 bot 的全局管理器)。
AI 引擎:Trigger → Strategy → Action 循环
这才是 mod-playerbots 的核心------一套自建的行为选择引擎 ,完全独立于 AzerothCore 的 CreatureAI 系统。
整个引擎围绕四个概念:
┌──────────┐ 检测条件 ┌───────────┐ 提供动作 ┌──────────┐
│ Trigger │───────────→ │ Strategy │───────────→ │ Action │
│ 触发器 │ │ 策略 │ │ 动作 │
└──────────┘ └───────────┘ └──────────┘
↑ ↑ │
│ 读取上下文数据 │ 编排多策略组合 ▼
└────── Value ←────────────┘ Engine(引擎)
缓存值 每帧执行一个Action
Trigger:条件检测器
每个 Trigger 检测一种条件,返回 bool。比如 HealthTriggers(生命值低于阈值)、LootTriggers(附近有可拾取物品)、PvpTriggers(附近有敌对玩家)。Triggers 本身不执行任何动作,只负责告诉引擎"现在是什么状态"。
Strategy:行为策略
Strategy 是一组 Trigger + Action 的绑定关系,加上优先级。一个 Strategy 说的是:"当这些条件满足时,你可以做这些动作。"
比如 CombatStrategy(战斗策略)绑定了一组战斗相关的 Trigger 和 Action。NonCombatStrategy(非战斗策略)绑定了脱战后的行为。多个 Strategy 可以同时激活,Engine 按优先级选择。
Action:原子操作
每个 Action 是一个不可再分的行为单元------"施放火球术"、"跟随队长"、"拾取尸体"、"购买面包"。ActionContext 里注册了 140+ 个 Action 工厂方法,覆盖了移动、战斗、物品、任务、社交等所有场景。
Engine:每帧驱动
Engine 是主循环,绑在 PlayerbotAIBase::UpdateAI() 上,每帧被 AzerothCore 的 worldserver 心跳调用。每帧的工作流程:
- 遍历所有激活的 Strategy 的 Triggers
- Triggers 中有任何一个
isActive(),对应的 Strategy 被标记为"可用" - 从可用 Strategy 的候选 Action 列表中,选出优先级最高的
- 执行该 Action
- 如果 Action 返回
ACTION_RESULT_IMPOSSIBLE,跳过换下一个 - 如果返回
ACTION_RESULT_OK,本帧结束
一个 bot 每帧最多执行一个 Action。这保证了行为可控------不会出现"同一帧施法 + 移动 + 拾取"的混乱。
Value:延迟计算的上下文缓存
在 Trigger 检测和 Action 执行之间,需要大量上下文数据:"最近的敌人是谁?"、"我的法力值百分比?"、"目标身上有没有特定 debuff?"。
如果每帧都重新计算,开销巨大。mod-playerbots 用 Value 系统解决------每个 Value 是一个懒计算的缓存值:
cpp
// 用法:从上下文中取值
Unit* target = AI_VALUE(Unit*, "current target");
float hp = AI_VALUE(float, "health");
Value<T> 有三种读取模式:
- Get():每次直接计算
- LazyGet():有缓存则返回缓存,无缓存才计算,之后缓存一段时间
- RefGet():返回引用,用于修改值
NamedObjectContext<UntypedValue> 管理所有 Value 的注册和查找,用字符串名字做键。ValueContext.h 里注册了 85 个 Value 工厂------每种上下文数据(敌人、血量、距离、法力值、冷却、目标......)各一个。
职业专属 AI:继承 + 扩展
Base 层提供了通用的 Trigger/Strategy/Action/Value(战斗、移动、物品、任务等),但不同职业的战斗逻辑差异很大。mod-playerbots 用职业子目录解决这个问题:
src/Ai/Class/
├── Dk/ 死亡骑士
├── Druid/ 德鲁伊
├── Hunter/ 猎人
├── Mage/ 法师
├── Paladin/ 圣骑士
├── Priest/ 牧师
├── Rogue/ 盗贼
├── Shaman/ 萨满
├── Warlock/ 术士
└── Warrior/ 战士
每个职业目录包含:
- Strategy :职业专属策略(如法师的
ArcaneMageStrategy、FireMageStrategy、FrostMageStrategy) - Actions:职业专属动作(如法师的 "寒冰箭"、"火焰冲击"、"奥术冲击")
- Triggers:职业专属条件(如法师的 "寒冰箭可用"、"需要唤醒")
- AiObjectContext:职业专属的工厂注册,继承基类并扩展
以法师为例,MageAiObjectContext.cpp 注册了 8 种非战斗/战斗策略(nc、pull、aoe、cure、buff、boost、cc、firestarter)和 3 种天赋路线(frost、fire、arcane、frostfire)。这些策略通过字符串名字("frost"、"fire")注册到 NamedObjectContext,运行时由命令切换:
.bot add 5 // 添加一个法师bot
.bot strategy frost // 切换到冰霜策略
10 个职业 × 每职业约 10 个 Strategy + 10 个 Action + 5 个 Trigger ≈ 250+ 个职业专属组件,加上 98 个 Class 目录下的源文件。
Dungeon/Raid/World:场景化 AI
除了 Base 和 Class,mod-playerbots 还有三个场景层:
- Dungeon(94 个文件):副本专用策略和动作,比如 ICC 的特定 Boss 机制
- Raid(84 个文件):团本专用,处理更复杂的团本机制
- World(5 个文件):世界事件相关
这三层叠加在 Base + Class 之上,形成四层架构:
World / Raid / Dungeon ← 场景层:副本/团本特化逻辑
Class (Mage/Warrior...) ← 职业层:职业特化策略和动作
Base (Actions/Strategy) ← 基础层:通用战斗/移动/物品逻辑
Engine ← 引擎层:每帧驱动Trigger→Strategy→Action循环
上层可以覆盖下层的同名策略。比如副本里的法师可能需要优先"驱散"而非"输出",Dungeon 层的法师策略就覆盖了 Class 层的默认行为。
2145 行配置文件:不写代码也能调 bot 行为
playerbots.conf.dist 有 2145 行配置,分为 20+ 个类别:通用设置、召唤、坐骑、装备、拾取、计时器、距离阈值、任务、战斗、增益策略、作弊、法术、飞行点、专业、随机 bot 的等级/装备/任务/活动/PVP......
这意味着很多行为参数不需要改代码重新编译,改配置重启即可。比如:
RandomBot.UpdateInterval.Second:随机 bot 的 AI 更新间隔Combat.Distance.Melee:近战攻击距离Loot.Distance:拾取距离RandomBot.Rpg.Teleport:是否允许随机 bot 使用传送门
配置驱动 + 策略组合 = 在不改一行代码的前提下,通过命令和配置文件调出截然不同的 bot 行为。
模块规模:数字会说话
整个 mod-playerbots 的代码分布:
| 层级 | 文件数 | 功能 |
|---|---|---|
| Bot/(核心框架) | ~30 | PlayerbotMgr、PlayerbotAI、Engine、AiObjectContext |
| Ai/Base/Actions | 141 | 通用动作(移动、战斗、物品、社交、战场) |
| Ai/Base/Strategy | 44 | 通用策略(战斗、非战斗、逃跑、跟随、PVP) |
| Ai/Base/Trigger | 17 | 通用触发器(血量、距离、物品、PVP) |
| Ai/Base/Value | 85 | 上下文缓存值 |
| Ai/Class/ | 98 | 10 个职业的专属 Strategy/Action/Trigger |
| Ai/Dungeon/ | 94 | 副本策略 |
| Ai/Raid/ | 84 | 团本策略 |
| Ai/World/ | 5 | 世界事件 |
| Script/ | 3 | 7 个 ScriptObject 钩子定义 |
| data/ | SQL 数据 | 数据库表 |
480+ 个源文件,纯 C++,通过 AzerothCore 的 CMake 模块系统独立编译。
和 CreatureAI 的区别
一个值得比较的问题:mod-playerbots 的 AI 和 AzerothCore 自带的 CreatureAI(smart_scripts 驱动的 NPC AI)有什么区别?
| 维度 | CreatureAI / SmartAI | mod-playerbots AI |
|---|---|---|
| 控制对象 | NPC(Creature) | 玩家角色(Player) |
| Hook 入口 | CreatureScript 的虚函数 | 7 个 ScriptObject 钩子 |
| 决策引擎 | 事件驱动(smart_scripts 事件链) | 每帧评估 Trigger→Strategy→Action |
| 行为丰富度 | 按模板配置,事件类型有限 | 480+ 文件,140+ Action |
| 可扩展性 | SQL 数据驱动 | 代码 + 配置双重驱动 |
| 适用场景 | NPC Boss/Mob 行为 | 模拟玩家行为(打怪/任务/PVP/副本) |
CreatureAI 是"简单但数据驱动"的路线------SQL 配置即可定义行为,但复杂行为写不出来。mod-playerbots 是"复杂但代码驱动"的路线------需要 C++ 编译,但能模拟完整的玩家行为。
两条路线互补:NPC 用 CreatureAI,bot 用 mod-playerbots。
总结:一个模块的四层工程
回头看 mod-playerbots,它展示了 AzerothCore 模块化体系的完整利用方式:
- Hook 层:7 个 ScriptObject 子类(PlayerScript、WorldScript 等)作为模块入口,精确声明 EnabledHooks
- 引擎层:自建的 Trigger→Strategy→Action 循环引擎,替代 CreatureAI 的事件驱动模型
- 扩展层 :Base→Class→Dungeon/Raid/World 四层架构,通过
NamedObjectContext工厂模式实现策略注册和覆盖 - 配置层:2145 行配置文件,让行为参数可调免编译
480+ 个文件、10 个职业子目录、94 个副本策略文件------这是 AzerothCore 模块化体系中最大的模块之一,也是 Hook 系统能承载多复杂业务的直接证据。
回过头看,mod-playerbots 展示了模块化和 Hook 系统的几个核心特点:Hook 入口轻、引擎自建、策略可叠加、配置可调。如果要给一个直观的对比,它就是 bot 版本的 Apache------功能靠模块堆,内核几乎不动。