Cat_Anchor 发表于 2024-8-21 07:00:59

附加包教程:47.自定义组件(二)

本帖最后由 Cat_Anchor 于 2024-8-22 09:25 编辑

注意:此页面所述功能是方块和物品事件的新版本,也就是假日创作者实验移除后的正式特性。由于作者刚学 JavaScript,内容可能有误,我会标注代码的游戏内效果和漏洞。




前言

上期,我们学习了如何注册自定义组件。这期,我们将列举所有自定义组件的触发器,并写出一些例子。


https://klpbbs.com/static/image/hrline/line5.png


触发器

以下是所有可用的方块和物品的触发器与它们的解释。触发器解释beforeOnPlayerPlace玩家放置方块前onEntityFallOn实体掉落在方块上后 ①②onPlace方块被放置后onPlayerDestroy玩家破坏方块后 ③onPlayerInteract玩家右键点击方块后onRandomTick方块收到随机刻更新后onStepOff实体走出方块后 ②onStepOn实体走上方块后 ②onTick方块收到计划刻更新后 ④onBeforeDurabilityDamage物品受到耐久损耗前onCompleteUse物品使用完成后onConsume物品被食用后onHitEntity物品使实体受伤后onMineBlock物品挖掘方块后onUse物品使用后onUseOn物品在方块上使用后①:此触发器需要 minecraft:entity_fall_on 组件。
②:此触发器需要方块在 Y 轴有至少 4 像素的碰撞箱。
③:玩家必须在生存模式下,而且必须完全破坏方块。
④:此触发器需要 minecraft:tick 组件。每个触发器能访问的数据可能是不一样的,比如方块触发器能访问方块本身和方块所处的维度,涉及到玩家的触发器还能访问玩家,而物品触发器能访问物品本身和使用物品的玩家等。



https://klpbbs.com/static/image/hrline/line3.png


示例

这是上期最后的代码。
import { world, system, BlockComponentPlayerInteractEvent, BlockComponentTickEvent, EquipmentSlot, ItemStack, Player, EntityComponentTypes } from '@minecraft/server';

const blockComponents = [{
id: "supplementary:invert_texture_shadow",
trigger: {
    onPlayerInteract: (e) => { 我是代码 }
}
},{
id: "supplementary:up_connected_detector",
trigger: {
    onTick: (e) => { 我也是代码 }
}
}];

world.beforeEvents.worldInitialize.subscribe((i) => {
for (const c of blockComponents) {
    i.blockComponentRegistry.registerCustomComponent(c.id, c.trigger);
}
});
这期,我们就来填充“我是代码”部分。我们先不看上面代码里的第二个自定义组件,而先看第一个自定义组件,它的作用是检测手中的物品并设置方块状态。具体来说,假如我手中的物品是木棍,那么将布尔方块状态 supplementary:texture_inverted_bit 设为它目前的相反值。所以只要稍微改造一下这个代码,就能变成骨粉催熟植物之类的功能,非常方便。

