跳到主要内容

初了解实体属性

· 阅读需 5 分钟

前几天我非常偶然地了解到了实体属性这个东西。

当时正值深夜时分,我躺在床上,没什么事,偶然间翻开了微软文档的更新内容。网易目前还是 1.20.50,所以我看看这个版本都能用一些什么东西。结果,不知不觉我就翻到其他页面去了。其中,有一篇内容吸引了我的注意:

1

其实我是知道 1.20.70 加了个has_property目标选择器参数这回事的。不过当时,我更多地从命令角度去考虑这个参数,当时下了一个结论:这东西也就行为包用用有用,给原版命令玩家用没啥用。

不过现在当我回过头来再看,点进去看的时候,我很惊讶:

woc,这不就是minecraft:variantminecraft:is_sheared的完美平替吗???

现在就是,你说我干嘛要从租赁服服主的角度去想这东西呢??

这东西是什么东西呢?准确来说,它算是一个标记实体状态的东西,和方块状态定义那些东西也差不多。但要是谈优势,那可太多了:

  • 可以自定义属性名称。我原先在一些项目中做过类似于“门”的实体。当时我的“解题”思路是这样的:用is_sheared去标记门是开是关,这也是当时在某个夏令营中所学习到的方法。但是这个is_sheared吧……就很模糊你知道吧,后来的我要怎么知道有它是开还是没它是开呢?对于我这种超级重视易读性,注释比代码多的开发者来说,真的是灾难。
  • 有 3 种类型可供选择。毕竟以前那些标记组件只支持intbool,或者说近似地认为它们支持bool吧,毕竟不是真的bool。但是现在它还支持enum,允许枚举值的存在。太好了!
  • 最多可以指定 32 种属性。放在以前,这种标记组件撑死 10 种。
  • 切换方便。以前想更改什么状态,还得先eventcomponent_groups,但是现在直接一个eventset_property就解决了。而且:它还支持 Molang!它支持 Molang 啊!!让我想起原先我做过一个类似于电池的实体,要用variant记录剩余能量信息。好在“甲方”给的数值范围不太大,1-10,要知道用variant就只能穷举,但现在可以用表达式了,太好了!
  • 以前能做的它也能做。比如在资源包可能用query.variant去检测,但是现在用query.property('...')==...能实现和以前同样的效果。
  • 最关键的:网 易 支 持 !!! 其实我能猜到为啥:ojng Mojang 可能是为了鼓励开发者使用实体属性,也是为了方便他们自己开发,所以原版蜜蜂就用了大量的实体属性,并且后来的实体也用了大量属性,结果导致网易这方便就算不想做适配也得适配,除非你网易宣布未来永远不更新。ojng Mojang 你干的好啊(无贬义)!

那请问,我还有什么理由拒绝这东西呢?以后再也不会用variantis_sheared这些东西了。

那天晚上,我还第一时间跟知名开发者 @E尘 聊了聊这回事。我发现他也挺兴奋的。

2

总之,这次的粗略了解确实是令我收获很多。显而易见的是,下一个作品中,我就要用到这东西了。

关于退出重进时if block和if blocks的表现研究,以及预加载常加载区域的表现研究

· 阅读需 15 分钟

最近巴豆在开发《冒险世界:筑梦》。在我的“怂恿”之下,他选择使用全函数。

不过,在开发到逍遥矿井地牢时,却出现了一些问题。退出重进游戏时,会导致一个小游戏的判定和御风珠的判定直接炸了。这是一个很难处理的问题,所以我和巴豆决定一起研究 bug 的成因,并尝试解决。

为什么判定炸了?

巴豆采用的方法,是判断玩家是否拿走了箱子里的物品。源代码如下:

#破解谜题
execute if score 3_shaft_2 data matches 0 if blocks -95 -38 128 -95 -34 130 -93 -38 128 all run tellraw @a {"rawtext":[{"text":"§e你破解了谜题,箱子上的障碍清除了!"}]}
execute if score 3_shaft_2 data matches 0 if blocks -95 -38 128 -95 -34 130 -93 -38 128 all run setblock -95 -40 119 air
execute if score 3_shaft_2 data matches 0 if blocks -95 -38 128 -95 -34 130 -93 -38 128 all run scoreboard players set 3_shaft_2 data 1

