AzerothCore 副本脚本开发实战:从普通怪物到 Boss AI 的完整实现 原创
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 的构建系统中:
- 将
shadowdeep_mine.h和shadowdeep_mine.cpp放入src/server/scripts/Kalimdor/目录(或自定义模块目录)。 - 在
src/server/scripts/Kalimdor/CMakeLists.txt中添加源文件引用。 - 在
src/server/scripts/Kalimdor/script_loader.cpp中添加AddSC_shadowdeep_mine()调用。 - 重新编译:
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中执行数据库查询或全地图遍历,使用缓存或事件驱动。 - 合理使用 EventMap:
EventMap比手动计时器更高效,且支持优先级和阶段过滤。 - 召唤物清理:使用
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 或 官方网站 的模块仓库,与其他开发者交流改进。
祝你编码愉快,副本开荒顺利!
本文为原创技术文章,作者保留所有权利。转载需注明出处。