首先,我们来检测一下触发这个事件的实体是不是玩家,如果不是就直接不执行下面的代码,防止非预期的作用。有时候为了性能考虑,使用某些触发器后可以不检测玩家,也就是不用写这一行。
if (!e.player) return;
然后,我们关注于这个组件要实现的功能:检测物品和设置方块状态。那么首先,我们要获取需要检测的物品和目前的方块状态值,方便以后直接设为相反值。
所以我们定义两个常量,stateValue 和 mainhandItem。
const stateValue = e.block.permutation.getState('supplementary:texture_inverted_bit');
const mainhandItem = e.player.getComponent('equippable').getEquipment("Mainhand");
我们首先来分析一下 stateValue。它被赋值为后面那一长串,我们来拆解一下:e 代表事件数据,e.block 就是被玩家点击的方块。那么 e.block.permutation 就是这个方块的附加数据——方块状态。这就是我们想要的!继续向后看,e.block.permutation.getState 中的 getState 就是获取方块状态的方法。它只有一个参数,那就是方块状态的名称,这里是 supplementary:texture_inverted_bit,所以 stateValue 被赋值为目前这个方块状态的值。
然后来看看 mainhandItem。与之前一样,e 代表事件数据,e.player 就是点击方块的玩家。玩家有很多组件,我们需要获取装备栏组件,也就是 getComponent('equippable')。获取了装备栏之后,我们需要获取装备栏中的主手栏,也就是 getEquipment("Mainhand"),它会返回一个物品堆栈。所以这一长串的意思就是将 mainhandItem 赋值为玩家手上的物品堆栈。
接下来,我们可以实现逻辑了。有很多情况导致执行失败,比如方块状态被改为其他类型而不是布尔型,或者方块状态还没有定义,或者手中没有物品等。还有一种情况,那就是手中物品不是木棍。这些情况下,我们需要停止执行接下来的代码,所以写:
if (stateValue === undefined || typeof stateValue !== 'boolean' || !mainhandItem || mainhandItem.typeId !== 'minecraft:stick') return;
多个条件之间是“或”的关系时,我们可以用 || 连接,这个符号表示逻辑上的“或”,只要有一个通过就通过。
如果 stateValue 返回了 undefined(也就是未定义),那么达成条件。这里的 === 是严格相等,只有类型相等才继续判断。
如果 mainhandItem 返回了 undefined(也就是手中没有物品),那么它会被转换为 false。前面加个 ! 会让它变成 true,达成条件。
而 typeof 关键字返回值的类型,这里它应该返回 boolean 字符串。这里的 !== 是严格不相等,只要类型不相等就不相等,如果返回了其他字符串,那么方块状态已经被修改了,最后返回 true,达成条件。
最后一个是主要的逻辑检测。mainhandItem 是一个物品堆栈,它有一个属性叫 typeId,它返回物品的 ID(字符串)。我们可以用 mainhandItem.typeId === 或 !== 'ID' 检测,这里就是检测手中物品是否不是木棍。这个就相当于以前的 q.is_item_name_any('slot.weapon.mainhand',0,'物品 ID') Molang 查询。不过更常见的可能是它的已弃用的前身——q.get_equipped_item_name == '物品 ID' Molang 查询。
总之,这串检测不仅保证了代码不会异常执行,还完成了主要逻辑检测。

接下来,为了保险,我们再写一个条件执行,如下:
else if (mainhandItem.typeId === 'minecraft:stick') {}
现在就可以设置方块状态了。
e.block.setPermutation(e.block.permutation.withState('supplementary:texture_inverted_bit', !stateValue));
又是一大串代码,我们来分解一下:e.block 代表事件涉及到的方块,setPermutation 是它的方法,用于设置方块状态。它只需要一个参数,那就是方块状态。e.block.permutation 里正好有一个 withState 方法,它的作用就是指定方块状态名称和值并返回方块状态。它有两个参数,方块状态的名称和它的值。名称是固定的,supplementary:texture_inverted_bit,而值就是之前获取的 stateValue 的相反值,所以在前面加 !。这一大串下来,就完成了把方块状态设为目前的相反值——也就是切换方块状态的功能。
设置完方块状态就返回,所以代码成了这样:
else if (mainhandItem.typeId === 'minecraft:stick') {
e.block.setPermutation(e.block.permutation.withState('supplementary:texture_inverted_bit', !stateValue));
return;
}
现在我们实现了所有功能——检测手中物品并设置方块状态。不过它有个漏洞,那就是拿着方块点击时不会放置,我不知道怎么修复。

接下来我们看第二个自定义组件,它的作用是根据上方的方块设置方块状态。首先,它是根据计划刻触发事件的,所以需要一个 minecraft:tick 组件告诉引擎这个方块多久触发一次计划刻,以及是不是循环触发。这个组件的语法基本相当于以前的 minecraft:queued_ticking 组件去掉了 on_tick 字段,如下:
"minecraft:tick": {
"looping": true,
"interval_range": [
    5,
    5
]
}
以上组件的意思是每 5 刻(0.25 秒)触发一次,而且要循环触发。

