AzerothCore学习笔记·实体01:实体系统——Object → Unit → Player 的继承链

你站在暴风城银行门口扫一眼周围:旁边有个法师在搓面包,对面有个猎人带着宠物警戒,门口站着两个卫兵,地上还有几个玩家留下的尸体。

所有这些「东西」在 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,
};

每个字段有一个偏移量0x00000x0002...),客户端和服务器靠这个偏移量同步数据。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,这条继承链不是随意设计的,而是精确对应了游戏对象的三层能力边界

  1. Object 层:我是谁?(GUID、类型)+ 我怎么同步数据?(Update Fields)
  2. WorldObject 层:我在哪?(坐标、地图、相位)+ 我能不能看到你?(可见性)
  3. Unit 层:我能战斗吗?(血量、法术、AI)+ 我有能量吗?(法力、怒气)
  4. Player/Creature 层:我是玩家还是怪物?(专属行为、专属数据)

每一层都只关心"这一层应该关心的事",把"下一层的事"留给子类。这种分层设计,让 AzerothCore 的对象系统既功能完整 (该有的都有),又结构清晰(不该混的不混)。

那么,这些对象怎么放进地图?上千个玩家和怪物同时在暴风城里时,服务器的性能怎么保证?答案是 AzerothCore 的空间分区系统------把地图切成 Grid(网格),每个 Grid 只加载附近的对象,让服务器的内存和 CPU 集中在该集中的地方。这正是空间分区系统要解决的核心问题。