触发执行顺序
前言
本文面向的读者为已经有一定制图基础的星际1地图作者,并非从零开始教你制图。读者需要具有的最低基础是能够成功从零开始制作一张“可以用use map settings成功开始游戏”的地图,并且知道一些基础触发(如switch, set resource等)的用法。本文主要探讨的内容为时间系统和wait的运行机制,对于其他的基础知识可能不会做过多的介绍。完全理解本文所讲的内容需要一定的逻辑能力和试验思想,必要时请仔细阅读、思考、用scmd自己试验。
程序是人用编程语言写出来的,每个“果”背后都有“因”,不存在“随缘”与“无法预知”。scmd中的wait语句看似无法预测、不可控,是因为没有搞懂它背后的机制。其实scmd中的wait语句并非杂乱无章,而是有固定的运行机制,只不过这个机制较为复杂而已。理解本文后,可以对wait语句了如指掌,并能完全理解加速触发为什么能加速,民间流传的加速触发为什么中间会卡一下,以及自己发明出各种各样的加速触发写法,即使触发中充斥着wait语句,也能精确预测执行结果。
本文编辑于2020年1月6日,需要的辅助软件为星际1的官方推荐地图编辑器 - ScmDraft2(简称scmd),目前的最新版为0.9.10,本文中的所有触发都是在此编辑器内编写的。官方下载地址为(在墙内可能要等待很久才能打开网页)
http://www.stormcoast-fortress.net/downloads/scmdraft2ZIP/
本文的例子均已用暴雪战网星际1重置版1.23.2.6926试验过,所有结论均为本人试验结果结合网上可靠资料的推理,因此仅供参考,如有错误欢迎指出并请结合试验来证伪。
我的qq是939040697,欢迎联系我。
【第一章】星际争霸1游戏的时间系统:
logical step是星际游戏时间系统的最小单位,又称frame(游戏帧),下文简称fr。如1fr代表1 logical step,即1游戏帧。
游戏帧(时间单位)是跟游戏时间紧密结合的,有以下亘古不变的恒等式:
1 logical step = 1fr = 1/16游戏秒 = 0.0625游戏秒
游戏时间就是地图编辑里面触发的Elapsed time,流逝速度等于游戏内位于顶端的倒计时时间的流逝速度。
上述等式与游戏速度、现实世界时间均无关。
此外,在无网络延迟的条件下,1游戏帧在不同游戏速度下对应不同的现实时间(都是精确值,无近似):
1fr = 0.167地球秒 (Slowest游戏速度)
1fr = 0.111地球秒 (Slower游戏速度)
1fr = 0.083地球秒 (Slow游戏速度)
1fr = 0.067地球秒 (Normal游戏速度)
1fr = 0.056地球秒 (Fast游戏速度)
1fr = 0.048地球秒 (Faster游戏速度)
1fr = 0.042地球秒 (Fastest游戏速度)
由以上可以算得,fastest游戏速度下:1游戏秒=16fr=0.672地球秒。1地球秒= 125/84 游戏秒 ≈ 1.488游戏秒。
我们经常听到别人说,现实中的1秒等于游戏中的1.5秒,这个说法其实是不准确的。
本文中出现的诸如t=2fr, t=1游戏秒之类的字眼中,t的意思就是时间(time),t=2fr就是代表一个时刻,是一个时间点。
普通触发每轮扫触发的间隔为31fr = 1.9375游戏秒,与游戏速度无关。
重要的事情说三遍:
间隔为31fr,即1.9375游戏秒!不是2游戏秒!
间隔为31fr,即1.9375游戏秒!不是2游戏秒!
间隔为31fr,即1.9375游戏秒!不是2游戏秒!
在fastest游戏速度下,这个间隔为1.302地球秒
注:本文可能会出现“扫触发”、“遍历检查触发”等词汇,都是表达同一个意思。
这里要注意,游戏开始的一瞬间(即0秒时不会检查触发),第一轮检查触发的时间是t=2fr=0.125游戏秒。
之后每轮检查触发的时间间隔均为1.9375游戏秒(普通触发)。即,普通触发检查触发的时间点为:
t= 2fr = 0.125 游戏秒
t= 33fr = 2.0625 游戏秒
t= 64fr = 4 游戏秒
t= 95fr = 5.9375 游戏秒
t=126fr = 7.875 游戏秒
t=157fr = 9.8125 游戏秒
t=188fr = 11.75 游戏秒
t=219fr = 13.6875游戏秒
t=250fr = 15.625 游戏秒
......
其中,所有的整数秒为31k+4游戏秒,所对应的是第16k+3轮遍历检查,其中k为自然数。
例:代入k=0,可得第3轮遍历检查的时间为4游戏秒。代入k=64可得第1027轮遍历检查的时间为1988游戏秒。
注意,scmd编辑器的触发器中的wait()动作会改变扫触发的时间间隔。相邻两轮扫触发的时间间隔可为[2fr,31fr]间的任意整数(通过并联wait可将上限强行拖到39fr,再加个倒计时器甚至可再拖到40fr),都可以通过wait来调整。运用串联并联叠加wait(0)而得到的加速触发系统中,每轮扫触发的时间间隔均为2fr(0.125游戏秒),之后的wait篇会详细介绍。正是由于这种加速触发的存在,我们才把无wait语句的触发系统叫做普通触发。
参考资料
http://www.staredit.net/wiki/index.php?title=Hyper_Triggers
http://www.staredit.net/wiki/index.php?title=Wait_blocks
注:以上资料中说每轮扫触发的间隔为2游戏秒(32fr),这是错的。正确的应该是1.9375游戏秒,即31fr。资料的其他内容没有问题。
【第二章】一轮遍历检查内,触发的执行顺序:
众所周知,一个触发的由执行对象、条件(Conditions)、动作(Actions)三部分构成,相当于编程中的if-then从句。
每隔一定的时间遍历检查一轮触发(或称扫触发),这个时间间隔为31fr=1.9375游戏秒。注意,本章中所有例子里,都没有任何触发存在wait()。wait会导致遍历检查的时间间隔产生变化,以后会有详解。
每轮遍历检查触发的规则为:
每个[多于一个执行对象]的触发(如对象是force 1触发,对象是all players的触发),都要被分解为若干个子触发,每个子触发都有唯一一个执行对象,即每个子触发的执行对象只能是player1或2或3或4或5或6或7或8。如一个All players的触发,假设游戏玩家总数(含电脑)为8,那么它就要被分解为针对player1,针对player2......针对player8这样8个子触发,每个子触发的conditions和actions都完全相同。如果一个触发的执行对象仅为1个player,那么它本身也可以算作一个子触发。注意,在scmd编辑器中,一个触发的对象若同时勾上了player1和AllPlayers,那么它就要拆成9个子触发而不是8个,其中有两个针对player1的子触发(在其他编辑器如eudEditor2中,是拆成8个。这个取决于编辑器)。然后,遍历检查时,先检查执行对象为player1的所有子触发,检查顺序为触发创建的顺序,所有针对player1的子触发都检查完之后,再检查所有对于player2的子触发,以此类推,最后检查player8的触发,边检查边执行。整个遍历检查时间极其迅速,1.9375游戏秒的时间内一定足够遍历检查一遍所有触发。我估计在大多数情况下0.01秒内就可以扫完一轮触发,除非你触发写得太多太多而且cpu太差运算速度跟不上。
判断是否执行触发:
首先,系统检查这个触发的执行对象是否存在于游戏中。如果不存在,则无视这条触发。如果存在,会检查触发的“条件列表”,以判断该触发是否满足执行条件。
“触发满足执行条件”的定义:该触发的条件列表里的所有条件都满足
“触发不满足执行条件”的定义:该触发的条件列表里有至少一条不满足
满足条件的触发就会被执行,即执行列表里的每个内容都会被瞬间依次执行(含有wait语句的触发为特例,之后介绍)
如果此触发非preserved(循环触发),则被执行一次后丢弃,之后的遍历检查都无视此触发;如果此触发的动作列表里面的任何位置有preserved语句,则这条触发为preserve触发(循环触发),则每轮扫触发都会检查这个触发是否执行(注:含有wait语句的触发可能会使得其在本轮扫触发时处于“正在执行”的待机状态,这时系统在本轮不会理会这个触发)。
不满足条件的触发就不执行,等待下一轮遍历检查时再判断是否执行。
[例2-1]:
Trigger("Player 1", "Player 3", "All players"){
Conditions:
Switch("Switch1", Set);
Actions:
Set Resources("Player 1", Add, 1, ore);
}
Trigger("Player 3", "Player 6", "Player 8"){
Conditions:
Always();
Actions:
Set Switch("Switch1", toggle);
}
假设开始游戏后玩家总数(含电脑)为8,那么这两个触发实际上就是13个小触发。分解后为:
player1: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
player3: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
player1: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
player2: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
player3: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
player4: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
player5: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
player6: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
player7: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
player8: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
player3: [Always()]?:[Set Switch("Switch1", toggle)]
player6: [Always()]?:[Set Switch("Switch1", toggle)]
player8: [Always()]?:[Set Switch("Switch1", toggle)]
根据触发的执行顺序对这13个触发进行排序并编号:(注意:由于本例中触发的condition和action里面都没有current player,所以在排序之后,触发的执行对象就不重要了,触发的执行对象仅被用来决定触发顺序)
(01) player1: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
(02) player1: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
(03) player2: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
(04) player3: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
(05) player3: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
(06) player3: [Always()]?:[Set Switch("Switch1", toggle)]
(07) player4: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
(08) player5: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
(09) player6: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
(10) player6: [Always()]?:[Set Switch("Switch1", toggle)]
(11) player7: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
(12) player8: [Switch("Switch1", Set)]?:[Set Resources("Player 1", Add, 1, ore)]
(13) player8: [Always()]?:[Set Switch("Switch1", toggle)]
那么,第一轮遍历检查触发时间为游戏时间开始后的0.125游戏秒。我们来走一遍:
(01)不满足条件,不执行,因为所有switch默认为关闭状态(cleared)
(02)不满足条件,不执行,因为所有switch默认为关(cleared)
(03)不满足条件,不执行
(04)不满足条件,不执行
(05)不满足条件,不执行
(06)满足条件,所以将Switch1打开(set)。执行完后丢弃此触发,下轮无视。
(07)满足条件,给player1加1个水晶矿。丢弃。
(08)满足条件,给player1加1个水晶矿。丢弃。
(09)满足条件,给player1加1个水晶矿。丢弃。
(10)满足条件,将Switch1关闭(clear)。丢弃。
(11)不满足条件,因为Switch1是关闭状态
(12)不满足条件,因为Switch1是关闭状态
(13)满足条件,将Switch1打开(set)。丢弃
这里再次强调,以上(01)至(13)都是在t=2fr这一瞬间完成的。
那么第一轮遍历检查之后,player1共有3块钱水晶矿。第二轮遍历检查时:
(01)满足条件,给player1加1个水晶矿。丢弃。
(02)满足条件,给player1加1个水晶矿。丢弃。
(03)满足条件,给player1加1个水晶矿。丢弃。
(04)满足条件,给player1加1个水晶矿。丢弃。
(05)满足条件,给player1加1个水晶矿。丢弃。
(11)满足条件,给player1加1个水晶矿。丢弃。
(12)满足条件,给player1加1个水晶矿。丢弃。
第二轮遍历检查总共给player1加了7块水晶矿,此时player1共有10块钱水晶矿。
所以在游戏中的效果就是,player1在一开局0.125秒后水晶矿变成3,然后过1.9375秒游戏时间之后水晶矿变成10。
在这里顺便说一下什么是Switch(开关)。Switch是一个环境变量。如果你学过编程,那么它就是一个Boolean variable(布尔变量)。一个Switch可以有两种状态,set(开启状态)和cleared(关闭状态),注意哦,这两个是形容词,所以switch作为一个触发的条件时只有这两个选项。默认为cleared状态。我们可以通过触发中的动作来改变开关的状态,我们可以:set(打开)一个switch,也可以clear(关上)一个switch,也可以toggle(扳动),也可以randomize(随机化),注意哦,这些都是动词,所以switch作为一个触发的动作时有更多的选项。