ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

冒险岛NPC脚本实战:cm.sendYesNo与cm.warp的20种用法解析

冒险岛NPC脚本实战:cm.sendYesNo与cm.warp的20种用法解析 1. 先搞懂cm对象写脚本之前必须知道的事很多朋友第一次打开冒险岛的NPC脚本文件时看到满屏的cm.xxx()函数会有点懵不明白这个cm到底是个什么东西。我这里先把这个基础概念讲透后面20个实战用法才能顺手。cm其实就是Character Manager的缩写你可以把它理解成“当前执行脚本的NPC的管家”。这个管家手里攥着跟NPC交互的那个玩家的全部信息比如他的等级、职业、任务进度、当前地图还有他正在跟哪张NPC脸对话。你调用的每个cm函数本质上都是在让这个管家帮你查数据、改数据或者帮你执行某个动作。脚本什么时候跑起来呢当一个玩家点击NPC弹菜单、提交任务、或者踩到某个脚本触发点时这些函数就会被执行。脚本执行遵循一条铁律函数里一旦调用cm.sendNext、cm.sendYesNo这类弹窗函数脚本就会停下来等玩家点按钮玩家点完按钮后回调函数再接着往下跑。这跟浏览器里的JavaScript跑alert()会阻塞页面一个道理理解了这一点你就明白了为什么脚本里到处都是回调函数嵌套。cm.sendYesNo和cm.warp两个函数一个是用来跟玩家确认意图的一个是用来完成空间移动的可以说是NPC脚本里出场率最高的两个兄弟。把这两个函数吃透你就能写出完整的任务链、副本入场NPC、职业转职脚本、活动传送员甚至一套带分支的挂机工具菜单。在进入实战示例之前我先给你打一个预防针不同服务端的脚本API在细节上会有差异比如cm.warp参数格式、返回值的布尔类型处理但大体的逻辑是完全一致的。我下面给的代码均以常见的服务端脚本引擎为基准是经过大量脚本验证过的通用写法你在不同端之间迁移时只需要微调参数名即可。2. cm.sendYesNo实战确认框弹得好玩家体验差不了2.1 语法与返回值的“坑”cm.sendYesNo的作用是向玩家弹出一个带“是”和“否”两个按钮的确认框。它的标准签名长这样cm.sendYesNo(text, yesCallback, noCallback)text是显示给玩家的文本支持#b这种颜色代码和#e加粗标签。yesCallback和noCallback是两个回调函数分别对应玩家点“是”和点“否”。最关键的运行机制是玩家点击按钮后脚本不会继续往下执行而是跳进对应的回调函数里等回调函数里的代码跑完这次交互就结束了。这里有个新手最容易掉进去的坑有些脚本引擎版本里cm.sendYesNo的第二个参数和第三个参数不是回调函数而是字符串形式的yes和no。比如有些老的脚本习惯这样写cm.sendYesNo(确定要离开吗, next, dispose)这种写法在部分引擎里会被解析成“切换回复类型”导致彻底失效。我最推荐的写法是永远用匿名函数做回调既兼容各种引擎又可读性最强cm.sendYesNo(确定要离开吗, function() { cm.warp(100000000, 0) }, function() { cm.dispose() })2.2 用法一传送前的二次确认最基础也最常用做传送NPC时要不要加确认框这个话题在老外论坛上吵过很多轮。我的结论是凡是传送到需要门票、有等级门槛、或者会离开当前练级区域的地图都建议加一个确认框可以很有效地避免玩家手滑点错。场景自由市场门口的“装备修理商”NPC玩家对话后弹菜单“是否传送到装备强化地图”function start() { cm.sendYesNo(你要去#r装备强化室#k吗那里可以给你的装备砸卷轴。, function() { cm.warp(910130000, 0) }, function() { cm.sendOk(那等你准备好了再来找我吧。) }) }这段代码的核心是两个函数的分工cm.sendYesNo负责拦截确认确认后cm.warp负责传送否掉以后用cm.sendOk给个口头回应。注意cm.sendOk只是弹一句纯文本不需要回调函数玩家点确定后脚本自动释放。2.3 用法二任务交付的条件判断任务类NPC是cm.sendYesNo的主战场因为几乎每个任务交付环节都要跟玩家确认“你真的完成了对吧”。这里的关键点在于条件判断必须前置先检查再确认顺序搞反了会让玩家白跑腿。场景收集类任务“帮忙收集10个蘑菇孢子”玩家点击NPC交付。function start() { var amount cm.itemQuantity(4000103) if (amount 10) { cm.sendOk(你还没有收集齐10个蘑菇孢子一共找到了 amount 个。) cm.dispose() return } cm.sendYesNo(你找到了 amount 个蘑菇孢子要现在交给我吗, function() { cm.gainItem(4000103, -10) cm.gainExp(500) cm.sendOk(谢谢你的帮忙这是你的奖励。) }, function() { cm.sendOk(那你再想想不着急。) }) }这段代码里体现了一个我特别想强调的实操心得条件不满足时用cm.dispose()主动结束脚本而不是让它继续往下执行弹确认框。不然玩家可以反复点NPC刷弹窗各种逻辑都会被绕过去。2.4 用法三多分支菜单与动态选项cm.sendYesNo其实还有一个常见用途跟cm.sendSimple搭配做成多级菜单。cm.sendSimple弹出一个选择列表玩家点每一项后进入对应分支每个分支里再用cm.sendYesNo做次级确认。function start() { var text 你想去哪里\r\n#L0##b射手村#l\r\n#L1##b勇士部落#l\r\n#L2##b废弃都市#l cm.sendSimple(text, function(selection) { if (selection 2) { cm.sendYesNo(废弃都市有很多怪物出没确定要去吗, function() { cm.warp(102000000, 0) }, function() { cm.sendOk(那你想清楚再找我。) }) } }) }这里selection的索引值对应菜单项的顺序从0开始数。我见过不少朋友在菜单很长时数错索引号导致点A传B的问题。我的习惯是写菜单时在旁边加注释比如// 0:射手村 1:勇士部落 2:废弃都市这样回头维护时不用再数一遍。2.5 用法四到十确认框在更多场景中的变体除了上面三个典型场景我再快速过几个高频变体每个都值得抄作业转职二次确认玩家完成转职任务后弹出确认框“你将转职为战士是否确认”只有在yesCallback里才执行cm.changeJobById(100)防止误点。商店购买确认用cm.sendYesNo做昂贵的消耗品购买确认比如花100万金币买一个高级药水让玩家手滑损失降到最低。副本退场确认离开副本前弹窗“离开将无法继续本次挑战”避免玩家误点退场丢失进度。任务放弃确认设计“放弃任务”选项时用cm.sendYesNo(你确定要放弃当前任务吗)点否保留任务点是才执行cm.forfeitQuest(1234)。队伍成创建确认迷宫入场NPC弹窗“需要组队才能进入你确定要创建队伍吗”确认后再cm.makeTeam()。使用付费服务确认比如“花50点积分重置技能点”必须弹确认框并且确认前要检查积分是否充足不够的话在yesCallback里再拦一次。剧情对话分支很多剧情脚本里用一个cm.sendYesNo来做剧情选择分支不同选择走向不同对话和奖励。这种变体把cm.sendYesNo当成了迷你剧情决策器配合变量记录选择结果可以做出多结局任务链。3. cm.warp实战传送函数的高级使用姿势3.1 语法与传参细节cm.warp的作用是让玩家切换到另一张地图。标准签名是cm.warp(mapId, portalId)mapId是目标地图的数值IDportalId是目标地图的出生点传送门ID。它还有两个变体重载三参数版本cm.warp(mapId, portalId, fromPortal)指从哪个传送门进入五参数版本还会额外传转场效果参数但90%的场景用两参数版本就够了。portalId是最容易被忽略的坑。如果你传了一个目标地图里不存在的传送门ID玩家会被甩到地图坐标(0,0)附近有时候能直接卡进墙里。我见过不少脚本新手传送后玩家“消失”后面一排查就是portalId传错了。比较稳妥的做法是在目标地图里用小号走一遍看角色落在哪个传送门然后用那个传送门的ID。还有个兜底方案portalId传0绝大多数地图的0号传送门都是安全出生点。3.2 用法十一基于玩家选择的传送这是跟cm.sendYesNo配合最紧密的场景也是标题里两个函数合体的经典用法。下面这个例子是一个完整的“出村传送员”NPC脚本支持传送到三个新手练级点每个点都有确认环节function start() { var text 你想去哪里修炼\r\n#L0##b蘑菇森林#l\r\n#L1##b绿蘑菇洞#l\r\n#L2##b野猪领地#l cm.sendSimple(text, function(selection) { var mapId if (selection 0) { mapId 104000000 } else if (selection 1) { mapId 104000001 } else { mapId 104000200 } cm.sendYesNo(确定要去 #b mapId #k 号地图吗, function() { cm.warp(mapId, 0) }, function() { cm.dispose() }) }) }这里先把mapId算好存进变量确认框文案里顺带显示地图号玩家能知道自己要去哪。我觉得这是一个非常实用的习惯把待传送的地图ID先算好而不是在yesCallback里临时算。原因很简单如果计算过程出现错误你希望错误发生在弹窗之前这样玩家能立刻看到问题而不是点“是”以后才爆错。3.3 用法十二带坐标偏移的“精确落点”传送有些场景下你不能只传地图ID还得让玩家精确落在某个点。比如副本开场动画玩家必须站在指定位置才能触发剧情。这时候可以用cm.warp的进阶写法cm.warp(mapId, 0) cm.setPlayerPosition(100, 200)cm.setPlayerPosition会把角色强制移动到指定坐标。我一般建议在地图初始落点不理想但不想改地图文件时用这个函数。注意cm.warp和cm.setPlayerPosition之间不要做其他操作保证落点无缝衔接。如果地图有死亡边界先确认目标坐标在地图安全区范围内不然传过去直接摔死或者被虚空吞噬。3.4 用法十三返回上一个地图记录玩家的来路设计一个“回城卷轴”或“离开副本”的NPC时最友好的做法是直接把玩家传回他之前待的地图这需要用到cm.getPreviousMapId()function start() { var prevMap cm.getPreviousMapId() cm.sendYesNo(要返回之前的地图吗, function() { cm.warp(prevMap, 0) }, function() { cm.dispose() }) }这个函数的返回值是玩家进入当前地图前的上一个地图ID。注意一个边界情况如果玩家登录后第一张地图就是你挂脚本的地图cm.getPreviousMapId()有可能返回0或无效ID。稳妥的做法是加个判断var prevMap cm.getPreviousMapId() if (prevMap 0 || prevMap null) { prevMap 100000000 // 回射手村 }3.5 用法十四跨频道传送与频道绑定冒险岛的脚本API里cm.warp默认在同频道内传送。如果你需要把身处不同频道的队员拉进同一个副本就要用到频道切换参数。以常见脚本引擎为例cm.warp(mapId, 0, 0, 0, 3) // 最后的3是目标频道频道参数的填法在不同引擎里不太一样有些引擎是cm.changeChannel(channel)配合cm.warp连用。我的经验是先查阅你服务端引擎的API文档搞清楚频道参数的位置再动手写。跨频道传送最容易出的问题就是频道号越界比如游戏只开了5个频道你传了频道号6脚本会直接静默失败玩家没反应也没报错排查半天。3.6 用法十五到二十传送家族的其余六种花式操作任务传送任务脚本里根据任务阶段传送比如接手任务后送进专属任务地图用cm.warp(910000000, 0)配合任务状态判断。队伍传送cm.warpParty(mapId, 0)可以把全队传送到指定地图适合BOSS场馆入场和团队副本。注意它只对同地图的队伍成员生效跨地图队员不会收到传送。定时传送用cm.setTimer(5, function() { cm.warp(910000000, 0) })实现倒计时后强制传送适合活动开场倒计时。要注意定时回调会在服务端线程里执行不要在里面写太重的逻辑。条件拦截传送等级不足或任务未完成时拒绝传送并弹提示。这是防止玩家越级刷图的关键手段很多野外BOSS门口的NPC都用这套逻辑。随机地图传送用一个数组存多个地图ID配合Math.floor(Math.random() * arr.length)随机选一个传送。适合做“随机修炼场”“神秘通道”这类趣味玩法。传送后自动触发传到目标地图后用cm.scriptProgress(target_map_script)触发目标地图的入口脚本实现“传送即开始”的无缝体验。比如传到BOSS地图后立刻刷新BOSS。4. 组合实战一个完整任务NPC的诞生过程单独看函数太散了我拆一个真实项目里的完整任务NPC脚本让你看看cm.sendYesNo和cm.warp怎么揉在一起构成一条有头有尾的任务链。我设计的是一个“消灭30只绿蘑菇”的入门任务NPC放在射手村外。玩家找他对话后第一步接任务第二步进任务地图第三步打满30只怪后回来交付第四步领取奖励并可选择传出地图。function start() { // 任务未接取时 if (cm.getQuestStatus(2001) 0) { cm.sendYesNo(射手村的蘑菇最近泛滥成灾你能帮我消灭30只绿蘑菇吗, function() { cm.startQuest(2001) cm.warp(104000001, 0) // 绿蘑菇地图 cm.sendOk(去吧小心别被蘑菇围攻) }, function() { cm.sendOk(没关系你可以再考虑一下。) }) return } // 任务进行中检查怪物数量 if (cm.getQuestStatus(2001) 1) { var count cm.getQuestProgress(2001, 30) if (count 30) { cm.sendYesNo(你已经消灭了 count 只绿蘑菇要领取奖励吗, function() { cm.completeQuest(2001) cm.gainExp(1000) cm.gainItem(2000000, 10) cm.sendOk(太感谢了这是你的奖励。) }, function() { cm.sendOk(奖励不会跑你想好了再来。) }) } else { cm.sendOk(你还需要消灭 (30 - count) 只绿蘑菇加油) } return } // 任务已完成询问是否离开 if (cm.getQuestStatus(2001) 2) { cm.sendYesNo(任务已经完成了要回射手村休息一下吗, function() { cm.warp(100000000, 0) }, function() { cm.dispose() }) } }这个脚本有三个值得你琢磨的地方。第一整个脚本完全靠任务状态getQuestStatus控制分支玩家不管从哪个状态来脚本都能给出正确回应。第二cm.warp一共出现了两次接任务时进图、完成后回村都是跟cm.sendYesNo配对出现的。第三交付奖励前先检查count 30把条件判断放在确认框之前。我在实际写这种任务链时通常还会加一个记录用cm.setScriptData(mushroom_task, inprogress)这种键值对存任务进度阶段。好处是你可以在多个NPC之间共享进度状态做出“打怪只能在特定地图掉落计数”的复杂任务。5. 常见报错与排查技巧脚本跑不起来的那些坑5.1 cm.sendYesNo 不弹窗现象玩家点NPC完全没反应控制台也没有报错。排查思路第一看脚本文件是否被服务端正确加载很多端改动脚本需要重载或重启NPC地图第二检查函数名大小写cm.sendYesNo里的Y和N是大写的写成cm.sendyesno直接静默失败第三确认脚本文件编码是UTF-8无BOM带BOM的脚本在某些引擎里第一行会报错导致整个脚本趴窝。5.2 cm.warp 传过去掉线现象玩家确认传送后客户端直接与服务器断开连接。这是我见过最头疼的问题。大概率原因是目标地图ID不存在或者该地图的Wz资源缺失。排插方法很简单在服务端后台手动用GM命令传送一次如果能传出说明是脚本通道的参数问题或目标地图限制比如地图不允许PVP传送或需要任务前置。如果GM命令也掉线那就是地图资源损坏得换地图ID。5.3 回调函数不执行现象玩家点了“是”但回调里的代码没跑弹窗倒是关了。大家不要怀疑人生90%的情况是回调写成了字符串或者忘了接回调参数。我建议严格按“第一个参数是文案第二是yes回调第三是no回调”的顺序。如果第三个参数没传不代表玩家点“否”没反应在很多引擎里点“否”会直接静默关闭弹窗。5.4 常见问题排查速查表问题现象常见原因快速排查方法弹窗文字乱码脚本编码格式不对转成UTF-8无BOM编码确认框出现两次脚本里忘了调cm.dispose()检查每个分支结尾是否释放脚本传送后在地图里portalId不存在改成0或确认目标地图传送门ID传送到错误地图目标地图ID写错对照Map.wz核对地图编号部分玩家无法传送地图有等级/任务限制查目标地图的进入条件玩家点“是”后卡住回调函数里调用了弹窗但没返回在回调里做完操作后接cm.dispose()5.5 一个让你少加班的自查习惯写NPC脚本最痛苦的不是报错而是报错信息不够直观。我的习惯是每写一段关键逻辑就用cm.log(here, 1)在服务端控制台打一行日志确认代码走到了哪个分支。调试完再删掉日志。这个方法看着笨但在没有断点调试器的脚本环境里它是最可靠的定位手段。另外一个小技巧写传送相关代码时永远先查目标地图的MapInfo。打开服务端的Map.wz节点里面能看到地图的进入限制、传送门列表、脚本入口。提前确认这些比写完脚本被玩家反馈“进不去”再回头救火高效得多。6. 我个人踩过几次坑之后的体会做了这么多年冒险岛NPC脚本最深的一个体会是脚本出问题的时候先怀疑自己不要急着怀疑引擎。cm.sendYesNo不弹窗、cm.warp不传送、回调不执行九成以上都是参数名打错、回调没传对、或者地图ID写岔了这类低级错误。养成滈试习惯、多看引擎源码和API文档比到处抄脚本有用得多。最后再分享一个小技巧写脚本前先写注释再写代码。把函数要处理的流程、分支、返回值先用中文写清楚再下笔码。看似多花了五分钟实际上能帮你省下大量调试时间。而且脚本写得久了注释就成了你自己的API文档回头维护老脚本时不用逐行猜。
返回列表