AzerothCore 副本脚本开发实战:从普通怪物到 Boss AI 的完整实现 原创

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

AzerothCore 副本脚本开发实战:从普通怪物到 Boss AI 的完整实现

分类:AzerothCore | 难度:进阶 | 预计阅读时间:25 分钟

一、引言

AzerothCore 是目前最活跃的魔兽世界模拟器开源项目之一,基于 TrinityCore 分支演化而来,以模块化、高性能和活跃的社区生态著称。对于希望深入理解魔兽世界服务器端开发的开发者而言,副本(Instance)脚本开发是最具价值的学习路径之一——它涵盖了怪物 AI、事件调度、阶段切换、团队机制等核心系统。

本文将以一个虚构的 5 人副本 「幽影矿坑」(Shadowdeep Mine) 为例,从零开始实现一个完整的副本脚本。你将学到:

  • 副本脚本的基础结构与注册机制
  • 普通怪物(Trash Mob)的 AI 实现
  • Boss 战斗的完整机制:阶段切换、技能调度、狂暴计时
  • Eluna 脚本集成——用 Lua 快速扩展副本逻辑
  • 调试与测试的最佳实践

本文假设你已具备 C++ 基础,并对 AzerothCore 的编译与运行环境有一定了解。如果你还没有搭建开发环境,建议先参考官方 Wiki 完成基础设置。

二、副本脚本基础结构

2.1 副本脚本的生命周期

在 AzerothCore 中,每个副本脚本都继承自 InstanceScript 类。这个类负责管理副本的整体状态,包括:

  • 怪物的生成与刷新
  • Boss 的击杀状态追踪
  • 副本内门、开关等游戏对象的控制
  • 遭遇战(Encounter)的进度管理

一个典型的副本脚本文件结构如下:

// ShadowdeepMine.h - 头文件
enum ShadowdeepMineData
{
    TYPE_BOSS_GORGRIM     = 0,  // 1号Boss
    TYPE_BOSS_VOIDWEAVER  = 1,  // 2号Boss
    TYPE_BOSS_OVERSEER    = 2,  // 3号Boss

    DATA_GORGRIM          = 0,
    DATA_VOIDWEAVER       = 1,
    DATA_OVERSEER         = 2,

    NPC_DARK_MINER        = 500001,  // 普通怪物
    NPC_CAVE_BAT          = 500002,
    NPC_GORGRIM           = 500010,  // Boss
    NPC_VOIDWEAVER        = 500011,
    NPC_OVERSEER          = 500012,

    GO_IRON_GATE          = 600001,  // 副本门
    GO_TREASURE_CHEST     = 600002,
};
// ShadowdeepMine.cpp - 实现文件
#include "ScriptMgr.h"
#include "InstanceScript.h"
#include "shadowdeep_mine.h"

class instance_shadowdeep_mine : public InstanceScript
{
public:
    instance_shadowdeep_mine(InstanceMap* map) : InstanceScript(map)
    {
        SetHeaders(DataHeader);
        SetBossNumber(MAX_ENCOUNTERS);
    }

    void OnCreatureCreate(Creature* creature) override
    {
        switch (creature->GetEntry())
        {
            case NPC_GORGRIM:
                gorgrimGUID = creature->GetGUID();
                break;
            case NPC_VOIDWEAVER:
                voidweaverGUID = creature->GetGUID();
                break;
            case NPC_OVERSEER:
                overseerGUID = creature->GetGUID();
                break;
        }
    }

    void OnGameObjectCreate(GameObject* go) override
    {
        switch (go->GetEntry())
        {
            case GO_IRON_GATE:
                ironGateGUID = go->GetGUID();
                break;
        }
    }

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

        // Boss击杀后开门
        if (state == DONE)
        {
            switch (type)
            {
                case TYPE_BOSS_GORGRIM:
                    if (GameObject* gate = GetGameObject(ironGateGUID))
                        gate->SetGoState(GO_STATE_ACTIVE);
                    break;
            }
        }
        return true;
    }

private:
    ObjectGuid gorgrimGUID;
    ObjectGuid voidweaverGUID;
    ObjectGuid overseerGUID;
    ObjectGuid ironGateGUID;
};

void AddSC_shadowdeep_mine()
{
    RegisterInstanceScript(instance_shadowdeep_mine, "instance_shadowdeep_mine");
}

2.2 注册脚本到 AzerothCore

脚本写完后,需要注册到 AzerothCore 的构建系统中:

  1. shadowdeep_mine.hshadowdeep_mine.cpp 放入 src/server/scripts/Kalimdor/ 目录(或自定义模块目录)。
  2. src/server/scripts/Kalimdor/CMakeLists.txt 中添加源文件引用。
  3. src/server/scripts/Kalimdor/script_loader.cpp 中添加 AddSC_shadowdeep_mine() 调用。
  4. 重新编译:mkdir build && cd build && cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/azeroth-server && make -j$(nproc)

三、普通怪物 AI 实现

