你站在暴风城门口一个 NPC 头上亮着黄色感叹号。你走上去点他他说我有个任务给你——但这个感叹号不是谁都能看到的。只有做完前置任务、达到等级要求、阵营对得上的玩家才会看到它亮起来。谁在替你做这道判断不是 NPC 的 AI不是任务的脚本而是一个叫ConditionMgr的单例——它从数据库读出条件在代码里跑一遍决定你看得见还是看不见。上一篇数据库篇06聊了conditions表的结构SourceType、ConditionType、三个 Value、ElseGroup 这些字段怎么排列组合。这篇从代码层面追问这些数据加载进内存后谁来用、怎么查、怎么算。一、数据加载从 SQL 到内存加载入口ConditionMgr是一个标准单例sConditionMgr启动时由worldserver调用LoadConditions()voidConditionMgr::LoadConditions(boolisReload){Clean();// 先清空所有容器QueryResult resultWorldDatabase.Query(SELECT SourceTypeOrReferenceId, SourceGroup, SourceEntry, SourceId, ElseGroup, ConditionTypeOrReference, ConditionTarget, ConditionValue1, ConditionValue2, ConditionValue3, NegativeCondition, ErrorType, ErrorTextId, ScriptName FROM conditions);一条SELECT * FROM conditions把整张表拉进内存。两条分流分组 vs 非分组加载时每条记录根据SourceType走不同的存储路径分组类型SourceGroup 有意义的——条件挂在一组东西上比如战利品模板、对话菜单、SmartAI 事件SourceType存到哪战利品模板CREATURE/GO/ITEM/FISHING 等12种注入对应的LootTemplate对象GOSSIP_MENU / GOSSIP_MENU_OPTION注入对话菜单数据SMART_EVENTSmartEventConditionStore[make_pair(entry, sourceType)][eventId]VEHICLE_SPELL / SPELL_CLICK_EVENT专用的二维 MapNPC_VENDORNpcVendorConditionContainerStore[creatureId][itemId]非分组类型SourceGroup 无意义的——条件挂在单个入口上比如任务可用性、法术施放条件// 统一三级 MapSourceType → SourceEntry → ConditionListConditionStore[cond-SourceType][cond-SourceEntry].push_back(cond);两种存储方式的区别分组条件在加载时就绑到业务对象上比如 LootTemplate 里的某个掉落项查询时直接从业务对象取非分组条件存在全局 Map 里查询时按SourceType SourceEntry两个 key 查找。条件引用ReferenceConditionTypeOrReferenceId为负数时这条记录不是条件而是引用模板if(iConditionTypeOrReference0)// it has a reference{cond-ReferenceIduint32(std::abs(iConditionTypeOrReference));// 引用模板存到独立容器ConditionReferenceStore[uRefId].push_back(cond);}引用模板允许你把一组常用条件定义一次多处引用——比如等级≥80且完成某个前置任务这个组合如果十个任务都要用写十条引用比复制十遍条件行干净得多。二、Condition 结构体一个条件的完整画像从数据库读出的每一行被构造成一个Condition对象structCondition{ConditionSourceType SourceType;// 挂在哪个系统uint32 SourceGroup;// 分组 ID战利品模板号/菜单号int32 SourceEntry;// 具体条目掉落物品ID/菜单选项IDuint32 SourceId;// 仅 SMART_EVENT 使用uint32 ElseGroup;// ELSE 逻辑分组ConditionTypes ConditionType;// 判断什么uint32 ConditionValue1;// 参数1uint32 ConditionValue2;// 参数2uint32 ConditionValue3;// 参数3uint32 ErrorType;// 失败时的错误码仅 SPELL 类型uint32 ErrorTextId;// 错误文本 IDuint32 ReferenceId;// 引用 ID负数条件类型uint32 ScriptId;// 脚本 IDuint8 ConditionTarget;// 判断哪个 targetboolNegativeCondition;// 取反};关键字段的作用ElseGroup实现 OR 逻辑的核心。同一 ElseGroup 内的条件是 AND不同 ElseGroup 之间是 OR。后面详述。ConditionTarget指定对ConditionSourceInfo中的哪个对象做判断0自己1目标2第三方最多 3 个。NegativeCondition结果取反。比如没有某个光环就用CONDITION_AURA NegativeConditiontrue。三、Condition::Meets()——判断的核心每个 Condition 对象有一个Meets(ConditionSourceInfo)方法这是条件判断的原子操作。50 种 ConditionType 的分发Meets()内部是一个巨型switch50 个 case 分支每个分支做一种判断ConditionType做什么示例CONDITION_NONE永远成立占位条件CONDITION_AURA判断有没有光环英雄祝福是否在身CONDITION_ITEM判断背包物品数量有没有5个魔铁矿CONDITION_ZONEID判断所在区域在不在冬泉谷CONDITION_QUESTREWARDED判断任务是否完成有没有做完通灵学院CONDITION_LEVEL判断等级≥58 级才能进外域CONDITION_TEAM判断阵营部落专属任务CONDITION_CLASS判断职业战士才能接的任务CONDITION_NEAR_CREATURE判断附近有没有某怪附近有没有卫兵马尔勒CONDITION_HP_PCT判断血量百分比血量低于 30% 才触发CONDITION_ACTIVE_EVENT判断节日活动是否开启暗月马戏团期间截取几段代码感受caseCONDITION_AURA:if(Unit*unitobject-ToUnit())condMeetsunit-HasAuraEffect(ConditionValue1,ConditionValue2);break;caseCONDITION_QUESTREWARDED:if(Player*playerunit-GetCharmerOrOwnerPlayerOrPlayerItself())condMeetsplayer-GetQuestRewardStatus(ConditionValue1);break;caseCONDITION_NEAR_CREATURE:condMeetsstatic_castbool(GetClosestCreatureWithEntry(object,ConditionValue1,float(ConditionValue2),!ConditionValue3));break;取反与失败记录Meets()的尾部有两步收尾if(NegativeCondition)condMeets!condMeets;// 取反if(!condMeets)sourceInfo.mLastFailedConditionthis;// 记录哪个条件挂了returncondMeets;mLastFailedCondition的用途法术施放条件失败时客户端需要知道为什么不能放这个字段指向最后一个失败的条件从中提取ErrorType和ErrorTextId返回给客户端显示红字提示。四、AND/OR/NOT——条件组合逻辑单条条件只是原子判断真正的威力在于组合。IsObjectMeetToConditionList()实现了完整的 AND/OR/NOT 逻辑boolConditionMgr::IsObjectMeetToConditionList(ConditionSourceInfosourceInfo,ConditionListconstconditions){std::mapuint32,boolElseGroupStore;// ElseGroup → 是否通过for(autoconstcond:conditions){if(!cond-isLoaded())continue;autoitrElseGroupStore.find(cond-ElseGroup);if(itrElseGroupStore.end())ElseGroupStore[cond-ElseGroup]true;// 初始假设通过elseif(!itr-second)continue;// 这组已经挂了跳过后续if(cond-ReferenceId){// 递归展开引用autorefConditionReferenceStore.find(cond-ReferenceId);if(ref!ConditionReferenceStore.end())if(!IsObjectMeetToConditionList(sourceInfo,ref-second))ElseGroupStore[cond-ElseGroup]false;}else{if(!cond-Meets(sourceInfo))ElseGroupStore[cond-ElseGroup]false;}}// 任意一个 ElseGroup 通过 整体通过ORfor(autoconst[group,passed]:ElseGroupStore)if(passed)returntrue;returnfalse;}逻辑翻译成人话同一 ElseGroup 内所有条件必须全部通过AND。任意一条Meets()返回 false整个组标记为失败。不同 ElseGroup 之间任意一组通过即可OR。引用递归展开用同一套 AND/OR 逻辑计算。举一个具体例子条件AElseGroup0, CONDITION_LEVEL ≥ 58 条件BElseGroup0, CONDITION_QUESTREWARDED 完成黑暗神殿 条件CElseGroup1, CONDITION_TEAM 部落 条件DElseGroup1, CONDITION_QUESTREWARDED 完成奥格瑞玛的召唤计算逻辑 (A AND B) OR (C AND D)翻译要么等级58以上且完成了黑暗神殿要么你是部落且完成了奥格瑞玛的召唤。这种组合在数据库里靠 ElseGroup 字段来拆分——不需要额外的逻辑表或嵌套结构。五、谁来调 ConditionMgr条件系统不是推的不会主动通知条件满足了而是拉的——业务系统在需要判断时主动查一次。任务可见性// PlayerQuest.cppboolPlayer::SatisfyQuestConditions(Questconst*qInfo,boolmsg){ConditionList conditionssConditionMgr-GetConditionsForNotGroupedEntry(CONDITION_SOURCE_TYPE_QUEST_AVAILABLE,qInfo-GetQuestId());if(!sConditionMgr-IsObjectMeetToConditions(this,conditions)){// 条件不满足任务不可见}}玩家走近 NPC 时服务端遍历该 NPC 关联的所有任务每个任务查一次条件——满足的亮感叹号不满足的隐藏。这就是千人千面任务的底层实现。法术施放条件// Spell.cppConditionSourceInfocondInfo(m_caster);condInfo.mConditionTargets[1]m_targets.GetObjectTarget();ConditionList conditionssConditionMgr-GetConditionsForNotGroupedEntry(CONDITION_SOURCE_TYPE_SPELL,m_spellInfo-Id);if(!conditions.empty()!sConditionMgr-IsObjectMeetToConditions(condInfo,conditions)){// 施放失败返回 ErrorType 给客户端}法术条件支持双目标判断ConditionTarget0判断施法者ConditionTarget1判断目标。比如只能对亡灵施放这个条件判断的就是 target 而不是 caster。战利品掉落条件战利品条件走分组路径加载时直接绑到LootTemplate的每个LootItem上。生成掉落时对每个潜在掉落项检查条件// LootTemplate::Process() 内部if(item.conditions){if(!sConditionMgr-IsObjectMeetToConditions(lootOwner,item.conditions))continue;// 跳过这个掉落项}这就是为什么同一个怪不同玩家看到的掉落不同——不是随机数不同是条件判断的结果不同。SmartAI 条件SmartAI 是条件系统的重度用户// SmartScript.cppConditionList conditionssConditionMgr-GetConditionsForSmartEvent(GetEntryOrGuid(),GetEventId(),GetSourceType());if(!sConditionMgr-IsObjectMeetToConditions(me,conditions))return;// 条件不满足不执行这个 ActionSmartAI 的每个 Event 都可以挂条件——让 Boss 的某个技能只在特定条件下触发比如血量低于 50% 时才用大招。六、ConditionTarget三个锚点ConditionSourceInfo最多携带 3 个 WorldObject 指针structConditionSourceInfo{WorldObject*mConditionTargets[3];Condition*mLastFailedCondition;};ConditionTarget字段0/1/2决定当前条件对哪个对象做判断。常见用法场景Target 0Target 1Target 2任务可见性玩家——法术施放施法者目标—SmartAI自己邀请目标—战利品击杀者——Meets()内部通过object sourceInfo.mConditionTargets[ConditionTarget]取到判断对象然后根据ConditionType转型为Unit*/Player*/Creature*再做具体检查。如果目标对象不存在直接返回 false。七、数据驱动的设计哲学回头看整个条件系统它的核心思路是把什么条件下做某事这件事从散落在各个系统的硬编码收敛到一张表 一个管理器。好处统一查询接口不管是任务、法术、战利品还是 NPC 商店全走IsObjectMeetToConditions()不用每个系统各写一套判断逻辑。纯数据配置新增一种条件组合只插数据库行不改动 C 代码。可组合AND/OR/NOT 引用模板条件可以像搭积木一样拼装。调试透明mLastFailedCondition记录最后一个失败条件定位问题很直观。代价全量加载启动时一次性加载所有条件到内存。条件数据量大数万条但不频繁变更所以内存换速度是划算的。可读性差数据库里的条件行不查枚举定义根本看不懂——ConditionType5, Value172, Value25是什么意思你得知道 5REPUTATION_RANK72 是阵营 ID5 是声望等级掩码。条件类型的扩展成本新增 ConditionType 需要改ConditionTypes枚举 Meets()里加 case isConditionTypeValid()里加校验——三处联动比纯数据配置重。这三点代价里第三点是最有意思的条件系统号称数据驱动但新增判断类型仍然要改代码。这其实是半数据驱动——组合是数据的原子是代码的。条件之间怎么拼AND/OR/NOT由数据库行决定但每个原子条件怎么算由 C 硬编码。这个折中是务实的组合逻辑变化频繁放在数据层灵活原子判断变化少放代码层稳定高效。一张表、一个 switch、一个递归——条件系统用最朴素的结构撑起了游戏里几乎所有的能不能判断。它不花哨但管用。