[ESA]永恒的星际联盟

 找回密码
 立即注册

!connect_header_login!

!connect_header_login_tip!

查看: 125|回复: 9

深入探讨scmd编辑器wait的执行机制 及 加速触发原理详解

[复制链接]
发表于 2020-2-6 16:44:51 | 显示全部楼层 |阅读模式
本帖最后由 PereC 于 2020-2-8 09:35 编辑

【前言】
本文面向的读者为已经有一定制图基础的星际1地图作者,并非从零开始教你制图。读者需要具有的最低基础是能够成功从零开始制作一张“可以用use map settings成功开始游戏”的地图,并且知道一些基础触发(如switch, set resource等)的用法。本文主要探讨的内容为时间系统和wait的运行机制,对于其他的基础知识可能不会做过多的介绍。完全理解本文所讲的内容需要一定的逻辑能力和试验思想,必要时请仔细阅读、思考、用scmd自己试验。
程序是人用编程语言写出来的,每个“果”背后都有“因”,不存在“随缘”与“无法预知”。scmd中的wait语句看似无法预测、不可控,是因为没有搞懂它背后的机制。其实scmd中的wait语句并非杂乱无章,而是有固定的运行机制,只不过这个机制较为复杂而已。理解本文后,可以对wait语句了如指掌,并能完全理解加速触发为什么能加速,民间流传的加速触发为什么中间会卡一下,以及自己发明出各种各样的加速触发写法,即使触发中充斥着wait语句,也能精确预测执行结果。
本文原稿编辑于2020年2月6日(之后可能会有修改),需要的辅助软件为星际1的官方推荐地图编辑器 - ScmDraft2(简称scmd),目前的最新版为0.9.10,本文中的所有触发都是在此编辑器内编写的。官方下载地址为(在墙内可能要等待很久才能打开网页)
http://www.stormcoast-fortress.net/downloads/scmdraft2ZIP/
本文的例子均已用暴雪战网星际1重置版1.23.2.6926试验过,所有结论均为本人试验结果结合网上可靠资料的推理,因此仅供参考,如有错误欢迎指出并请结合试验来证伪。

我的qq是939040697,欢迎联系我。

入门例子:[例0-1]
用scmd随便新建一张新地图,删除所有的内置触发,然后加上下面这条触发,保存地图后开始游戏观察自己水晶矿数的变化,可以看到钱数在匀速增加。本例中,要保持player1为Human玩家。
温馨提示:除了player1之外的剩下7个玩家中,把其中至少一个玩家变成电脑,并加上对应的start location,不然无法开始单人游戏。
例0-1.png
Trigger("Player 1"){
Conditions:
        Always();

Actions:
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
}


[例子1-1]
在上一个例子的基础上,再加上如下两条新的触发,注意这两条触发的先后顺序不能调换。
例0-2.png
Trigger("All players"){
Conditions:
        Always();
        Switch("Switch1", Set);

Actions:
        Wait(0);
        Preserve Trigger();
}

//-----------------------------------------------------------------//

Trigger("All players"){
Conditions:
        Always();

Actions:
        Set Switch("Switch1", set);
}


开始游戏后,你可以看到你的钱数也是在匀速增长,但是速度变快了很多。你也可以把游戏速度调到最慢,仔细感受一下这个速度。其实,本例子中的钱数增加速度是上一个例子的15.5倍。
这篇文章将带你详细了解这个现象背后的原因。


【第一章】星际争霸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游戏秒。
下图中的红点代表一轮扫触发,之所以画成“点”而不是“线段”,是因为一轮扫触发是几乎一瞬间完成的事情。
ruler.png
注意,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。资料的其他内容没有问题。




 楼主| 发表于 2020-2-6 16:42:49 | 显示全部楼层
本帖最后由 PereC 于 2020-2-6 16:53 编辑

【第二章】一轮遍历检查内,触发的执行顺序
众所周知,一个触发的由执行对象、条件(Conditions)、动作(Actions)三部分构成,相当于编程中的if-then从句。
每隔一定的时间遍历检查一轮触发(或称扫触发),这个时间间隔为31fr=1.9375游戏秒。注意,本章中所有例子里,都没有任何触发存在wait()。wait会导致遍历检查的时间间隔产生变化,以后会有详解。 执行顺序.png
每轮遍历检查触发的规则为:
每个[多于一个执行对象]的触发(如对象是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]:

例2-1.png

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作为一个触发的动作时有更多的选项。



回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-2-6 16:50:18 | 显示全部楼层
本帖最后由 PereC 于 2020-2-8 19:05 编辑

(继续第二章的内容)
下面介绍一个极为实用的例子。我们在编辑rpg地图时,可能常常会遇到“每隔固定时间刷一个单位”这样的需求。了解了系统每轮扫触发的时间间隔之后,很容易就可以利用preserve来写出“每隔31fr(1.9375游戏秒)刷一个单位”的触发。那么,如果我想每隔62fr(每两轮扫触发执行一次)呢?如果我想每隔124fr(每4轮扫触发执行一次)呢?这个时候,我们就要用到计数器,让计数器的值在m到n之间循环(m通常取0),我们就可以做到“让目标触发每n-m轮扫触发执行一次”。计数器中最常用的便是死亡数计数器,或者死亡计数器。它利用修改某个单位的死亡数值来达到计数的目的。学过编程的朋友都知道,编程要用到很多变量,这个变量的值会储存在电脑的内存中,编程的语句可能会对这个变量进行赋值(修改),其他语句可能会用到这个变量的值。而死亡数便是一个很好的变量,尤其是那些在游戏中根本不会出现的单位(比如Cave)的死亡数。任何的单位的死亡数都默认为0。注意,每个玩家都会有cave的死亡数,修改一个玩家的cave死亡数并不会影响其他玩家,所以一个单位的死亡数至少可以生成8个相互独立的变量供我们使用(player12等等非常规玩家我们在此不做考虑)。下面,我们利用玩家2的Cave这个单位的死亡数,来制作一个循环计数器。
[例2-2]:

例2-2.png
Trigger("Player 1"){   (Trigger A)
Conditions:
        Always();

Actions:
        Set Deaths("Player 2", "Cave", Add, 1);
        Preserve Trigger();
}
//-----------------------------------------------------------------//
Trigger("Player 1"){   (Trigger B)
Conditions:
        Deaths("Player 2", "Cave", Exactly, 4);

Actions:
        Set Resources("Player 1", Add, 1, gas);
        Preserve Trigger();
}
//-----------------------------------------------------------------//
Trigger("Player 1"){   (Trigger C)
Conditions:
        Deaths("Player 2", "Cave", At least, 4);

Actions:
        Set Deaths("Player 2", "Cave", Set To, 0);
        Preserve Trigger();
}

