【MCBE·命令】断链的运用及递归的转入和转出

引言
使用过 McFunction 的小伙伴都知道,我们可以在一个函数中调用函数,也可以对多个对象使用函数,也就是诸如 Execute Target ~ ~ ~ Function FilePath 的用法。
函数就是包含指令的 .mcfunction 文件,我们执行函数就是执行里面包含的指令。如果函数外有预设指令的执行环境,例如使用 Execute 来执行函数,那么函数内的指令在被执行时也会继承执行环境,例如执行位置、执行维度、执行朝向、命令执行者等内容都会发生继承。
一般地,在使用函数的时候,我们不希望每次都执行所有函数中的所有命令(功能项),因此我们通过使用 Execute 命令 、 目标选择器 及 目标选择器参数 来限制执行条件,以达到“我需要执行哪些命令就执行哪些命令”的目的。
也就是说,我们在函数包内定义一个主函数 run(.mcfunction) ,然后让这个主函数根据我们定义的 限制条件 来访问和调用其他函数。在不满足条件的时候,主函数 run 就不会调用对应的函数,而满足条件时,对应的函数被调用。这样,我们可以保证我们制作的指令系统在被闲置的时候,每GameTick内平均执行的命令量都不会太多。
正文
那么前面扯了这么多关于函数的东西,它跟我们的命令方块有些什么关系呢?
我认为这之间的关系可大了!

假想一下,如果我们将以上关于函数的知识运用到非函数领域,也就是租赁服中纯命令方块制作指令系统这一领域来,是不是也可以优化租赁服,减少指令执行量呢?
“我需要执行哪些命令就执行哪些命令”是很容易达到的。如果主函数 run 中要调用的其他子函数不必按顺序运行,即调换这些子函数的执行顺序都不会对最终结果造成影响,那么我们可以为主函数 run 中提到的每一个子函数制作一个断链,然后这些断链都用于运行主函数 run 中提到的每一个子函数中的命令。
那么,什么是断链呢?
如图就是一个断链。

就如同字面意思,整个链是断开的。如果我们仍然把整个断链看作函数,那么我们可以近似的认为,当条件满足时运行函数,即填上断链上的空缺、执行对应的命令然后再重新设置空缺,让它回到断链状态。当条件不满足时就不会运行函数,即不填补断链上的空缺,后续的指令不会被执行。
需要注意的是,我们建议您在制作断链时使用诸如以下格式的命令:
[重复·无条件的·保持开启] testfor 目标
[链·有条件的·保持开启] setblock 坐标 chain_command_block 数据值
[空气]
[链·无条件的·保持开启] setblock 坐标 air
但我们为什么要这么做呢,特别是使用 Testfor 这条命令。
很显然,机智的小伙伴肯定想到了,我们可以简化以上指令,把 Testfor 和第二个 Setblock 命令改写为 Execute 型的命令,即 Execute 目标 ~ ~ ~ Setblock 坐标 chain_command_block 数据值 。
不过,我们很快就会发现问题——如果断链在主世界,而满足条件的 目标 不在主世界且只有1个时,即使其本身满足条件,断链上的空缺也不会被填上。
造成这个问题的根本原因在于 Execute 命令会继承命令执行者的执行环境,这也包括执行维度。因此,我们无法确定最终是在哪个维度执行的 Setblock 命令,因为它可能是在下界的 坐标 放置链命令方块,也可能是在 末地 的坐标放置链命令方块。
因此,如果确实要使用 Execute 命令来放置链命令方块,那么必须要确定最终执行的维度是在 主世界 ,我们可以多嵌套一层 Execute 以解决问题,例如:
Execute 目标 ~ ~ ~ Execute 一个在主世界的实体 ~ ~ ~ Setblock chain_command_block 数据值
但考虑到实体可能会被清理等问题,我建议您将一条指令改写为两条指令。但无论如何,您都可以很简单的解决维度问题。

那么,断链就说完了。不过我们只实现了一部分关于 Function 的功能。如果我们要将一个 Function 转换为指令意义上等效的命令方块形式,那么我们应该怎么做呢?
假设有一个函数A,它里面有如下几个命令:
Execute @e[tag=C] ~ ~ ~ Function B
Execute @e[tag=D] ~ ~ ~ Kill @s
然后有一个函数B,它里面有如下几个命令:
Say 1
Say 2
然后,我们如何将命令“/Function A”转化为纯命令方块形式呢?也就是说,我们要只依靠命令方块实现函数A的功能。但我们仔细观察函数A,我们发现它可能对多个实体使用了函数B……
于是显而易见的,我们不容易使用纯命令方块来实现函数A的功能,因为我们无法确定函数B到底会被执行几次,以及哪些实体会被执行这些命令。
这便是函数的优势所在。但我们仍然可以只用命令方块完成函数A的功能,我们需要“给指令加点糖”。
那么,我们怎么“加点糖”呢?答案是使用递归(俗称“自闭链”)。
让我们看一个递归链的模型:

按照正常的逻辑,一般情况下执行这一个链只会执行7个命令方块内的指令。但我们可以使用特殊方法让它执行更多的命令,也就是在1GameTick内重复执行右边6个链命令方块里面的命令。
那么,我们应该怎么做呢?
很简单,我们只需要了解一个特性就行了——假定一个链命令方块G已经在该时刻内执行过,那么我们可以把另一个没有在该时刻内被执行过的链命令方块复制到链命令方块G所在的位置,然后链命令方块G处的命令方块就又可以被执行了。
利用这一个特性,我们可以做到“自闭”的效果。
我们为这些命令方块标上号,如 图1 所示

然后,我们再把这些命令方块复制一个模板到另一个位置,然后为模板上的命令方块标上号。需要注意的是,模板中的命令方块不会被执行,具体如 图2 所示。

然后,我们执行①,接着就会执行②、③、④、⑤、⑥和⑦。我们在⑦和7所在的命令方块放入同一个 Clone 命令,其作用是将2、3和4复制到②、③和④。这样,我们更新了第二行中的链命令方块,然后游戏就会认为这些链命令方块没有被执行过,然后重新再执行一次②、③和④。
同理,我们可以在④和4中放入同一个 Clone 命令,其作用是将5、6和7复制到⑤、⑥和⑦,然后游戏就会再执行一次⑤、⑥和⑦。
然后,一个递归链就做成了。一般地,①不会参与递归,那么从①到②这一过程,我们可以称为递归的转入。
于是,我们可以很容易地实现函数A的功能了。我们再次看到函数A和函数B:
函数A:
Execute @e[tag=C] ~ ~ ~ Function B
Execute @e[tag=D] ~ ~ ~ Kill @s
函数B:
Say 1
Say 2
将“Execute @e[tag=C] ~ ~ ~ Function B”解析一下,其大意就是让每一个在加载区域内且带有标签C的存活的实体执行函数B。那么,我们只需要统计这些实体的数量,然后将这些实体加入到待遍历列并 逐一的 遍历它们。
我们刚刚统计的实体总数将作为遍历总次数,也就是我们需要递归的次数(要重复执行命令的次数)。
每遍历一个实体,我们就从总次数中减去1,直到总次数小于等于0,不再需要遍历。
也就是说,我们在开始递归前生成一个辅助实体J,然后将次数赋值给它,当实体J的分数≤0时就不再更新命令方块,也就是不再 Clone 。当实体J的分数≥1时就更新命令方块,也就是继续 Clone 。
让我们重新看到刚刚的那两个图和函数A:


不难发现,在命令“Execute @e[tag=C] ~ ~ ~ Function B”的后面还有一个命令“Execute @e[tag=D] ~ ~ ~ Kill @s”。
很显然,我们得在递归完成后执行一个“Execute @e[tag=D] ~ ~ ~ Kill @s”命令。因此,我们需要在递归完成后转出递归以进入非递归状态。
那么,我们可以在实体J分数≤0时选择将链命令方块⑦的朝向改变为向上的朝向,然后在这个方向上放置剩余的命令方块(如 图3 所示),然后递归就成功转出了。在转出递归后,我们先要清理辅助实体J,然后再执行函数A剩下的指令“Execute @e[tag=D] ~ ~ ~ Kill @s”。

需要注意的是,我们绝对不会在执行⑦时才改变朝向,而是在执行⑥或之前的命令方块时就改变了朝向。而且,由于最终是在⑦处转出的递归,那么你必须让第一行的命令方块始终保持更新,否则可能会出现第二行的命令方块执行后就不再执行命令的问题。
一般地,⑦上面的命令方块不会参与递归,那么从⑦到其上方命令方块的这一过程,我们可以称为递归的转出。


结语
那么本篇专栏的干货到此就结束了。我主要讲述了 Function 与 命令方块 之间的联系与转译函数的具体操作过程。可以说,使用 Function 的思维可以很有效的解决卡顿问题。
有了本篇专栏的知识传播,我也相信任何函数都可以很简便的被转为纯命令方块形式,供纯命令方块玩家的借鉴、使用和命令的优化。