3.1 基础怪物 AI 类

普通怪物(Trash Mobs)是副本体验的基础。AzerothCore 提供了 ScriptedAI 基类,我们通过继承它来实现自定义行为。

// 黑暗矿工 - 基础近战怪物
class npc_dark_miner : public ScriptedAI
{
public:
    npc_dark_miner(Creature* creature) : ScriptedAI(creature) { }

    enum Spells
    {
        SPELL_PICKAXE_SWING = 500101,  // 矿镐挥击
        SPELL_DUST_CLOUD    = 500102,  // 扬尘(致盲)
    };

    enum Events
    {
        EVENT_PICKAXE_SWING = 1,
        EVENT_DUST_CLOUD    = 2,
    };

    void Reset() override
    {
        _events.Reset();
    }

    void JustEngagedWith(Unit* who) override
    {
        _events.ScheduleEvent(EVENT_PICKAXE_SWING, 5s);
        _events.ScheduleEvent(EVENT_DUST_CLOUD, 12s);
    }

    void UpdateAI(uint32 diff) override
    {
        if (!UpdateVictim())
            return;

        _events.Update(diff);

        while (uint32 eventId = _events.ExecuteEvent())
        {
            switch (eventId)
            {
                case EVENT_PICKAXE_SWING:
                    if (Unit* victim = me->GetVictim())
                        me->CastSpell(victim, SPELL_PICKAXE_SWING, false);
                    _events.ScheduleEvent(EVENT_PICKAXE_SWING, 8s);
                    break;
                case EVENT_DUST_CLOUD:
                    if (Unit* victim = me->GetVictim())
                        me->CastSpell(victim, SPELL_DUST_CLOUD, false);
                    _events.ScheduleEvent(EVENT_DUST_CLOUD, 20s);
                    break;
            }
        }

        DoMeleeAttackIfReady();
    }

private:
    EventMap _events;
};

3.2 带特殊机制的怪物

有些普通怪物需要更复杂的逻辑。例如「洞穴蝙蝠」会随机飞向远处玩家并施加流血效果:

class npc_cave_bat : public ScriptedAI
{
public:
    npc_cave_bat(Creature* creature) : ScriptedAI(creature) { }

    enum Spells
    {
        SPELL_SONIC_SCREECH = 500201,  // 音波尖啸(AOE 恐惧)
        SPELL_BLOOD_BITE    = 500202,  // 血咬(DOT)
    };

    enum Events
    {
        EVENT_SONIC_SCREECH = 1,
        EVENT_BLOOD_BITE    = 2,
        EVENT_FLY_TO_RANDOM = 3,
    };

    void Reset() override
    {
        _events.Reset();
    }

    void JustEngagedWith(Unit* who) override
    {
        _events.ScheduleEvent(EVENT_SONIC_SCREECH, 10s);
        _events.ScheduleEvent(EVENT_BLOOD_BITE, 3s);
        _events.ScheduleEvent(EVENT_FLY_TO_RANDOM, 8s);
    }

    void UpdateAI(uint32 diff) override
    {
        if (!UpdateVictim())
            return;

        _events.Update(diff);

        while (uint32 eventId = _events.ExecuteEvent())
        {
            switch (eventId)
            {
                case EVENT_SONIC_SCREECH:
                    me->CastSpell(me, SPELL_SONIC_SCREECH, false);
                    _events.ScheduleEvent(EVENT_SONIC_SCREECH, 25s);
                    break;
                case EVENT_BLOOD_BITE:
                    if (Unit* target = SelectTarget(SelectTargetMethod::Random, 0))
                        me->CastSpell(target, SPELL_BLOOD_BITE, false);
                    _events.ScheduleEvent(EVENT_BLOOD_BITE, 7s);
                    break;
                case EVENT_FLY_TO_RANDOM:
                {
                    // 飞到随机远程玩家位置
                    if (Unit* target = SelectTarget(SelectTargetMethod::Random, 1, 10.0f, true))
                    {
                        me->GetMotionMaster()->MovePoint(0, target->GetPosition());
                    }
                    _events.ScheduleEvent(EVENT_FLY_TO_RANDOM, 12s);
                    break;
                }
            }
        }

        DoMeleeAttackIfReady();
    }

private:
    EventMap _events;
};

3.3 怪物组联动

在副本设计中,经常需要让一组怪物产生联动——比如一个怪物死亡后,附近的同伴会进入狂暴状态:

class npc_dark_miner_foreman : public ScriptedAI
{
public:
    npc_dark_miner_foreman(Creature* creature) : ScriptedAI(creature) { }

    enum Spells
    {
        SPELL_ENRAGE_NEARBY = 500301,  // 激怒附近矿工
        SPELL_WHIP_CRACK    = 500302,  // 鞭笞
    };