一个死亡计数器一般由3部分组成,我分别称它们为:加一触发清零触发目标动作。另外,还要有一个用来循环的变量(循环变量)。其中,加一触发和清零触发是必备的两个触发,是死亡计数器的核心,这两个触发让循环变量的取值一直循环,个人建议把加一触发写在清零触发之前。而目标动作是规定在循环变量取到某个特定值时执行某个的目标动作,可以与清零触发合并在一起,也可以是单独的触发,甚至可以写多个。三个触发的顺序谁先谁后都可以,但是顺序很重要。不同顺序会有略微不同的效果,需要一定的逻辑能力。如果脑子想不明白,建议在纸上打草稿。注:清零触发的条件里面可以写Exactly也可以写At least,在逻辑上两个没有区别。在这个例子中,循环变量为“player2的Cave死亡数”,为了方便,我将这个变量记为小写字母i。本例中,我的目的为每4轮扫触发(每隔124fr)给player1加1个气,因此目标动作即为TriggerB的动作,这个触发的内容是我们要达到的目的,具体每当i=4时,给player1加1个气。而TriggerA是加一触发,它的作用是每一轮扫触发都给i加一。TriggerC就是清零触发,负责规定i值的循环范围,在此例中,清零触发只要发现i变成了4,就会把i再变回0,使得i在0到4之间循环,每4轮扫触发完成一个循环周期。
这三个触发以任意先后顺序排列都可以,看你个人的喜好,下面我用一张图简单说一下“加一触发写在清零触发之前”的情况。下图中,每个长方形代表一轮扫触发,长方形内为从上到下按照被扫顺序排列好的触发,用灰色横线表示,箭头分别为加一触发和清零触发的位置,分别用红色和绿色强调。长方形内的数字代表扫触发过程中i值的变化。从图中可以一目了然,i值在0到4之间循环,每4轮扫触发为一个周期。

rect.png
在此例中,目标触发的位置位于加一触发和清零触发之间,并且要求是i=4时执行我们的目标动作(当然也可以跟清零触发合并,因为条件都是i=4),可以看到,我们的目标动作是第4、8、12...轮扫触发的时候执行。如果保持加一触发、目标触发、清零触发的相对位置不变,而把目标触发给改成要求在i=3时执行目标动作,那么就是第3、7、11...轮扫触发执行目标动作。如果改成i=0呢?则永远不执行!如果把目标触发移动到加一触发之前,那么目标触发里的i值只能写0,1,2或3才能达到我们的目的,而写4的话就是永不执行。这也就是我说这三个触发的相对位置很重要的原因。
上述计数器的本质是“每隔若干轮触发执行某个目标动作”,其实它等价于“每隔固定的时间执行某个目标动作”,但是由于触发是每31fr才扫一轮,所以这个“固定时间”只能是31fr的整数倍,很不利于操控。在学习了加速触发的概念后,我们就可以精确操控时间了。要想理解加速触发的原理,必须搞懂wait语句的原理,这将在下一章介绍。

在介绍wait之前,我们先要了解set countdown timer这个触发的执行原理,因为之后的各种实验都最好配合set countdown timer一起使用,这样能对试验结论进行更好的验证:
Set Countdown Timer(Set To, x秒)这个动作中的“x秒”指的是游戏时间而非现实时间。
当此动作被执行之后,在下一个整数游戏秒时,屏幕正上方会出现从x-1游戏秒的倒计时,直到0:00,保持1游戏秒后消失。
(此外,它会在0:00这个时间点的下一帧添加一个“预约”,具体会在wait章节中介绍)
比如,一个条件为Always,动作为Set Countdown Timer(Set To, 10秒)的触发,会在t=2fr=0.125游戏秒时执行,在t=16fr=1游戏秒时,屏幕顶部出现0:09,然后在t=2游戏秒时变成0:08,以此类推,在t=10游戏秒时变成0:00,在t=11游戏秒时消失。
再比如,在t=4游戏秒时执行了Set Countdown Timer(Set To, 60秒)的触发,那么在t=5游戏秒时,屏幕顶部出现0:59
再比如,在t=9.8125游戏秒时执行了Set Countdown Timer(Set To, 3600秒),则在t=10游戏秒时屏幕顶部出现59:59



回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-2-6 16:52:21 | 显示全部楼层
【第三章】关于wait()动作的详解
【第一节】 单个wait语句
当一个不含wait语句的触发在某轮扫触发时满足执行条件时,它的所有动作都应该在本轮扫触发内一次性执行完毕。wait语句只能存在于“动作”中,而不能存在于“条件”中,它将一个触发的动作隔开,要若干轮扫触发才能执行完毕。如果一个触发中有一个wait语句,则该触发在满足执行条件后,该触发的动作会被分隔成两批,分别位于wait语句的前后,分别在本轮扫触发和之后的某一轮扫触发分两次执行完毕。如果一个触发有两个wait语句,则它的动作要拆成3批,分别在3轮不同的扫触发中执行。以此类推。本章将介绍scmd2编辑器中wait语句的执行法则,以及如何利用wait来精准控制游戏时间。 wait后面的数值以毫秒为单位,并且是指现实时间的毫秒,因此在触发中的wait(1000)即wait(1000ms)在不同游戏速度下会对应不同的游戏时间长度(为方便表达,下文中游戏时长皆以转化后的fr为单位)。wait的执行原理是让同一个触发内wait之前的内容与wait之后的内容执行的时间间隔为wait内所写的时间,所以会导致改变触发的遍历检查间隔,即会使得本轮扫触发与之后某轮(可能是下一轮)扫触发之间的时间间隔为wait内所写的时间。要注意,任意两轮扫描的时间间隔必须以fr为单位,即整数个fr,且最小为2fr。因此在执行wait时,我们必须先将wait后的数字转化为以fr为单位的时间(要根据此时的游戏速度来进行转化)。转化公式为
当t_{ms}=0时,t_{fr} = 2
当t_{ms}>0时,t_{fr} = 1 + [t_{ms}/a]向上取整
其中t_{ms}为触发内wait后面写的毫秒数,a为当前游戏速度下1fr等于多少地球毫秒(fastest游戏速度下1fr=42地球ms,即a=42),t_{fr}为转换出来的以fr为单位的时间。
或者简单点来说,在fastest游戏速度下:
wait(0ms)~wait(42ms)   等价于wait(2fr),即wait(0.125游戏秒)
wait(43ms)~wait(84ms)  等价于wait(3fr),即wait(0.1875游戏秒)
wait(85ms)~wait(126ms) 等价于wait(4fr),即wait(0.25游戏秒)
wait(127ms)~wait(168ms)等价于wait(5fr),即wait(0.3125游戏秒)
......
wait((a-2)x+1毫秒)~wait((a-1)x毫秒) 等价于wait(xfr),即wait((x/16)游戏秒)
......
比如,触发里写了wait(1000ms):
那么假设游戏速度为fastest,那么可以得到对应的游戏时间为 1 + [1000/42]向上取整 = 25fr,即wait(25fr)
但如果游戏速度为faster,那么对应的游戏时间则为 1 + [1000/48]向上取整 = 22fr,即wait(22fr)
wait真正影响触发执行效果的是转化后的时间(以fr为单位),而不是转化前的时间(以现实时间毫秒为单位),因此之后的例子中我将常用诸如wait(2fr)这样的转化后的时间来举例,如果wait内的数字没写单位,比如wait(0),那就代表是转化前的时间,即wait(0ms),读者要自己把它转化为wait(2fr)。

