我的传奇服务端狂报'脚本死循环'错误!都卡在'宗派经验'这步了,咋办

来源: 作者: 点击:
遇到的问题是传奇游戏服务端(M2引擎)在执行脚本时检测到了一个潜在的死循环错误,并在不断地刷屏提示你。提示信息非常明确地指出了问题所在:

[脚本死循环] NPC:QFunction 位置:0(0:0) 命令:GOTO @宗派经验 1秒1次

提供了出问题的脚本片段:

;-------------------【GetExp触发】------------------------
[@GetExp]
act

goto @宗派经验 ; <--- 这里跳转到出问题标签
goto @烽火001
goto @冲级赛
break

[@宗派经验]
if

CHECKNAMELIST ..\QuestDiary\宗师系统\宗主名单.txt
ACT

GetRandomName ..\QuestDiary\宗师系统\经验\<$USERNAME>.txt S28
mov d21 <$STR(S28)>
MOV d22 <$GETEXP>
INC d21 <$STR(d22)>
MOV S27 <$STR(d21)>
DelTextList <STR(S28)> ..\QuestDiary\宗师系统\经验\<USERNAME>.txt
AddTextList <STR(S27)> ..\QuestDiary\宗师系统\经验\<USERNAME>.txt
break

核心问题分析:
触发频率过高: [@GetExp] 是一个极其高频率的事件。玩家获得任何一点经验(打怪、任务奖励、活动等)都会触发这个脚本标签。

复杂的文件操作: @宗派经验 标签里的脚本执行了非常耗时的操作:

CHECKNAMELIST: 检查一个文本文件。

GetRandomName: 从一个文本文件中随机读一行。

DelTextList: 删除文本文件中的一行。

AddTextList: 向文本文件中添加一行。

关键点: 这些文件读写操作 (I/O) 相对于游戏引擎内部的计算是非常慢的!频繁操作文件是导致引擎认为“卡死”的主要原因。
引擎的“死循环”检测: M2引擎有一个保护机制,如果一个NPC(这里是QFunction触发的)在一个非常短的时间内(比如1秒)反复执行,引擎就会认为它陷入了死循环,从而报错提示。

脚本结构: [@GetExp] 中连续使用了多个 GOTO。虽然在逻辑上,第一个 GOTO @宗派经验 跳走之后,后面的 GOTO 其实不会执行(因为引擎开始执行新的标签了)。但这个结构本身在引擎的计时机制下,触发点还是在 [@GetExp]。所以每次触发 [@GetExp],引擎记录一次触发,然后看到它 GOTO @宗派经验。紧接着 @宗派经验 在执行那堆慢操作时,下一条经验获取事件又触发了,引擎记录到 [@GetExp] 在极短时间内(文件操作还没结束)又被调用并 GOTO @宗派经验。这就满足了引擎“1秒1次”的告警条件,引擎认为 [@GetExp] 调用 @宗派经验 的指令在快速重复执行,没有正常间隔,判定为潜在的“脚本死循环”。

简单比喻:

想象你开了一个非常繁忙的面馆(游戏服务器)。
[@GetExp] 就像每卖出一碗面(玩家获得经验),你就立刻要求:

@宗派经验 工作:去后面仓库(硬盘文件)里查库存单(名单文件),撕掉一张旧单子(DelTextList),重新写一张新单子(AddTextList)。

问题是,面馆生意太好(玩家获得经验频繁),1秒钟卖出1碗(甚至多碗)。仓库本来地方就小,跑一趟路就远(文件操作慢)。

刚有人喊要第一碗面,你就跑去仓库撕纸条、写纸条,还没写完(文件操作没结束),第二碗面要卖了,你又立刻被命令跑去仓库撕纸条、写纸条...这样仓库门口(引擎执行队列)就堵了一堆等着“撕纸条、写纸条”的任务。

面馆老板(M2引擎)一看这情况,发现你(脚本)因为跑去仓库折腾纸条,导致厨房(主游戏逻辑)快被新订单压垮了,就大吼一声:“脚本死循环了!GOTO去仓库这活儿1秒搞一次不行啊!要出事!”

高手解决方案:

核心思路:绝不能把频繁的文件读写(GetRandomName, DelTextList, AddTextList)放在像 [@GetExp] 这种可能每秒触发几十上百次的标签里!

需要将耗时的操作移到触发频率低得多的地方执行。以下是几种常用解决方案:

方案一:使用在线定时器 (推荐)
修改 [@GetExp] (只记录数据,不操作文件):


[@GetExp]
#IF
CHECKNAMELIST ..\QuestDiary\宗师系统\宗主名单.txt
#ACT
将玩家获得的经验值累加到一个变量(比如个人触发变量<S$ExpAcc>)上

或者用全局变量记录(需要更复杂处理,避免冲突,比如按人物名存储)

简单示例:INC U2 <$GETEXP> ; U2是个人数字变量

MOV U2 <STR(U2)> + <GETEXP>
设置一个时间标记,表示玩家有经验需要处理

MOV S1 1 ; 例如 S1=1 表示需要处理宗派经验
break

这个脚本非常轻量,只做加减和标记操作,几乎不耗时。
创建定时器脚本:

在 QFunction-0.txt 里(或者专门处理全局时间的NPC脚本里)增加一个定期执行的标签:

[@OnTimer30] ; 假设设置30秒触发一次
#IF
检查标记 S1 是否为1 (或检查玩家U2>0)

Equal S1 1
#ACT
调用执行文件操作的标签 (这个操作30秒才执行一次)

GoTo @处理宗派经验
重置标记

MOV S1 0
或者 MOV U2 0 ; 如果用的是变量累加,这里清零

