pca006132
本帖最后由 pca006132 于 2017-10-22 15:24 编辑

概述
本文将会简述如何使用命令函数实现一些编程里常用的控制结构,包括if, while循环, for循环,以及如何实现exit, break及continue等中断执行的语句。最后会探讨如何实现命令函数之间的参数传递及结果回传。目标版本为1.13,虽然大部分也不特别需要1.13的特性。
下文的转换方法是特地为了程序而设计的,人手进行操作可能会比较麻烦,仅供参考之用。这些转换方法主要考虑的是实际执行效率而不是可读性,故此生成出的东西可读性会比较差。例子里经常会以 A/B/C/X/Y/Z 等代号指代不限数量的命令,故并不会对看似单个命令的代号进行优化,但读者应清楚若实际上那部分只是单一命令就不必放在独立命令函数里。
这里一切转换建基于命令函数是由实体执行,因为大量数据需要依赖执行者来储存。如果不是实体执行就需要透过 execute as <entity> then function <name> 来让实体执行此命令函数。
语法定义
为了方便起见,我们会定义一些语法供本文例子使用。本文语法使用 Off-side rule ,也就是以缩进表示代码块的范围。
<>代表需要替换为指定内容。
注释:
  1. //单行注释
  2. /* 多行
  3.      注释 */
复制代码

--------------------------
命令函数定义:
  1. def <名称>:
  2.     内容
复制代码
内容内每一行代表一条命令。
本文假设所有命令函数都是在 system 命名空间里的。

--------------------------
If:
  1. if [not] <selector>:
  2.     选择[不]到目标时执行的内容
  3. elif [not] <selector>: //else if,如果上方失败时才进行检测。
  4.     选择[不]到目标时执行的内容
  5. //经过任意个elif之后
  6. else: //前方所有检测都失败
  7.     前方所有检测都失败后执行的内容
复制代码
注意不一定需要有elif及else部分,但必须有if部分。
[not]可选,如果写上了就代表检测不到目标时执行,检测到目标代表失败。

--------------------------
While(先检测条件后循环):
  1. while [not] <selector>:
  2.     当selector选择到目标时循环执行的内容
复制代码

--------------------------
Exit: 离开当前的命令函数(def的部分,不是生成后的命令函数)
  1. exit //好像一般命令那样使用
复制代码

--------------------------
Continue: 跳过当前迴圈继续执行
  1. continue //好像一般命令那样使用
复制代码

--------------------------
Break:    离开当前循环继续执行
  1. break
复制代码

--------------------------
If处理
如果只有if部分而没有任何elif或else,则可以进行特殊优化。首先把内容部分放进另外的命令函数,名字不会和其他发生冲突即可,把名字记为 name。然后把if的语句进行以下转换:

--------------------------
假如有elif/else的部分,我们则会先找出符合哪个条件,储存在分数里,然后根据分数来选择执行哪个部分。这是为了避免先执行内容部分可能导致的副作用,如影响条件判断等。注意该记分板变量(Objective)必须为独一无二,不同If部分需要有不同的记分板变量,不然可能有别的命令修改了这个分数导致错误。
  1. if A:
  2.     W
  3. elif B:
  4.     X
  5. elif not C:
  6.     Y
  7. else:
  8.     Z
复制代码
转换为
  1. scoreboard players set @s $a 0
  2. execute if A then scoreboard players set @s $a 1
  3. //如果分数$a <= 0则代表前面的条件不成功
  4. execute if @s[score_$a=0] if B then scoreboard players set @s $a 2
  5. execute if @s[score_$a=0] unless C then scoreboard players set @s $a 3

  6. //假设system:$w代表 W 命令组,system:$x代表 X 命令组,如此类推。
  7. execute if @s[score_$a_min=1,score_$a=1] then function system:$w
  8. execute if @s[score_$a_min=2,score_$a=2] then function system:$x
  9. execute if @s[score_$a_min=3,score_$a=3] then function system:$y
  10. //所有都不成功则$a分数还是0
  11. execute if @s[score_$a_min=0,score_$a=0] then function system:$z
复制代码
While 循环
我们会使用递归模拟循环。
  1. def bla:
  2.     while A: //这里的A代表的是选择器而不是命令组
  3.         X
复制代码
会被转换为
  1. def bla:
  2.     //先判断条件后执行内容。
  3.     //如果需要做到not则把function if更换为unless
  4.     execute if A then function system:$a

  5. //while的命令函数
  6. def $a:
  7.     //while的内容
  8.     X
  9.     //最后判断需不需要继续递归
  10.     //如果需要做到not则把function if更换为unless
  11.     execute if A then function system:$a