    void JustDied(Unit* killer) override
    {
        // 激怒周围 30 码内的所有黑暗矿工
        std::list<Creature*> miners;
        me->GetCreatureListWithEntryInGrid(miners, NPC_DARK_MINER, 30.0f);
        for (Creature* miner : miners)
        {
            if (miner->IsAlive())
            {
                miner->CastSpell(miner, SPELL_ENRAGE_NEARBY, true);
                // 提升攻击速度 50%,持续 30 秒
                miner->SetAttackTimer(BASE_ATTACK, 
                    miner->GetAttackTimer(BASE_ATTACK) * 0.5f);
            }
        }
    }
};

这种联动机制极大地提升了副本的战斗节奏感——玩家需要策略性地选择击杀顺序,而不是无脑 AOE。同时,这也对坦克的拉怪技巧提出了更高要求:需要将工头与普通矿工拉开距离,或者优先集火工头,避免其死亡后激怒整个怪物群。

除了死亡联动,我们还可以实现警戒联动——当一个怪物进入战斗时,附近的同伴会一同加入战斗,防止玩家逐个击破(俗称「拉火车」)。这在 AzerothCore 中可以通过设置 creature_addon 表中的 auras 字段或直接在 C++ 脚本中实现:

// 在怪物 AI 的 JustEngagedWith 中实现警戒联动
void JustEngagedWith(Unit* who) override
{
    // 通知 20 码内的同 entry 怪物
    std::list<Creature*> nearby;
    me->GetCreatureListWithEntryInGrid(nearby, me->GetEntry(), 20.0f);
    for (Creature* ally : nearby)
    {
        if (ally != me && ally->IsAlive() && !ally->IsEngaged())
        {
            ally->EngageWithTarget(who);
        }
    }
}

这种设计在 5 人副本中尤为常见,它迫使队伍在拉怪时更加谨慎,避免因引到过多怪物而导致灭团。

四、Boss AI 实现:第一阶段 Boss

4.1 戈格里姆(Gorgrim)—— 狂暴矿工头目

戈格里姆是幽影矿坑的第一个 Boss,一个手持巨大矿镐的食人魔。他的战斗分为两个阶段,考验坦克的走位和全队的躲避能力。

class boss_gorgrim : public CreatureScript
{
public:
    boss_gorgrim() : CreatureScript("boss_gorgrim") { }

    struct boss_gorgrimAI : public BossAI
    {
        boss_gorgrimAI(Creature* creature) : BossAI(creature, TYPE_BOSS_GORGRIM)
        {
            Initialize();
        }

        void Initialize()
        {
            _phase = PHASE_ONE;
            _enrageTimer = 180000;  // 3 分钟狂暴
        }

        enum Spells
        {
            SPELL_GROUND_SMASH   = 500401,  // 地面粉碎(AOE)
            SPELL_BOULDER_TOSS   = 500402,  // 投掷巨石
            SPELL_ENRAGE         = 500403,  // 狂暴
            SPELL_CAVE_COLLAPSE  = 500404,  // 洞穴坍塌(P2)
        };

        enum Events
        {
            EVENT_GROUND_SMASH  = 1,
            EVENT_BOULDER_TOSS  = 2,
            EVENT_PHASE_TWO     = 3,
            EVENT_ENRAGE_CHECK  = 4,
            EVENT_CAVE_COLLAPSE = 5,
        };

        enum Phases
        {
            PHASE_ONE = 1,
            PHASE_TWO = 2,
        };

        void Reset() override
        {
            BossAI::Reset();
            Initialize();
        }

        void JustEngagedWith(Unit* who) override
        {
            BossAI::JustEngagedWith(who);

            _events.ScheduleEvent(EVENT_GROUND_SMASH, 8s);
            _events.ScheduleEvent(EVENT_BOULDER_TOSS, 15s);
            _events.ScheduleEvent(EVENT_ENRAGE_CHECK, 1s);
            _events.ScheduleEvent(EVENT_PHASE_TWO, 60s);  // 60秒后进入P2
        }

        void UpdateAI(uint32 diff) override
        {
            if (!UpdateVictim())
                return;

            _events.Update(diff);

            // 狂暴检查(两个阶段都执行)
            if (_enrageTimer <= diff)
            {
                me->CastSpell(me, SPELL_ENRAGE, true);
                Talk(SAY_ENRAGE);
                _enrageTimer = 0;
            }
            else
                _enrageTimer -= diff;

            if (me->HasUnitState(UNIT_STATE_CASTING))
                return;

            while (uint32 eventId = _events.ExecuteEvent())
            {
                switch (eventId)
                {
                    case EVENT_GROUND_SMASH:
                        me->CastSpell(me, SPELL_GROUND_SMASH, false);
                        _events.ScheduleEvent(EVENT_GROUND_SMASH, 12s);
                        break;
                    case EVENT_BOULDER_TOSS:
                        if (Unit* target = SelectTarget(SelectTargetMethod::Random, 1))
                            me->CastSpell(target, SPELL_BOULDER_TOSS, false);
                        _events.ScheduleEvent(EVENT_BOULDER_TOSS, 18s);
                        break;
                    case EVENT_PHASE_TWO:
                        _phase = PHASE_TWO;
                        Talk(SAY_PHASE_TWO);
                        me->CastSpell(me, SPELL_CAVE_COLLAPSE, false);
                        _events.CancelEvent(EVENT_GROUND_SMASH);
                        _events.CancelEvent(EVENT_BOULDER_TOSS);
                        _events.ScheduleEvent(EVENT_CAVE_COLLAPSE, 15s);
                        // P2 中 Boss 攻击速度提升 30%
                        me->SetAttackTimer(BASE_ATTACK,
                            me->GetAttackTimer(BASE_ATTACK) * 0.7f);
                        break;
                    case EVENT_CAVE_COLLAPSE:
                        // P2 周期性 AOE
                        me->CastSpell(me, SPELL_CAVE_COLLAPSE, false);
                        _events.ScheduleEvent(EVENT_CAVE_COLLAPSE, 20s);
                        break;
                }
            }

            DoMeleeAttackIfReady();
        }

