AzerothCore 自定义副本开发指南:从地图加载到 Boss 脚本的完整链路 原创

温馨提示:
本文最后更新于 2026-09-21,已超过 0 天没有更新。 若文章内的图片失效(无法正常加载),请留言反馈或直接 联系我

引言

AzerothCore 是基于 CMake 的开源 MMO 服务器模拟器,采用模块化架构。自定义副本(Instance)开发是服务端定制化中最常见也最容易踩坑的领域之一。本文从地图加载机制出发,覆盖 ScriptName 绑定、Boss AI 脚本、副本数据表到生成与掉落的完整链路。

一、AzerothCore 架构概览

AzerothCore 由三大核心组件构成:

  • worldserver:世界服,处理地图、玩家、生物、战斗等核心逻辑
  • authserver:认证服,处理登录鉴权
  • Database:三库结构 — auth(账号)、characters(角色数据)、world(游戏数据)

副本系统涉及 worldserver 的 MapManager、InstanceScript,以及 world 数据库中的 instance_template、creature、gameobject 等表。

二、副本地图加载流程

当玩家进入副本传送门时,服务器执行以下流程:

  1. MapManager::CreateInstance:根据 mapId 创建或复用实例
  2. InstanceTemplate 查询:从 world.instance_template 读取副本配置(maxPlayers、resetDelay 等)
  3. InstanceScript 实例化:通过 ScriptName 字段关联到 C++ 脚本类
  4. BOSS/creature 生成:根据 creature 表和 instance_spawn_group 生成 NPC
-- 查看副本模板
SELECT entry, name, maxPlayers, resetDelay FROM instance_template WHERE entry=332;

三、ScriptName 绑定机制

每个副本在 instance_template 表中有一个 ScriptName 字段,指向 C++ ScriptMgr 注册的脚本名。以经典副本”影牙城堡”为例:

-- 数据库端
UPDATE instance_template 
SET ScriptName='instance_shadowfang_keep' 
WHERE entry=332;

C++ 端需要注册对应的 InstanceScript:

// InstanceShadowfangKeep.cpp
class instance_shadowfang_keep : public InstanceScript
{
public:
    InstanceShadowfangKeep(InstanceMap* map) : InstanceScript(map)
    {
        // 初始化 BOSS 进度检查
        SetBossNumber(4);  // 影牙城堡有4个 BOSS
    }

    void OnCreatureCreate(Creature* creature) override
    {
        switch (creature->GetEntry())
        {
            case NPC_BARON_SILVERLAINE:
                AddObject(creature, DATA_BARON_SILVERLAINE);
                break;
            case NPC_COMMANDER_SPRINGVALE:
                AddObject(creature, DATA_COMMANDER_SPRINGVALE);
                break;
        }
    }

    void OnGameObjectCreate(GameObject* go) override
    {
        switch (go->GetEntry())
        {
            case GO_BARON_DOOR:
                AddObject(go, DATA_BARON_DOOR);
                if (GetBossState(DATA_BARON_SILVERLAINE) == DONE)
                    go->SetGoState(GO_STATE_ACTIVE);
                break;
        }
    }

    bool SetBossState(uint32 id, EncounterState state) override
    {
        if (!InstanceScript::SetBossState(id, state))
            return false;

        switch (id)
        {
            case DATA_BARON_SILVERLAINE:
                if (state == DONE)
                {
                    // 开门逻辑
                    if (GameObject* go = GetGameObject(DATA_BARON_DOOR))
                        go->SetGoState(GO_STATE_ACTIVE);
                }
                break;
        }
        return true;
    }
};

void AddSC_instance_shadowfang_keep()
{
    new InstanceMapScript<instance_shadowfang_keep>(
        "instance_shadowfang_keep", 332);
}

四、Boss AI 脚本开发

Boss 脚本通过 ScriptName 关联到 creature 表:

-- 关联 BOSS 脚本
UPDATE creature_template 
SET ScriptName='boss_baron_silverlaine' 
WHERE entry=3887;
// BossBaronSilverlaine.cpp
class boss_baron_silverlaine : public CreatureScript
{
public:
    boss_baron_silverlaine() : CreatureScript("boss_baron_silverlaine") {}

    struct boss_baron_silverlaineAI : public BossAI
    {
        boss_baron_silverlaineAI(Creature* creature) : BossAI(creature)
        {
            // 初始化技能列表
        }

        void Reset() override
        {
            BossAI::Reset();
            // 重置 BOSS 状态、召唤物等
        }

        void EnterCombat(Unit* victim) override
        {
            BossAI::EnterCombat(victim);
            Talk(SAY_AGGRO);
            events.ScheduleEvent(EVENT_CURSED_VEIL, 15s);
            events.ScheduleEvent(EVENT_SUMMON_WORGEN_SPIRIT, 12s);
        }

        void ExecuteEvent(uint32 eventId) override
        {
            switch (eventId)
            {
                case EVENT_CURSED_VEIL:
                    DoCastVictim(SPELL_CURSED_VEIL);
                    events.ScheduleEvent(EVENT_CURSED_VEIL, 20s);
                    break;
                case EVENT_SUMMON_WORGEN_SPIRIT:
                    DoCastSelf(SPELL_SUMMON_WORGEN_SPIRIT);
                    events.ScheduleEvent(EVENT_SUMMON_WORGEN_SPIRIT, 25s);
                    break;
            }
        }

