13674307070 发表于 2026-8-20 08:38:01

怪物系统是 VV 引擎最庞杂的模块,但生老病死四条线把它串清楚了

一、生:刷怪与召唤,怪物的两种出生方式
怪物的 "生" 有两种完全不同的方式:地图刷新和脚本召唤。

地图刷新是引擎自动管理的,根据地图配置里的刷怪点、怪物类型、刷新间隔,定时生成怪物。这套机制引擎底层做好了,脚本一般不需要干预。但有一个接口可以干预 —— resetmongentick,重置某个地图某个坐标的怪物生成计时器。这个接口的应用场景是:你手动清掉了某个刷怪点的怪物,不想等它按正常间隔刷新,想立刻重置计时让它马上刷,或者反过来想延迟刷新。大部分版本用不到,但做特殊地图(比如副本里的怪物刷新控制)的时候会用到。

脚本召唤是开发者主动生成怪物,用 GenMon 和 GenMonEx。基础版 GenMon 传地图、坐标、怪物名、范围、数量、颜色,就能在指定位置周围刷出指定数量的怪物。拓展版 GenMonEx 多了一大堆参数 —— 归属对象、自定义名称、是否过滤数字、国家名称、国家间攻击规则、是否内功怪。

GenMonEx 的参数里有一组很有意思:国家相关的四个参数。 countryName 给怪物指定国家, nAttack 控制怪物能不能攻击同国家玩家, nNatMonPk 控制不同国家的怪物之间能不能 PK, nPlayerPk 控制玩家能不能攻击同国家怪物。这四个参数说明引擎内置了国家阵营系统,怪物可以归属某个国家,并且国家之间的攻击关系可以精细控制。做阵营对战、国战版本的时候,这组参数非常有用 —— 你可以刷出 "华夏国的守卫",它们不会攻击华夏玩家,但会攻击其他国家的玩家和怪物。

还有 isFilt 参数,控制自定义名称是否过滤数字。这个细节说明引擎的怪物名称显示有数字过滤机制 —— 可能是为了防止同名怪物用数字区分(比如 "神兽 1"" 神兽 2"),默认会把名字里的数字过滤掉。如果你想保留数字(比如自定义名称叫"BOSS 第 3 阶段 "),就要设成不过滤。这种小细节平时注意不到,但需要的时候找不到就很麻烦。

刷怪接口返回的是怪物列表 table,不是单个怪物对象。因为一次可以刷多个,返回列表方便你遍历处理 —— 比如给每个刷出来的怪物设置属性、挂标记、改名字。刷一只的时候列表里就一个元素,取第一个就行。

召唤宝宝是另一种 "生",和刷怪不同的是,召唤出来的怪物有主人,属于玩家的宠物。 RecallMob 是基础版,传玩家、怪物名、等级、叛变时间、颜色,召唤一个宝宝。 RecallMobEx 是拓展版,参数多了很多 —— 出生坐标、数量、叛变时间、颜色、自动变色、宝宝不升级、隐藏主人名。

RecallMobEx 里有几个参数设计得很贴心。 upgrade 控制宝宝是否升级,设为 1 就不升级,适合做 "固定强度的召唤物",比如技能召唤出来的临时宠物,不需要跟着玩家涨等级。 hidemastername 隐藏主人名字,适合做 "看起来像野生怪物但其实是玩家召唤的" 这种特殊效果,比如迷惑敌人的幻象。

还有一种特殊的 "生" 是 setmonmaster,把一个已经存在的怪物直接设置成某个玩家的宝宝。这个接口和 RecallMob 的区别是: RecallMob 是新建一个怪物然后设为宝宝, setmonmaster 是把已有的怪物(比如地图上本来就有的野怪)收服成宝宝。做 "诱惑之光" 技能的时候用这个 —— 先对怪物施法,成功了就调 setmonmaster 把它变成宝宝,不需要重新生成。

宝宝的出生方式有三种,对应三种场景:技能召唤用 RecallMob,复杂召唤用 RecallMobEx,收服野怪用 setmonmaster。三种方式各有适用场景,不要混着用。

二、老:怪物的行为与 AI,从巡逻到嘲讽
怪物的 "老" 不是年龄,而是行为 —— 怪物怎么移动、怎么攻击、怎么被控制。

寻路和巡逻 是怪物最基础的行为。 monmission 可以让怪物按一组坐标点移动,支持两种模式:寻路模式(按顺序走到每个点,走完结束,结束后触发 missionend)和巡逻模式(按坐标点循环走,永不停歇)。坐标用分号串联,最多 9 个点。