下面介绍系统扫触发的时间间隔机制。为了方便大家了解这个机制,我引入一个概念:扫触发的“预约”与“预约列”。
之前介绍过,如果所有触发中都没有wait语句,那么系统就是从t=2fr开始,每隔31fr扫一轮触发。但是只要一有wait语句的执行,扫触发的时间间隔就会随之改变,这是因为如下机制:
每一轮扫触发时,系统都会预约之后的扫触发的时间点。预约列中标注着已经预约的时间。
(1)在扫触发时,每执行一个wait(xfr),系统就会在预约列中加一个预约时间,预约“xfr后”扫一轮触发。从本轮扫触发,一直到wait(xfr)预约的那轮扫触发,之间的时间都视为wait(xfr)正在执行中。
(2)在本轮该执行的wait语句全部开始执行(完成预约)后,如果预约列中距离本轮扫触发31fr内都没有预约的时间点,则系统会在预约列中加一个“31fr后”的预约时间点。
(3)当预约列中出现两个相同的时间点时,这两个时间点会合并成一个,即届时只进行一轮扫触发。
(4)在步骤(2)和(3)之后,系统会再次检查预约列中是否存在相差为1fr的预约时间,如果存在,则较早的向较晚的妥协,两个预约时间合并成一个,即较晚的那个。假如预约列中同时存在多个连续的相差时间为1fr的时间点(比如同时存在5fr,6fr,7fr,8fr),则这几个时间点全部合并为一个预约点,变为最晚的那个时间点(8fr)。我称这个为妥协法则
(5)下一轮扫触发的时间为预约列中最靠前的时间点。同时系统在预约列中移除此时间点。
此外,Set Countdown Timer(Set To, x游戏秒)这个动作也会添加一个预约,预约的时间点为倒计时变成0:00的下一帧,效果相当于额外再并联一个wait。
-------
预约列中不能同时存在两个或以上“由执行对象为同一玩家的触发中的wait语句预约”的预约时间,这一重要性质将在wait串联中详细说明,本节不做说明。
-------
[例3-1-1]
例3-1-1.png
2个触发AB的执行对象皆为player1,条件均为Always,动作分别为:
Trigger A:
        Display Text Message(Always Display, "Hello");
        Wait(1301);
        Display Text Message(Always Display, "Bye");
Trigger B:
        Set Resources("Player 1", Add, 1, gas);
        Preserve Trigger();
遇到wait语句,我们首先要把wait后面以毫秒为单位的时间转化为以fr为单位的时间。假设我们用fastest游戏速度开始游戏,则Wait(1301)会被转化为wait(32fr),所以游戏开始后发生的事情如下,括号内为触发扫描的轮数:
游戏开始的2fr没有触发扫描
(1) t = 2fr = 0.125游戏秒:  
公屏出现"Hello"
执行wait(32fr),即在预约列中添加时间“32fr之后”,预约列变为:t=34fr
player1的gas变成1
本轮所有触发执行完毕,发现预约列中的最早时间点为t=34fr(32fr之后),所以距离本轮扫触发之后31fr之内没有任何预约,所以系统会在预约列中添加一个时间点:31fr之后,即t=33fr。
现在,预约列中有两个时间点,t=33fr和t=34fr,相差为1fr,所以根据妥协定律,两个时间点合并成一个,即t=34fr
(间隔32fr)
(2) t = 34fr = 2.125游戏秒:
Wait语句执行完毕,执行display语句,公屏出现"Bye",triggerA全部执行完毕,被丢弃。
player1的gas变成2
本轮触发执行完毕,预约列中空无一物,因此在预约列中添加一个时间点:31fr之后,即t=65fr。
(间隔31fr)
(3) t = 65fr = 4.0625游戏秒:
player1的gas变成3
之后每隔31fr,player1的gas就加1

再看一例:
[例3-1-2]
Trigger A:
        Display Text Message(Always Display, "Hello");
        Wait(1303);
        Display Text Message(Always Display, "Bye");
Trigger B:
        Set Resources("Player 1", Add, 1, gas);
        Preserve Trigger();
这个例子仅仅是把上面的例子中Wait(1301)变成了Wait(1303),在fastest游戏速度下也就是变成了wait(33fr):
(1) t = 2fr = 0.125游戏秒:  
公屏出现"Hello"
执行wait(33fr),即在预约列中添加时间t=35fr
player1的gas变成1
本轮所有触发执行完毕,发现预约列中的最早时间点为33fr之后(t=35fr),因此系统添加一个“31fr后”的预约,即t=33fr。
现在,预约列中有两个时间点:t=33fr和t=35fr
(2) t = 33fr
Wait语句正在执行中,故triggerA继续等待,不触发任何事情。
player1的gas变成2
本轮触发执行完毕,预约列中有1个时间点:t=35fr(即“2fr后”),故系统不添加预约。
(间隔2fr)
(3) t = 35fr
Wait语句执行完毕,执行display语句,公屏出现"Bye",triggerA全部执行完毕,被丢弃。
player1的gas变成3
预约列空,系统添加“31fr后”的预约。
(间隔31fr)
(4) t = 66fr
player1的gas变成4
之后每隔31fr,player1的gas就加1

当整个触发中只有1个含wait语句时,用以上两个例子就可以精确预测执行情况。即转化为wait(xfr)之后,把x拆成31+31+31+...+y。比如wait(40fr),那就是40=31+9。比如wait(94fr),那就是40=31+31+32。而wait(95fr)就是95=31+31+31+2
如果在某轮扫触发过程中遇到了两个或两个以上的触发都含有wait语句,则情况会变得略微复杂,若干个wait语句的先后顺序、串联与并联关系都影响结果,且有些结果非常反直觉,这将在下文的wait的串联与并联中介绍。




