你站在暴风城银行门口扫一眼周围:旁边有个法师在搓面包,对面有个猎人带着宠物警戒,门口站着两个卫兵,地上还有几个玩家留下的尸体。
所有这些「东西」在 AzerothCore 的代码里继承自同一个基类------Object。法师和卫兵行为天差地别,但从 Object 到 Player/Creature,继承链的四层结构刚好对应了四层能力边界。

WoW 里你能看到的所有东西,在代码里都叫 Object 。不是"对象"那个抽象概念,而是一个具体的 C++ 类 Object。
玩家是 Object,怪物是 Object,物品是 Object,甚至地上的尸体、空中的魔法特效(DynamicObject)、载具(Transport)都是 Object。
但这样就完了吗?显然不是。玩家和怪物虽然都是 Object,但玩家能登录、能加好友、能进公会,怪物能 AI 寻路、能掉装备------它们的行为天差地别。如果所有逻辑都塞进 Object 一个类,这个类会有几万行代码,维护噩梦。
AzerothCore 的解法是继承链------把"共有属性"往上提,把"特有行为"往下放。最终形成一条清晰的层次结构:
Object
└── WorldObject(有坐标、能放地图)
└── Unit(能战斗、能施法、有血量)
├── Player(能登录、有背包、有任务)
└── Creature(有AI、会掉装备、能刷新)
这条链不是随便设计的,每一层都对应游戏里一个"能力边界"。
第一层:Object------所有游戏对象的基类
Object 是整棵继承树的根,代码在 src/server/game/Entities/Object/Object.h。
它定义了所有游戏对象都有的最基础能力:
1. GUID(全局唯一 ID)
cpp
ObjectGuid GetGUID() const { return GetGuidValue(OBJECT_FIELD_GUID); }
每个 Object 都有一个 ObjectGuid,服务器里绝对唯一。GUID 不是简单的自增整数,而是编码了类型信息(Player/Creature/Item/...)和实例 ID,客户端和服务器通信时靠它识别"你在跟谁说话"。
2. 类型识别
cpp
TypeID GetTypeId() const { return m_objectTypeId; }
bool isType(uint16 mask) const { return (mask & m_objectType); }
TypeID 是一个枚举(ObjectGuid.h 里定义):
cpp
enum TypeID {
TYPEID_OBJECT = 0, // 基础对象
TYPEID_ITEM = 1, // 物品
TYPEID_CONTAINER = 2, // 容器(背包)
TYPEID_UNIT = 3, // 生物(玩家+怪物)
TYPEID_PLAYER = 4, // 玩家
TYPEID_GAMEOBJECT = 5, // 游戏对象(门、矿、草药...)
TYPEID_DYNAMICOBJECT = 6, // 动态对象(魔法特效、光环可见部分)
TYPEID_CORPSE = 7 // 尸体
};
IsPlayer()、IsCreature()、IsGameObject() 这些判断,底层都是查 TypeID。
3. Update Fields(数据同步机制)
这是 Object 最核心的机制之一。每个 Object 都有一块内存数据结构,记录了客户端需要知道的"所有属性"------血量、位置、装备、buff......
这块数据在代码里叫 Update Fields ,定义在 UpdateFields.h:
cpp
enum EObjectFields {
OBJECT_FIELD_GUID = 0x0000, // GUID(8字节,客户端识别用)
OBJECT_FIELD_TYPE = 0x0002, // 类型掩码
OBJECT_FIELD_ENTRY = 0x0003, // 模板 ID(对应 *_template 表)
OBJECT_FIELD_SCALE_X = 0x0004, // 模型缩放
OBJECT_END = 0x0006,
};
每个字段有一个偏移量 (0x0000、0x0002...),客户端和服务器靠这个偏移量同步数据。OBJECT_END 是 Object 层的字段结束位置,下一层(Item/Unit/Player...)的字段从 OBJECT_END 往后接着排。
这就是"继承"在数据传输层的体现------Item 的字段排在 Object 后面,Unit 的字段排在 Item 后面,Player 的字段排在 Unit 后面。客户端拿到一段 Update Data,按偏移量解析,就知道"这是哪个字段、属于哪一层"。
4. 世界存在性
cpp
bool IsInWorld() const { return m_inWorld; }
virtual void AddToWorld();
virtual void RemoveFromWorld();
每个 Object 可以选择"在地图里"或"不在地图里"。只有 AddToWorld() 之后,其他玩家才能看到它、与它交互。RemoveFromWorld() 不是删除对象,只是让它从地图里消失(比如玩家下线、怪物被杀死后尸体消失)。
第二层:WorldObject------有坐标的对象
Object 只知道"我是谁",但不知道"我在哪"。WorldObject(同样在 Object.h 里定义)解决了这个问题:
cpp
class WorldObject : public Object, public WorldLocation
它继承了 Object,同时多重继承 了 WorldLocation(位置信息)。这意味着 WorldObject 知道自己的坐标(X, Y, Z, MapID, ZoneID)。
WorldObject 新增的能力:
1. 地图管理
cpp
Map* GetMap() const { return m_currMap; }
void SetMap(Map* map) { m_currMap = map; }
每个 WorldObject 都知道自己在哪个 Map(地图实例)里。Map 是 AzerothCore 空间管理的核心单元------一个 Map 是一块地图区域,里面装着各种对象,服务器负责把它们全部管理起来。
2. 可见性计算
cpp
bool IsWithinDist3d(float x, float y, float z, float dist) const;
bool IsWithinDist2d(float x, float y, float dist) const;
bool IsInRange(WorldObject* target, float minDist, float maxDist) const;
WorldObject 能计算"我在不在某个范围内"、"我能不能看到某个对象"。这是 AI 寻路、技能释放、战斗判定的基础。
3. 相位系统(Phasing)
cpp
uint32 GetPhaseMask() const { return m_phaseMask; }
void SetPhaseMask(uint32 phaseMask) { m_phaseMask = phaseMask; }
WoW 的"相位"系统(同一个地图里,不同玩家看到不同的 NPC/对象)在 WorldObject 层实现。每个 WorldObject 有一个 PhaseMask,只有 PhaseMask 匹配的玩家才能看到它。
第三层:Unit------能战斗的对象
Unit 是继承链的分水岭。从这一层开始,对象能战斗、能施法、有血量、有能量。
代码在 src/server/game/Entities/Unit/Unit.h:
cpp
class Unit : public WorldObject
Unit 是 Player 和 Creature 的共同父类,它定义了"战斗单位"的所有能力:
1. 战斗属性
cpp
int32 GetHealth() const { return GetUInt32Value(UNIT_FIELD_HEALTH); }
int32 GetMaxHealth() const { return GetUInt32Value(UNIT_FIELD_MAXHEALTH); }
void SetHealth(int32 val) { SetUInt32Value(UNIT_FIELD_HEALTH, val); }
// 能量系统(法力/怒气/能量/符文能量...)
Powers GetPower(Powers powerType) const;
void SetPower(Powers powerType, int32 value);
血量、法力值、怒气、能量......这些在 Unit 层统一管理。不同职业的能量类型不同,但底层接口一样。
2. 战斗状态机
cpp
bool IsInCombat() const;
void Attack(Unit* target, bool meleeAttack);
void CombatStop();
void Kill(Unit* target, bool durabilityLoss = true);
Unit 维护一个"攻击者列表"(AttackerSet),记录"谁在打我"。IsInCombat() 就是检查这个列表是否为空。战斗状态的进入/退出触发一系列事件(Hook),驱动 AI 和玩家脚本。
3. 法术系统
cpp
bool CastSpell(Unit* target, uint32 spellId, bool triggered = false);
bool CastSpell(Unit* target, SpellInfo const* spellInfo, bool triggered = false);
void InterruptSpell(CurrentSpellTypes index, SpellCastResult result = SPELL_FAILED_INTERRUPTED);
Unit 能施放法术。CastSpell() 是核心接口,内部会检查法力、冷却、距离、免疫......所有施法条件。施法过程不是立即完成,而是分成"施法前 → 施法中 → 施法完成"几个阶段,每个阶段都有 Hook 可以介入(这就是上一篇 SpellScript 的用武之地)。
4. Aura(光环/增益减益)
cpp
Aura* AddAura(uint32 spellId, Unit* target);
void RemoveAura(Aura* aura, AuraRemoveMode mode = AURA_REMOVE_BY_DEFAULT);
bool HasAura(uint32 spellId) const;
Buff/Debuff 在代码里叫 Aura。Unit 能给自己或别人上 Aura,也能移除。Aura 有持续时间、能叠加、能被驱散,是 WoW 战斗系统的核心机制之一。
5. AI 接口
cpp
virtual UnitAI* GetAI() { return nullptr; }
Unit 定义了一个虚函数 GetAI(),让子类(Player/Creature)返回自己的 AI 实现。Player 的 AI 是玩家自己操作(或 mod-playerbots 的 AI),Creature 的 AI 是 SmartAI 或自定义 AI。
第四层:Player 和 Creature------继承链的两个分支
从 Unit 开始,继承链分成两支:Player (玩家)和 Creature(怪物/NPC)。
Player:能登录的 Unit
代码在 src/server/game/Entities/Player/Player.h:
cpp
class Player : public Unit, public GridObject<Player>
Player 在 Unit 的基础上,增加了大量"玩家专属"的能力:
1. 会话管理
cpp
WorldSession* GetSession() const { return m_session; }
每个 Player 对象都绑定一个 WorldSession(网络连接)。玩家下线时,m_session 置空,但 Player 对象不一定立即销毁(可能有离线邮件、拍卖等需要异步处理)。
2. 背包与物品
cpp
Item* GetItemByPos(uint8 bag, uint8 slot) const;
bool AddItem(uint32 itemId, uint32 count);
void DestroyItem(uint8 bag, uint8 slot, bool update = true);
Player 有背包(Container),背包里有格子(Slot),格子里有物品(Item)。这套系统在 Unit 层完全没有,是 Player 专属的。
3. 任务系统
cpp
bool HasQuest(uint32 questId) const;
bool CanTakeQuest(Quest const* quest, bool msg);
void CompleteQuest(uint32 questId);
void RewardQuest(Quest const* quest, uint32 rewardId, ObjectGuid receiver, bool announce = true);
Player 能接任务、做任务、交任务。任务状态存在 characters 数据库的 character_queststatus* 系列表里(系列一篇10讲过)。
4. 社交系统
cpp
PlayerSocial* GetSocial() { return m_social; }
bool HasFriend(ObjectGuid guid) const;
bool IsInGroup(Group const* group) const;
好友、公会、组队、黑名单......这些社交功能只有 Player 有。
5. PvP 与战场
cpp
bool InBattleground() const;
Battleground* GetBattleground() const;
void TeleportToBattleground();
Player 能进战场、能排竞技场、能打世界 PvP。
Creature:有 AI 的 Unit
代码在 src/server/game/Entities/Creature/Creature.h:
cpp
class Creature : public Unit, public GridObject<Creature>,
public MovableMapObject, public UpdatableMapObject
Creature 在 Unit 的基础上,增加了"怪物专属"的能力:
1. AI 控制器
cpp
CreatureAI* GetAI() const { return m_AI; }
void SetAI(CreatureAI* AI) { m_AI = AI; }
每个 Creature 都有一个 CreatureAI(AI 控制器)。AI 可以是 SmartAI(数据驱动,走 smart_scripts),也可以是自定义 AI(继承 CreatureAI 重写虚函数)。
2. 模板与实例
cpp
uint32 GetCreatureTemplateID() const { return GetUInt32Value(OBJECT_FIELD_ENTRY); }
CreatureTemplate const* GetCreatureTemplate() const;
Creature 的"种类"由 creature_template 表定义(系列一篇3讲过),具体刷新的位置由 creature 表定义。代码里用 GetCreatureTemplate() 拿到模板数据,用 GetPosition() 拿到实例位置。
3. 掉落与战利品
cpp
Loot* GetLootForPlayer(Player* player);
bool HasLootForPlayer(Player* player) const;
void AllLootRemovedFromCorpse();
Creature 被杀死后,会生成战利品(Loot)。战利品不是立即给玩家,而是生成一个"可拾取对象",玩家右键点击后才分配物品。
4. 刷新机制
cpp
void ForcedDespawn();
void Respawn();
time_t GetRespawnTime() const;
Creature 被杀死后,不会立即消失,而是进入"尸体状态"(Corpse),一段时间后自动消失并重新刷新(Respawn)。刷新时间和位置由 creature 表的 spawntimesecs 字段控制。
继承链的设计哲学
回头看这条继承链,它体现了几个重要的设计决策:
1. 能力分层,而不是数据分层
每一层继承都对应一种"能力":
- Object:我能识别(GUID)、我能同步数据(Update Fields)
- WorldObject:我知道自己在哪里(坐标)、我能判断远近(可见性)
- Unit:我能战斗(血量、法术、AI)
- Player/Creature:我是玩家/怪物(专属行为)
不是"把所有字段都放 Object,然后用标志位区分",而是"每一层只加自己需要的字段和方法"。这符合面向对象设计的单一职责原则。
2. Update Fields 的偏移量继承
前面提到,UpdateFields.h 里的字段偏移量是层叠的:
cpp
OBJECT_END = 0x0006, // Object 层结束
ITEM_END = 0x003A, // Item 层结束(接在 Object 后面)
UNIT_END = ..., // Unit 层结束(接在 Item 后面)
PLAYER_END = ..., // Player 层结束(接在 Unit 后面)
这意味着:客户端拿到一段 Update Data,按偏移量解析,就能知道"这是哪个字段" ,不管这个字段属于哪一层。这是"继承"在网络协议层的体现------C++ 的继承关系,直接映射到了数据包的字段布局。
3. TypeID 与多态的配合
TypeID 枚举和 C++ 的虚函数配合,实现了"运行时类型识别":
cpp
void SomeFunction(Object* obj) {
if (obj->GetTypeId() == TYPEID_PLAYER) {
Player* player = static_cast<Player*>(obj);
player->DoSomethingPlayerSpecific();
}
}
这是 C++ 里常见的"类型开关"模式。AzerothCore 大量使用这种模式,因为游戏逻辑经常需要"对玩家做 X,对怪物做 Y"。
总结:一条继承链,三层能力边界
Object → WorldObject → Unit → Player/Creature,这条继承链不是随意设计的,而是精确对应了游戏对象的三层能力边界:
- Object 层:我是谁?(GUID、类型)+ 我怎么同步数据?(Update Fields)
- WorldObject 层:我在哪?(坐标、地图、相位)+ 我能不能看到你?(可见性)
- Unit 层:我能战斗吗?(血量、法术、AI)+ 我有能量吗?(法力、怒气)
- Player/Creature 层:我是玩家还是怪物?(专属行为、专属数据)
每一层都只关心"这一层应该关心的事",把"下一层的事"留给子类。这种分层设计,让 AzerothCore 的对象系统既功能完整 (该有的都有),又结构清晰(不该混的不混)。
那么,这些对象怎么放进地图?上千个玩家和怪物同时在暴风城里时,服务器的性能怎么保证?答案是 AzerothCore 的空间分区系统------把地图切成 Grid(网格),每个 Grid 只加载附近的对象,让服务器的内存和 CPU 集中在该集中的地方。这正是空间分区系统要解决的核心问题。