AzerothCore学习笔记·模组04:模组机制——mod-playerbots 实战解析

你一个人想打英雄本,但组不到人。这时候 .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 心跳调用。每帧的工作流程:

  1. 遍历所有激活的 Strategy 的 Triggers
  2. Triggers 中有任何一个 isActive(),对应的 Strategy 被标记为"可用"
  3. 从可用 Strategy 的候选 Action 列表中,选出优先级最高的
  4. 执行该 Action
  5. 如果 Action 返回 ACTION_RESULT_IMPOSSIBLE,跳过换下一个
  6. 如果返回 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 :职业专属策略(如法师的 ArcaneMageStrategyFireMageStrategyFrostMageStrategy
  • 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 模块化体系的完整利用方式:

  1. Hook 层:7 个 ScriptObject 子类(PlayerScript、WorldScript 等)作为模块入口,精确声明 EnabledHooks
  2. 引擎层:自建的 Trigger→Strategy→Action 循环引擎,替代 CreatureAI 的事件驱动模型
  3. 扩展层 :Base→Class→Dungeon/Raid/World 四层架构,通过 NamedObjectContext 工厂模式实现策略注册和覆盖
  4. 配置层:2145 行配置文件,让行为参数可调免编译

480+ 个文件、10 个职业子目录、94 个副本策略文件------这是 AzerothCore 模块化体系中最大的模块之一,也是 Hook 系统能承载多复杂业务的直接证据。

回过头看,mod-playerbots 展示了模块化和 Hook 系统的几个核心特点:Hook 入口轻、引擎自建、策略可叠加、配置可调。如果要给一个直观的对比,它就是 bot 版本的 Apache------功能靠模块堆,内核几乎不动。