触发执行顺序

来自星际争霸重制版地图研究所
Pere讨论 | 贡献2023年4月10日 (一) 22:16的版本 (增加了关于ElapsedTime()的详解)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索

本文需要的辅助软件为星际1的官方推荐地图编辑器 - ScmDraft2(简称scmd)

每轮扫触发时,触发的运行逻辑:

TrigOrder 01.png

运行单个触发的逻辑:

TrigOrder 02.png


【第一章】星际争霸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 游戏秒

......

注意,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。资料的其他内容没有问题。


注意,条件 ElapsedTime(Exactly/AtMost/AtLeast, x) 中的x是游戏时间的整数部分。比如:

条件 ElapsedTime(Exactly, 5) 翻译为人类语言是“当前游戏时间(游戏秒)的整数部分为5”,因此(在一个不含任何wait语句触发的地图中)该条件会在第4轮扫触发时被判定为“真”,因为第4轮扫触发时的游戏时间为5.9375游戏秒,整数部分为5

条件 ElapsedTime(Exactly, 6) 将永远不会被判定为真,因为第6游戏秒不会发生任何扫触发(第4轮扫触发时的游戏时间整数部分为5秒,而第5轮扫触发的游戏时间整数部分为7)。同理,ElapsedTime(Exactly, 8) 也将永远不会被判定为真。

同理,条件CountdownTimer(Exactly/AtMost/AtLeast, x)中的x也是指屏幕上方倒计时的整数部分。

所以,我们在写有关时间判定的条件时,永远不要用Exactly,要用AtLeast或者AtMost

【第二章】一轮遍历检查内,触发的执行顺序:

众所周知,一个触发的由执行对象、条件(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作为一个触发的动作时有更多的选项。