回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-2-6 16:53:50 | 显示全部楼层
(继续第三章第一节)
要注意,对于含有preserve的wait触发,即使wait语句写在触发执行列表中的最后,执行完该wait语句之后的那一轮扫触发中,也要将本触发视为“未执行完毕”,本轮扫触发之后才能将其视为执行完毕,下一轮扫触发才能从头开始执行动作。看下面的例子:
[例3-1-3]
Trigger A: (对象player1,条件Always)
        Preserve Trigger();
        Display Text Message(Always Display, "Hello");
        Wait(0);
        Wait(0);
Trigger B:
        Set Resources("Player 1", Add, 1, gas);
        Preserve Trigger();
(1)t=2fr,公屏出现"Hello",并执行第一个wait(0)。同时gas变成1
wait(0)的执行导致与下轮扫触发间隔2fr
(2)t=4fr,第一个wait(0)执行完毕,开始执行第二个wait(0)。同时gas变成2
间隔2fr
(3)t=6fr,此时虽然TriggerA的wait语句执行完毕,但是由于triggerA是preserve的,所以triggerA仍视为未执行完毕,所以本轮触发执行的结果仅为gas=3,本轮触发执行完毕后才能将triggerA视为执行完毕,下一轮扫触发才能从头开始执行triggerA
间隔31fr
(4)t=37fr,公屏出现"Hello",gas=4
间隔2fr
(5)t=39fr,gas=5
间隔2fr
(6)t=41fr,gas=6
间隔31fr
(7)t=72fr,公屏出现"Hello",gas=7
......
所以,单纯的一个含两行wait(0)语句的触发,即使加了preserve也不能保证之后每轮扫触发的时间间隔为2fr,而是间隔2fr,2fr,31fr,2fr,2fr,31fr...看上去就像每三轮一组,每组之间有个间隔较长的延迟。因此,preserve的含wait(xfr)的触发并不能帮我们达到“固定每x帧干一件事”的目的。
同理,如果所有触发中只有一个触发有wait语句,且这个触发有x个wait(0),且这个触发是preserve的,那么扫触发的间隔就是x个2fr,31fr,x个2fr,31fr......看上去像是每x+1轮一组,每组之间有个延迟。
那么如果是多个触发都含有wait语句呢?那就要取决于这些语句的串联并联关系。

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-2-6 16:54:59 | 显示全部楼层
本帖最后由 PereC 于 2020-2-7 11:54 编辑

【第二节】wait的串联
如果【针对同一个玩家】的若干触发中,包含两个或以上含有wait的触发,则我称它为wait的串联,或者说这两个wait语句为串联关系。具有串联关系的两个wait在被同时扫到时(或者其中一个正在执行时另一个被扫到),情况会变得复杂,本节内容也是专门介绍这种情况。尤其是含有Preserve的wait触发,会导致情况更加复杂。这一部分更是加速触发的理论基础。(当然,如果两个串联的wait在时间上是分开出现的,比如其中一个含wait的触发的执行时段是10分钟之前而另一个含wait触发的执行时段是20分钟之后,那么两个wait的效果仅用上一节单个wait的理论便可分析。)在介绍串联wait的执行机制之前,让我们先看几个实例:
[例3-2-1]
3个触发ABC的执行对象皆为player1,条件均为Always,动作分别为:
Trigger A:
        Display Text Message(Always Display, "1");
        Wait(0);   #fastest游戏速度下为Wait(2fr)
        Display Text Message(Always Display, "2");
        Wait(100); #fastest游戏速度下为Wait(4fr)
        Display Text Message(Always Display, "3");
-----------------------------------------------
Trigger B:
        Display Text Message(Always Display, "4");
        Wait(260); #fastest游戏速度下为Wait(8fr)
        Display Text Message(Always Display, "5");
        Wait(600); #fastest游戏速度下为Wait(16fr)
        Display Text Message(Always Display, "6");
-----------------------------------------------
Trigger C:
        Display Text Message(Always Display, "7");
        Wait(1060);#fastest游戏速度下为Wait(27fr)
        Display Text Message(Always Display, "8");
-----------------------------------------------
如果用fastest游戏速度开始游戏,则:
开始2fr没有事情发生
(1)t = 2fr: 公屏出现1 4 7
间隔2fr
(2)t = 4fr: 公屏出现2
间隔4fr
(3)t = 8fr: 公屏出现3
间隔8fr
(4)t =16fr: 公屏出现5
间隔16fr
(5)t =32fr: 公屏出现6
间隔27fr
(6)t =59fr: 公屏出现8
(所有触发都已执行完毕)
间隔31fr
(7)什么也不发生
间隔31fr
(8)什么也不发生
...

[例3-2-2]
如果给以上3个触发都加上preserve,那么情况将变得非常复杂:
Trigger A:
        Display Text Message(Always Display, "1");
        Wait(0);   #fastest游戏速度下为Wait(2fr)
        Display Text Message(Always Display, "2");
        Wait(100); #fastest游戏速度下为Wait(4fr)
        Display Text Message(Always Display, "3");
        Preserve Trigger();
-----------------------------------------------
Trigger B:
        Display Text Message(Always Display, "4");
        Wait(260); #fastest游戏速度下为Wait(8fr)
        Display Text Message(Always Display, "5");
        Wait(600); #fastest游戏速度下为Wait(16fr)
        Display Text Message(Always Display, "6");
        Preserve Trigger();
-----------------------------------------------
Trigger C:
        Display Text Message(Always Display, "7");
        Wait(1060);#fastest游戏速度下为Wait(27fr)
        Display Text Message(Always Display, "8");
        Preserve Trigger();
-----------------------------------------------
用fastest游戏速度开始游戏:
开始2fr没有事情发生
(1)t = 2fr: 公屏出现 1 4 7
间隔2fr
(2)t = 4fr: 公屏出现 2
间隔4fr
(3)         公屏出现 3
间隔8fr
(4)         公屏出现 1
间隔2fr
(5)         公屏出现 2
间隔4fr
(6)         公屏出现 3 5
间隔16fr
(7)         公屏出现 1
间隔2fr
(8)         公屏出现 2
间隔4fr
(9)         公屏出现 3 6
间隔27fr
(10)        公屏出现 1 4
间隔2fr
(11)        公屏出现 2
间隔4fr
(12)        公屏出现 3
间隔8fr
(13)        公屏出现 1
间隔2fr
(14)        公屏出现 2
间隔4fr
(15)        公屏出现 3 5
间隔16fr
(16)        公屏出现 1
间隔2fr
(17)        公屏出现 2
间隔4fr
(18)t=113fr:公屏出现 3 6 8
间隔31fr后,就是(1)-(18)再循环一遍
(19)t=144fr:与(1)相同
....

