查看: 22|回复: 1

VV 引擎最像 "世界编辑器" 的模块,空间、时间、对象三层架构把它讲透

[复制链接]

30

主题

72

回帖

1301

积分

金牌会员

积分
1301
发表于 2026-8-20 08:40:59 | 显示全部楼层 |阅读模式
一、移动与传送:三种方式,三个场景

玩家在地图之间的移动,VV 引擎给了三种完全不同的方式,对应三种不同的场景。


第一种是随机跳转,用 map 接口。传玩家和地图 ID,玩家被传送到目标地图的随机坐标。这个接口最简单,适用于 "进入地图" 这种不需要精确位置的场景 —— 比如进副本、进活动地图,只要到了这个地图就行,具体落在哪不重要。


第二种是指定坐标传送,用 mapmove 接口。比 map 多了坐标、范围、特效参数。你可以精确指定传送到哪个坐标,也可以给一个范围让玩家在范围内随机落点。还有一个 effect 参数控制是否播放传送特效 —— 设为 1 就不播放,适合 "无缝切换" 的场景,比如进室内地图不想有传送动画,就关掉特效。


mapmove 的范围参数很实用。做 "传送入场" 的时候,如果所有人都传到同一个坐标,会挤在一起,给个范围让大家分散开,体验好很多。


第三种是自动寻路,用 gotonow 接口。这个和前两种本质不同 —— 前两种是瞬间传送,这个是让玩家自己跑到目标坐标。引擎自动规划路径,玩家沿着路径走过去。


自动寻路有三个配套触发: findpathbegin(寻路开始)、findpathstop(寻路中断)、findpathend(寻路结束)。这三个触发把寻路的生命周期完整覆盖了 —— 开始的时候你可以做提示("正在前往目标地点"),中断的时候处理异常(玩家被攻击、被阻挡、主动取消),结束的时候做到达后的逻辑(触发任务、打开 NPC 对话)。


寻路目标坐标可以用 <$ToPointX> 和 <$ToPointY> 这两个常量获取,在触发里能拿到玩家正在前往的目标点。这个设计在 findpathstop 里特别有用 —— 玩家中途放弃了,你可以告诉他 "距离目标还有多远",或者记录下来下次继续。


三种移动方式的选择,本质上是根据 "是否需要精确位置" 和 "是否需要过程" 来决定的。不需要精确位置用 map,需要精确位置用 mapmove,需要玩家自己跑过去用 gotonow。场景选对了,接口就用对了。


还有两个组队相关的传送: groupmapmove 是编组传送,把队长和附近的队员一起传到目标地图,还能带等级限制和传送后的触发函数。做 "组队进副本" 的时候用这个,不用自己遍历队员一个个传。 movemapplay 是把整个地图的玩家全部移到另一个地图,做 "地图关闭清场"" 活动结束传送回主城 " 的时候用。



二、空间层:镜像地图与动态连接,副本的底层支撑

地图系统最核心的空间操作,是镜像地图和动态连接。这两个功能合在一起,支撑了几乎所有副本和独立空间玩法。


镜像地图是整个地图系统最有技术含量的功能。它的原理是:多个地图 ID 共享同一份地图文件,但逻辑上是独立的空间。文档里的配置示例很清楚 ——MapInfo.txt 里写 [01|0 比奇-1],意思是地图 ID 01 镜像自地图 ID 0,名字叫 "比奇 - 1"。这样你可以有 3 个比奇(01、02、03),但客户端只需要读一份 0.map 文件,节省资源。


addmirrormap 接口可以动态创建镜像地图,参数非常完整:原地图 ID、新地图 ID、新地图名、有效时间(秒)、回城地图(时间到了传回去的地图)、小地图编号、返回坐标。创建出来的镜像地图有独立的 ID、独立的名字、独立的玩家空间,但地形和原地图一模一样。


这个功能就是副本系统的底层。做单人副本的时候,每个玩家进副本都创建一个以玩家 ID 命名的镜像地图,玩家在自己的副本里打怪,互不干扰。副本时间到了,镜像地图自动销毁,玩家被传回回城地图。做多人副本的时候,用队伍 ID 命名镜像地图,队伍里的人都进同一个副本。


镜像地图的管理接口也齐全: delmirrormap 手动删除, mirrormaptime 获取或设置剩余时间, checkmirrormap 检测是否存在。做副本的时候,玩家中途退出可以调 delmirrormap 提前销毁;副本里通关了可以调 mirrormaptime 延长时间让玩家领奖;玩家想再进一次,先 checkmirrormap 确认旧副本还在不在。