这个接口的应用场景很广。做巡逻 NPC(其实是怪物外形的巡逻单位)、做 BOSS 的阶段移动(从一个位置走到另一个位置)、做护送任务(怪物沿着路线走,玩家保护),都可以用。寻路结束触发 missionend 这个设计很重要,它让你可以在怪物到达目的地后做后续处理 —— 比如 BOSS 走到位置后开始释放技能、护送对象到达终点后任务完成。

mission 是另一个寻路接口,比 monmission 复杂得多,参数里带了国家系统、攻击规则、名字颜色等。这个接口更像是 "生成一批怪物并让它们按路线行进",而不是对已有怪物下达移动命令。做怪物潮、军团入侵这种玩法的时候用这个比较合适 —— 一次性生成一批有阵营的怪物,沿着指定路线推进。

movemontopos 是最简单的移动命令,把指定地图指定坐标的怪物移动到另一个坐标。没有寻路逻辑,就是直接移动过去。做瞬间位移、BOSS 传送的时候用。

嘲讽 是控制怪物仇恨的手段。 dotaunt 让玩家嘲讽周围一定范围内的怪物,怪物会被吸引过来攻击玩家。 mobdotaunt 是宝宝嘲讽,让某个宝宝去吸引周围怪物。 dotauntex 是嘲讽单个怪物,精确控制。

嘲讽接口有个 maxlevel 参数,只嘲讽等级不超过指定值的怪物。这个设计很合理 —— 高等级 BOSS 不应该被低级嘲讽技能影响,不然 BOSS 战就太好打了。做嘲讽技能的时候,根据技能等级设置不同的 maxlevel,就能实现 "高级嘲讽能拉住高级怪" 的效果。

速度控制 用 ChangeMonSpeed,可以改怪物或宝宝的移动速度和攻击速度,单位是毫秒,正数加速负数减速,还能带持续时间。这个接口做减速技能、加速 buff、BOSS 阶段变化(比如第二阶段攻速加快)的时候非常好用。注意单位是毫秒,不是百分比,改的时候要算清楚 —— 比如原攻击间隔是 1000 毫秒,想加速 20% 就改成 800 毫秒,不是传 20。

释放技能 用 mon_docustommagic,但有个限制:只支持自定义怪物(race=156,RaceImg=19)。这个限制说明普通怪物的技能是引擎内置的,由怪物数据库配置,脚本不能随意让普通怪物放自定义技能。只有特定类型的自定义怪物,脚本才能控制它释放指定技能。做自定义 BOSS 的时候,需要把 BOSS 设成 race=156 的自定义怪物,才能用脚本控制它的技能释放节奏。

这个限制其实是一种保护机制 —— 如果任何怪物都能被脚本随意控制放技能,可能会和引擎内置的怪物 AI 冲突。自定义怪物相当于引擎给你开了一个 "白名单",在这个范围内你可以完全控制,出了问题也是你自己的自定义逻辑,不会影响普通怪物。

优先攻击 用 killmobappoint,让玩家在指定位置优先攻击指定怪物,还支持多个怪物名用竖线分隔。这个接口的应用场景是挂机辅助 —— 设置挂机地点和优先攻击的怪物,玩家自动打怪的时候优先打这些。做内挂、自动战斗系统的时候用。

怪物的行为体系看下来,最大的感受是:基础行为引擎内置,复杂行为脚本控制。普通怪物的移动、攻击、仇恨这些基础 AI,引擎底层已经做好了,你不需要管。但巡逻路线、嘲讽、速度变化、技能释放、优先攻击这些特殊行为,引擎提供了接口让你脚本控制。这种分工很合理 —— 基础行为保证稳定性,特殊行为给自由度。

三、病:属性修改与标记,怪物的状态变化
怪物的 "病" 指的是属性和状态的修改 —— 怪物不是生成之后就一成不变的,可以动态改它的属性、给它挂标记、调整它的状态。

宝宝属性修改 有一套专门的接口。 changemobability 改宝宝的各项属性,HP、MP、攻防道术、攻击速度、移动间隔,支持加减等于,还能带有效时间。改完之后要调 recalcabilitys 刷新,这个和人物改属性是一样的逻辑。

changeslavelevel 改宝宝等级,支持加减等于。宝宝等级和属性是挂钩的,改等级可能会影响属性计算,具体要看引擎底层的宝宝成长公式。做 "宝宝升级丹" 这种道具的时候用这个。

getslavelevel 查宝宝等级, changemonname 改宝宝名字。宝宝的名字可以自定义,做 "给宠物改名" 功能的时候用。

