--- date: 2024-06-17 category: - RA2 - 地图 tag: - 地图编辑 - 触发 - 局部变量 star: true --- # 浅谈红警 2 触发组件的运行逻辑 ::: warning 观前注意 由于涉及一些编程知识点,本综述可能存在亿些阅读困难。 虽然经过与 Zero Fanker 等人的讨论后决定做些~~修缮~~重写,但难免仍有需要改进之处。 ::: ## 绪论 ### 研究背景 红警 2 的地图创作中,流程设计是战役最重要的一环,其中触发发挥着举足轻重的作用。好的剧情流程能够让人印象深刻,如何用触发设计出好的流程,就得凭借地图师们的逻辑智慧了。但长久以来,红红的地图教程偏实用性居多,大多数地图师对触发并没有明晰的认识,他们或许在脑内对任务流程有着天马行空的设想,但落到触发实现上往往束手无策。 本文基于已有的触发和编程实践,尝试阐明触发组件的运作**逻辑**,并就这套系统的一些缺陷提出个人的见解,希望能够对各位读者有所启发。如果能稍稍地推进触发设计的简化事业,那就再好不过了。 ### 研究目的与意义 本文旨在用程序化的思想阐述触发的运作**逻辑**(而非原理,更不是底层原理),指出这套系统存在的**逻辑**缺陷,并试着给出可行的解决思路。通过对触发**逻辑**的分析,用不同的视角去看待触发,或许可以**一定程度上**简化触发的表达,增强地图师们的逻辑思维,启发他们对优秀设计模型(如状态机)的借鉴、化用,提升剧情的观赏性,为观众们带来更多精彩的“剧目”吧。 ::: note 逻辑与实现 必须指出的一点是:逻辑与实现并不等同。逻辑关心客观事物的**本质、规律**,实现则关心符合这个规律的**解决方案**。 ::: ## 一、触发组件相关概念 ### 1.1 触发 地图触发是早在《命运与征服:泰伯利亚黎明》就引入的系统,负责处理地图当中的“事件”^1^。 一局游戏瞬息万变,其中总有一些**既成的、游戏引擎能感知的事,叫做事件**。比如什么关键建筑被打爆了啊,哪家缺电缺钱了,等等。 这些事件会被触发捕捉到,并驱动后者去执行相应的**行为,也就是游戏引擎能做到的各种效果**:可以是刷兵,改变光照,炸个桥,平地起心灵信标……诸如此类。 再次强调,捕获到的事件、要执行的行为,都仅限**引擎能做到的**范围之内。 ::: note 事件和行为 有一些地编调整过用词,事件改称“条件”,行为改叫“结果”。你可以简单这么理解。 ::: ### 1.2 局部变量 变量系统则在《命运与征服:泰伯利亚之日》才开始出现,根据`[VariableNames]`小节所处的位置不同, 分为 Rules 里的**全局变量**,和地图里的**局部变量**^2^。本文主要讨论局部变量。 在地图创作中,局部变量就是**对设计者有具体意义的**,**会因触发**(和动作脚本)**做出改变的**,**临时在某一局游戏起作用的**数值。 值得一提的是,从原版一直到 Ares 扩展平台,局部变量均有如下限制: - **存在数目上限**:至多能设置 100 个; - **取值范围有限**:变量的值只能为`0`(清除)和`1`(设置)两种。 一直到 [@secsome](https://github.com/secsome) 在 Phobos 平台扩展了变量系统之后,上述限制才被打破。 > 参考链接: > [pr#321](https://github.com/Phobos-developers/Phobos/pull/321), > [pr#424](https://github.com/Phobos-developers/Phobos/pull/424), > [pr#425](https://github.com/Phobos-developers/Phobos/pull/425). ::: info 扩展变量系统 船新的变量系统大大扩充了局部变量池,几乎可以认为是“无限量”使用。~~真的有人会堆料到要用 2^31^ - 1 个以上的变量吗?~~ 除此之外,变量取值范围也极大地延展了。但官方文档指出,这种延展并不对等: - **纯触发组件中**,范围可达 $[-2^{31}, 2^{31} - 1]$(即上至`2147483647`); - **动作脚本中**参数位有限,范围**缩减**为 $[-2^{15}, 2^{15} - 1]$(上至`32767`)。出界不保证预期效果。 至于为何会有这种缩减,还请移步《计算机组成原理》第二章:数据的表示与运算。 ::: ## 二、触发组件的程序逻辑分析 > [!IMPORTANT] > 由于本博客使用的`katex`插件不支持编写 LaTeX 伪代码,因此本文的伪代码仍然基于 Python 语法。 > 但考虑到 Python 的很多特性存在理解门槛,本文将只着眼于`if`等通用表达。 ### 2.1 触发的逻辑本质 提及触发,各位地图师看过教程、初步上手后应该会建立这样的基本印象: - 触发有一定**前置事件**。 - 触发指明了**执行行为**。 - 触发要等**前置事件(条件)完成**,才会**执行相应行为**,表现出各种结果。 > 比如简单的延时字幕,你确实需要等上那一段时间,然后才会冒出来一句“必须重新集结部队”。 这种“等待条件”的现象与高中数学讨论的“若 p 则 q”命题(即条件命题)非常类似。一个条件命题要能判断其真伪,条件就必不可少;同样的,一个触发要能执行,首先要等待前置事件完成。在程序框图中,像这种具有前置条件的语义用**条件分支**表示(如下图),又称**选择结构**。 ![流程图中的“选择结构”](https://image.woshipm.com/wp-files/2017/08/8Ynwb53uWeo9QMHb7Xi5.png =40%x40%) 从游戏实际运行的现象来看,触发会在条件满足(为真)后执行相应的“处理程序”(也就是行为),这一点符合基本印象。而当条件**尚未满足**时,触发则阻塞等待。 假如**逻辑上**就是希望判断条件不满足(为假),由于触发一直等不到条件满足,则直到游戏结束为止这个条件都得不到任何处理。如此看来,触发的逻辑本质就是如上左图的**条件单分支结构**。 而在程序语言里,这种条件单分支常用`if`表示: ```python if condition: # 条件 do_sth() # 结果 ``` 在这段代码中,只有当条件`condition`满足(为真),才会去走`do_sth()`那一步,执行出结果来。 而触发也是等到条件满足,才会执行结果。既然如此,抛开触发的所属、难度开关等等其他属性,不妨就把一个触发当成这种`if`结构。 > [!note] > 1. 为了方便讨论,本章 *Q53/Q54 允许/禁止触发* 不会直接使用`do_sth()`这种函数表示。 > 2. 如没有文字说明,文中使用到的触发条件、结果会在号码前面标明 P、Q 加以区分。 至于事件`condition`和行为`do_sth()`,事实上它们可以不止一条。多条件姑且不论,有些朋友可能很喜欢在单一触发里塞十几条结果。 那么多个条件、多个结果之间是如何串起来的呢?你可以自行翻阅各扩展平台的 YRpp,或是在 FA2 等地图编辑器中实验一下,这边就直接说结论了: - 多个条件之间**以逻辑且(AND)连接**,所有条件**必须全部满足**,才可以执行对应的结果; - 多个结果之间形成队列**按序执行**,但因为每一条结果执行耗时很短,表面上看似乎是同时完成。 至此,可以用这样的伪代码描述一个触发的 $p_1 \land \ldots \land p_n \to Q$ 逻辑了: ```python if event1 and event2() and ...: action1() action2() ... ``` ::: details 参考资料:触发行为在 INI 和引擎中的实际表示 以触发行为为例,触发行为在地图里是这种 INI 表示: ```ini 01019810 = len_actions, a1_type, a1p1, a1p2, ..., a1p6, a1_wp, a2_type, ... ``` 在游戏引擎中,一条 Action 由`TActionClass`管理(下列声明有所省略,详见 [YRpp](https://github.com/Phobos-developers/YRpp/blob/c8d4da4f57a80a3cc2b9ecbde56c335e082c8335/TActionClass.h)): ```cpp class TActionClass : public AbstractClass { public: TriggerAction ActionKind; // aX_type union { RectangleStruct Bounds; // map bounds for use with action 40 struct { int Param3; int Param4; int Param5; int Param6; }; }; // It's enough for calling Bounds.X, just use a union here now. - secsome int Waypoint; // aX_wp int Value2; // multipurpose // aXp2 int Value; // multipurpose // aXp1 }; ``` 其中 P1 决定了 P2 参数的类型。由于 P3-P6 均为`int`类型,无法满足文本、数值、触发等多种类型需求,所以由 Value (P1) 采取类似`enum`的设计,Value2 则记录真实参数 P2 的指针地址。 ::: ::: details 参考资料:触发行为的具体实现 游戏引擎仍然用`TActionClass`声明和实现原版的行为(也就是地编靠前的 100 多号),扩展平台则用`TActionExt`实现扩展。 以 Phobos 的 *编辑变量* 这一结果为例: ```cpp bool TActionExt::EditVariable( TActionClass* pThis, HouseClass* pHouse, ObjectClass* pObject, TriggerClass* pTrigger, CellStruct const& location) { // blabla return true; } ``` - `pThis`为`TActionClass`的指针,在参考资料「触发行为在 INI 和引擎中的实际表示」中已经介绍过,它可以记录行为参数; - `pHouse`系触发所属方,比如行为 *36 全部更改所属* 要变走\*触发所属方\*的全部东西。 其余的函数形参本人没有具体研究过,恕不做介绍。 ::: ### 2.2 顺序结构 顺序结构是最简单、最基本、最符合思维直觉的结构。大部分战役任务的流程也都是线性、有序的流程。以被广为改编的《脑死》为例,它的任务流程具有很典型的顺序性: 1. 建立一座~~锅盖~~苏军雷达 2. 超时空援军就位后,摧毁最后一座心灵控制器 3. 秋风扫落叶,清除残余敌军。 在线性系统中,**按时间先后执行的流程**我们认为就是顺序结构^3^。 然而两个触发之间常常在宏观上表现为异步:同样都是延时 10s,A 触发和 B 触发看起来好像是同时执行、互不相干的。那要如何保证顺序呢? 一个简单的办法是用行为 *53 允许触发*。假定有两个触发 $t_0$ 和 $t_1$,要求先触发 $t_0$ 后触发 $t_1$。只需在触发编辑器里完成如下步骤: - 为 $t_1$ 触发勾上“禁止”选项;($t_0$ 无需再做处理) - 在 $t_0$ 触发的行为(或者结果)页面中,添加一项(行为类型选中 53,参数选择 $t_1$ 触发)。 如需链式允许一系列触发,比如 $t_1 \to t_2 \to \ldots \to t_n$ ,也是如法炮制。以此类推,就形成触发链: ![通过“允许触发”一级一级破坏“禁止”,把链条打通](trigger_chain.webp) 而在程序代码当中,顺序是通过代码行的先后次序来体现的: ```python x = input("输入 x:") y = input("输入 y:") print(x + y) ``` 可以很清楚地看出,这段程序先读入`x`,后读入`y`,最后输出它们两相加 ~~(实际上是拼接)~~ 的结果。**自上而下、逐行执行**,这就是程序代码的顺序结构。 从 [2.1 一节](#_2-1-触发的逻辑本质)的讨论可知,触发在逻辑上可以表达成`if`单分支的伪代码。那么不妨将这些`if`也按照地图师期望的顺序,自上而下地排列起来: ```python # t0. start if anyEvent: # P8 任何事件(当*单独使用*时,它会令触发*立即执行*) lock_input() # Q46 禁止用户输入 play_speech("EVA_EstablishBattlefieldControl") # Q21 播放 EVA 语音 # t1. intro.brief.A if elapsedTime(6): # Q13 流逝时间 text_trigger("mission:naosi_A") # Q11 文本触发 ``` 其中,$t_0$ 触发的条件是`anyEvent`。结合右边的注解和前面对于`if`执行过程的讨论,这个条件肯定是恒满足的,也就是为真(`True`)。 上述代码段的排布与前面的案例一致,$t_1$ 是排在 $t_0$ 后面的,这样 $t_1$ 必定会等 $t_0$ 先触发;除此之外,$t_1$ 还往右缩进,与 $t_0$ 的那两条结果对齐,意味着 $t_0$ 不触发时,绝对不会触发 $t_1$,以此避免独立触发和触发链的歧义。 但随着触发链越来越长,这种层层缩进的形式也会因为不同结果的穿插变得混乱不堪。除此之外,这种表示也很难解释行为*54 禁止触发*。由于本人水平有限,如何解决这些缺陷、让逻辑表达更贴近 Q53、Q54 的表述,就权当是留给有兴趣的读者一道思考题吧。 ### 2.3 循环结构 早期扩展平台对于屏幕界面的利用并不充分,地图师只能每隔一段时间在屏幕左上方提示玩家当前的任务目标。那么像这种需要**重复执行同一个流程**的结构,就叫循环结构。 对于循环结构,[已知的触发教程](https://www.bilibili.com/video/BV1Zw411y7DZ?spm_id_from=333.1387.collection.video_card.click)大概会让你关注触发的「重复类型」: ![英文原版直接称作 Type
早期汉化也直接译作“类型”](fa2_trigger_ui.webp =50%x50%) 通常来说,选择 *2 - Repeating OR* 那一项便足以满足很多简单的重复需求。 ::: info 重复类型 事实上,用重复这个概念描述触发(实际上是标签)的类型并不准确。由于本文内容不允许做过多展开,有机会再单独做个“实验”。 ::: 而在程序代码中,`while`循环则取代`if`扮演这个执行重复主体的角色: ```python while True: print("Hello World") ``` 将上述代码粘贴进 Python IDLE 回车运行,你也能看到终端里打出来一行行`Hello World`,除非用任务管理器干掉这个进程,否则它会无休止地输出下去。 分析这个运行表现不难发现,它是类似下图图二的流程:首先它走到`while`处执行判断,由于条件恒满足,往下执行`print`;然后循环并没有结束,它重新回到`while`重复执行前面说的流程,将坏掉的乐土打字机事业推进下去~~爱莉希雅死辣~~。 ![流程图中的循环结构](https://image.woshipm.com/wp-files/2017/08/HRej5VdT9M9sRxXBYTJv.png =40%x40%) 上面的`Hello World`案例也是类似图二的流程。于是,重复触发可以用`while`循环表示: ```python while elapsedTime(6): # 每隔 6 秒 text_trigger("mission:naosi_enemyarmada") # 输出文本 reinforcement("01014514") # Q7 援军(小队) ``` ### 2.4 线性多分支选择结构 在触发设计中,有的时候还会希望在某一个节点,根据不同的情况分别做出不同的反应,这就涉及到了选择。选择结构是创建分支流程的重要组成部分,可用于实现**条件发生变化时流程也随之发生改变**的效果^3^。 选择结构其实前面 [2.1 一节](#_2-1-触发的逻辑本质)已经介绍过,并且基于触发运行的现象指出触发本质是“条件单分支结构”。由于存在阻塞等待,触发并不会主动判断条件不满足(为假)的情形,所以在实际应用中比起去实现双分支,地图师更倾向于关注**多个条件分支**之间如何组织。 > [!tip] > 实际上借助局部变量也可以实现双分支选择,但并不是本章的重点。详见 [3.3.1 小节](#_3-3-1-门电路的搭建)。 当大量的选择结构简单串联之后,实际上就构成了线性多分支结构^3^。而在涉及到多个条件时,不同条件之间的关系也会影响选择结构的实际效果。具体来说,是互斥与否的关系。 **互斥**是说,这组条件分支**不可能**同时满足,因此必然只会运行**其中一个**处理程序。用程序语言来说,就是`if-elif-else`。比如用公式法求一元二次方程,不可能 $\Delta > 0$ 还说方程找不到实根。 **非互斥**是指,多条件中**至少有两条**同时满足,有可能**同时经过若干个**处理程序。比如说重叠区间: - 若 $x \in (0, 233]$,则令 $x = -x$; - (反之)若 $x \in (220, 512]$,则又令 $x = x^3$。 如果没有加上那个“反之”,考虑输入一个特殊值 $x = 230$:首先 $230 < 233$,$x$ 变换成 $-230$;随后 $230 > 220$,对 $-x$ 做幂运算得出 $-12,167,000$。 而一旦加上“反之”,上述处理就是互斥的:**“反之”要求 $0 < x \le 233$ 这一条件首先不满足**。对于特殊值 $230$,显然 $230 < 233$ 满足条件,$x$ 变成 $-230$ 就直接结束了。 由于触发的 $p \to q$ 语义无法通过逻辑上互斥实现选择,因此实践中更倾向物理实现这个互斥:一旦某一分支成功触发,就需要**尽快阻止触发其他分支**。实践中通常会考虑 *Q54 禁止触发*、摧毁选择器等方式,让用户**来不及**进入另外的分支。 比如近年来的子阵营指挥:设有三支部队分别从三个方向开进,你需要**选择**指挥其中一支。这种桥段通常会刷三个假单位充当选择信标,玩家选中一个就把那一路部队更改所属。那么这种情况会在玩家选择之后*立刻摧毁其他选择信标*(*Q119 摧毁所属方*)。 ```python if objSelected(BeaconA): del BeaconA, BeaconB, BeaconC ... # B、C 信标也是一样,就不再展开了。 ``` 非互斥的多分支实际上就是**顺序地经过这些条件分支**,如此便又回归顺序结构的处理方式。其中比较典型的设计类似数学上的“大、小前提”。 考虑一个争夺战:玩家与敌军互相争夺油田,但狡猾的敌军可能直接摧毁油田。假如有两个 Phobos 扩展局部变量充当计数器: - `remaining`计算地图上还有多少油田。若余量小于玩家应占数目,任务失败; - `player_owns`计算玩家占了多少油田。大于等于应占数目,任务完成。 那么当一个油田 *P48 被摧毁* 时,首先想到余量`remaining`减一。*如果这个油田先前是玩家持有*,那就再对`player_owns`减一。简单的允许触发即可: ```python if objDestroyed(OilA): remaining -= 1 # 编辑变量 (Phobos) if oil_A_captured and objDestroyed(OilA): player_owns -= 1 ``` ## 三、局部变量的程序逻辑分析 前面的分析都是基于触发系统本身,贴的代码也只是对假想条件和结果的调用。而在地图创作中,除了直接利用已有的条件库,还会用局部变量间接地做条件判断。为了讲清楚它如何参与到触发逻辑当中,不妨引入经典案例“运输船找妈妈”。 ### 3.1 案例实现思路 “运输船找妈妈”简单来说,就是在**乘客还有事要做**的情况下,让运载乘客的**载具打哪来,回哪去**。观察一下 YR S01:时空转移的相关演出,易得流程如下: - **满载**的运输船从地图外刷出,移动到定点; - 等待所有乘客下船。然后乘客有可能还会打光几秒、更改所属方……; - **空载**的运输船从定点返回地图外。 难点就在于,如何得知**卸出乘客后船是空的**。翻看条件库,好像也没有类似的选项。 于是考虑设`apc unload`变量初始为 0,然后在运输船卸出乘客之后*Q56 设置局部变量*,触发得知 *P36 局部变量被设置* 了,就建一个空船小队把它拉走。 > [!note] > 篇幅原因,具体实现步骤我就不贴出来了。这种实用向教程应该也很容易找到。 ### 3.2 案例逻辑分析 已知这一经典例子中运用了局部变量,那么引入局部变量后触发逻辑有什么变化呢? 首先考虑局部变量的位置。从语义、代码语法上说,不可能未经定义就使用一个变量——好比解应用题,只设了未知数`x`却凭空跑出个`y`来。 在 FA2 的局部变量窗口中,你需要为局部变量起名(声明),然后 FA2 默认会令它等于 0(初始化,当然你可以改这个初始值)。那么这些局部变量保存到地图里肯定也是带着初始值的。 这些局部变量最终又被引擎读进内存,组成数列(或者说数组)。所以,**在游戏开始之前,这些变量就已经就位了**。 那么不妨把局部变量放在程序代码的开头: ```python apc_unload = 0 # 多数编程语言并不允许变量名带空格 if ...: ... ``` 既然声明了局部变量,那么就要用起来。触发里有一组事件和一组结果分别读写局部变量(设待操作的局部变量为 $x$): - P36:局部变量被设定(为 1),即当 $x = 1$ 时 - P37:局部变量被清除(0),即当 $x = 0$ 时 * Q56:设置局部变量(值为 1),即令 $x = 1$ * Q57:清除局部变量(值),即令 $x = 0$ ::: info 赋值与相等 在数学中描述等量关系用等号:`a = 0`时,……。同时,赋值也用等号:令`x = 1`。 而在编程中二者是不同的运算。为了避免混淆,你经常会看到用`==`指代相等,用`=`来赋值。 ::: 那么上述案例便可以用如下伪代码表示: ```python apc_unload = 0 if elapsedTime(20): play_speech("EVA_ReinforcementsHaveArrived") reinforcement("LCRFCome") if apc_unload == 1: create_team("LCRFBack") # Q4 建立小队 ``` 触发就是通过对局部变量值的变动,来实现一些较为复杂的逻辑判断。并且随着 Phobos 扩展了变量系统,触发对变量的判断也不再局限于 0 和 1 的“左手倒右手”,而是与科技类型、超级武器、随机数等联系了起来,实现更加精细的随机机制和流程控制。 ### 3.3 高阶用法:逻辑门电路 触发条件仅以**逻辑“且”** 相连。其阻塞等待决定了它没有类似“否则”的设计,也就莫得直给的逻辑“非”;已知的触发实践也表明,多条件不可能在仅满足其中一个的情况下执行触发,无法直接判断逻辑“或”。那是否说明,触发就是做不到逻辑完备,如同早期面对平台限制一样无解呢?非也。 事实上,依据软硬件的逻辑等价性原理,触发确实可以做到或、非逻辑的判定。但显然,用累加去实现相乘,比起直接列个竖式配合九九乘法表,总是麻烦得多。自行实现的或、非逻辑也一样。 #### 3.3.1 门电路的搭建 > “逻辑智能所有的基础,都不过是这小小的开关。” > ::: right > ——ElectroBOOM > ::: 在 [1.2 一节](#_1-2-局部变量)中已知,原版的局部变量只有`0`和`1`两种取值,恰如一个“开关”。前面两个小节的实例分析也展示了局部变量作为“开关”,在触发系统中的**辅助判断**作用。有了开关,就可以人为建立起“或”、“非”,乃至更为复杂的逻辑通路。 我们不妨从先前一直遗漏的“条件双分支结构”入手。很显然,双分支摘去一支就退化为了单分支;反过来,双分支的真、假两路恰好是一个开关的两极。那么有没有可能,可以通过局部变量连结两个单分支,组合成一个“条件双分支”呢?这就是接下来要讨论的单刀双掷开关(SPDT)设计。 从 [3.2 一节](#_3-2-案例逻辑分析)的介绍可以看出,触发是取局部变量的**瞬时值**做的判断,变量值为`0`还是为`1`是两个独立事件。既然**逻辑上**通过局部变量反映了条件的真假,不妨假设经过这个双分支时,该局部变量的值“保持恒定”,于是得出以下的实现思路: ```python # t0: if-else 双分支:条件判断器 if elapsedTime(114514) and ...: your_local_var = 1 # t1: 条件为真时的处理 if your_local_var == 1: ... # t2: 条件为假时的处理 if your_local_var == 0: ... ``` 注意前面 2.4 讨论多分支时,还提到要“尽快阻止触发其他分支”。在上面这个简单模型中,令 $t_1$ 第一时间“禁止”$t_2$,反过来也一样,就可以了。 事实上,不单是局部变量,像 YR A03:集中攻击那关争抢电厂的桥段,“电厂是否属于玩家”也可以采用 SPDT 设计,直接判断关联电厂的归属是敌是友,并相应地允许(或禁止)敌军反占电厂的演出。 像这样将条件映射到局部变量,用变量代为影响执行流的“逻辑电路”思想,接下来会多次体现。 #### 3.3.2 或运算——殊途同归 如今的任务有一个利好玩家的设计:如果你实在没钱(假定低于 $100),**或者**你的矿车被打没了,就给你派送几个钱箱子救急,所谓“战争援助”。不妨以这个设计为例。 那首先判断“缺钱”是有现成事件*52 金钱低于* 的;至于判断“缺少矿车”,在原版中可反向考虑用事件*20 生产特定类型的载具* 判断玩家生产了矿车。如果任务流程足够简单,玩家和其他 AI 阵营*绝不可能生产出相同种类的矿车*,也可以用 YR 的事件*61 科技类型不存在*。 条件输入、结果输出两端都 OK,就可以用局部变量搭建“或门”电路了: - 设一局部变量`player low funds`,**初值为 0**; - 触发 $I_1$:若玩家\*金钱低于\* $100,则令该变量值为 1; - 触发 $I_2$:若玩家补牛(没有牛车),也令该变量值为 1; - 触发 $O$:若该变量**值为 1**,则在指定路径点刷出奖励箱子。 对应的伪代码如下: ```python player_low_funds = 0 if creditsBelow(100): player_low_funds = 1 if noMiner: # 取决于你怎么判断玩家缺牛车 player_low_funds = 1 if player_low_funds == 1: create_crate(0, waypoint=81) # 参数写 0 表示*大量金钱* ``` “一真皆真”,这就是或门的精髓。当然 Python 里真正的`or`有短路特性,这里就不再展开了。 #### 3.3.3 非运算——逆向思维 原版 RA2 A08 有这样一个任务流程:在心灵信标影响到谭雅之前摧毁心灵信标。换言之,要求指定时间内摧毁目标(也就是计时器**没有超时**,并且目标被摧毁)。 在开始之前,不妨先捋一捋这个条件组的逻辑。这实际上是个必要不充分条件:要保证*完成流程*,必需满足以下两个分条件:(一)没有超时,计时器没有走完;(二)目标被摧毁。但反过来,单纯“没有超时”推不出任务完成——目标可能还在。 由于*目标被歼灭* 这个分条件不需要非运算,因此重点关注 *流程能完成 $\Rightarrow$ 没超时* 这一分路。这一路条件按照设计得是真命题,原命题为真,其逆否命题也为真:*超时了 $\Rightarrow$ 流程完不成*。这样就得到非运算的关键实现了。 有思路之后打开触发编辑器,现有这些条件与计时器有关:流逝时间、计时器时间到、流逝游戏时间……无不判断一个计时器*超时*。根据刚刚得到的逆否命题,用局部变量实现“非门”电路: - 设一局部变量`obj1 reachable`,**初始为 1**; - 触发 $I$:若计时器超时,则令该变量值为 0; - 触发 $O$:若变量值**仍为 1**(即未超时),且目标不再存在,则宣布任务完成。 对应的伪代码如下: ```python obj1_reachable = 1 if timeout: # 取决于你用 P13 P14 还是 P47 obj1_reachable = 0 if obj1_reachable == 1 and techTypeNotExist("NAPSYB"): play_speech("EVA_ObjectiveComplete") ... ``` --- 当然,虽然理想很美好,但从原版一直到纯 Ares(特别是 Hares 平台),100 个局部变量的限制依然不容忽视。对于有限的资源,还是需要做出合理的分配。 ## 结论 综上所述,红警 2 的触发组件在任务设计当中举足轻重,把握其运行的逻辑,有利于互相交流(至少不需要甩一堆截图)、有利于把更精妙的脑洞付诸实践、有利于优秀触发设计的提炼与发掘,对于触发编写乃至地图创作都有重要意义。 触发系统在逻辑层面上类似`if`单分支语句的设计,使其就入门而言并无太大门槛;支持顺序、选择、循环三种结构,可以实现大部分任务所需的线性叙事。然而其在逻辑运算上又有所欠缺,稍微复合一点的或、非逻辑判断必须通过局部变量绕路实现,对于地图师的逻辑思维能力是一大考验。 ## 参考文献 1. ModEnc. [Triggers](https://modenc.renegadeprojects.com/Triggers) \[EB/OL\], 1.31.2024, 6.17.2024. 2. ModEnc. [VariableNames](https://modenc.renegadeprojects.com/VariableNames) \[EB/OL\], 5.16.2024, 6.17.2024. 3. RN Studio. [map_tutorial](https://github.com/revengenowstudio/map_tutorial) \[EB/OL\], 4.29.2024, 5.6.2024. > [!NOTE] > 便利起见,文献附注格式基于 GB/T 7714 规范作了简化。 ## 致谢 时光荏苒,距离最开始做大白板雪原、遭遇战一样的战役地图应该也有九个年头了。能够写到这里,全拜各位大佬、同行、玩家朋友所赐。Lin 在此谢过诸位。 首先,感谢 RN Studio 指导本次“实验”。感谢制作组的地图教程,为“选题”指引方向;感谢 Zero Fanker 等人提出的指导和改进意见,以及触发实际执行的补充、佐证和更正。 其次,感谢曾经与仍在为红警 2 模组创作贡献智慧的各路人才,为论证提供各种帮助。特别感谢地图师同行们对触发系统所作的各种实地测试和表述修订,同时感谢 Phobos 团队开源触发组件的扩展实现,以及 Heli 等人为简化地图开发所做的各种尝试。 最后,感谢各位喜爱战役的红红玩家,特别是《星辰之光》的测试员和玩家朋友们拨冗测试和体验。 ## 后记 比起“逻辑”,modder 们始终更在乎切实的、能实装进游戏和编辑器的改善。除此之外,红警 2 的触发逻辑本身就与“引擎实现”密不可分。比如: - 越靠近`[Triggers]`小节头的触发越容易触发; - *通过所属方的启动是经由所属方的某个函数执行的,此函数调用为**每 8 帧一次**,根据所属方列表从上而下依次执行。*([FA2spHDM](https://github.com/handama/FA2sp/releases) 附文档) - *包含两个**非持续伴随事件**的触发永远不会启动,因为这两个事件是相继发生的,而不是同时发生的,因此永远不可能同时满足条件。*(FA2spHDM 附文档,有关概念参见 [Ares 文档](https://ares-developers.github.io/Ares-docs/new/triggerevents.html)) - …… 凡此种种,都加深了我个人的自我怀疑——我写这些真的有意义吗? 但至少我是很想找到“存在的意义”的。所以哪怕本文多么“空中楼阁”,至少就让它烂在这里吧。 ::: right 04.19.2025 :::