以上(1)-(18)这18轮触发扫描,可以称为1簇扫描1 block of trigger cycles
不难得到,18=(2+1)*(2+1)*(1+1)。具体为什么结果是相乘而不是相加,见后面的串联wait执行法则。如果有n个触发串联(这些触发必须有相同的执行对象),每个触发内含有的wait数分别为x_1,x_2,...,x_n,且每个触发都是preserve,那么最终得到的1簇扫描共有(x_1 + 1)*(x_2 + 1)*...*(x_2 + 1)轮扫描。每一簇trigger cycle之间有一个31fr的延迟(known as an NEO, or Next Ending Occurrence)。这31fr的间隔给人的感觉像是触发运行累了,疲软了休息一下。其实是因为本轮刚好执行完所有含wait的触发,导致没有任何wait执行,从而使扫触发的间隔回归原本的31fr。
如果把每个wait都写成wait(0)或者wait(小于1fr的毫秒数),比如写4个触发,每个触发里面含有63个wait(0)和一个preserve,且所有触发里面只有这4个触发有wait语句,则会导致每轮扫描触发的间隔都为2fr,即0.125游戏秒,扫64^4=16777216轮触发后才会有一次31fr的延迟。这几个含有很多wait(0)的串联触发,称为加速触发,或者超级触发(hyper trigger),效果是可以将原本的每31fr扫一轮触发,加速到每2fr扫一轮。当然,如果把所有wait(0)都改成wait(3fr),比如改成wait(43)并用fastest游戏速度,那么效果就是每3fr扫一轮触发。如果你想要每1游戏秒扫一轮触发,那就要用wait(16fr),即改成wait(589)至wait(630)之间的数并用fastest游戏速度。
此外我们还能看到,触发位置越靠后,则wait后面的内容需要等待的时间越长,这个在下文的串联wait运行法则会有解释。
所以这里要强调,如果在加速触发的基础上再串联一个wait(xfr),其中x大于2,则会破坏加速触发的结构,得到你不想要的结果,详见[例3-2-5]。

另外,如果触发ABC中只有B是preserve而AC不是preserve:
[例3-2-3]
Trigger A:
        Display Text Message(Always Display, "1");
        Wait(0);   #fastest游戏速度下为Wait(2fr)
        Display Text Message(Always Display, "2");
        Wait(100); #fastest游戏速度下为Wait(4fr)
        Display Text Message(Always Display, "3");
-----------------------------------------------
Trigger B:
        Display Text Message(Always Display, "4");
        Wait(260); #fastest游戏速度下为Wait(8fr)
        Display Text Message(Always Display, "5");
        Wait(600); #fastest游戏速度下为Wait(16fr)
        Display Text Message(Always Display, "6");
        Preserve Trigger();
-----------------------------------------------
Trigger C:
        Display Text Message(Always Display, "7");
        Wait(1060);#fastest游戏速度下为Wait(27fr)
        Display Text Message(Always Display, "8");
-----------------------------------------------
则有如下情况:
开始2fr没有事情发生
(1)t = 2fr: 公屏出现1 4 7
间隔2fr
(2)t = 4fr: 公屏出现2
间隔4fr
(3)t = 8fr: 公屏出现3
间隔8fr
(4)         公屏出现5
间隔16fr
(5)         公屏出现6
间隔27fr
(6)         公屏出现4
间隔8fr
(7)         公屏出现5
间隔16fr
(8)         公屏出现6 8
间隔31fr
(9)         公屏出现4
间隔8fr
(10)        公屏出现5
间隔16fr
(11)        公屏出现6
间隔31fr
循环(9)-(11)

那么,如果有其中一个wait的时长超过32fr呢?请看下面的例子,执行对象依然都是player1。
为方便起见,下面4个触发用更简洁的伪代码来表示,wait后直接写上等价的fr数:
[例3-2-4]
Trigger A:
        Display text("1");
        Wait(16fr);
        Display text("2");
        Wait(2fr);
        Display text("3");
        Preserve Trigger();
Trigger B:
        Display text("4");
        Wait(3fr);
        Display text("5");
        Wait(120fr);  #120=31+31+31+27
        Display text("6");
        Preserve Trigger();
Trigger A:
        Display text("7");
        Wait(8);
        Display text("8");
Trigger D:
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
结果是:
(间隔2fr)
ore=1, 公屏出现 1 4 7
(间隔16)
ore=2, 公屏出现 2
(间隔2)
ore=3, 公屏出现 3
(间隔3)
ore=4, 公屏出现 1
(间隔16)
ore=5, 公屏出现 2
(间隔2)
ore=6, 公屏出现 3 5
(间隔31)
ore=7,公屏出现 1
(间隔31)
ore=8
(间隔31)
ore=9
(间隔27)
ore=10
(间隔16)
ore=11, 公屏出现 2
(间隔2)
ore=12, 公屏出现 3 6
(间隔8)
ore=13至ore=24重复ore=1至ore=12的情景
唯一的不同是:
ore=24, 公屏出现 3 6 8


回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-2-6 16:55:29 | 显示全部楼层
(继续第三章第二节)
通过以上实例,我们可以得到串联wait()语句的一些运行法则:
wait(m毫秒)首先要通过游戏速度转化为wait(x个游戏帧),简称wait(xfr)
【法则一】wait(xfr)执行的直观效果就是使得距离本轮扫触发xfr后的时间有一轮扫触发,即预约(注意妥协法则),“本轮扫触发”到“xfr后的那轮扫触发”之间都看做wait(xfr)正在执行。这是wait语句的法则,之前讲过,不在赘述。
【法则二】在串联的wait中,最多只能同时存在一个正在执行的wait。即之前在wait的法则中提到过的:预约列中不能同时存在两个或以上“由执行对象为同一玩家的触发中的wait语句预约”的预约时间。
详见例4的ore=7至ore=11之间发生的事情:本来扫触发时扫到了triggerA中的Wait(2fr),但是由于triggerB中的Wait(120fr)仍在执行中,故Wait(2fr)不敢插队,而是要等到Wait(120fr)执行完毕后才能执行,导致Wait(2fr)之后的数字2迟迟不能出来。
【法则三】串联的wait中,当本轮扫触发时没有“正在执行的wait”时,本轮会执行第一个被扫到的wait语句(比如wait(xfr))。同一轮中如果在扫到一个wait语句之后又扫到了另一个wait语句(比如wait(yfr)),则含有wait(yfr)的这个触发会被卡在wait(yfr)之前,等待执行,且wait(yfr)连排队的资格都没有,必须要等到某一轮扫触发它被首先扫到才能执行。
详见例子1的(1),本轮扫触发时先后扫到了triggerA的wait(2fr)、triggerB的wait(8fr)、triggerC的wait(27fr),本轮执行的为wait(2fr),即(1)和(2)之间的时间间隔变成2fr。而wait(8fr)和wait(27fr)根本无权排队。在第2轮扫触发时,由于先扫到的是triggerA的wait(4fr),所以第2轮执行的是wait(4fr),即(2)和(3)之间的时间间隔变成4fr。可以看到wait(8fr)是在第三轮才执行的,因为第三轮恰好triggerA执行完毕,所以第三轮扫到的第一个wait是triggerB的wait(8fr)。
【法则四】某个触发中的某个wait执行完毕后,位于wait后面的语句要想执行,必须保证该触发之前的所有(满足执行条件的)触发都已经执行完毕,否则该触发就会卡在wait后面的第一个动作之前。
详见例2中的(4),明明在第3轮中已经执行了triggerB的wait(8fr),在本轮(第4轮)扫触发中扫到了triggerB中的wait(8fr)之后的display("5"),为什么不执行呢?就是因为此时triggerA刚刚打出一个1,本轮正在执行triggerA的wait(2fr),所以triggerA还没执行完毕,所以triggerB中的wait(8fr)之后的display("5")不能执行,而要等到第6轮triggerA执行完display("3")(triggerA执行完毕)之后才能执行,这也就是为什么是(6)中3、5同时出现而不是(4)中1、5同时出现。

