2024 - 31.05.2025 All Bumped.
Some checks failed
部署文档 / build (push) Failing after 2s

Thanks to the following contributors:
- @snowykami
- @frg2089
- Nya_Twisuki
This commit is contained in:
2025-05-31 14:30:19 +08:00
commit 4c90f70fc4
62 changed files with 10618 additions and 0 deletions

View File

@ -0,0 +1,476 @@
---
category:
- RA2
- 地图
tag:
- 地图编辑
- 触发
- 局部变量
star: true
---
# 浅谈红警 2 触发组件的运行逻辑
::: warning 观前注意
由于涉及一些编程知识点,本综述可能存在亿些阅读困难。
虽然经过与 Zero Fanker 等人的讨论后决定做些~~修缮~~重写,但难免仍有需要改进之处。
:::
## 绪论
### 研究背景
红警 2 的地图创作中,流程设计是战役最重要的一环,其中触发发挥着举足轻重的作用。好的剧情流程能够让人印象深刻,如何用触发设计出好的流程,就得凭借地图师们的逻辑智慧了。但长久以来,红红的地图教程偏实用性居多,大多数地图师对触发并没有明晰的认识,他们或许在脑内对任务流程有着天马行空的设想,但落到触发实现上往往束手无策。
本文基于已有的触发和编程实践,尝试阐明触发组件的运作**逻辑**,并就这套系统的一些缺陷提出个人的见解,希望能够对各位读者有所启发。如果能稍稍地推进触发设计的简化事业,那就再好不过了。
### 研究目的与意义
本文旨在用程序化的思想阐述触发的运作**逻辑**(而非原理,更不是底层原理),指出这套系统存在的**逻辑**缺陷,并试着给出可行的解决思路。通过对触发**逻辑**的分析,用不同的视角去看待触发,或许可以**一定程度上**简化触发的表达,增强地图师们的逻辑思维,启发他们对优秀设计模型(如状态机)的借鉴、化用,提升剧情的观赏性,为观众们带来更多精彩的“剧目”吧。
::: note 逻辑与实现
必须指出的一点是:逻辑与实现并不等同。逻辑关心客观事物的**本质、规律**,实现则关心符合这个规律的**解决方案**。
:::
## 一、触发组件相关概念
### 1.1 触发
地图触发是早在《命运与征服:泰伯利亚黎明》就引入的系统,负责处理地图当中的“事件”^1^。
一局游戏瞬息万变,其中总有一些**既成的、游戏引擎能感知的事,叫做事件**。比如什么关键建筑被打爆了啊,哪家缺电缺钱了,等等。
这些事件会被触发捕捉到,并驱动后者去执行相应的**行为,也就是游戏引擎能做到的各种效果**:可以是刷兵,改变光照,炸个桥,平地起心灵信标……诸如此类。
再次强调,捕获到的事件、要执行的行为,都仅限**引擎能做到的**范围之内。
::: note 事件和行为
有一些地编调整过用词,事件改称“条件”,行为改叫“结果”。你可以简单这么理解。
:::
<!-- 在红警 2触发更类似于高中数学中的“命题”若 p 则 q。其中事件 p 和行为 q 都可以不止一条,并且 p1 p2 p... 之间、q1 q2 q... 之间有一定的连接关系。 -->
### 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^。
然而两个触发之间常常在宏观上表现为异步:同样都是延时 10sA 触发和 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<br>早期汉化也直接译作“类型”](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
:::