复制代码
Exit, break和continue
这个的概念比较复杂,和代码块有关。我们先以exit为例,其余两者亦是类似做法。
我们把if, while, def等视作一个代码块, exit 命令后和 exit 在同一个代码块内的命令会被忽略掉,因为逻辑上它们不会被执行。如:
  1. def test:
  2.     if A:
  3.         exit
  4.         //B在exit之后并且和exit在同一个代码块内,所以会被忽略掉。
  5.         B
  6.     //X在exit之后但不和exit在同一个代码块内,所以不会被忽略掉。
  7.     X
复制代码
exit 命令会被替换为 scoreboard players tag @s add $exit_tag
exit 命令所在代码块(包括直接和间接,比如上方例子中的 def test 也是exit命令所在的代码块)后的命令会被放进别的命令函数里,根据执行者有没有那 $exit_tag 来判定是否需要执行剩下的命令。这一步会递归处理直至到达“目标代码块”,比如exit就是def部分,continue就是while的内容部分(就是不包括最后的判定是否继续递归),break就是整个while的命令函数(就是包括最后的判定是否继续递归)。
最后在“目标代码块”完结的时候移除掉那个tag以重用tag($break_tag $continue_tag$exit_tag 都可以重用,因为不会被其他命令函数影响及不会影响别的命令函数。读者可以自行尝试证明。)
  1. def a:
  2.     if A:
  3.         if B:
  4.             exit
  5.         X
  6.     Y
复制代码
变换为
  1. def a:
  2.     execute if A then function $ifa
  3.     execute if @s[tag=!$exit_tag,tag=!$break_tag,tag=!continue_tag] then function $aremaining
  4.     scoreboard players tag @s remove $exit_tag
  5. def $ifa:
  6.     execute if B then function $ifb
  7.     execute if @s[tag=!$exit_tag,tag=!$break_tag,tag=!continue_tag] then function $ifaremaining
  8. def $ifb:
  9.     scoreboard players tag @s add $exit_tag
  10. def $aremaining:
  11.     Y
  12. def $ifaremaining:
  13.     X
复制代码

--------------------------
我们可以对break进行优化,与其在每次循环的最后移除一次tag造成大量重复的无用执行,我们可以选择在循环的调用完结后移除$break_tag
  1. def bla:
  2.     while A:
  3.         if B:
  4.             break
  5.         X
  6.     Y
复制代码
可以转换为
  1. def bla:
  2.     execute if A then function system:while_a
  3.     scoreboard players tag @s remove $break_tag
  4.     Y
  5. def while_a:
  6.     execute if B then function system:b
  7.     execute if @s[tag=!$break_tag] then function system:while_a_remaining
  8. def while_a_remaining:
  9.     X
  10.     execute if A then function system:while_a
  11. def b:
  12.     scoreboard players tag @s add $break_tag
复制代码
参数传递及结果回传
我们只容许传递记分板分数。
参数假名名称为 #param$<id>id 从0开始,比如是 #param$0 #param$1
回传假名名称为 #return$<id>id从0开始,n个回传的数据就有n个
共同记分板名称为 $internaldata
因为id是共用的,如果用户需要调用别的函数后继续使用那些参数的数值则需要对那些数值进行备份,因为其他函数的执行可能会改变那些参数的数值。
如果强制所有参数传递都进行备份或者是实现栈一类的结构,在游戏里的效率会比较低,而且很多时候也是不必要的,故此建议由程序整理参数的使用然后根据需求进行备份。
建议使用假名进行备份,离开function的时候reset那些假名的分数。
所有暂时使用的假名的id为
#<模块名称的hash,32位16进制>$<在该模块内独一无二的标识>
#69E7AEA8$1

特别鸣谢


pineapple_
没人气了,先收藏吧

就算1.13特性用的很多也没关系!因为假如等这个帖子被审一遍,1.13应该也就出了

仅仅对于mc的命令方面来说,越来越接近编程游戏了……

秋一
先顶个帖,慢慢看一下……

Mithey
java版越来越偏向专业玩家了emmm

縻风
前几天刚看完书,现在一看清楚多了。谢谢lz

1377023219
minecraft Java 1.13可以用if了吗?

45gfg9
港真,感觉这个语法好像Python的说

pca006132
45gfg9 发表于 2017-10-23 22:47
港真,感觉这个语法好像Python的说

没错,我喜欢python的语法

tineseack_bk
凑个热闹....
好久没开bbs了刚刚才看到抱歉

花旦
来写个伪解释器吧0.0   寒假开坑系列0.0

Hao_cen
最新的快照 execute 的 then 已经改成 run 了 希望能把代码改一下