根据串联触发的法则以及以上若干实例,我们可以得出串联触发的一个重要性质:触发的先后顺序很重要。我们可以看到上面几个例子中,出现最多的数字都是123,而456次之,最少的是78,这是由这几个触发的先后顺序所决定的,如果把上面例子中的triggerA和B调换位置,则出现最多的数字就要变成456了。
我们看下面一组例子:
[例3-2-5]
Trigger A:
        Display text("1");
        Wait(0); 即wait(2fr)
        Display text("2");
        Preserve Trigger();
Trigger H1:
        Wait(0);
        Wait(0);
        Wait(0);
        Wait(0);
        Preserve Trigger();
Trigger H2:
        Wait(0);
        Wait(0);
        Wait(0);
        Wait(0);
        Preserve Trigger();
Trigger H3:
        Wait(0);
        Wait(0);
        Wait(0);
        Wait(0);
        Preserve Trigger();
其中triggerH1至H3是加速触发的雏形,根据例1-例4,我们很容易得出,执行的结果为数字1和2交替出现,间隔均为2fr,并且持续125轮,然后间隔31fr之后再重复。
那么,如果把triggerA中的wait(2fr)改成wait(16fr)呢?可想而知,结果是:数字1出现之后间隔16fr出现数字2,数字2出现之后间隔2fr出现数字1,即16fr,2fr,16fr,2fr这样的循环间隔,并且可以发现,triggerA中的wait(16fr)把后面的整个“加速触发系统”全部给废掉了,因为我们希望的加速触发是每轮扫触发的时间间隔都是2fr,但串联一个wait(16fr)后得到结果确是16fr与2fr交替。
再想,如果我们把triggerA挪动到这一串“加速触发”的后面:
Trigger H1:
        Wait(0);
        Wait(0);
        Wait(0);
        Wait(0);
        Preserve Trigger();
Trigger H2:
        Wait(0);
        Wait(0);
        Wait(0);
        Wait(0);
        Preserve Trigger();
Trigger H3:
        Wait(0);
        Wait(0);
        Wait(0);
        Wait(0);
        Preserve Trigger();
Trigger A:
        Display text("1");
        Wait(0); 即wait(2fr)
        Display text("2");
        Preserve Trigger();
执行的结果便是:首先出数字"1",然后等待上面那一坨加速触发全部执行完之后,即(5*5*5*2-1)*2fr=498fr=31.125游戏秒之后,才能出现数字2
假设上面那一坨“加速触发”有更多的wait语句和更多的触发数量,那么可以说,TriggerA中的Display("2")几乎永远不会被执行。
所以,如果我们使用了加速触发,就一定不要再用wait跟加速触发串联了。其实,用wait语句跟一簇加速触发并联也会产生很多复杂的后果,这会在下一节讲到。
我个人认为,scmd中串联wait语句的这些法则有点不合理、反直觉,因此我们用scmd写触发时要尽量避免wait语句的串联,甚至尽量避免使用wait语句。如果想要精确控制时间,就用死亡计数器配合加速触发就好了,除了加速触发中含有wait语句之外,其他任何触发中都不要有wait。



回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-2-6 16:57:03 | 显示全部楼层
【第三节】wait的并联
如果两个含有wait语句的触发的执行对象为两个不同的玩家,则我称这两个wait为并联关系,或者称这种现象为wait的并联
并联的wait语句的执行法则与串联wait不同,两个并联的wait语句可以同时执行,互不干涉,但是妥协法则依然适用。
也就是说,并联wait与串联wait的共同法则只有【法则一】,这也是wait动作的基本法则。而串联wait的法则二三四都对并联wait不适用。
以下例子中,两个触发的条件均为Always
[例3-3-1]
Trigger("Player 1") (TriggerA)
        Wait(3fr);
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
Trigger("Player 2") (TriggerB)
        Wait(6fr);
        Set Resources("Player 1", Add, 1, gas);
        Preserve Trigger();
效果是:
(1)t=2fr:
第一轮扫触发便同时扫到两个wait语句,并且本轮同时执行这两个wait,即保证3fr后(t=5fr)有一轮扫触发,且6fr以后(t=8fr)也有一轮扫触发。我将这成为预约,即预约在t=5fr和t=8fr这两个时间点扫触发。另外,计时器也相当于额外的并联,因为即使器倒计时时间到0的时候,必然会有一轮扫触发。
结果: ore=0, gas=0, 预约t=5fr, t=8fr
        (间隔3fr)
(2)t=5fr
TriggerA运行完毕,本轮扫描触发没有扫到wait语句,
TriggerB的wait(6fr)还未运行完毕,因此等待。
结果: ore=1, gas=0, 预约t=8fr
        (间隔3fr)
(3)t=8fr
TriggerA的wait(3fr)被扫到并执行,预约t=11fr
TriggerB的wait(6fr)执行完毕,然后执行加1气,TriggerB执行完毕。
结果: ore=1, gas=1, 预约t=11fr
        (间隔3fr)
(4)t=11fr
TriggerA的wait(3fr)执行完毕,执行加1钱,完毕。
TriggerB的wait(6fr)被扫到并执行,预约t=17fr
结果: ore=2, gas=1, 预约t=17fr
        (间隔6fr)
(5)t=17fr
TriggerA的wait(3fr)被扫到并执行,预约t=20fr
TriggerB的wait(6fr)执行完毕,并执行加1气
结果: ore=2, gas=2, 预约t=11fr
        (间隔3fr)
(6)t=20fr
TriggerA的wait(3fr)执行完毕,并执行加1钱
TriggerB的wait(6fr)被扫到并执行,预约t=26
结果: ore=3, gas=2, 预约t=26fr
        (间隔6fr)
看到这里就很明朗了,接下来就是如同(5)、(6)的循环了,间隔时间则是3fr,6fr交替
       