    private:
        uint8 _phase;
        uint32 _enrageTimer;
    };

    CreatureAI* GetAI(Creature* creature) const override
    {
        return new boss_gorgrimAI(creature);
    }
};

4.2 战斗机制详解

戈格里姆的战斗设计体现了几个关键原则:

从实现角度看,BossAI 类相比 ScriptedAI 提供了更多内置功能:自动管理遭遇战状态、与 InstanceScript 的集成、以及更方便的召唤物管理。使用 BossAI 时,我们只需要关注 Boss 特有的技能逻辑,而战斗的进入、离开、重置等通用逻辑由基类自动处理。

在技能调度方面,EventMap 是 AzerothCore 中最高效的事件调度器。它支持:

  • 阶段过滤:通过 SetPhase()ScheduleEvent(..., phase) 让事件只在特定阶段触发
  • 优先级:高优先级事件会优先执行
  • 延迟与重复:支持一次性事件和周期性事件
  • 批量取消CancelEventGroup() 可以一次取消一组事件

例如,如果我们希望 P2 阶段的技能在 Boss 血量低于 30% 时触发频率翻倍,可以这样实现:

// 在 UpdateAI 中动态调整技能频率
if (_phase == PHASE_TWO && me->HealthBelowPct(30))
{
    // 缩短技能冷却时间
    _events.RescheduleEvent(EVENT_CAVE_COLLAPSE, 10s);
}
  • 渐进式难度:P1 是基础技能循环,让队伍熟悉战斗节奏;P2 增加全队 AOE 压力,考验治疗能力。
  • 软狂暴机制:3 分钟狂暴计时,防止队伍通过风筝无限拖长战斗。
  • 阶段切换清晰:使用 Talk() 在阶段切换时发送喊话,帮助玩家感知战斗进程。
  • 技能多样性:地面粉碎(近战 AOE)迫使近战走位,投掷巨石(远程点名)考验分散站位。

五、Boss AI 实现:多阶段复杂 Boss

5.1 虚空编织者(Voidweaver)—— 最终 Boss

虚空编织者是幽影矿坑的关底 Boss,一个操控虚空能量的暗影法师。他的战斗分为三个阶段,每个阶段都有独特的机制。

class boss_voidweaver : public CreatureScript
{
public:
    boss_voidweaver() : CreatureScript("boss_voidweaver") { }

    struct boss_voidweaverAI : public BossAI
    {
        boss_voidweaverAI(Creature* creature) : BossAI(creature, TYPE_BOSS_VOIDWEAVER)
        {
            Initialize();
        }

        void Initialize()
        {
            _phase = PHASE_ONE;
            _voidCrystalCount = 0;
            _phaseTransitioning = false;
        }

        enum Spells
        {
            // P1 技能
            SPELL_VOID_BOLT       = 500501,  // 虚空箭
            SPELL_SHADOW_NOVA     = 500502,  // 暗影新星
            // P2 技能
            SPELL_VOID_SHIELD     = 500503,  // 虚空护盾(免疫物理)
            SPELL_VOID_CRYSTAL    = 500504,  // 召唤虚空水晶
            SPELL_CRYSTAL_EXPLODE = 500505,  // 水晶爆炸
            // P3 技能
            SPELL_VOID_CORRUPTION = 500506,  // 虚空腐蚀(全团 DOT)
            SPELL_DIMENSIONAL_RIFT = 500507, // 次元裂隙
        };

        enum Events
        {
            EVENT_VOID_BOLT       = 1,
            EVENT_SHADOW_NOVA     = 2,
            EVENT_PHASE_TWO       = 3,
            EVENT_VOID_CRYSTAL    = 4,
            EVENT_CRYSTAL_EXPLODE = 5,
            EVENT_PHASE_THREE     = 6,
            EVENT_VOID_CORRUPTION = 7,
            EVENT_DIMENSIONAL_RIFT = 8,
        };