#拿到御风珠
execute if score 3_shaft_item data matches 0 if blocks -139 -33 111 -139 -33 111 -139 -35 111 all run tellraw @a {"rawtext":[{"text":"§e你拿到了御风珠,向奇怪的方块扔出试试吧!"}]}
execute if score 3_shaft_item data matches 0 if blocks -139 -33 111 -139 -33 111 -139 -35 111 all run scoreboard players set 3_shaft_item data 1

两段代码的核心内容,都是if blocks ...检测待检测的箱子和空箱子是否一致,以判断玩家是否拿走了物品。

在正常情况下,这段代码并没有任何问题。但是,如果我们进入逍遥矿井后,退出游戏再重进,就很容易看到手上多了一个物品,在聊天栏也能看得到那句“你拿到了御风珠,向奇怪的方块扔出试试吧!”。看来,这条命令在不该执行的情况下执行了。

我们来看一下这条命令是如何执行的。它只有两个条件:

  • if score 3_shaft_item data matches 0
  • if blocks -139 -33 111 -139 -33 111 -139 -35 111 all

但是,记分板的问题应该是可以立刻排除的。因为退出重进涉及到的最重要的问题就是加载问题,但是记分板一般不太涉及到加载问题;而if blocks涉及到方块加载,就很可能涉及到加载问题了。

看来,if blocks在加载过程中的检测很可能出现了问题。

修复的尝试思路 1 - 使用if block

问题找到了,怎么解决呢?巴豆一开始告诉我的方案是,用if block检测箱子。

1

不过我并不明白这么做能否切实地修复这个问题。既然if blocks炸了,if block就不会吗?

2

对这个解决思路的验证

过了一天后,我决定做一个简易的实验来看看这种修复方法是否有效。我在main.mcfunction中(这个函数是始终运行的)写入了下面的命令:

实验代码
execute unless entity @a if block -165 -59 124 chest run say if block 检测的结果为 true
execute unless entity @a unless block -165 -59 124 chest run say if block 检测的结果为 false
execute unless entity @a if blocks -165 -59 124 -165 -59 124 -166 -58 124 all run say if blocks 检测的结果为 true
execute unless entity @a unless blocks -165-59 124-165 -59 124-166 -58 124 all run say if blocks 检测的结果为 false

其中,unless entity @a在检测不到玩家时通过,所以就代表了在玩家加载前所执行的命令。而我在(-165,-59,124)和(-166,-58,124)放了一个同向的箱子,其中一个放了物品,另一个则没放:

3

那么,如果一切正常的话,退出重进后if block应该得到trueif blocks应该得到false。但实际情况,却得到这样的结果:

4