通过这个例子我们可以看出,两个wait语句的并联产生的效果很可能出乎我们的意料。我们直觉上以为,其中一个wait的时间是另一个wait时间的两倍,则在同一时间内执行的速度就应该是两倍之差,然而事实上它们两个各自的执行速度(单位时间内的执行次数)竟然几乎是同步的!

之前介绍wait的执行法则时,提到了一个重要概念:妥协法则。即在本轮扫触发完毕后,系统要查看预约列,把所有仅相差1fr的预约时间都合并,妥协到较晚的那个。
看下面的例子,8个触发的条件均为Always:
[例3-3-2]
Trigger("Player 1")
        Wait(32fr);
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
Trigger("Player 2")
        Wait(33fr);
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
Trigger("Player 3")
        Wait(34fr);
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
Trigger("Player 4")
        Wait(35fr);
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
Trigger("Player 5")
        Wait(36fr);
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
Trigger("Player 6")
        Wait(37fr);
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
Trigger("Player 7")
        Wait(38fr);
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
Trigger("Player 8")
        Wait(39fr);
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
这8条触发的执行对象分别为player1到8,wait的时长分别为32fr到39fr,实质性的动作都是给player1加1水晶矿。
由于这8个wait是并联关系,所以游戏开始后第一轮扫触发时8个wait会同时执行:
(1)t=2fr:
8个并联wait同时执行,同时预约8个时间,预约列为:32fr后,33fr后,34fr后,35fr后,36fr后,37fr后,38fr后,39fr后
即34fr,35fr,36fr,37fr,38fr,39fr,40fr,41fr
此时系统检查预约列,发现最小的预约时间为32fr后,故加一个预约:31fr后
最后,系统发现预约列中9个预约时间全都是相邻差1,所以全部合并为一个预约时间 - 39fr后,即41fr
现在预约列中只存在一个时间:41fr
(2)t=41fr:
8个wait同时执行完毕,并执行8个+1ore,使得player1的钱数变为8
本轮没有任何wait语句执行,故系统在预约列中添加:31fr后,即72fr
(3)t=72fr: 跟(1)相同
(4)t=111: 钱数变为16
整体来看,就是扫触发间隔为39fr,31fr交替。

下面这个例子,被我称为纯并联,即每个player内串联wait的情况都完全一样:
[例3-3-3]
Trigger("All players"){
Conditions:
        Always();
Actions:
        Wait(0);
        Wait(0);
        Wait(0);
        Wait(0);
        Wait(0);
        Wait(0);
        Wait(0);
        Preserve Trigger();
}
假设游戏开始后,8个player全部都在,那么,这就相当于8个完全相同的wait触发进行并联。其对扫触发间隔的影响效果等同于不并联。