现在可以专注于脚本了,我们来看看要实现的逻辑:检测上方一格的方块是否有指定标签并设置方块状态。
所以我们先获取上方的方块,定义一个常量 topBlock 并赋值,如下:
const topBlock = e.block.above();
这里的 e.block 是事件涉及到的方块,而后面的 above 方法会获取上方一格的方块。它还有个可选的参数,指定获取上方多少格的方块,不过默认为一,我们就省略了。
还有两种方法获取周围的方块,第一种是 block 类中的 offset 方法,获取相对坐标系中的方块。第二种是 dimension 类中的 getBlock 方法,获取绝对坐标系中的方块。这两个方法都需要定义一个向量数组,比如 const topBlockLocation = [{x:0,y:0,z:0}];,太麻烦了,这里用不到。

接下来,我们就能判断这个方块到底有没有指定标签了。
if (topBlock && topBlock.hasTag('valley_tile_bracket')) {}
block 类中有一个 hasTag 方法,用于判断这个方块有没有特定标签,它的唯一一个参数是标签名,我们正好可以用一下。
上面的第一个 topBlock 判断首先保证了这个方块不是空的,然后判断是否有 valley_tile_bracket 标签,两个条件都满足后才设置方块状态,如下:
if (topBlock && topBlock.hasTag('valley_tile_bracket')) {
e.block.setPermutation(e.block.permutation.withState('supplementary:connected_bit', true));
return;
}
这里的 e.block.setPermutation 之前讲过了,不再赘述。
接下来,如果上面没有方块,或者没有指定标签,就要把这个方块的方块状态设为 false,如下:
else if (!topBlock || !topBlock.hasTag('valley_tile_bracket')) {
e.block.setPermutation(e.block.permutation.withState('supplementary:connected_bit', false));
return;
}
第二个自定义组件也填写完成了,我们实现了所有功能。我暂时没有找到这个组件的漏洞,不过没有检测方块状态是一个潜在的漏洞。
以上所有代码综合起来,再放在数组里,是下面这样的:
const blockComponents = [{
id: "supplementary:invert_texture_shadow",
trigger: {
    onPlayerInteract: (e) => {
      if (!e.player) return;
      const stateValue = e.block.permutation.getState('supplementary:texture_inverted_bit');
      const mainhandItem = e.player.getComponent('equippable').getEquipment("Mainhand");
      if (stateValue === undefined || typeof stateValue !== 'boolean' || !mainhandItem || mainhandItem.typeId !== 'minecraft:stick') return;
      else if (mainhandItem.typeId === 'minecraft:stick') {
      e.block.setPermutation(e.block.permutation.withState('supplementary:texture_inverted_bit', !stateValue));
      return;
      }
    }
}
}, {
id: "supplementary:up_connected_detector",
trigger: {
    onTick: (e) => {
      const topBlock = e.block.above();
      if (topBlock && topBlock.hasTag('valley_tile_bracket')) {
      e.block.setPermutation(e.block.permutation.withState('supplementary:connected_bit', true));
      return;
      } else if (!topBlock || !topBlock.hasTag('valley_tile_bracket')) {
      e.block.setPermutation(e.block.permutation.withState('supplementary:connected_bit', false));
      return;
      }
    }
}
}]


https://klpbbs.com/static/image/hrline/line7.png


总结

这一期,我解释了两个自定义组件。我的代码可能有误,或者不完善,或者可以优化,欢迎指出错误并提出建议。




第四十六期 第四十七期 第四十八期

指令凋灵 发表于 2024-8-21 12:23:32

本帖最后由 指令凋灵 于 2024-8-21 12:29 编辑

如果为了性能考虑一些事件不需要判断是否为player,matches的地方可以改成ItemStack.typeId === 物品英文id
还有一个建议,用不到的模块可以不导入(比如文中的Block、BlockEvent等)
页: [1]
查看完整版本: 附加包教程:47.自定义组件(二)