blogs/docs/archives/RA2TriggerLogics.md
SilverAg.L 4c90f70fc4
Some checks failed
部署文档 / build (push) Failing after 2s
2024 - 31.05.2025 All Bumped.
Thanks to the following contributors:
- @snowykami
- @frg2089
- Nya_Twisuki
2025-05-31 14:30:19 +08:00

30 KiB
Raw Permalink Blame History

category, tag, star
category tag star
RA2
地图
地图编辑
触发
局部变量
true

浅谈红警 2 触发组件的运行逻辑

::: warning 观前注意 由于涉及一些编程知识点,本综述可能存在亿些阅读困难。 虽然经过与 Zero Fanker 等人的讨论后决定做些修缮重写,但难免仍有需要改进之处。 :::

绪论

研究背景

红警 2 的地图创作中,流程设计是战役最重要的一环,其中触发发挥着举足轻重的作用。好的剧情流程能够让人印象深刻,如何用触发设计出好的流程,就得凭借地图师们的逻辑智慧了。但长久以来,红红的地图教程偏实用性居多,大多数地图师对触发并没有明晰的认识,他们或许在脑内对任务流程有着天马行空的设想,但落到触发实现上往往束手无策。

本文基于已有的触发和编程实践,尝试阐明触发组件的运作逻辑,并就这套系统的一些缺陷提出个人的见解,希望能够对各位读者有所启发。如果能稍稍地推进触发设计的简化事业,那就再好不过了。

研究目的与意义

本文旨在用程序化的思想阐述触发的运作逻辑(而非原理,更不是底层原理),指出这套系统存在的逻辑缺陷,并试着给出可行的解决思路。通过对触发逻辑的分析,用不同的视角去看待触发,或许可以一定程度上简化触发的表达,增强地图师们的逻辑思维,启发他们对优秀设计模型(如状态机)的借鉴、化用,提升剧情的观赏性,为观众们带来更多精彩的“剧目”吧。

::: note 逻辑与实现 必须指出的一点是:逻辑与实现并不等同。逻辑关心客观事物的本质、规律,实现则关心符合这个规律的解决方案。 :::

一、触发组件相关概念

1.1 触发

地图触发是早在《命运与征服:泰伯利亚黎明》就引入的系统,负责处理地图当中的“事件”^1^。

一局游戏瞬息万变,其中总有一些既成的、游戏引擎能感知的事,叫做事件。比如什么关键建筑被打爆了啊,哪家缺电缺钱了,等等。 这些事件会被触发捕捉到,并驱动后者去执行相应的行为,也就是游戏引擎能做到的各种效果:可以是刷兵,改变光照,炸个桥,平地起心灵信标……诸如此类。

再次强调,捕获到的事件、要执行的行为,都仅限引擎能做到的范围之内。

::: note 事件和行为 有一些地编调整过用词,事件改称“条件”,行为改叫“结果”。你可以简单这么理解。 :::

1.2 局部变量

变量系统则在《命运与征服:泰伯利亚之日》才开始出现,根据[VariableNames]小节所处的位置不同, 分为 Rules 里的全局变量,和地图里的局部变量^2^。本文主要讨论局部变量。

在地图创作中,局部变量就是对设计者有具体意义的会因触发(和动作脚本)做出改变的临时在某一局游戏起作用的数值。

值得一提的是,从原版一直到 Ares 扩展平台,局部变量均有如下限制:

  • 存在数目上限:至多能设置 100 个;
  • 取值范围有限:变量的值只能为0(清除)和1(设置)两种。

一直到 @secsome 在 Phobos 平台扩展了变量系统之后,上述限制才被打破。

参考链接: pr#321, pr#424, pr#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表示:

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 逻辑了:

if event1 and event2() and ...:
  action1()
  action2()
  ...

::: details 参考资料:触发行为在 INI 和引擎中的实际表示

以触发行为为例,触发行为在地图里是这种 INI 表示:

01019810 = len_actions, a1_type, a1p1, a1p2, ..., a1p6, a1_wp, a2_type, ...