下面这个例子稍微复杂一些,是wait语句的混联(既串联、也并联),是在例1的基础上串联上一个加速触发。条件都是Always,其中triggerH1到H6为一组加速触发,wait数量足够多。
[例3-3-4]
Trigger("Player 1") {(TriggerA)
        Wait(3fr);
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
Trigger("Player 2") (TriggerB)
        Wait(6fr);
        Set Resources("Player 1", Add, 1, gas);
        Preserve Trigger();
Trigger("All players") {(TriggerH1)
        Wait(0);
        Wait(0);
        .....
        Wait(0);
        Preserve Trigger();
}
...
Trigger("All players") {(TriggerH6)
        Wait(0);
        Wait(0);
        .....
        Wait(0);
        Preserve Trigger();
}
...
这个例子就比例1复杂多了。
分析混联的例子,首先要把并联的关系拆开来,先分析每一路的规律,即先分析串联。串联分析清楚之后,再并联起来看看相互之间的影响。
先来看看player1的串联部分,假设我们的触发仅有:
Trigger("Player 1") {(TriggerA)
        Wait(3fr);
        Set Resources("Player 1", Add, 1, ore);
        Preserve Trigger();
Trigger("Player 1") {(TriggerH1)
        Wait(0);
        Wait(0);
        .....
        Wait(0);
        Preserve Trigger();
}
即一个含wait的触发后面串联一组加速触发。如果对于串联wait掌握熟练,则可以轻松预测执行结果,即:
时间间隔为3fr,2fr交替,交替很多很多轮(因为加速触发有很多wait)之后,间隔31fr,再次重复。这个例子中我们假设加速触发写的足够多(比如能坚持1亿轮),因此可以看做永远为3fr,2fr交替。每次出现3fr的间隔之后的那一轮扫触发,钱数加1。
同理,我们假设只有player2的部分,间隔时间则是6fr,2fr的交替,每次出现6fr的间隔之后的那一轮扫触发,气矿加1。
下面我们来走一遍:
(1)t=2fr:
TriggerA和B同时执行wait,分别预约3fr后、6fr后,预约列:5fr,8fr
(2)t=5fr:
TriggerA的wait执行完毕,执行下一语句:“钱加1”。串联的加速触发的wait语句生效,预约2fr后,预约列:7fr,8fr
TriggerA的wait正在执行中,因此待机。
预约列中存在相差1的预约时间。应用妥协定理,预约列变为:8fr
(3)t=8fr:
TriggerA执行wait预约3fr后,预约列为:11fr
TriggerB的wait执行完毕,“气体+1”,执行串联的加速触发的wait语句,预约2fr后,预约列为:10fr,11fr
预约列中存在相差1的预约时间。应用妥协定理,预约列变为:11fr
(4)t=11fr
TriggerA执“行钱+1”。执行加速触发的wait,预约2fr后,预约列为:13fr
TriggerB执行wait,预约6fr后,预约列为:13fr,17fr
(5)t=13fr
TriggerA执行wait,预约3fr后,预约列为:16fr,17fr
TriggerB的wait正在执行中,因此待机。
妥协定理使得预约列变为:17fr
(6)t=17fr
TriggerA和B的wait同时执行完毕,钱和气都+1,并同时执行加速触发,预约2fr后,预约列为:19fr
此时玩家1的钱是3,气是2
(7)t=19fr
TriggerA和B同时执行wait,分别预约3fr后、6fr后。。。。。。跟(1)一样

最终的效果是,每6轮触发为一个循环,周期为17fr,每个周期内钱+3,气+2,下图为这个触发的执行过程的汇总。其中粉色框代表一个执行周期。红点为每轮扫触发中出现的预约情况,第一排为player2那一路的情况,第二排为player1那一路的情况。箭头为妥协定理的应用,空心圈为妥协前预约。

例3-3-4.png
在这个例子中,我们看到,wait的串联和并联混用会导致极其复杂的情况出现,结果反直觉、难以预测,必须一步一步推理。因此,非常不建议使用过多的wait语句。

思考题:第二节讲串联的时候的[例3-2-5]是一个wait和一组加速触发的串联。那么如果1个wait和一组加速触发并联,会发生什么效果呢?(提示:与wait中的fr数的奇偶有关)


回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-2-6 16:58:20 | 显示全部楼层
本帖最后由 PereC 于 2020-2-8 09:43 编辑

【第四节】加速触发
在学习了wait的串联和并联的执行原理之后,我们就很容易理解加速触发了。我们运用wait语句的串联和并联,可以把每组扫触发的时间间隔强制设定为2fr。凡是能让扫触发间隔保持为2fr的触发组,我们都称它为加速触发。本节内容便是建立在前两节的基础知识之上的实际应用部分。
[例3-4-1]
Trigger("Player 1")
        Wait(0); (63个wait)
        Wait(0);
        .....
        Wait(0);
        Preserve Trigger();

Trigger("Player 1")
        Wait(0); (63个wait)
        Wait(0);
        .....
        Wait(0);
        Preserve Trigger();

Trigger("Player 1")
        Wait(0); (63个wait)
        Wait(0);
        .....
        Wait(0);
        Preserve Trigger();

Trigger("Player 1")
        Wait(0); (63个wait)
        Wait(0);
        .....
        Wait(0);
        Preserve Trigger();
        
此例中每个触发的条件都是Always。
我将这类加速触发称为串联式加速触发。这是最普通的流传于民间的加速触发,这个例子中我们用了4条触发,每条触发有63个wait(0)和1个preserve。这个加速触发,每一簇共有64^4=16777216轮触发。这个已经在串联触发的[例3-2-2]和[例3-2-3]之间提到过,在此不做赘述。这个加速触发的优点是仅用1个player就可以完成。缺点是比较冗长,看上去不美观。并且在足够长的时间后,有一次31fr的间隔。当然,可以通过多串联几个触发(4个就足够了)来使这个“足够长”的时间真的足够长。当地图里面存在一个固定玩家(比如,无论是单人游戏还是多人游戏,这个地图的player8的位置一定存在玩家,那么Player8就叫做固定玩家),那么这个加速触发就可以写在这个固定玩家上。如果地图里面不存在固定玩家(比如该地图里面没有computer玩家,而且human玩家的出生点全都是随机的),那么就要以All players作为执行对象来实现加速触发。注意,即使把执行对象改成All players,这个加速触发仍然属于串联式加速触发,因为每个player分开来之后,这些串联的wait能够独立实现加速触发的效果,而并联起来之后,属于不同players的对应位置的wait完全是同时执行的,有同步性,因此属于之前提到过的纯并联,“并联”这件事并不对加速触发这个效果起到决定性的作用。

注:[例3-4-2]及之后的例子都是我原创的加速触发写法,比我给的参考资料中的complex hyper trigger给的方法还要简洁。
[例3-4-2]
例3-4-2.png
Trigger("Player 1"){
Conditions:
        Switch("Switch1", Set);

Actions:
        Wait(0);
        Preserve Trigger();
}
//-----------------------------------------------------------------//
Trigger("Player 2"){
Conditions:
        Always();

Actions:
        Set Switch("Switch1", set);
        Wait(0);
        Preserve Trigger();
}
我将这类加速触发称为并联式加速触发,因为它运用了并联的原理实现了加速触发。这个例子可以看做是受了第三节[例3-3-1]的启发。我们在学习串联触发时,发现无论怎么串联,之后总会有轮扫触发的时候没有任何wait执行,导致等待31fr。而并联触发可以使得触发之间相互作用,每轮扫触发都会有wait执行。这个例子就是让两个含有wait的触发岔开一轮来执行,这样,具有并联关系的两个wait交错执行,使得每一轮扫触发都会有一个wait(0)被执行,也就能保证每轮触发的时间间隔都是2fr,实现加速触发。这个触发的关键在于,一定要让第二个触发的执行对象位于第一个触发的执行对象之后,因为一定要保证第一轮扫触发时player1的wait不执行而player2的执行,才能使得两个wait刚好交错执行。这种加速触发的优点是比较简洁,而且可以保持无限长的时间都是每轮扫触发间隔2fr。

其实,这一组触发还有另外一个更简洁的写法,看下面的例子。
[例3-4-3]
例3-4-3.png
Trigger("Player 7", "Player 8"){
Conditions:
        Switch("Switch1", Set);

Actions:
        Wait(0);
        Preserve Trigger();
}
//-----------------------------------------------------------------//
Trigger("Player 7", "Player 8"){
Conditions:
        Always();

Actions:
        Set Switch("Switch1", set);
}

这个加速触发跟上面那个的运行原理完全相同,只不过仅仅需要一个wait就可以实现。需要注意的是,两个触发的先后位置很关键,因为一定要保证第一轮扫触发时,player7的wait不执行而player8的执行,才能使得两个wait刚好交错执行。
当地图里面存在两个或者以上的个固定玩家时,比如player7和8为固定玩家,那么就可以套用这个例子。如果地图里面不存在固定玩家(比如8人游戏的随机分组图),那么就要以All players作为执行对象来实现加速触发,原理也是一样,保证在第一轮扫触发时第一个存在的玩家的wait不执行、后几个存在的玩家的wait纯并联执行。我个人也很推荐这种写法。


其实在理论上来讲,在已经写了加速触发的情况下(比如Player7和8被用来写加速触发),可以再在其他的player(player1至6)每人再并联一个偶数fr数的wait,这对加速触发的执行是毫无影响的,详见之前的思考题。但是,无论用哪种加速触发,我都不建议再在其他任何地方写任何其它的wait语句。毕竟,在地图编辑器里面的wait后面只能写现实时间的毫秒数,这个毫秒数在不同游戏速度下会转化出不同的fr数,即使在fastest速度下转化出了偶数fr数,那么在其他速度下也不能保证是偶数。如果想要实现wait的效果,请用死亡数计数器来代替。加速触发使得地图编辑者对地图时间的掌控更加精确,也使得“死亡数计数器”变为“死亡数计时器”,或者死亡计时器加速触发配合死亡数计时器是一个合格的rpg作者的基本功,我鼓励广大作者熟练应用这个技能。如果你嫌死亡数计时器太麻烦,那么我推荐你使用eud编辑器(EudEditor2)里面的wait,这个wait语句后面跟的frame数不是本文的fr(游戏帧),而其实是扫触发的轮数,在此不做介绍,以后有机会再出文章介绍。



回复 支持 反对

使用道具 举报

发表于 前天 06:06 | 显示全部楼层
大神膜拜
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|站点地图|永恒的星际联盟 ( 苏ICP备17020173号-1 )

GMT+8, 2020-2-19 19:52 , Processed in 0.056374 second(s), 6 queries , Gzip On, File On.

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

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