文档里有两个注意事项很关键。第一,被镜像的原地图必须在 MapInfo.txt 里先被服务器读取,因为配置是从上到下读的,原地图要写在镜像地图的上面。第二,用 addmirrormap 创建的镜像地图,原地图 ID 必须是基础地图(比如 0),不能以另一个镜像地图(比如 01)为原地图。这两个坑不注意的话,镜像地图会创建失败或者读取错误的地形。


动态地图连接是另一个空间操作。 addmapgate 可以在地图上动态创建一个传送点,指定从哪个地图的哪个坐标范围,传到哪个地图的哪个坐标,还能带有效时间。做临时传送门、活动入口、副本出口的时候用 —— 活动开始的时候在主城创建一个传送门,活动结束了调 delmapgate 删掉,玩家就进不去了。


动态连接的坐标支持随机 ——X 或 Y 传小于 0 的值就是随机坐标。做 "随机传送门" 的时候用,每次进去落到不同的位置。


镜像地图和动态连接配合起来,几乎能实现所有副本和空间玩法。创建镜像地图做独立空间,动态连接做入口,定时器做时间控制,玩家传送做进出管理。这套组合拳打下来,副本系统的骨架就有了。



三、时间层:地图定时器与持续效果,让世界自己运转

地图不只是静态的空间,它还需要时间驱动 —— 隔几秒刷一波怪、活动倒计时、地图持续掉血、天气效果持续播放。VV 引擎的时间层主要靠地图定时器和各种带持续时间的效果。


地图定时器用 setenvirontimer 设置,参数是地图 ID、计时器 ID、时长(秒)、触发函数。到时间后自动调用指定的函数,函数的第一个参数是系统对象,后面可以跟自定义参数。


这个定时器和人物系统的 delaygoto 不同 —— delaygoto 是绑定玩家的,玩家下线或换地图可能就没了;地图定时器是绑定地图的,只要地图还在,定时器就会跑。做副本里的 "30 秒后刷 BOSS"" 每隔 1 分钟刷一波小怪 ",用地图定时器最合适,因为它不依赖某个特定玩家。


定时器的管理有 setenvirofftimer 关闭和 hasenvirtimer 判断是否存在。做副本逻辑的时候,玩家通关了要关掉还没触发的定时器,不然 BOSS 可能在玩家走了之后刷出来,浪费资源。


地图定时器的触发函数支持多参数,用逗号分隔。这个设计让你可以在设置定时器的时候就把参数传进去,到时候函数直接用,不用全局变量存。比如 setenvirontimer(mapID, 1, 30, "@spawn_boss,火龙神,100"),30 秒后调用 spawn_boss 函数,参数是 "火龙神" 和 100。


带持续时间的效果是时间层的另一部分。地图特效 MapEffect 有持续时间参数,天气效果 setweathereffect 有有效时间,镜像地图有有效时间,动态连接有有效时间。这些带时间的效果都是引擎自动管理的 —— 到时间自动消失,不需要你写定时器去关。


地图特效的可见模式设计得很精细:0 所有人可见、1 仅自己可见、2 组队可见、3 行会可见、4 敌对可见。做 "只有队友能看到的传送阵特效"" 只有敌对能看到的陷阱预警 " 的时候,这个参数直接就能用,不用自己做可见性判断。模式 1 到 4 需要传玩家对象,因为可见性是相对于这个玩家的社交关系来算的。


MapEffectByDir 是根据人物方向播放特效,做 "面朝哪个方向就往哪个方向放技能特效" 的时候用。比如玩家释放一个前方扇形技能,根据玩家朝向确定特效播放方向,就用这个。


天气效果是比较新的功能(引擎 64_24.03.14 新增),支持黄沙、花瓣、下雪三种。做地图氛围的时候很有用 —— 沙漠地图下黄沙、春天地图飘花瓣、冬天地图下雪。 delweathereffect 可以删除,传 0 关闭所有效果。


时间层的设计思路是:短周期、需要自定义逻辑的用定时器,长周期、纯展示的用带持续时间的效果。定时器灵活但要自己写逻辑,效果接口简单但只能做展示。根据场景选,不要什么都用定时器 —— 比如天气效果如果用定时器每隔几秒播一次特效,性能和效果都不如直接用天气接口。



四、对象层:地图上有什么,全靠查

地图是空间,空间里有玩家、怪物、NPC、物品、地图事件。对象层的接口就是用来查询和操作这些东西的。


玩家查询有几个接口。 getplaycount 获取地图内的玩家对象列表,支持忽略死亡角色和假人。这个返回的是 table,可以遍历每个玩家做操作 —— 比如地图里所有玩家加 buff、发消息、扣血。注意如果地图里没人,返回的是字符串 "0",不是空 table,用之前要判断类型。


getplaycountinmap 只返回玩家数量,不要对象列表。做 "地图人数检测" 的时候用这个更轻量 —— 比如副本需要至少 3 人才能开启,查一下人数够不够,不需要拿每个玩家的对象。