        void JustDied(Unit* killer) override
        {
            BossAI::JustDied(killer);
            Talk(SAY_DEATH);
            // 实例脚本通知
            if (InstanceScript* instance = GetInstanceScript())
                instance->SetBossState(DATA_BARON_SILVERLAINE, DONE);
        }
    };

    CreatureAI* GetAI(Creature* creature) const override
    {
        return GetShadowfangKeepAI<boss_baron_silverlaineAI>(creature);
    }
};

void AddSC_boss_baron_silverlaine()
{
    new boss_baron_silverlaine();
}

五、副本进度与门控制

AzerothCore 的副本进度通过 EncounterState 枚举管理:

状态 含义
NOT_STARTED 0 BOSS 未被触发
IN_PROGRESS 1 战斗中
FAIL 2 战斗失败(玩家逃跑/死亡)
DONE 3 BOSS 已击杀
SPECIAL 4 特殊状态(如跳过 BOSS)

门控制通过 OnGameObjectCreate + SetBossState 联动:BOSS 击杀后自动开门,这是最常见的副本流程控制模式。

六、掉落表与掉落系统

Boss 掉落通过 creature_loot_template 表配置:

-- 为 Baron Silverlaine 配置掉落
INSERT INTO creature_loot_template 
(Entry, Item, Reference, Chance, QuestRequired, LootMode, GroupId, MinCount, MaxCount)
VALUES
(3887, 60000, 0, 20, 0, 1, 0, 1, 1),  -- 蓝装 20% 掉率
(3887, 60001, 0, 15, 0, 1, 0, 1, 1),  -- 另一件蓝装
(3887, 60002, 0, 3, 0, 1, 0, 1, 1),   -- 紫装 3% 掉率
(3887, 60003, 0, 100, 0, 1, 1, 1, 2), -- 组1: 100% 掉一个
(3887, 60004, 0, 100, 0, 1, 1, 1, 2), -- 组1: 二选一
(3887, 60005, 0, 0, 0, 1, 2, 1, 1);   -- 组2: 引用模板

掉落系统核心概念:

  • Chance:独立掉率。如果 GroupId=0,则每个物品按各自的 Chance 独立判定
  • Group:同组物品共享 100% 概率池。Chance=100 表示必定掉落该组中某一件
  • Reference:引用 reference_loot_template,实现掉落表嵌套
  • QuestRequired:只有接了对应任务的玩家才能拾取

七、自定义副本的完整开发流程

假设要新增一个自定义 5 人副本,完整步骤:

1. 数据库配置

-- 副本模板
INSERT INTO instance_template (entry, name, ScriptName, maxPlayers, resetDelay)
VALUES (1000, '自定义副本', 'instance_custom_dungeon', 5, 3600);

-- 地图区域
INSERT INTO instance_template (entry, parent, mapType)
VALUES (1000, 0, 1);

-- BOSS creature_template
INSERT INTO creature_template 
(entry, name, faction, rank, ScriptName, minLevel, maxLevel)
VALUES 
(100000, '副本守门 BOSS', 14, 1, 'boss_custom_guardian', 80, 80),
(100001, '最终 BOSS', 14, 1, 'boss_custom_final', 82, 82);

-- 掉落表
INSERT INTO creature_loot_template (Entry, Item, Chance, GroupId, MinCount, MaxCount)
VALUES (100000, 60100, 25, 0, 1, 1);

2. C++ 脚本注册

// RegisterCustomDungeon.cpp
void AddSC_instance_custom_dungeon()
{
    new InstanceMapScript<instance_custom_dungeon>(
        "instance_custom_dungeon", 1000);
}

void AddSC_boss_custom_guardian()
{
    new boss_custom_guardian();
}

void AddSC_boss_custom_final()
{
    new boss_custom_final();
}

3. 编译与加载

# 确保脚本在 ScriptLoader 中注册
# 编辑 src/server/game/Scripting/ScriptLoader.cpp
// void AddCustomScripts()
// {
//     AddSC_instance_custom_dungeon();
//     AddSC_boss_custom_guardian();
//     AddSC_boss_custom_final();
// }

# 重新编译
cd build
cmake .. -DSCRIPTS=ON
make -j$(nproc) worldserver

4. 测试与调试

# GM 命令测试
.go 1000          # 进入副本
.npc add 100000   # 召唤 BOSS 测试
.kill             # 击杀测试掉落
.go creature 100000  # 定位 BOSS

# 查看实例状态
.instance list    # 查看当前实例进度

八、常见坑与注意事项

  • ScriptName 拼写:数据库中的 ScriptName 必须与 C++ 注册名完全一致(区分大小写)
  • BossAI 事件系统:使用 events.ScheduleEvent 时,时间单位必须用 chrono 字面量(15s, 1min)
  • InstanceScript 数据持久化:自定义进度数据用 SetData/GetData,BOSS 进度用 SetBossState
  • creature 表 vs creature_template:template 是模板(spawn 原型),creature 是实际刷出点
  • 地图 VMap/MMAP:自定义副本需要对应的 .vmap 和 .mmap 文件,否则玩家穿墙/卡墙

总结

AzerothCore 副本开发的核心链路:instance_template 绑定 ScriptName → C++ InstanceScript 管理进度 → BossAI 处理战斗逻辑 → creature_loot_template 配置掉落。掌握这条链路后,自定义副本的开发就是在这四个层面填内容。建议从修改现有副本开始练习,再尝试从头创建完整副本。