怪物标记 用 setcurrent 和 getcurrent,可以给人物或怪物对象挂 10 个标记位(index 0-9),值是字符串。这个功能非常实用 —— 你可以给怪物挂自定义数据,比如 "这只 BOSS 是第几阶段"" 这只怪物是哪个玩家召唤的 ""这只怪物的特殊掉落 ID"。10 个标记位看起来不多,但每个值是字符串,可以存 JSON,理论上能存任意复杂的数据。

标记值的设计是怪物系统里最灵活的功能之一。引擎不可能预见到所有需要存在怪物身上的自定义数据,所以给了你 10 个通用标记位,你想存什么存什么。做 BOSS 阶段管理的时候,把阶段号存在标记位里;做特殊怪物掉落的时候,把掉落配置 ID 存在标记位里;做怪物归属的时候,把主人 ID 存在标记位里。

用标记的时候要注意规划 ——10 个位置,每个位置存什么要提前定义好,不要乱存。建议做一个常量定义,比如 MON_TAG_STAGE = "0"(阶段存在 0 号位), MON_TAG_OWNER = "1"(主人存在 1 号位),代码里用常量不要直接写数字,方便后期维护。

怪物信息查询 有几个接口。 getmonbaseinfo 按怪物 IDX 查基础信息,能拿名称、名字颜色、经验值。 GetDBMonFieldValue 按字段名查数据库原始值,和物品系统的 getdbitemfieldvalue 一样,是底层查询接口,能拿到上层接口没暴露的字段。

getmonbyuserid 按唯一 ID 获取怪物对象,需要传地图 ID 和怪物 UserId。这个和物品系统的 getitembymakeindex 类似,是精确查找 —— 你知道怪物的唯一 ID,想拿到它的对象,就用这个。注意要传地图 ID,因为怪物是属于某个地图的,不同地图可能有相同的 UserId(虽然概率低)。

checkrangemoncount 查指定地图指定坐标范围内的怪物数量,可以按怪物名筛选,也可以查所有。做 "地图怪物数量检测" 的时候用 —— 比如副本里需要清完所有怪才开门,就用这个检测数量是否为 0。做刷怪控制的时候也用 —— 检测到范围内怪物太多就不刷新,防止怪物堆积。

属性和状态这一层的设计,和人物系统、物品系统的思路一致:常用属性有专门接口,自定义数据用通用标记位,底层信息用字段查询接口。三层结构,覆盖了从简单到复杂的所有需求。

四、死:击杀与掉落,怪物生命的终点
怪物的 "死" 是整个怪物系统最核心的部分,因为杀怪掉装备是传奇类游戏的核心玩法。

击杀 有几个接口,对应不同场景。 KillMonsters 按地图和怪物名批量杀,可以指定数量,控制是否掉落。传怪物名为空或星号就是杀全部,数量传 0 就是杀所有。做 "清屏技能"" 地图重置 " 的时候用这个。

killmonbyobj 是精确杀死某个怪物对象,参数更细 —— 是否掉落、是否触发 killmon 事件、是否显示死亡动画。这里有个很重要的参数组合: showdie 设为 false 时,视为系统杀怪,不会掉落物品和经验。这个设计很关键 —— 做 "怪物因机制消失"(比如 BOSS 阶段转换、副本结束清怪)的时候,设成系统杀怪,玩家就拿不到掉落和经验,避免玩家利用机制刷怪。

trigger 参数控制是否触发 killmon 事件。如果你的脚本里有 killmon 触发做特殊处理(比如统计击杀数、任务进度),用脚本杀怪的时候就要考虑要不要触发。设为 true 的话,脚本杀怪也会走正常的击杀事件流程,和玩家手动杀一样;设为 false 就不触发。大部分情况下应该设为 true,保持逻辑一致性,但特殊场景(比如 GM 命令清怪)可以设为 false 避免触发不必要的逻辑。

killmapmon 是清除地图某范围内的怪物,支持是否爆物品、是否清尸体。清尸体这个参数很实用 —— 普通杀怪会留下尸体,过一会儿才消失,如果做 "瞬间清场然后立刻刷新" 的场景,尸体还在就会显得很奇怪,设成清尸体就干净了。

KillSlave 是专门杀宝宝的,可以指定宝宝名字,支持删除名字上的数字和清理尸体。做 "收回召唤物" 技能的时候用。

掉落 是怪物死亡后的产出,这一块接口比较多,而且有新旧两套体系。