        enum Phases
        {
            PHASE_ONE   = 1,  // 100% - 70%
            PHASE_TWO   = 2,  // 70% - 30%
            PHASE_THREE = 3,  // 30% - 0%
        };

        void Reset() override
        {
            BossAI::Reset();
            Initialize();
            me->SetReactState(REACT_AGGRESSIVE);
        }

        void JustEngagedWith(Unit* who) override
        {
            BossAI::JustEngagedWith(who);
            _events.ScheduleEvent(EVENT_VOID_BOLT, 3s);
            _events.ScheduleEvent(EVENT_SHADOW_NOVA, 10s);
        }

        void DamageTaken(Unit* attacker, uint32& damage) override
        {
            BossAI::DamageTaken(attacker, damage);

            // 阶段切换基于血量
            if (!_phaseTransitioning)
            {
                if (_phase == PHASE_ONE && me->HealthBelowPct(70))
                {
                    _phaseTransitioning = true;
                    _phase = PHASE_TWO;
                    _events.ScheduleEvent(EVENT_PHASE_TWO, 1s);
                }
                else if (_phase == PHASE_TWO && me->HealthBelowPct(30))
                {
                    _phaseTransitioning = true;
                    _phase = PHASE_THREE;
                    _events.ScheduleEvent(EVENT_PHASE_THREE, 1s);
                }
            }
        }

        void UpdateAI(uint32 diff) override
        {
            if (!UpdateVictim())
                return;

            _events.Update(diff);

            if (me->HasUnitState(UNIT_STATE_CASTING))
                return;

            while (uint32 eventId = _events.ExecuteEvent())
            {
                switch (eventId)
                {
                    // ===== P1 技能 =====
                    case EVENT_VOID_BOLT:
                        if (Unit* victim = me->GetVictim())
                            me->CastSpell(victim, SPELL_VOID_BOLT, false);
                        _events.ScheduleEvent(EVENT_VOID_BOLT, 4s);
                        break;
                    case EVENT_SHADOW_NOVA:
                        me->CastSpell(me, SPELL_SHADOW_NOVA, false);
                        _events.ScheduleEvent(EVENT_SHADOW_NOVA, 18s);
                        break;

                    // ===== P2 切换 =====
                    case EVENT_PHASE_TWO:
                    {
                        Talk(SAY_PHASE_TWO);
                        me->CastSpell(me, SPELL_VOID_SHIELD, true);
                        // 取消 P1 技能,安排 P2 技能
                        _events.CancelEvent(EVENT_VOID_BOLT);
                        _events.ScheduleEvent(EVENT_VOID_CRYSTAL, 5s);
                        _events.ScheduleEvent(EVENT_SHADOW_NOVA, 12s);
                        _phaseTransitioning = false;
                        break;
                    }

                    // ===== P2 技能 =====
                    case EVENT_VOID_CRYSTAL:
                    {
                        // 在随机玩家位置召唤虚空水晶
                        if (Unit* target = SelectTarget(SelectTargetMethod::Random, 0))
                        {
                            me->SummonCreature(NPC_VOID_CRYSTAL,
                                target->GetPositionX(),
                                target->GetPositionY(),
                                target->GetPositionZ(),
                                0.0f,
                                TEMPSUMMON_TIMED_DESPAWN,
                                15000);
                            _voidCrystalCount++;
                        }
                        _events.ScheduleEvent(EVENT_VOID_CRYSTAL, 10s);
                        _events.ScheduleEvent(EVENT_CRYSTAL_EXPLODE, 14s);
                        break;
                    }
                    case EVENT_CRYSTAL_EXPLODE:
                    {
                        // 所有虚空水晶爆炸
                        std::list<Creature*> crystals;
                        me->GetCreatureListWithEntryInGrid(crystals,
                            NPC_VOID_CRYSTAL, 100.0f);
                        for (Creature* crystal : crystals)
                        {
                            crystal->CastSpell(crystal, SPELL_CRYSTAL_EXPLODE, true);
                            crystal->DespawnOrUnsummon(500);
                        }
                        break;
                    }

                    // ===== P3 切换 =====
                    case EVENT_PHASE_THREE:
                    {
                        Talk(SAY_PHASE_THREE);
                        me->RemoveAurasDueToSpell(SPELL_VOID_SHIELD);
                        me->SetReactState(REACT_AGGRESSIVE);
                        _events.CancelEvent(EVENT_VOID_CRYSTAL);
                        _events.CancelEvent(EVENT_CRYSTAL_EXPLODE);
                        _events.ScheduleEvent(EVENT_VOID_CORRUPTION, 5s);
                        _events.ScheduleEvent(EVENT_DIMENSIONAL_RIFT, 12s);
                        _events.ScheduleEvent(EVENT_VOID_BOLT, 3s);
                        _phaseTransitioning = false;
                        break;
                    }

                    // ===== P3 技能 =====
                    case EVENT_VOID_CORRUPTION:
                        me->CastSpell(me, SPELL_VOID_CORRUPTION, false);
                        _events.ScheduleEvent(EVENT_VOID_CORRUPTION, 15s);
                        break;
                    case EVENT_DIMENSIONAL_RIFT:
                    {
                        // 在随机位置召唤次元裂隙
                        float x, y, z;
                        me->GetRandomPoint(me->GetPositionX(),
                            me->GetPositionY(),
                            me->GetPositionZ(), 20.0f, x, y, z);
                        me->SummonCreature(NPC_DIMENSIONAL_RIFT, x, y, z, 0.0f,
                            TEMPSUMMON_TIMED_DESPAWN, 20000);
                        _events.ScheduleEvent(EVENT_DIMENSIONAL_RIFT, 20s);
                        break;
                    }
                }
            }

            DoMeleeAttackIfReady();
        }