maphanghcyguild 获取地图内某个行会的成员数量,传星号就是查未加入行会的玩家数。做沙巴克攻城的时候用 —— 检测皇宫里有多少个本行会的人、多少个敌对行会的人,决定归属。


怪物查询也有几个。 getmapmon 获取指定范围内的怪物对象列表,和怪物系统的 checkrangemoncount 参数一样,但这个返回对象而不是数量。需要对范围内的怪物做操作(比如全部定身、全部扣血)的时候用这个。


getmoncount 获取地图内的怪物数量,可以指定怪物 ID,传 - 1 就是所有怪物,还能选择是否忽略宝宝。做 "地图怪物清完了没" 的检测用这个 —— 数量为 0 就是清完了。


mapbossinfo 是一个很强大的接口,获取地图内 BOSS 的状态信息。返回的每条数据包含:怪物名称、剩余 HP 百分比、剩余刷新时间(存在的怪物为 0)、当前 X 坐标、当前 Y 坐标、归属玩家名字。做 BOSS 刷新提示、BOSS 血量面板、BOSS 归属统计的时候,这个接口直接就能拿到所有信息,不用自己遍历怪物查血量。


但这个接口有配置要求 —— 刷怪表的第 7 列和第 10 列必须填 1,怪物表的 "是否是 BOSS" 字段必须填 1。也就是说不是所有怪物都能被这个接口查到,只有标记为 BOSS 的才行。做版本配置的时候要注意,想让某个怪能被 mapbossinfo 查到,就得在配置表里标好。


getmonrefresh 是另一个怪物状态接口,获取小地图上提示的怪物的位置和复活时间。返回 JSON,time=0 表示已复活,大于 0 表示 N 秒后复活。做 "BOSS 刷新倒计时" 功能的时候用这个 —— 查一下 BOSS 还有多久刷新,显示给玩家。


通用对象查询有两个接口,功能类似但参数风格不同。 getobjectinmap 用二进制位标记来指定要查哪些类型的对象:1 玩家、2 怪物、4NPC、8 物品、16 地图事件、32 人形怪、64 英雄、128 分身。要查多种就把值加起来 —— 比如查玩家和怪物就是 1+2=3。


GetMapRangObject 用布尔参数分别控制是否计算玩家、英雄、宝宝、怪物、NPC。这个更直观,每个类型一个开关,不用算二进制。两个接口功能差不多,按个人喜好选。


这两个通用查询接口是做范围技能、范围检测的核心。比如 "以玩家为中心,5 格范围内所有怪物扣血",先查范围内的怪物对象列表,然后遍历扣血。"检测玩家周围 3 格内有没有其他玩家",查范围内的玩家,列表非空就是有。


物品操作在地图上的部分。 throwitem 在地图上放置物品,参数极多 —— 范围、数量、存在时间、是否掉落提示、是否立即拾取、是否仅自己拾取、按顺序还是随机放、单个物品叠加数量、是否可自动拾取。这个接口基本覆盖了 "往地上扔东西" 的所有需求,做怪物掉落、活动奖励、宝箱开启的时候都能用。


gendropitem 是新版的掉落接口(引擎 64_24.05.23 新增),比 throwitem 更规范 —— 用 JSON 传掉落物品列表,带归属对象(归属的物品有拾取 CD),带物品来源数据。做标准化的怪物掉落的时候推荐用这个,物品来源可以追溯,方便做经济风控。


clearitemmap 清理地图上的物品,可以指定坐标范围和物品名,传星号就是清理所有。做地图重置、活动结束清场的时候用 —— 副本结束了把地上没捡的装备清掉,防止堆积。


地图信息查询有几个基础接口。 getmapinfo 查地图的宽和高。 getmapname 按地图 ID 查地图名。 isemptyinmap 判断某个坐标是不是空的(有没有被占)。 gridattr 检测地图逻辑格的属性 —— 能不能到达、是不是安全区、是不是攻城区。


gridattr 是做位置检测的核心。判断玩家能不能走到某个点,查 "能否到达";判断某个位置能不能 PK,查 "安全区";判断沙巴克皇宫的范围,查 "攻城区"。这些都是地图的基础属性,引擎已经在地图文件里标好了,脚本直接查就行,不用自己配置。



五、地图参数与全局操作:动态改变世界的规则

地图不只是被动的空间,它的规则也可以动态修改。


地图参数用 AddMapParam 和 DelMapParam 操作。参数名参考 mapinfo.txt 的配置说明,比如 INCHP 是间隔加 / 扣血。文档里的示例是给 3 号地图设置全局每 5 秒扣 100 点血 —— AddMapParam(3,"INCHP","5","-100")。做 "毒沼地图(持续掉血)"" 温泉地图(持续回血)" 的时候用这个。