旧体系是 monitems 和 monitemsex。 monitems 给玩家增加杀怪物品掉落次数,也就是说玩家接下来杀怪会多爆几次东西。 monitemsex 更精确,指定某种怪物的可爆出次数,还能带延迟。这两个接口需要在 killmon 触发中使用,也就是说要在杀怪的那一刻调用,才能影响这次击杀的掉落。

monitemsex 的应用场景很明确:做 "击杀 A 怪物后,接下来击杀 B 怪物多爆物品" 这种连锁掉落机制。比如杀了食人花之后,5 秒内杀稻草人多爆 2 次。在食人花的 killmon 触发里调 monitemsex,指定稻草人、次数 2、延迟 5000 毫秒,就实现了这个效果。

新体系是 OnMonsterScatter 触发和 ScatterBagItems 接口。 OnMonsterScatter 是新版怪物掉落触发,怪物死亡时触发,带怪物对象和玩家对象。 ScatterBagItems 是执行大爆,把怪物的背包物品散落到地上。这两个配合使用,可以完全自定义怪物的掉落逻辑 —— 在 OnMonsterScatter 里判断怪物类型、阶段、击杀者,然后决定要不要调 ScatterBagItems 执行大爆,或者自己用 giveitem 直接给玩家发物品。

还有 additemtodroplist,临时给怪物增加爆出物品,需要在 KillMon 触发中使用,仅一次有效。多个物品用竖线分隔。这个接口做 "击杀特定怪物必掉某物品" 的时候很方便 —— 在 killmon 触发里判断怪物,然后调 additemtodroplist 把物品加进掉落列表,怪物死亡时就会爆出来。

掉落体系的新旧两套并存,说明引擎在迭代过程中重做了掉落机制。旧体系是 "增加掉落次数" 的思路,新体系是 "触发 + 执行" 的思路,更灵活也更可控。实际用的时候建议优先用新体系( OnMonsterScatter + ScatterBagItems),旧体系可能是为了兼容老版本保留的。但如果你的版本比较老,新体系可能还没实装,就得用旧的。

掉落这一块还有个值得注意的设计:大部分掉落相关的接口都要求在 KillMon 触发中使用。这是因为掉落是和击杀事件绑定的 —— 只有在怪物死亡的那一刻,才能决定它掉什么、掉多少。如果你在其他地方调掉落接口,怪物还没死,掉落列表就没有意义。所以做自定义掉落的时候,逻辑要写在 killmon 或 OnMonsterScatter 触发里,不要写在别的地方。

五、宝宝系统:贯穿生死的特殊线
宝宝系统是怪物系统里一条特殊的线,因为宝宝本质上是 "属于玩家的怪物"—— 它有怪物的属性和行为,但又受玩家控制,和玩家的关系比普通怪物紧密得多。

宝宝管理 有几个基础接口。 getslavebyindex 按索引遍历玩家的宝宝列表,索引从 0 开始。宝宝数量可以从 getbaseinfo(actor,38) 拿到。遍历宝宝是做批量操作的基础 —— 比如给所有宝宝加属性、治疗所有宝宝、收回所有宝宝,都要先遍历。

SetSlaveTarget 设置宝宝的攻击对象,可以指定某个宝宝,也可以传 nil 表示所有宝宝。做 "集火" 命令的时候用 —— 玩家选中一个目标,发命令让所有宝宝攻击它。

changeslaveattackhumpowerrate 调整宝宝攻击人物的威力倍率。这个接口的存在说明引擎默认宝宝可能会攻击人物(比如叛变之后),而你可以控制宝宝打玩家的伤害。做 "宝宝叛变" 机制的时候,叛变后宝宝攻击主人的伤害可以用这个调整 —— 比如设成 3000 就是 3 倍伤害,让叛变更有威胁。设成 0 就是宝宝完全不攻击人物,做和平宠物的时候用。

宝宝属性 前面说过了, changemobability 改属性, changeslavelevel 改等级, getslavelevel 查等级, changemonname 改名。这里补充一个心得:改宝宝属性之后要调 recalcabilitys(slave) 刷新,和人物一样。不刷新的话,属性改了但实际战斗中用的还是旧值。

宝宝系统的接口不算多,但每个都很实用。和普通怪物相比,宝宝多了 "主人" 这个维度,所以接口都需要知道是哪个玩家的宝宝。遍历、设置目标、改属性,这些操作都围绕 "玩家 - 宝宝" 的关系展开。

六、特殊功能:拾取精灵与其他杂项
怪物系统里还有一些不太好归类的特殊功能,单独说一下。

拾取小精灵 是一套完整的子系统。 createsprite 召唤精灵(需要怪物表配置 Race=216), checkspritelevel 检测玩家有没有精灵, releasesprite 回收精灵, pickupitems 设置拾取模式, stoppickupitems 停止拾取。