    private:
        uint8 _phase;
        uint8 _voidCrystalCount;
        bool _phaseTransitioning;
    };

    CreatureAI* GetAI(Creature* creature) const override
    {
        return new boss_voidweaverAI(creature);
    }
};

5.2 虚空水晶与次元裂隙的 AI

召唤物也需要自己的 AI 脚本:

// 虚空水晶 - 召唤后对周围玩家造成持续伤害
class npc_void_crystal : public ScriptedAI
{
public:
    npc_void_crystal(Creature* creature) : ScriptedAI(creature)
    {
        me->SetReactState(REACT_PASSIVE);
        me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE);
    }

    enum Spells
    {
        SPELL_VOID_AURA = 500508,  // 虚空光环(周围 DOT)
    };

    void IsSummonedBy(Unit* summoner) override
    {
        me->CastSpell(me, SPELL_VOID_AURA, true);
    }
};

// 次元裂隙 - 将玩家吸入造成伤害
class npc_dimensional_rift : public ScriptedAI
{
public:
    npc_dimensional_rift(Creature* creature) : ScriptedAI(creature)
    {
        me->SetReactState(REACT_PASSIVE);
    }

    enum Spells
    {
        SPELL_RIFT_PULL = 500509,  // 吸引周围玩家
    };

    void IsSummonedBy(Unit* summoner) override
    {
        _events.ScheduleEvent(1, 3s);  // 3秒后开始吸引
    }

    void UpdateAI(uint32 diff) override
    {
        _events.Update(diff);
        while (uint32 eventId = _events.ExecuteEvent())
        {
            if (eventId == 1)
            {
                me->CastSpell(me, SPELL_RIFT_PULL, false);
                _events.ScheduleEvent(1, 5s);
            }
        }
    }

private:
    EventMap _events;
};

六、Eluna 脚本集成

6.1 什么是 Eluna

Eluna 是 AzerothCore 的 Lua 脚本引擎,允许开发者用 Lua 语言编写游戏逻辑,无需重新编译 C++ 代码。对于快速原型开发、活动脚本和简单的副本机制,Eluna 是非常高效的选择。

启用 Eluna 需要在编译时加入 -DELUNA=1 选项:

cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/azeroth-server -DELUNA=1
make -j$(nproc)

6.2 用 Eluna 实现副本事件

以下是一个完整的 Eluna 脚本,为幽影矿坑添加了额外的机制——比如 Boss 击杀后的宝箱奖励和副本内的事件触发:

-- lua_scripts/shadowdeep_mine_events.lua

local SHADOWDEEP_MINE = {
    MAP_ID = 3000,
    BOSS_GORGRIM = 500010,
    BOSS_VOIDWEAVER = 500011,
    BOSS_OVERSEER = 500012,
    TREASURE_CHEST = 600002,
}

-- Boss 击杀后召唤宝箱
local function OnBossKill(event, killer, creature)
    local map = creature:GetMap()
    if not map then return end

    local instance = map:GetInstanceScript()
    if not instance then return end

    -- 检查是否所有 Boss 都已击杀
    local allDead = true
    local bosses = {
        SHADOWDEEP_MINE.BOSS_GORGRIM,
        SHADOWDEEP_MINE.BOSS_VOIDWEAVER,
        SHADOWDEEP_MINE.BOSS_OVERSEER,
    }

    for _, entry in ipairs(bosses) do
        local boss = map:GetCreatureByEntry(entry)
        if boss and boss:IsAlive() then
            allDead = false
            break
        end
    end

    if allDead then
        -- 在副本出口召唤宝箱
        local chest = map:SpawnGameObject(
            SHADOWDEEP_MINE.TREASURE_CHEST,
            123.45,  -- X
            678.90,  -- Y
            15.0,    -- Z
            0.0,     -- O
            0,       -- 持续时间(0 = 永久)
            3600     -- 刷新时间(秒)
        )
        if chest then
            chest:SetRespawnTime(3600)
            chest:SetLootRecipient(killer)
            -- 发送全副本通告
            map:SendChatMessageToAllPlayers(
                "|cffFFD700[幽影矿坑]|r " ..
                "所有首领已被击败!宝藏出现在出口附近!"
            )
        end
    end