地图参数的好处是引擎底层自动执行,不需要你写定时器。设置好之后,地图里的玩家就会自动按规则掉血 / 加血,省了很多脚本逻辑。而且可以动态添加和删除,活动开始加上、活动结束删掉,非常灵活。


mapinfo.txt 里的参数远不止 INCHP,不同版本支持的参数不同,具体要看配置说明。但核心思路是一样的 —— 地图有一组可动态开关的规则参数,脚本可以按需开启和关闭。


全局操作是对整个地图做的批量处理。 mapkillmonexprate 设置地图的杀怪经验倍数,倍率除以 100 是真实倍率(200 就是 2 倍),传星号就是所有地图。做双倍经验活动、经验地图的时候用。0 表示关闭。


randomkillmon 随机杀死地图中的怪物,可以指定怪物名和数量,控制是否掉落。做 "地图事件:随机天劫劈死几只怪" 这种趣味玩法的时候用。


movemapplay 前面提过了,把整个地图的玩家移走。做地图关闭、活动结束的时候用。


这些全局操作的共同特点是:作用范围是整个地图,不需要遍历玩家或怪物,引擎底层批量处理。比你自己写循环一个个操作效率高得多,也不容易漏。



六、几个实际使用中的心得和坑

把地图系统过完,结合实际开发场景,总结几个心得和容易踩的坑。


第一,getplaycount 没人的时候返回字符串 "0",不是空 table。 这个和很多接口的返回习惯不一样。用之前一定要判断类型 —— if type(players) == "table" then 再遍历,不然对字符串 "0" 做 ipairs 会报错。这个坑很隐蔽,因为有人的时候一切正常,没人的时候才炸,测试的时候如果没测空地图场景就发现不了。


第二,镜像地图的原地图 ID 必须是基础地图,不能嵌套镜像。 文档明确说了用 addmirrormap 创建的镜像地图不能以 01、02、03 这种镜像地图为原地图,必须以 0 这种基础地图为原地图。做副本的时候注意,不要拿一个镜像地图去镜像另一个镜像地图,会出问题。


第三,MapInfo.txt 的读取顺序是从上到下,原地图要写在镜像地图前面。 这个配置顺序很重要,写反了服务器读不到原地图,镜像地图就会加载失败。配地图的时候注意顺序,基础地图写在前面,镜像地图写在后面。


第四,自动寻路的三个触发要配套使用。 用 gotonow 的时候,不要只调接口不管触发。 findpathend 里做到达后的逻辑, findpathstop 里做中断处理(比如玩家被攻击了要不要取消、取消了要不要提示)。只调接口不处理触发,寻路就只是 "走过去",没有后续逻辑,浪费了这个功能的潜力。


第五,地图定时器是绑定地图的,地图销毁了定时器也会没。 做副本的时候,如果副本镜像地图被删除了,上面的定时器也会一起消失。所以不需要在删地图的时候手动关定时器,但反过来 —— 如果你想让定时器在地图销毁后继续跑(比如统计数据),就不能用地图定时器,要用全局的计时方式。


第六,mapbossinfo 需要配置表支持,不是所有怪物都能查到。 刷怪表第 7、10 列要填 1,怪物表 "是否 BOSS" 要填 1。做 BOSS 相关功能之前先确认配置对不对,不然接口返回空,你还以为是代码有问题。


第七,throwitem 的参数很多,用之前想清楚每个开关的含义。 仅自己拾取、立即拾取、可自动拾取这些参数组合起来效果很灵活,但也容易搞混。比如 onlyself=true 的时候 isAuto 才生效,这个依赖关系不注意就会设置了自动拾取但不生效。建议做一层封装,把常用的掉落场景(普通掉落、个人专属掉落、自动拾取)封装成几个函数,不要每次都调原始接口传一大堆参数。


第八,getobjectinmap 的 flag 是二进制位,要算对。 查玩家 + 怪物 + NPC 就是 1+2+4=7,查所有类型就是把所有位加起来。不要漏算或多算,查错了类型返回的对象列表就不对。如果觉得二进制麻烦,用 GetMapRangObject 的布尔参数更直观。


第九,mapmove 的 effect 参数设为 1 可以关掉传送特效。 做室内外切换、无缝地图的时候,不想要传送动画就设为 1。但注意关了特效之后玩家可能没意识到自己被传送了,最好配合屏幕提示或界面变化。


第十,天气效果是比较新的接口,注意版本兼容。 setweathereffect 和 delweathereffect 是引擎 64_24.03.14 新增的,如果你的版本比较老可能没有。用之前先确认版本,或者做好降级处理。



30

主题

72

回帖

1301

积分

金牌会员

积分
1301
楼主 发表于 2026-8-20 08:41:36 | 显示全部楼层
挠头的500贡献
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表