拾取模式有四种:以人物为中心捡、以小精灵为中心捡、以小精灵为中心一个个捡、以人物为中心一个个捡。还能设范围和间隔(最小 500ms)。这套系统做 "自动拾取宠物" 的时候用 —— 玩家召唤一个小精灵,自动帮他捡地上的物品,不用手动捡。四种模式覆盖了不同的拾取策略,范围和间隔控制效率和性能。

拾取精灵是一个很完整的小系统,从召唤到回收、从模式设置到停止拾取,接口齐全。做随身宠物、拾取助手这类功能的时候,这套接口直接就能用,不需要自己模拟拾取逻辑。

怪物速度 ChangeMonSpeed 前面在行为部分提过了,这里补充一下:它同时支持怪物和宝宝对象,不用区分。改速度的时候注意单位是毫秒,而且正数是加速(间隔变小)、负数是减速(间隔变大),这个和直觉可能相反 —— 数值越大速度越快,但这里传的是间隔,所以正数是减少间隔也就是加速。用的时候先小范围测试,确认方向对了再正式用。

重置刷怪计时 resetmongentick 前面在 "生" 的部分提过了,做副本刷怪控制的时候用。注意它需要地图 ID、坐标、怪物名三个参数,精确到某个刷怪点,不是整个地图的刷新都重置。

七、几个实际使用中的心得和坑
把怪物系统过完,结合实际开发场景,总结几个心得和容易踩的坑。

第一,刷怪返回的是列表,哪怕只刷一只。 GenMon 和 GenMonEx 返回的都是 table,不是单个对象。只刷一只的时候要取列表的第一个元素,不要直接把返回值当怪物对象用。而且返回的列表可能是空的(刷怪失败),用之前先判空。

第二,杀怪接口的参数组合要想清楚。 killmonbyobj 的三个布尔参数 —— 是否掉落、是否触发 killmon、是否显示死亡动画 —— 不同组合适用于不同场景。玩家正常击杀应该是全 true;系统清怪应该是不掉落、不触发、不显示动画(showdie=false);GM 测试杀怪可以不掉落但触发事件。不要一概而论,根据场景选择。

第三,掉落接口要在击杀触发里用。 monitems、monitemsex、additemtodroplist 这些接口都要求在 KillMon 触发中使用,因为掉落是和击杀绑定的。在其他地方调可能不生效,或者效果不对。做自定义掉落的时候,逻辑写在 killmon 或 OnMonsterScatter 里。

第四,宝宝遍历的数量从 getbaseinfo (actor,38) 拿。 文档里 getslavebyindex 的示例用了这个,说明宝宝数量存在人物基础信息的 38 号位。这个不是很直观,查文档的时候可能注意不到。记下来,遍历宝宝的时候用这个做循环上限。

第五,标记值只有 10 个位置,要提前规划。 setcurrent/getcurrent 的 index 是 0-9,只有 10 个位置。每个位置存什么要提前定义好,用常量不要用数字。而且值是字符串,要存复杂数据就转 JSON,不要试图用多个位置拼一个数据(那样太浪费位置了)。

第六,怪物释放自定义技能有类型限制。 mon_docustommagic 只支持 race=156、RaceImg=19 的自定义怪物。做自定义 BOSS 的时候,先确认怪物的 race 配置对不对,不然技能放不出来。普通怪物的技能靠数据库配置,脚本不能直接控制。

第七,嘲讽有等级上限。 dotaunt 和 mobdotaunt 都有 maxlevel 参数,只嘲讽等级不超过指定值的怪物。做嘲讽技能的时候别忘了设这个参数,不设的话可能默认嘲讽所有怪(包括 BOSS),导致 BOSS 战失衡。

第八,改速度的单位是毫秒,方向容易搞反。 ChangeMonSpeed 的 value 单位是毫秒,正数加速(减少间隔)、负数减速(增加间隔)。和直觉相反,用之前先测试确认。

第九,按 UserId 找怪物要传地图 ID。 getmonbyuserid 需要地图 ID 和怪物 UserId 两个参数,因为怪物是属于地图的。不要只传 UserId,找不到的。

第十,新旧掉落体系注意版本兼容。 旧体系( monitems/monitemsex)和新体系( OnMonsterScatter/ScatterBagItems)可能不是所有版本都有。用之前先确认你的版本支持哪套,或者两套都写做兼容。


页: [1]
查看完整版本: 怪物系统是 VV 引擎最庞杂的模块,但生老病死四条线把它串清楚了