之前很多新手(不过好像其中有不少是隔壁蛋黄的人格分裂小号)和我说搞不懂局部变量有什么用,让我仔细讲讲。最近又恰好看到OPERATE一篇很老的局部变量教程被翻出来,但是那篇教程写的比较浅显,并不完善。于是我决定写一篇局部变量教程,对这个玩意做一个由浅入深的总结。
——Heli(竹蜻蜓), 2021.12.4
本教程前置知识:至少了解了触发的基本概念。
局部变量的编辑入口位于FinalAlert的“编辑”菜单内,在“AI触发编辑”、“AI触发允许”之下,“单人任务设置”之上。
在下拉框中选中“No name”那一项并输入你想要的名字,就相当于新建了一个局部变量了。
局部变量有且仅有3个属性:号码、值、名称。
局部变量最多有100个,号码从0到99。
强行读取100及以上号码的局部变量的值,得到的结果始终为1不变——这都不变了,也就失去了它作为“变量”的意义。
新手面对一样未知的东西,最大的疑惑经常是:这东西有什么用?
所以对局部变量也要发问一句:局部变量有什么用?
我现在给出的答案是:局部变量是触发的附属组件,用于表示状态,一般用于做逻辑联系的桥梁——在新手阶段,大部分时候,局部变量没有用,因为简单的逻辑用不上。
每一个局部变量就是一个用于存储一个0/1值的位置,可以表示一种“是/否”、“开/关”的状态,用来配合触发——至于这种状态有何意义,还要看与它配合的触发有何意义。
一个好的局部变量使用者,应该给每一个局部变量起一个易懂的好名字,尽量完整的表达其作用,因为它可能出现在逻辑链的任何地方!
如果一个触发只有局部变量相关的条件,初始是禁止状态,在被允许之前条件已经满足,那么在被允许后,这个触发会立刻执行。
因为触发逻辑是完备的——也就是说,在制作触发逻辑时,使用局部变量能做到的逻辑,不使用局部变量(仅使用触发之间的互相允许、禁止)一样能做到。
没有它也能做到,那岂不是“没有用”?
比如说我们制作一个胜利条件:两个建筑都被摧毁时任务完成。
(这里为举例方便,先假设我们不知道标记类型1,也不知道使用科技类型判断是否被摧毁——只能使用2个触发关联2个建筑,分别判定每个建筑被摧毁,然后使用后续的逻辑判断“两个建筑都被摧毁”这个“且”的关系。)
使用局部变量的方法:
设置2个局部变量:第X个建筑被摧毁,初始值“0”。
设置2个触发:第X个建筑被摧毁;
条件[29 被任何事物摧毁];
结果[56 局部设置],分别以上一步设置2个局部变量为参数。
把2个触发的标签分别关联到两个建筑上。
再设置1个触发:两个建筑都被摧毁,初始状态为“禁止”;
条件有2个,都是[36 局部设置开启],参数分别填第一步设置的2个局部变量;
结果[67 宣告胜利]。
这样就做完了。
实际上这里是使用局部变量,储存了2个建筑是否被摧毁的状态:如果某个建筑没有被摧毁,那么这个建筑对应的局部变量值是“0”;一旦它被摧毁了,它对应的局部变量值就被触发设置成了“1”。2个建筑对应的局部变量都变成了“1”,就说明:2个建筑都被摧毁啦,可以判断胜利啦。
那么不用局部变量怎么做呢?
触发:第1个建筑被摧毁;
条件:[29 被任何事物摧毁];
结果:[53 允许目标触发]第1个建筑被摧毁-状态存储,[22 强制触发事件]第2个建筑被摧毁-状态存储;
触发:第2个建筑被摧毁;
条件:[29 被任何事物摧毁];
结果:[53 允许目标触发]第2个建筑被摧毁-状态存储,[22 强制触发事件]第1个建筑被摧毁-状态存储;
触发:第1个建筑被摧毁-状态存储;
初始状态:禁止;
条件:[60 科技类型存在]不填参数(用作一个永远不能被正常触发的空事件,但这个空事件可以被“[22 强制触发事件]”刷新);
结果:[53 允许目标触发]两个建筑都被摧毁;
触发:第2个建筑被摧毁-状态存储;
初始状态:禁止;
条件:[60 科技类型存在]不填参数(同上一条);
结果:[53 允许目标触发]两个建筑都被摧毁;
触发:两个建筑都被摧毁;
初始状态:禁止;
条件:[8 任何事件];
结果:[67 宣告胜利];
可能还有别的实现方式,此写法仅供参考……emmmm还是看看就好吧,别学。
上一章节我们说了局部变量“没有用”,但这一章节我们怎么又说它“很有用”了呢?且容我细细说来。
你可能会注意到,在上面那个“判断2个建筑都被摧毁”的例子中,不使用局部变量的触发组含义要模糊许多。如果把需要判断的条件改为“判断4个目标都被完成”,不使用局部变量的话,触发组的复杂程度又会成倍上升,复杂到几乎难以书写了!
局部变量用于表示状态具有天然的优势——它就是为了表达状态而生的!虽然没有它也可以完成完善的逻辑,但是合理使用会使触发组大大的简化,逻辑更加清晰,这便是局部变量的“有用”所在了。
当你需要一个表示“开/关”、“有/无”、“是/否”、“完成/未完成”等二元状态的工具时,尤其是你可能遇到以下的状况:
这时候,请毫不犹豫的使用局部变量来简化你的逻辑!
我们举一个实战中的例子:
玩家执行一个“劫狱”任务,前半段指挥小部队渗透,后半段带领被救部队逃跑。可能有以下几种情况判定玩家“被发现”了:
而“被发现”又会导致如下的后果:
“被发现”和其它状态又会产生如下的组合:
玩家“被发现”、玩家选择从监狱西门冲出、玩家未破坏机场:敌人会对西门附近进行随机轰炸以封锁路口。
玩家“被发现”、玩家选择从监狱北门冲出、玩家未破坏重工:敌人的坦克部队会前往北门进行封锁。
玩家“被发现”、玩家选择从监狱东门冲出、玩家未破坏兵营:敌人会出动步兵含狙击手小队进行追击。
玩家“被发现”、玩家选择从监狱西门冲出、玩家联络了友军:友军会在监狱西门提供支援。
玩家“被发现”、玩家选择从监狱北门冲出、玩家联络了友军:友军会在监狱北门提供支援。
玩家“被发现”、玩家选择从监狱北门冲出、玩家联络了友军:友军会在监狱东门提供支援。
……
这个时候,如果使用触发之间的相互允许禁止,来建立这一系列事件的逻辑,就会显得非常繁琐且难以维护:比如“玩家被发现”这一个触发的结果里面,可能需要[53 允许目标触发]一大长串,一旦和其它状态交织起来,就难以判断哪里有遗漏了。
我们把“玩家被发现”、“玩家选择从监狱X门冲出”、“玩家破坏XX”、“玩家联络了友军”都设置为局部变量如何?这样当事件状态改变的时候,就不用去因此允许/禁止大量的触发,而是直接更改相应的局部变量,各事件也都以局部变量的状态变化作为条件。这样逻辑就清晰多了。
比如上面的例子里面,组合状态的第一个事件可以用这样的触发来表示:
触发:敌人追击小队-西门(空军);
条件:[36 局部设置开启]玩家被发现,[36 局部设置开启]玩家选择从监狱西门冲出,[37 局部设置关闭]玩家破坏机场;
结果:(……);
总之,局部变量是复杂逻辑中用于表示“状态”的最佳选择。当一个“状态”对很多逻辑有影响的时候,用局部变量来表示它,绝对会让你少掉头发。
局部变量还有一个其它模块都无法做到的功能:它可以帮助从动作脚本向触发传递信息,也就是,动作脚本引发触发。
这使得我们可以用局部变量来确认一些小队完成了动作脚本中的一项任务,准备执行下一项任务了,诸如运输船的卸载、摧毁了某个路径点上的建筑,等等。
借助脚本的其他特性(比如脚本的跳步执行)还可以做出更加复杂的逻辑。本教程的重点不是脚本,就不展开讲了。
本章节介绍一些局部变量使用时需要注意的小细节。从这里开始进入深水区。
局部变量有一个非常细节的特性:当一个触发的条件同时包含“延时”([13 流逝时间]、[51随机延时])和“局部变量状态判断”([36 局部设置开启]、[37 局部设置清除])时,局部变量会对“延时”的行为产生干扰,具体表现为:相关的局部变量状态改变时,延时的计时会重置(从头重新开始计时)。
不妨做一个这样的试验:
使用2个触发让局部变量A的状态在0和1之间反复变化,周期2秒。
添加一个触发:
条件:[13 流逝时间]10,[36 局部设置开启]局部变量A;
结果:[11 文本触发事件]显示一段文本;
结果这段文本永远也不会被显示。问题出在哪里?
当然是因为局部变量A的反复重置,导致了另一个条件“[13 流逝时间]10”的10秒计时器也不停的被重置,永远也数不到10而引起触发。这种表现就像把这个触发反复禁止再允许引起的倒计时重置一样。由此我们也许可以窥见引擎内部对局部变量处理的逻辑……咳咳,扯远了,只要知道结论就好了。
这个特性有时可以被利用,但有时又是有害的,使用起来应该格外小心。
前面就提过,局部变量在一张地图中最多100个。
别以为这是个很充裕的数字,在制作复杂的逻辑时,是有可能把这100个都用完的。为了继续发挥其作用,有时我们会被迫选择对局部变量进行复用。
复用的局部变量容易导致状态冲突,必须是两种不可能同时出现的状态才可以复用同一个局部变量,比如一个任务的多个阶段中两个不相关的逻辑。
复用局部变量时,应当起好名字、划分好阶段。在复杂的逻辑面前,还是要保护好头发和脑细胞的……
复用是一种迫不得已的手段,应当先想办法尽可能地省变量,实在搞不定再去复用。
全局变量是什么?它是“全局”的变量——各方面都与局部变量性质相似,但是它是在rules之中注册的(这意味着你无法在一张地图里面注册新的全局变量,而是所有任务地图共用一套全局变量),它的值也可以跨关继承:进入下一关时,全局变量的值可以保留,因此你可以用它在不同的关卡之间传递信息。
除了注册位置、最大数量(50)和跨关继承特性以外,全局变量的其余性质与局部变量完全一致,本教程就不做过多讨论了。
本章节会举出我使用过的一些有趣的高级触发组,来介绍一些局部变量的使用场景。
左上角弹出的字幕是向玩家提示的重要手段,但一瞬间弹出太多的字幕可能会让玩家应接不暇;而如果玩家同时触发了两段对话,两段对话相互穿插可能会出现前言不搭后语的窘迫状况……如何让这些弹出的字幕相互让开,自动一条一条、一段一段的弹出呢?我构建了“字幕队列”触发组来解决这一问题。
这个触发组的本质是:使用一个局部变量“字幕开启”的值为“1”来表示当前左上角有字幕正在占用玩家的阅读时间——比如,字幕弹出后12秒之内,不允许另一段无关字幕的弹出;而在这个阅读时间过后,才使用触发将这个局部变量的值设为“0”,允许下一条字幕的显示。
触发结构如下:
一个局部变量:字幕开启。初始值为0。
一个触发:字幕计时重置
触发类型“2”(重复),初始为禁止状态;
条件:[13 流逝时间]12秒(占用玩家的阅读时间);
结果:[57 局部清除]字幕开启,[54 禁止目标触发]自身;
对于每一条字幕触发,使用如下结构:
条件:[37 局部设置关闭]字幕开启;
结果:[56 局部设置]字幕开启,[53 允许目标触发]字幕计时重置,[11 文本触发事件](你需要的字幕),还有其它结果……
注意条件里面不能带有延时。如果这条字幕是延时触发的,则需要用另一个专门负责延时的触发来允许它,否则会因为局部变量导致的计时重置引发时序问题。
如果是一连串的字幕(比如一段对话),使用如下结构:
第1条触发的条件:[37 局部设置关闭]字幕开启
所有触发的结果带上:[56 局部设置]字幕开启,[53 允许目标触发]字幕计时重置,[11 文本触发事件](你需要的字幕),还有其它结果……
其余按照正常的延时事件链制作即可(但是事件链的延时时间要小于字幕计时重置触发的延时时间,否则会导致字幕开启的状态中途结束)。这样保证整段字幕连续放出,且不会被其余的字幕打断。
这个触发组结构相当简单,对于一些需要密集文本提示的场景的优化也十分有效。尤其是可以有效的防止一些循环出现的提示粗暴的插入到重要的对话中。
不过对于这个触发组而言,“局部变量重置计时”的特性是有害的,需要时刻提防——由于“字幕开启”这个局部变量的状态可能一直在改变,较长的延时根本来不及触发就被重置,甚至会出现某些字幕永远也显示不出来的状况。这就是在这个触发组中需要把延时和局部变量分开成两个触发的原因。
触发结果[53 允许目标触发]也会重置延时时间。这就是连串字幕中每一个触发的结果都要带上“[53 允许目标触发]字幕计时重置”的原因,为的就是不断重置它,等这一连串字幕结束后才让它正常运行。
这应该说是一个非常漂亮且简洁实用的触发组……写出它的时候,我自己都被它的简洁程度惊到了。
这个触发组起到了这样的作用:每次随机延时X秒以后,随机发生N个事件的其中一种,循环执行。比如:敌人会随机在N个位置中选1个随机空降伞兵。
猜猜需要几个触发?
只需要N个触发,和随机事件的数量相等。加之其触发结构也极其简单易拓展,因此我称其为“最简随机循环”。
触发结构如下:
一个局部变量:随机事件组A。其初始值决定了这个触发组的开关状态,“0”为关,“1”为开。
每个随机事件对应1个触发。
每个的触发的参数是这样的:
类型“2”(重复);
条件:[51 随机延时]X秒,[36 局部设置开启]随机事件组A;
结果:[57 局部清除]随机事件组A,[56 局部设置]随机事件组A(“局部设置”必须在“局部清除”后面),然后加上这个事件你想要的其它结果。
想要打开这个循环,就把局部变量“随机事件组A”设为“1”;想要关掉,就设为“0”。
这个触发组主要利用了“局部变量状态更新会重置计时器”的特性,各触发之间也没有复杂的关联,而是通过一个局部变量,把若干触发编为一组,一个触发执行就会重置其余几个触发的计时器,开启下一轮循环。
虽然在随机延时较短的时候,各事件的概率会出现偏差(我打算专门出一篇教程来做相关的数学计算),但只要随机的时间足够长,各事件出现的概率基本一致,也是基本满足了实用要求。
这个触发组在「林德拉克计划2:命运之瞳」的 尤里06-铁幕降临 中使用。情况是这样的:敌人拥有4座铁幕装置,都按照CD时间进行计时;在特定条件下,会动用已经就绪了的铁幕向地图上的一些防御建筑物释放,以给玩家推进带来阻碍;如果没有已经就绪了的铁幕,则不会释放。
我们可以把它抽象成这样的逻辑:有若干个资源(铁幕)可能为“可用”或“不可用”状态,其它触发可能需要申请使用这些资源。触发组要做到从中寻找可用的资源并释放。我把这个触发组叫做“资源申请”。
触发组分为4部分:状态加载、状态更新、资源释放、申请
用于判定各资源何时可用的触发组。比如说对于铁幕,我们可以简单的认为它经过CD以后就进入了可用状态:
局部变量:铁幕X就绪,初始状态“0”;
触发:铁幕X就绪;
触发类型:2(重复);
事件:[13 流逝时间]300;
结果:[56 局部设置]铁幕X就绪,[54 禁止目标触发]自身,[53 设置局部变量]状态更新;
触发:铁幕X被摧毁;
事件:[29 被任何事物摧毁];
结果:[57 局部清除]铁幕X就绪,[12 摧毁触发事件]铁幕X就绪;
主动进行状态更新,通过所有铁幕的状态来判断当前是否至少有一座铁幕可用——这决定着是申请成功还是申请失败。
局部变量:有铁幕可用,初始状态“0”;
局部变量:状态更新,初始状态“0”;
触发:状态更新X;
触发类型:2(重复);
事件:[36 局部设置开启]状态更新,[36 局部设置开启]铁幕X就绪;
结果:[57 局部清除]状态更新,[56 局部设置]有铁幕可用;
触发:状态更新-全否;
触发类型:2(重复);
事件:[36 局部设置开启]状态更新,[36 局部设置关闭]铁幕1就绪,[36 局部设置关闭]铁幕2就绪,……,[36 局部设置关闭]铁幕X就绪;
结果:[57 局部清除]状态更新,[57 局部清除]有铁幕可用;
这个触发组决定了有铁幕可用的时候,释放哪一个。按顺序申请铁幕,一个不行就下一个。
触发:申请铁幕
触发类型:2(重复);初始状态为“禁止”;
事件:[8 任何事件];
结果:[53 允许目标触发]申请铁幕1成功,[53 允许目标触发]申请铁幕1失败,[54 禁止目标触发]自身;
触发:申请铁幕X成功
触发类型:2(重复);初始状态为“禁止”;
事件:[36 局部设置开启]铁幕X就绪;
结果:[54 禁止目标触发]申请铁幕X成功,[54 禁止目标触发]申请铁幕X失败,[57 局部清除]铁幕X就绪,[53 允许目标触发]铁幕X就绪,[53 设置局部变量]状态更新,[134 超级武器复位]对应的铁幕超武,[21 播放语音]EVA_IronCurtainActivated
触发:申请铁幕X失败
触发类型:2(重复);初始状态为“禁止”;
事件:[37 局部设置关闭]铁幕X就绪;
结果:[53 允许目标触发]申请铁幕X+1成功,[53 允许目标触发]申请铁幕X+1失败(最后一个就无需X+1了),[54 禁止目标触发]申请铁幕X成功,[54 禁止目标触发]申请铁幕X失败;
每一个铁幕的申请位置由两个触发组成:
触发:铁幕位置N申请成功;
关联触发:铁幕位置N申请失败;(如果涉及到了关联单位的属性的话)
条件:[36 局部设置开启]有铁幕可用,和其它你想要的铁幕申请条件;
结果:[109 铁幕装置作用于]你想要的路径点,[53 允许目标触发]申请铁幕;
触发:铁幕位置N申请失败;
条件:[37 局部设置关闭]有铁幕可用,和其它你想要的铁幕申请条件(和上面的相同);
结果:[54 禁止目标触发]铁幕位置N申请成功;(如果涉及到了关联单位的属性的话,结果甚至可以留空)
其实就是使用局部变量判定此刻有无可用的铁幕,如果有就申请成功,没有就申请失败。
局部变量在map中的存储结构如下:
1[VariableNames]
2[号码]=[名称],[初始状态]
3[号码]=[名称],[初始状态]
4......