看来,猜的确实不错,if blocks在加载过程中的检测确实出现了问题,它先是返回了一堆true,然后在玩家加载的“前夕”返回了正确结果false。所以,从if blocks的返回角度分析,可以猜测这样的加载顺序:

  1. if blocks开始检测(加载到有箱子,但没有检测里面的内容,所以为true
  2. NBT加载(检测到里面的内容不一致,所以为false
  3. 玩家加载(后续停止运行)

我接着从if block的角度分析,发现它一个if block都没有返回。看来,if block至少是在玩家加载之后才开始进行检测的。那么这个过程就可以扩展出第 4 步:

  1. if blocks开始检测(加载到有箱子,但没有检测里面的内容,所以为true
  2. NBT加载(检测到里面的内容不一致,所以为false
  3. 玩家加载(后续停止运行)
  4. if block开始检测(因为所有if block都没有反馈)。

至少从现阶段来说,巴豆的修复方法是可行的。因为当if block开始检测时,这时的if blocks已经能够返回一个正确结果。不过,现在看来,其实加一个if entity @a也是可行的修复方法。

修复的尝试思路 2 - 使用预加载的常加载区域

然而,我给出的修复思路和巴豆不同:添加一个预加载的常加载区域。

既然问题出在加载时判断错误,那么只要让它保持加载不就好了吗?

道理当然是这个道理,但是其实我一开始根本不知道预加载的常加载区域有什么用!

其实我们很早就发现了这个问题,很早就发现了问题根源在于if blocks检测错误,并且已经尝试过修复。当时的修复尝试,是添加常加载区域。但是,最终却没能成功修复这个问题。所以,我才想试一下如果添加预加载,是否可行。

这是一个在 1.18.30 加入的功能。当时我尝试过这个预加载参数[preload: Boolean],然而根本就没有搞清楚这个东西的作用,今天正是一个好机会。

说白了,我给出解决思路的时候,和巴豆给出他的解决思路的时候,我们两个人都没有任何把握能够保证这个问题能够以此解决。

事后证明:我们是很幸运的,因为两个办法都是可行的

对这个解决思路的验证

我们还是使用上面的实验代码,同样地箱子环境,进行退出重进。不过这次,我们为这个常加载区域添加了预加载参数(preload=true)。结果和普通的常加载区域完全不同,看来预加载起作用了!

5

相比于普通的常加载区域,不仅if block开始返回数值,if blocks也从一开始就能够返回正确的数值。这说明,这些区块早早地就在玩家加载完成之前加载完毕,相比于其他加载区块,这些「预加载的常加载区域」的优先级在加载世界时也是非常高的,以至于连if block都能够成功执行。对于预加载情况来说,就是下面的步骤:

  1. 区块预先加载
  2. if blocks已经开始正确检测(加载到有箱子,但没有检测里面的内容,所以为true);
    if block开始检测(因为所有if block都没有反馈)
  3. 玩家加载(后续停止运行)

等下!如果没有常加载区域……

这是巴豆问的一个很关键的问题。我在上面跟他分析叭叭了一大堆之后,他就问出了这个问题。这个问题提的非常好,所以我就做了一个这个实验。

6

移除它的常加载区域后,然后跑到很远很远的地方,并退出重进执行这段实验代码。下图就是实验结果。

7

是的,我当时在看老三国,太好看了,就挂在后台上看(逃

和有普通常加载区域的结论不同的是,if blocks光返回错误信息了,正事不干。

这引起了我的好奇。既然区域都完全没有加载,if blocks是怎么执行的呢?因为如果单独在未加载的区块执行if blocks,无论如何都会报错的呀?

8

这背后定有猫腻!所以我决定:干脆挖掉一个箱子,让一个空气和一个箱子检测。这绝不可能出现 NBT 检测差异的可能性,无论如何都要返回false了吧?

但结果,却让我大吃一惊。居然还返回true?一个箱子和一个空气居然还是检测成功???

9

看来,if blocks在这个世界加载完毕之前,根本就是瞎检测的!我大吃一惊,这根本就是一个游戏漏洞啊!难怪 Mojang 要加入预加载常加载区域,难道就是为了处理这种问题吗?

结论:世界加载顺序的结论推测与问题修复方法

经过上面的分析,我和巴豆得出了世界加载的顺序的结论:

  1. 区块预加载,则区块加载完毕:
    (1)if block开始检测:有预加载常加载区域时:检测到有箱子,所以为true
    (2)if blocks开始检测:有预加载常加载区域时:检测到里面的内容不一致,所以为false
  2. 如果未开启区块预加载,if blocks开始检测(瞎检测的,无论是什么东西都返回true
  3. 区块开始加载,if blocks能够正确检测:
    (1)有普通常加载区域时:检测到里面的内容不一致,所以为false; (2)无常加载区域时:无法执行,所以不会返回任何东西。
  4. 玩家加载(后续停止运行)
  5. 未开启预加载,区块后于玩家加载而加载完毕,if block开始检测:
    (1)有普通常加载区域时:因为玩家加载完毕,所以无法执行; (2)无常加载区域时:因为玩家加载完毕,所以无法执行。

也就是说,if blockif blocks都是需要区块加载完才能执行的,但是if blocks自身存在问题,在区块开始加载前会有一小段执行期,乱检测(可能是全检测为空气了)而全部返回true

以及相关问题的修复方法,有以下几种:

  1. 额外插入if entity @aif block ...,这两种情况返回true时,if blocks已经能够返回一个正确的结果。
    虽然后来,巴豆跟我反馈说if entity @a似乎并不会奏效,个人猜测或许是没有加常加载区域导致的问题。
  2. 添加一个预加载的常加载区域,它可以大幅提高世界加载时该区块加载的优先级,对于这种需要if blocks检测的情况很有帮助。这似乎也是更合理的解决方式。
  3. 或者,寻求一种方法,干脆避免使用if blocks。很明显就是因为这东西有坑

希望这篇文章可以帮助到所有被相关问题困扰的开发者!