end

-- 注册 Boss 击杀事件
RegisterCreatureEvent(
    SHADOWDEEP_MINE.BOSS_GORGRIM,  -- 也可以只注册一个
    CREATURE_EVENT_ON_DIED,
    OnBossKill
)

-- 玩家进入副本时的欢迎消息
local function OnPlayerEnter(event, player, map)
    if map:GetId() == SHADOWDEEP_MINE.MAP_ID then
        player:SendBroadcastMessage(
            "|cffFF4444[幽影矿坑]|r " ..
            "欢迎来到幽影矿坑!矿坑深处传来不祥的低语..."
        )
    end
end

RegisterPlayerEvent(PLAYER_EVENT_ON_MAP_CHANGE, OnPlayerEnter)

-- 副本重置时清理状态
local function OnInstanceReset(event, map, difficulty)
    if map:GetId() == SHADOWDEEP_MINE.MAP_ID then
        -- 清理所有召唤物
        local crystals = map:GetCreatureByEntry(500020)  -- 虚空水晶
        if crystals then
            for _, crystal in ipairs(crystals) do
                crystal:DespawnOrUnsummon()
            end
        end
    end
end

RegisterGameObjectEvent(0, GAMEOBJECT_EVENT_ON_RESET, OnInstanceReset)

6.3 C++ 与 Eluna 的协作策略

在实际项目中,C++ 和 Eluna 各有适用场景:

场景 推荐方案 理由
核心 Boss 机制 C++ 性能关键,需要精细控制
副本进度追踪 C++ (InstanceScript) 需要与核心系统深度集成
活动/节日事件 Eluna (Lua) 快速迭代,无需重新编译
奖励/宝箱逻辑 Eluna (Lua) 逻辑简单,频繁调整
调试辅助功能 Eluna (Lua) 热加载,即时生效

一个实用的策略是:核心战斗逻辑用 C++ 保证性能,外围系统用 Eluna 保持灵活性。两者通过事件系统进行通信——C++ 脚本触发事件,Eluna 脚本响应事件。

七、调试与测试

7.1 调试命令

在 AzerothCore 中调试副本脚本,最常用的命令是 .instance 系列 GM 命令:

# 重置副本
.instance unbind 0

# 传送到副本
.go x y z mapId

# 查看副本状态
.instance listbinds

# 查看当前脚本状态
.debug instance

# 直接击杀 Boss(测试掉落)
.die

# 修改血量(测试阶段切换)
.modify hp 50

7.2 日志输出

在 C++ 脚本中添加日志输出是调试的基础:

// 在 BossAI 中使用
void JustEngagedWith(Unit* who) override
{
    BossAI::JustEngagedWith(who);
    TC_LOG_INFO("scripts.shadowdeep", 
        "Boss Gorgrim engaged by %s", 
        who->GetName().c_str());
}

void DamageTaken(Unit* attacker, uint32& damage) override
{
    BossAI::DamageTaken(attacker, damage);
    TC_LOG_DEBUG("scripts.shadowdeep",
        "Gorgrim took %u damage, HP now %u%%",
        damage, me->GetHealthPct());
}

在配置文件中启用日志:

# worldserver.conf
LogLevel.scripts = 1  # 0=禁用, 1=基本, 2=详细
LogFilter.ScriptsShadowdeep = 1

7.3 常见问题排查

以下是副本脚本开发中常见的问题及解决方案:

问题 可能原因 解决方案
Boss 不进入战斗 UNIT_FLAG_IMMUNE_TO_PC 未清除 在 Reset() 中清除免疫标志
阶段切换不触发 DamageTaken 中 damage 为引用但未修改 检查血量百分比条件
技能不释放 Spell ID 未正确注册到 Spell.dbc 确认 spell.dbc 中存在该 ID
召唤物不出现 Creature_template 中未配置 检查 creature_template 表
Eluna 脚本不执行 Eluna 未编译或路径错误 确认 -DELUNA=1,检查 lua_scripts 目录

八、完整项目结构

一个完整的副本脚本项目应当包含以下文件:

shadowdeep_mine/
├── sql/
│   ├── creature_template.sql      -- 怪物模板
│   ├── creature.sql               -- 怪物放置
│   ├── gameobject_template.sql    -- 游戏对象模板
│   ├── gameobject.sql             -- 游戏对象放置
│   ├── spell_custom.sql           -- 自定义法术
│   └── instance_template.sql      -- 副本模板
├── src/
│   ├── shadowdeep_mine.h          -- 头文件(枚举、常量)
│   ├── instance_shadowdeep_mine.cpp -- 副本脚本
│   ├── boss_gorgrim.cpp           -- 1号Boss
│   ├── boss_voidweaver.cpp        -- 2号Boss
│   ├── boss_overseer.cpp          -- 3号Boss
│   └── npc_shadowdeep_mine.cpp    -- 普通怪物
├── lua_scripts/
│   └── shadowdeep_mine_events.lua -- Eluna 事件脚本
└── README.md                      -- 项目说明