在游戏引擎中,一条 Action 由TActionClass管理(下列声明有所省略,详见 YRpp

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 的 编辑变量 这一结果为例:

bool TActionExt::EditVariable(
  TActionClass* pThis, HouseClass* pHouse, ObjectClass* pObject,
  TriggerClass* pTrigger, CellStruct const& location)
{
  // blabla
  return true;
}
  • pThisTActionClass的指针,在参考资料「触发行为在 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 ,也是如法炮制。以此类推,就形成触发链:

通过“允许触发”一级一级破坏“禁止”,把链条打通

而在程序代码当中,顺序是通过代码行的先后次序来体现的:

x = input("输入 x")
y = input("输入 y")
print(x + y)

可以很清楚地看出,这段程序先读入x,后读入y,最后输出它们两相加 (实际上是拼接) 的结果。自上而下、逐行执行,这就是程序代码的顺序结构。

2.1 一节的讨论可知,触发在逻辑上可以表达成if单分支的伪代码。那么不妨将这些if也按照地图师期望的顺序,自上而下地排列起来:

# 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 循环结构

早期扩展平台对于屏幕界面的利用并不充分,地图师只能每隔一段时间在屏幕左上方提示玩家当前的任务目标。那么像这种需要重复执行同一个流程的结构,就叫循环结构。

对于循环结构,已知的触发教程大概会让你关注触发的「重复类型」:

![英文原版直接称作 Type
早期汉化也直接译作“类型”](fa2_trigger_ui.webp =50%x50%)

通常来说,选择 2 - Repeating OR 那一项便足以满足很多简单的重复需求。

::: info 重复类型 事实上,用重复这个概念描述触发(实际上是标签)的类型并不准确。由于本文内容不允许做过多展开,有机会再单独做个“实验”。 :::

而在程序代码中,while循环则取代if扮演这个执行重复主体的角色:

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循环表示:

while elapsedTime(6):  # 每隔 6 秒
  text_trigger("mission:naosi_enemyarmada")  # 输出文本
  reinforcement("01014514")  # Q7 援军(小队)

2.4 线性多分支选择结构

在触发设计中,有的时候还会希望在某一个节点,根据不同的情况分别做出不同的反应,这就涉及到了选择。选择结构是创建分支流程的重要组成部分,可用于实现条件发生变化时流程也随之发生改变的效果^3^。

选择结构其实前面 2.1 一节已经介绍过,并且基于触发运行的现象指出触发本质是“条件单分支结构”。由于存在阻塞等待,触发并不会主动判断条件不满足(为假)的情形,所以在实际应用中比起去实现双分支,地图师更倾向于关注多个条件分支之间如何组织。

Tip

实际上借助局部变量也可以实现双分支选择,但并不是本章的重点。详见 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 摧毁所属方)。

if objSelected(BeaconA):
  del BeaconA, BeaconB, BeaconC
  ...
# B、C 信标也是一样,就不再展开了。

非互斥的多分支实际上就是顺序地经过这些条件分支,如此便又回归顺序结构的处理方式。其中比较典型的设计类似数学上的“大、小前提”。 考虑一个争夺战:玩家与敌军互相争夺油田,但狡猾的敌军可能直接摧毁油田。假如有两个 Phobos 扩展局部变量充当计数器:

  • remaining计算地图上还有多少油田。若余量小于玩家应占数目,任务失败;
  • player_owns计算玩家占了多少油田。大于等于应占数目,任务完成。

那么当一个油田 P48 被摧毁 时,首先想到余量remaining减一。如果这个油田先前是玩家持有,那就再对player_owns减一。简单的允许触发即可:

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初始化当然你可以改这个初始值。那么这些局部变量保存到地图里肯定也是带着初始值的。
这些局部变量最终又被引擎读进内存,组成数列(或者说数组)。所以,在游戏开始之前,这些变量就已经就位了

那么不妨把局部变量放在程序代码的开头:

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
而在编程中二者是不同的运算。为了避免混淆,你经常会看到用==指代相等,用=来赋值。 :::

那么上述案例便可以用如下伪代码表示:

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 一节中已知,原版的局部变量只有01两种取值,恰如一个“开关”。前面两个小节的实例分析也展示了局部变量作为“开关”,在触发系统中的辅助判断作用。有了开关,就可以人为建立起“或”、“非”,乃至更为复杂的逻辑通路。

我们不妨从先前一直遗漏的“条件双分支结构”入手。很显然双分支摘去一支就退化为了单分支反过来双分支的真、假两路恰好是一个开关的两极。那么有没有可能可以通过局部变量连结两个单分支组合成一个“条件双分支”呢这就是接下来要讨论的单刀双掷开关SPDT设计。

3.2 一节的介绍可以看出,触发是取局部变量的瞬时值做的判断,变量值为0还是为1是两个独立事件。既然逻辑上通过局部变量反映了条件的真假,不妨假设经过这个双分支时,该局部变量的值“保持恒定”,于是得出以下的实现思路:

# 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,则在指定路径点刷出奖励箱子。

对应的伪代码如下:

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(即未超时),且目标不再存在,则宣布任务完成。

对应的伪代码如下:

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 [EB/OL], 1.31.2024, 6.17.2024.
  2. ModEnc. VariableNames [EB/OL], 5.16.2024, 6.17.2024.
  3. RN Studio. 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 附文档)
  • 包含两个非持续伴随事件的触发永远不会启动,因为这两个事件是相继发生的,而不是同时发生的,因此永远不可能同时满足条件。FA2spHDM 附文档,有关概念参见 Ares 文档
  • ……

凡此种种,都加深了我个人的自我怀疑——我写这些真的有意义吗?
但至少我是很想找到“存在的意义”的。所以哪怕本文多么“空中楼阁”,至少就让它烂在这里吧。

::: right 04.19.2025 :::