break

需要配置玩家上线时启动定时器:

[@Login]
#ACT
设置每30秒触发一次@OnTimer30标签

SetOnTimer 30 1
break

(放在其他合理的位置也可以,保证玩家活跃期间定时器在运行即可)
修改 @处理宗派经验 (将原来的 @宗派经验 逻辑搬过来):


[@处理宗派经验]
这里执行原@宗派经验内的所有文件操作,但操作的数据源是定时器周期内累积的经验(U2),不是当次的<$GETEXP>

CHECKNAMELIST ... (如果必要,虽然定时器触发频率低,但也可以先判断是否还在名单里)

GetRandomName ..\QuestDiary\宗师系统\经验\<$USERNAME>.txt S28
mov d21 <$STR(S28)>
MOV d22 <$GETEXP> ; 不再用当次经验值

MOV d22 <$STR(U2)> ; 用累计的经验值
INC d21 <$STR(d22)>
MOV S27 <$STR(d21)>
DelTextList <STR(S28)> ..\QuestDiary\宗师系统\经验\<USERNAME>.txt
AddTextList <STR(S27)> ..\QuestDiary\宗师系统\经验\<USERNAME>.txt
break


方案二:使用离线托管 (适用于某些引擎)
原理:将文件操作指令放入 Offline 标签中。引擎会在玩家下线时(或者服务器认为比较空闲时)统一处理这些任务。这能保证操作不在游戏高峰期间进行。

修改 @宗派经验:


[@宗派经验] ; 假设这个标签被低频率触发或保留原有调用点,但里面的操作变成“离线托管”
#if
CHECKNAMELIST ..\QuestDiary\宗师系统\宗主名单.txt
#ACT
将需要处理的数据(玩家获得的<$GETEXP>)和指令排队到离线处理队列

AddOfflineProcCmd ..\QuestDiary\宗师系统\经验\<USERNAME>.txt <GETEXP> ; 这个命令名是假设的
或者使用引擎提供的特定的离线处理指令

break

注意: 这个方案高度依赖于引擎是否支持Offline标签或有专门的离线处理指令 (AddOfflineProcCmd, OfflineProc 等)。请查阅你所使用的M2引擎的说明书,看其是否支持类似机制以及具体用法。

优点: 对实时游戏性能影响极小。

缺点: 不是所有引擎都支持(尤其是老引擎),执行时机不确定(下线后可能才执行),调试稍麻烦。

方案三:降低触发频率 (不得已的选择)
如果上述两种方案实现有困难,可以考虑修改 [@GetExp]:


[@GetExp]
#IF
加入一个随机率控制,只有一定几率才执行后续的复杂操作

Random 5 ; 20%的几率触发 (5代表1/5几率)
CHECKNAMELIST ..\QuestDiary\宗师系统\宗主名单.txt
#ACT
goto @宗派经验
break
#IF
即使不执行宗派经验,也可以执行其他烽火/冲级赛(如果必要)

#ACT
goto @烽火001
goto @冲级赛
break

原理: 通过 Random 指令,让只有部分经验获得事件才会去触发消耗巨大的 @宗派经验 文件操作,大大降低了执行频率。

优点: 实现简单。

缺点:

是一种掩耳盗铃的方法,文件操作本质还是慢,在高频事件里用几率触发只是降低了触发次数,但每次触发它还是慢的。在高负载下,如果几率设置不合适,引擎仍然可能在某些时刻检测到“快速重复执行”。

宗派经验的实际获取量会不准确(因为你丢弃了部分触发的经验)。

哪个方案最好?
首选方案一 (在线定时器): 最通用,逻辑清晰,将高频触发与低频文件操作解耦,效果最好。强烈推荐!

次选方案二 (离线处理): 如果引擎支持且你熟悉其机制,也是很优雅的解决方案。

最后考虑方案三 (几率控制): 只在前两种无法实现时的权宜之计。

操作步骤总结 (按方案一):
备份! 修改任何脚本前,务必备份 QFunction-0.txt!

修改 [@GetExp]:

去掉原来的 goto @宗派经验。

改为使用变量累加玩家在宗主名单下获得的经验值 (如 INC U2 <$GETEXP>)。

设置一个标记变量 (如 MOV S1 1)。
添加玩家登录定时器初始化: 在适当的标签如 [@Login] 中增加 SetOnTimer 30 1。

添加定时器处理标签 [@OnTimer30]:

检查标记变量 (如 Equal S1 1)。

如果满足,调用新的文件操作标签 GoTo @处理宗派经验。

重置标记变量 (如 MOV S1 0)。
创建/修改 @处理宗派经验 标签: 将原 @宗派经验 里面的文件操作复制过来。

修改 MOV d22 <GETEXP> 为 MOV d22 <STR(U2)> (使用累计的经验)。

添加在操作结束后重置累计经验变量 MOV U2 0 (如果需要)。
保存脚本。

重启/重新加载 M2Server 服务端。

测试: 让一个在宗主名单里的玩家打怪获取经验,观察M2Server控制台是否还会报“脚本死循环”,同时检查 ..\QuestDiary\宗师系统\经验\<玩家名>.txt 文件内容是否按预期更新(可能需要等待定时器触发后)。

高手提示:
考虑并发问题: 如果是全局变量文件,多个玩家同时写入需要锁机制(如果引擎支持)或使用唯一命名的文件(如你当前的按玩家名分文件就很安全)。

性能监控: 设置定时器周期不宜过短(30-60秒比较常见,看你对经验同步及时性的要求)。可以通过日志输出 @处理宗派经验 的执行耗时,评估其实际执行速度。