SQL 文件示例(创建副本入口):

-- instance_template.sql
DELETE FROM `instance_template` WHERE `map` = 3000;
INSERT INTO `instance_template` (`map`, `parent`, `script`, `allowMount`)
VALUES (3000, 0, 'instance_shadowdeep_mine', 0);

-- creature_template.sql(Boss 模板片段)
DELETE FROM `creature_template` WHERE `entry` = 500010;
INSERT INTO `creature_template` (`entry`, `name`, `subname`, `minlevel`, `maxlevel`,
    `faction`, `npcflag`, `speed_walk`, `speed_run`, `scale`,
    `mindmg`, `maxdmg`, `attackpower`, `dmg_multiplier`, `baseattacktime`,
    `unit_class`, `unit_flags`, `mechanic_immune_mask`, `ScriptName`)
VALUES (
    500010, '戈格里姆', '矿坑监工',
    82, 82, 16, 0, 1.0, 1.142, 1.0,
    200, 300, 350, 2.5, 2000,
    1, 0, 0, 'boss_gorgrim'
);

九、进阶优化建议

9.1 使用 SmartScripts 简化逻辑

对于简单的怪物行为,AzerothCore 的 SmartScripts 系统可以让你通过数据库配置实现 AI,无需编写 C++ 代码。例如,让矿工在被攻击时喊话:

-- SmartScripts 配置(通过数据库或 SAI 编辑器)
INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`,
    `link`, `event_type`, `event_phase_mask`, `event_chance`,
    `event_flags`, `event_param1`, `event_param2`, `event_param3`,
    `event_param4`, `action_type`, `action_param1`, `action_param2`,
    `target_type`, `comment`)
VALUES (
    500001, 0, 0, 0,
    4, 0, 100, 0, 0, 0, 0, 0,  -- 事件:进入战斗
    1, 0, 0,  -- 动作:说话
    7,  -- 目标:自身
    'Dark Miner - On Aggro Say Text'
);

9.2 性能优化

副本脚本的性能优化要点:

  • 避免每帧查询:不要在 UpdateAI 中执行数据库查询或全地图遍历,使用缓存或事件驱动。
  • 合理使用 EventMapEventMap 比手动计时器更高效,且支持优先级和阶段过滤。
  • 召唤物清理:使用 TEMPSUMMON_TIMED_DESPAWN 自动清理临时召唤物,避免内存泄漏。
  • 日志级别控制:生产环境使用 TC_LOG_INFO,开发环境使用 TC_LOG_DEBUG,避免日志 I/O 成为瓶颈。

9.3 模块化发布

如果你想将副本脚本发布到 AzerothCore 社区,建议打包为模块(Module)格式:

shadowdeep_mine_module/
├── CMakeLists.txt                 -- 模块构建文件
├── setup.sh                      -- 一键安装脚本
├── sql/
│   └── world/
│       └── 2026_01_01_00_shadowdeep_mine.sql  -- 带时间戳的 SQL
├── src/
│   └── ShadowdeepMine/
│       ├── include/
│       │   └── shadowdeep_mine.h
│       ├── src/
│       │   ├── instance_shadowdeep_mine.cpp
│       │   ├── boss_gorgrim.cpp
│       │   ├── boss_voidweaver.cpp
│       │   ├── boss_overseer.cpp
│       │   └── npc_shadowdeep_mine.cpp
│       └── CMakeLists.txt
└── README.md

模块的 CMakeLists.txt 示例:

# 顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(shadowdeep_mine)

add_subdirectory(src/ShadowdeepMine)

# 安装 SQL 文件
install(DIRECTORY sql/ DESTINATION "${CMAKE_INSTALL_PREFIX}/data/sql")

十、总结

本文从零开始实现了一个完整的 AzerothCore 副本脚本,涵盖了从项目结构设计、普通怪物 AI、Boss 多阶段战斗到 Eluna 脚本集成的全流程。通过幽影矿坑这个实例,我们展示了:

  • InstanceScript 管理副本整体状态和进度
  • ScriptedAI / BossAI 实现怪物和 Boss 的行为逻辑
  • EventMap 实现精确的技能调度和阶段切换
  • Eluna Lua 脚本 快速扩展外围功能
  • 调试技巧性能优化 确保脚本质量

副本脚本开发是理解 AzerothCore 架构的最佳途径之一。掌握了这些技能后,你可以进一步探索:

  • 英雄模式和史诗模式的难度差异化
  • 副本内剧情触发和过场动画
  • 成就系统集成
  • Loot 系统定制
  • 跨副本的关联任务链

AzerothCore 社区非常活跃,官方论坛和 Discord 上有大量优秀开发者分享经验。建议将你的作品发布到 AzerothCore GitHub官方网站 的模块仓库,与其他开发者交流改进。

祝你编码愉快,副本开荒顺利!


本文为原创技术文章,作者保留所有权利。转载需注明出处。