跳到主要内容

关于网易更新到 1.21.0 的开发内容更新汇总

· 阅读需 13 分钟

首先也是恭喜我们的中国版终于跟上了国际版的步伐,很可能是自从 2017 年代理以来首次在次版本上和国际版保持一致。能看到中国版变得越来越好,我也很开心,祝贺祝贺!

现在摆在我们开发者面前的一个新问题就出现了:我们都能用什么新东西了呢?

首先,在 ModAPI 方面,我这里不再给出过多解释,直接看网易官方给出的文档即可。这里给一个链接:3.4 更新信息

那么在国际版的版本号上,这次是进了 4 个国际版的小版本号:1.20.60、1.20.70、1.20.80 和 1.21.0。更新内容上从狼铠到试炼密室,内容还是很多的,但本文的重点并不在这些普通的游戏内容上,主要是对国际版新支持的开发内容进行梳理。当然,也包括 SAPI,虽然中国版不能用 SAPI,但是对于像我这种双端开发者来说,了解 SAPI 都能做到哪些功能也还是有意义的嘛。


以下部分内容摘抄自中文 Minecraft Wiki 和微软文档

命令方面

  • 新增对/hud的支持。本文档也已经完成了相关教程的编写,感兴趣的读者可以在这里查阅

  • 新增对has_property目标选择器参数的支持。这个参数配合自定义附加包使用会相当有用,纯原版嘛……emmm,聊胜于无吧,哈哈。

  • ID 拆分,这是保留节目了。总之就是爆拆巨拆,大拆特拆

    物品或方块旧 ID新 ID
    海龟鳞甲scuteturtle_scute
    草方块grassgrass_block
    树叶leavesleaves2oak_leaves​spruce_leaves​birch_leaves​jungle_leavesacacia_leaves​dark_oak_leaves
    木头woodoak_wood​spruce_wood​birch_wood​jungle_wood​acacia_wood​dark_oak_wood​stripped_oak_wood​stripped_spruce_wood​stripped_birch_wood​stripped_jungle_wood​stripped_acacia_wood、​stripped_dark_oak_wood
    木台阶wooden_slaboak_slab​spruce_slab​birch_slab​jungle_slab​acacia_slab​dark_oak_slab
    双层木台阶double_wooden_slaboak_double_slab​spruce_double_slab​birch_double_slab​jungle_double_slab​acacia_double_slab​dark_oak_double_slab
    珊瑚扇coral_fantube_coral_fan​brain_coral_fan​bubble_coral_fan​fire_coral_fan​horn_coral_fan
    失活的珊瑚扇coral_fan_deaddead_tube_coral_fandead_​brain_coral_fandead_​bubble_coral_fandead_​fire_coral_fandead_​horn_coral_fan
    red_flowerpoppy​blue_orchid​allium​azure_bluet​red_tulip​orange_tulip​white_tulip​pink_tulip​oxeye_daisy​cornflower​lily_of_the_valley
    树苗saplingoak_sapling​spruce_sapling​birch_sapling​jungle_sapling​acacia_sapling​dark_oak_sapling
    珊瑚块coral_blocktube_coral_block​brain_coral_block​bubble_coral_block​fire_coral_block​horn_coral_blockdead_tube_coral_blockdead_​brain_coral_blockdead_​bubble_coral_blockdead_​fire_coral_blockdead_​horn_coral_block
    草和蕨tallgrassshort_grass​fern
    石台阶等stone_block_slabsmooth_stone_slab​sandstone_slab​petrified_oak_slab​cobblestone_slab​brick_slab​stone_brick_slab​quartz_slab​nether_brick_slab
    高草丛和大型花等double_plantsunflower​lilac​tall_grass​large_fern​rose_bush​peony
  • 将伤害类型suicide重命名为self_destruct,这对于部分附加包方面的需求也是适用的。

  • /titleraw/tellraw现在支持渲染输入键位字形。

    • 备注:键位字形(Input Key Glyphs)是预览版 1.20.60.25加入的一种可以渲染玩家设置的键位的代码。例如,当玩家在 Windows 游玩时,若其前进键设置为W,则输入:_input_key.forward:就会返回W。有以下几种键位字形受到支持:
      • :_input_key.forward:
      • :_input_key.back:
      • :_input_key.left:
      • :_input_key.right:
      • :_input_key.inventory:
      • :_input_key.use:
      • :_input_key.chat:
      • :_input_key.attack:
      • :_input_key.sprint:
  • 随着 1.20.80 和 1.21 的更新,新增了一些新物品、新方块、新状态效果等可用于命令的 ID。详情请见相关 Wiki 页面,这里不再赘述。

  • 新增游戏规则:

    • tntExplosionDropDecay:用于控制 TNT 爆炸后是否会 100% 掉落。
    • showDaysPlayed:在左上角显示游玩天数(游戏内天数)。
  • 新增粒子:breeze_ground_particle​infested_ambient​infested_emitter​ominous_spawning_particle​oozing_ambient​oozing_emitter​raid_omen_ambient​raid_omen_emitter​small_soul_fire_flame​trial_omen_ambient​trial_omen_emitter​trial_spawner_detection​trial_spawner_detection_ominous​smash_ground_particle​smash_ground_particle_center​vault_connection_particle​weaving_ambient​weaving_emitter​white_smoke_particle​wind_charged_emitter​wind_charged_ambient​wind_explosion_emitter。至于有什么效果,请读者自行尝试吧。

数据驱动实体

组件

  • minecraft:ageable
    • 加入了interact_filters字段,允许指定活动对象可以被喂食时的条件。
  • minecraft:damage_sensor
    • damage_modifier​damage_multiplier字段现在会在受击后伤害免疫计算过程中被考虑,以使被调整为低于或等于实体在免疫时间受到的最高伤害的伤害会被正确忽略。
  • minecraft:entity_sensor
    • format_version1.20.60或更高版本下支持多个“子检测器”:
      • event​require_all​minimum_count​maximum_count​range​event_filter现在是每个子检测器可单独配置的字段。
      • 子检测器拥有新的字段cooldown,用以定义每个子检测器检测实体的频率。
    • format_version1.20.70或更高版本下的range字段现在支持两个值,第一个值控制水平范围,第二个值控制垂直范围。
  • minecraft:interact
    • format_version1.20.60或更高版本下:
      • 现在支持vibration字段的entity_act附加值。
      • 加入了drop_item_slot字段,以允许指定物品栏槽位以移除和丢弃其中的物品。
    • format_version1.20.80或更高版本下:
      • equip_item_slot​drop_item_slot字段现在支持盔甲槽位(slot.armor.head​slot.armor.chest​slot.armor.legs​slot.armor.feet)和物品栏槽位(正数字符串,例如"1"):
      • 加入了repair_entity_item字段,允许修复实体物品栏或盔甲槽中的物品。
  • 新增minecraft:body_rotation_blocked组件,用于阻止生物在视觉上转身以与自身朝向相匹配。

事件

  • queue_command正式可用!终于不需要用动画控制器转一下了(泣
    • 用于直接使实体执行命令的事件。
    • 通过queue_command运行的命令可能会推迟至下一刻。
    • 若在命令运行前移除实体,则不会执行该命令。
  • emit_vibration
    • 允许实体以自身为振动源产生振动。

过滤器

  • is_panicking:检查实体是否在执行minecraft:behavior.panic组件。
  • is_sprinting:检查实体是否在疾跑。
  • was_last_hurt_by:检查对象是否为攻击过该实体的最后一个玩家或生物。
  • is_sitting:检查实体是否处于坐下状态。
  • has_damaged_equipment:检查实体的指定槽位中是否存在特定的已损坏装备。

生成规则

  • minecraft:spawns_on_block_filterminecraft:spawns_on_block_prevented_filterminecraft:spawns_above_block_filter现在支持方块描述符。

模型

  • 支持格式版本为1.21.0及更高版本的minecraft:geometry,更新了实体几何结构以支持 UV 旋转。允许在应用于实体的立方体面前以 90 度的增量旋转指定 UV 矩形。

Molang

开放的 Molang 还挺多的哦,有你需要的吗?

  • 以下 Molang 走出实验性玩法:
    • 方块查询 Molang:query.relative_block_has_any_tagquery.relative_block_has_all_tagsquery.block_neighbor_has_any_tagquery.block_neighbor_has_all_tagsquery.block_has_any_tagquery.block_has_all_tags
    • 骨骼:query.bone_orientation_trsquery.bone_orientation_matrix
    • 物品冷却:query.is_cooldown_typequery.cooldown_timequery.cooldown_time_remaining
    • 记分板:query.scoreboard(仅限服务端)。
    • 坐骑 & 乘客:query.rider_body_x_rotationquery.rider_body_y_rotationquery.rider_head_x_rotationquery.rider_head_y_rotationquery.ride_body_x_rotationquery.ride_body_y_rotationquery.ride_head_x_rotationquery.ride_head_y_rotationquery.is_attachedquery.has_player_rider
  • 新增query.armor_slot_damage以返回指定槽位中的盔甲物品的损坏值。

数据驱动物品(国际版)

没什么新组件,只有一些更改而已。

  • 自定义盔甲附着物现在可以使用原版纹饰作为纹饰,经过修改的原版图案现在可以应用于自定义盔甲附着物和物品。
    • 原版纹饰的纹理可通过attachable组件覆盖。
    • 原版纹饰可通过attachable组件应用于使用自定义盔甲材料的盔甲。
    • 经过修改(以适应使用新的盔甲材料的盔甲)的原版纹饰图案可通过attachable组件应用。
    • 自定义盔甲上的盔甲纹饰图案要求附着物和物品的格式版本为1.20.60+

数据驱动方块(国际版)

同样,没什么新组件,只有一些更改而已。

组件

  • minecraft:geometry
    • 对于format_version1.20.60或更高版本,加入了minecraft:geometry.full_block标识符。
    • 对于format_version1.21.0或更高版本,更新了方块几何结构以支持 UV 旋转,且允许在应用于方块面前以 90 度的增量旋转指定 UV 矩形。
  • minecraft:transformation
    • 加入了轴心点缩放和旋转。
  • minecraft:crafting_table
    • 在带有该组件的自定义方块的crafting_tags字段中,使用自定义标签的方块支持自定义可解锁配方。

配方

  • 为有序配方加入了assume_symmetry 数据类型图标assume_symmetry属性,以允许对称的有序配方使用不同的输出方式。

生物群系

  • 对于format_version1.20.60或更高版本的文件,现在 JSON 文件中的生物群系标签在tags数组中的minecraft:tags组件下指定,而不是作为松散 JSON 对象。

ScriptAPI

备注:直到 1.21.0,@minecraft/server可用的最新版本为1.11.0。从 1.20.50 的1.7.0到 1.21.0 的1.11.0,可以看到进步是不小的,开放了众多的事件、组件和接口,其中更是包括自定义附魔、方块 ID 等的获取方法,实用性可以说得到了进一步的增强。

小吐槽:ojng的更新日志是真的乱啊,真就学新三国编剧一样左脑打右脑是吧……

  • 以下 API 开放至1.8.0
    • 爆炸事件:ExplosionAfterEventExplosionAfterEventSignalExplosionBeforeEventExplosionBeforeEventSignal
    • 状态效果事件:EffectAddBeforeEventEffectAddBeforeEventSignalEffectAddAfterEventEffectAddAfterEventSignal
    • 数驱实体事件:DataDrivenEntityTriggerAfterEventDataDrivenEntityTriggerAfterEventSignalEntityDataDrivenTriggerEventOptionsDefinitionModifierWorldAfterEvents.dataDrivenEntityTrigger
    • 方块:BlockTypeBlock.getTagsBlock.hasTagFluidTypeBlockPermutation.withStateBlockPermutation.getState
  • 以下 API 开放至1.9.0
    • 维度:Dimension.createExplosionExplosionOptionsDimensionTypeDimensionTypes
    • 方块:BlockPermutation.matchesBlockPermutation.getAllStatesBlockStateTypeBlockStatesDyeColorSignSide
    • 活塞事件:BlockPistonStatePistonActivateAfterEventPistonActivateAfterEventSignal
    • 方块组件:BlockSignComponentBlockPistonComponent
    • 实体:Entity.setOnFireEntity.extinguishFire
    • 实体组件:EntityOnFireComponentEntityEquippableComponent.getEquipmentSlot
    • 物品堆叠的动态属性:ItemStack.clearDynamicPropertiesItemStack.getDynamicPropertyItemStack.getDynamicPropertyIdsItemStack.getDynamicPropertyTotalByteCountItemStack.setDynamicProperty
    • 物品组件:ItemFoodComponentItemDurabilityComponent
    • 容器:ContainerSlotInvalidContainerSlotErrorContainer.getSlot
    • 状态效果:EffectTypeEffectTypes
    • 原始 JSON 文本:RawText
    • 世界事件:WorldAfterEvents.effectAddWorldBeforeEvents.effectAddWorldAfterEvents.explosionWorldBeforeEvents.explosion
      • 备注:这里是没有写错的,因为 1.8.0 版本中的脚本只开放了对应的事件,但是并没有开放对应爆炸事件和状态效果事件的方法,好诡异的更新
  • 以下 API 开放至1.10.0
    • 组件枚举:BlockComponentTypesEntityComponentTypesItemComponentTypes
    • 方块:Block.getItemStackBlockPermutation.getItemStack
    • 实体:EntityTypeEntityTypes
      • 玩家:Player.playMusicPlayer.queueMusicPlayer.stopMusic
    • 实体组件:EntityProjectileComponentEntityTypeFamilyComponentEntityComponent.entity
    • 物品组件:ItemCooldownComponent
    • 世界事件:WorldAfterEvents.worldInitializeWorldInitializeAfterEventWorldInitializeAfterEventSignal
    • 世界(结构管理器):全面开放结构管理器World.structureManager,包括:
      • StructureManager.createEmptyStructureManager.deleteStructureManager.getStructureManager.place
      • Structure.idStructure.sizeStructure.getBlockPermutationStructure.getIsWaterloggedStructure.isValid
      • StructureSaveModeStructureRotationStructureAnimationModeStructureMirrorAxisInvalidStructureErrorStructureCreateOptionsStructurePlaceOptions
  • 以下 API 开放至1.11.0
    • 维度:Dimension.playSound
    • 物品组件:ItemEnchantableComponent
    • 物品附魔:Enchantment。包括:Enchantment接口、EnchantmentSlot枚举、EnchantmentType类、EnchantmentTypes类。
    • 玩家:Player.startItemCooldownPlayer.getItemCooldownPlayer.getGameModePlayer.setGameModePlayer.selectedSlotIndex
    • 实体组件:
      • 寻路组件:EntityNavigationComponentEntityNavigationClimbComponentEntityNavigationFloatComponentEntityNavigationFlyComponentEntityNavigationGenericComponentEntityNavigationHoverComponentEntityNavigationWalkComponent
      • 驯服组件:EntityTameMountComponent.tame
      • 骑乘组件:EntityAddRiderComponentEntityRideableComponent
      • 颜色组件:EntityColorComponentEntityColor2ComponentPaletteColor枚举。
    • 实体:Seat
    • 方块:ListBlockVolumeBlockVolumeBaseBlockLocationIteratorBlock.setTypeBlockTypeBlockTypesBlock.type
    • 显示:ScreenDisplay.getHiddenHudElementsScreenDisplay.isForcedHiddenScreenDisplay.resetHudElementsScreenDisplay.setHudVisibilityScreenDisplay.hideAllExcept
    • HUD 元素:HudElements枚举、HudElementsCounts变量、HudVisibility枚举、HudVisibilityCounts变量
    • 游戏规则:GameRules枚举、world.gameRulesGameRuleChangeAfterEventGameRuleChangeAfterEventSignal
    • 世界事件:WeatherChangeBeforeEventWorldAfterEvents.gameRuleChangeworldBeforeEvents.playerGameModeChangeworldAfterEvents.playerGameModeChangePlayerGameModeChangeAfterEventPlayerGameModeChangeAfterEventSignalPlayerGameModeChangeBeforeEventPlayerGameModeChangeBeforeEventSignal
    • 结构管理器:Structure.saveToWorldStructure.saveAsStructureManager.createFromWorldStructureManager.getWorldStructureIds
    • 其他:各接口的volume参数。
  • ScoreboardaddObjective中的显示名称参数更改为可选参数。
  • ItemReleaseUseAfterEvent中的itemStack更改为可选参数。
  • EntityMountTamingComponent重命名为EntityTameMountComponent,并将其中的方法setTamed重命名为tame
  • Player类中,getItemCooldownstartItemCooldown方法的itemCategory参数重命名为cooldownCategory
  • EntityTameableComponent类:
    • getTameItems的返回类型更改为ItemStack[]
    • tame更改为带动一个玩家。
    • 加入了tamedToPlayer​tamedToPlayerId​isTamed方法。

继续更新公告

· 阅读需 1 分钟

本人毕设已经忙完,之后将重新按照 2-3 天一更新的频率更新教程系列。

初了解 ModAPI - Day 1

· 阅读需 14 分钟

因为《冒险小世界:剑之试炼》4.2 版本的要求,今天我正式开始了解中国版的 ModAPI。

在学校曾经学过 Python 3,所以自认为有一点基础,然而中国版用的还是 Python 2。唉,没想到居然还是有大公司还揪着 v2 不放,一开始的路线感觉就没走对啊。

不管这些了。还是用曾经学 SAPI 的路线走,在了解基础概念的情况下,逻辑问题都交给 AI 帮我解决,例如 Kimi 或 Deepseek,Kimi 的反应快一些,但准确性差,Deepseek 的反应慢,经常无响应,但是总能抓住问题关键。

两种类的对比

先了解一下 Python 2 的类怎么写。想必这东西跟 JavaScript 应该是类似的。比如在定义上,Python 2 的实例化调用__init__,而 js 就是用constructor()了。

类的定义与构建
class MyClass:
awa = 5

def __init__(self, value):
self.value = value

def printValue():
print(self.value)
类的定义与构建
class MyClass {
awa = 5;

constructor(value) {
this.value = value;
}

printValue() {
console.log(this.value)
}
}

基本上,self应该就是 js 中this的含义了。理解了这些,剩下的就都很简单了。反正对于我来说,不需要理解那么多过于底层的东西,会用就行了。

有几个区别要注意:

第一点,看来 js 中的类必须要用constructor,但是Python 2不一定要用__init__。例如下面也是可以的。

class awa:
qwq = 2
print awa().qwq

第二点,属性调用,Python在调用类的属性的时候似乎不需要括号,比如上面的例子中awa.qwq也是可行的,不过主要还是用awa().qwq会更好一些,也能和 js 的语法同步。(诶,我为什么要追求 js 和 Python 的统一?

学习中国版的脚本系统

先前问过 E 尘大大,应该从什么地方入门,他发给了我这么一期视频:

MODAPI教程第零讲 一基础面向实战教学 - 我的世界

但是这期视频稍微老了一些,所以我想看看官方给的文档,结果才发现入门文档写的那叫一个烂——完全过时!其中光是一开始要求在 MCS 安装的入门模板,在现在就已经分裂成了两个,而且文件路径也不一样,内容也不一样,解析也没有,整个就是一头雾水 —— 我连它如何运行都看不明白。而且在游戏里,我按照它的示意输入“钻石剑”也没有反应。这包真能正常运行吗?算了,还是看视频吧。

后来,还是让我找到这个示例包了,原来中国版有两个版本的教程,第二个版本的教程给了示例包。正好视频也需要这个包,这下就搞定找不到示例包的问题了。

果然,还是示例包给的逻辑清晰。只是 Python 2 依然是硬伤,VSC 装了 Pylance 之后就总是在print语句上报错。唉!

中国版脚本的入口文件是modMain.py,这和国际版在manifest.json的入口文件的定义"entry":"..."是类似的。不过modMain.py是没有办法改名的,必须锁死为这个文件。嗯,好理解的。

接下来学了一下装饰器,比如那个@Mod.Binding(name = "TutorialMod", version = "0.0.1"),其实就是一种函数,大概了解了,但具体怎么运行的还不太清楚,以后用到再说。

然后学了一下服务端和客户端。其实不算学吧,以前我就知道。但是提醒我可以把模块 2 的教学中的服务端和客户端说得更清楚一些。

服务端脚本入口

服务端的入口,也就是这段代码所定义的:

class TutorialMod(object):

...

# InitServer绑定的函数作为服务端脚本初始化的入口函数,通常是用来注册服务端系统system和组件component
@Mod.InitServer()
def TutorialServerInit(self):
print "===== init tutorial server ====="
# 函数可以将System注册到服务端引擎中,实例的创建和销毁交给引擎处理。第一个参数是MOD名称,第二个是System名称,第三个是自定义MOD System类的路径
# 取名名称尽量个性化,不能与其他人的MOD冲突,可以使用英文、拼音、下划线这三种。
serverApi.RegisterSystem("TutorialMod", "TutorialServerSystem", "tutorialScripts.tutorialServerSystem.TutorialServerSystem")

...

主要是这个serverApi.RegisterSystem(nameSpace: str, systemName: str, clsPath: str)起到一个入口的作用。这里:

  • 参数一,nameSpace就是上面的class定义的类名,也就是"TutorialMod"
  • 参数二,systemName是要实例化的类名,也就是示例中那个tutorialServerSystem.pyTutorialServerSystem类。
  • 参数三,则是这个类的路径,但似乎路径并不是以/\分隔的,而是以.分隔的。

客户端脚本也是类似的,看来这就是脚本的注册方法了。

服务端脚本

入口会链接到对应路径的类里面并把它实例化:

# -*- coding: utf-8 -*-

# 获取引擎服务端API的模块
import mod.server.extraServerApi as serverApi
# 获取引擎服务端System的基类,System都要继承于ServerSystem来调用相关函数
ServerSystem = serverApi.GetServerSystemCls()
# 获取组件工厂,用来创建组件
compFactory = serverApi.GetEngineCompFactory()

# 在modMain中注册的Server System类
class TutorialServerSystem(ServerSystem):

# ServerSystem的初始化函数
def __init__(self, namespace, systemName):
# 首先调用父类的初始化函数
super(TutorialServerSystem, self).__init__(namespace, systemName)
print "===== TutorialServerSystem init ====="
# 初始时调用监听函数监听事件
self.ListenEvent()

# 监听函数,用于定义和监听函数。函数名称除了强调的其他都是自取的,这个函数也是。
def ListenEvent(self):
...

# 反监听函数,用于反监听事件,在代码中有创建注册就对应了销毁反注册是一个好的编程习惯,不要依赖引擎来做这些事。
def UnListenEvent(self):
...

...

也就是这个TutorialServerSystem(ServerSystem),代表它继承了ServerSystem类,这样它才能够被中国版的脚本调用。在构建对象的时候,首先调用了一个super继承,然后调用自己的ListenEvent,这样就可以监听事件了。这里中国版给了两个示例:

# 在自定义的ServerSystem中监听引擎的事件ServerChatEvent,回调函数为OnServerChat
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerChatEvent", self, self.OnServerChat)
# 监听引擎的事件 ServerBlockUseEvent, 回调函数为 OnServerBlockUseEvent
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerBlockUseEvent", self, self.OnServerBlockUseEvent)

这里,因为继承了ServerSystem,所以它就可以监听事件了。这个监听事件ListenForEvent的逻辑和 SAPI 的afterEventsbeforeEvents简直太像了,都是监听事件,然后回调什么函数。看一下这里面的 5 个参数:

  • serverApi.GetEngineNamespace()serverApi.GetEngineSystemName():引擎的命名空间和名称,这个东西看过教程后就知道,和服务端客户端之间的联络似乎是有关系的,并不是通用的。
  • "ServerChatEvent":调用的事件,类似于 SAPI 的itemUseentityDied这种事件。
  • self:这个我还真不知道,教程也没提,应该是调用自身的方法?
  • self.OnServerChat:执行的回调函数。类似于 SAPI 的subscribe()里面允许的那个函数,比如subscribe(event=>{console.log("1")}),只不过它这个地方不是执行函数function(),而是引用函数function,这个需要注意的。

还是以这个监听聊天栏为例(国际版你能不能快点把chatSend开放掉?都2.0.0了还实验性?):

# 监听ServerChatEvent的回调函数
def OnServerChat(self, args):
print "==== OnServerChat ==== ", args
# 生成掉落物品
# 当我们输入的信息等于右边这个值时,创建相应的物品
# 创建Component,用来完成特定的功能,这里是为了创建Item物品
playerId = args["playerId"]
comp = compFactory.CreateItem(playerId)
if args["message"] == "钻石剑":
# 调用SpawnItemToPlayerInv接口生成物品到玩家背包,参数参考《MODSDK文档》
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_sword", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石镐":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_pickaxe", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石头盔":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_helmet", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石胸甲":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_chestplate", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石护腿":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_leggings", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石靴子":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_boots", "count":1, 'auxValue': 0}, playerId)
else:
print "==== Sorry man ===="

这里,中国版就是通过返回了一个字典,执行回调函数。这和 js 的回调函数有若干的不同,以至于我还不太适应。比如,对于相同的功能来说,SAPI 就要写成:

world.afterEvents.chatSend.subscribe(event => {
const message = event.message;
const player = event.sender;
const playerContainer = player.getComponent("minecraft:inventory").container
if (message === "钻石剑") playerContainer.addItem(new ItemStack("diamond_sword"))
else if (message === "钻石镐") playerContainer.addItem(new ItemStack("diamond_pickaxe"))
else if (message === "钻石头盔") playerContainer.addItem(new ItemStack("diamond_helmet"))
else if (message === "钻石胸甲") playerContainer.addItem(new ItemStack("diamond_chestplate"))
else if (message === "钻石护腿") playerContainer.addItem(new ItemStack("diamond_leggings"))
else if (message === "钻石靴子") playerContainer.addItem(new ItemStack("diamond_boots"))
else console.log("==== Sorry man ====")
})

其实本来想夸 js 比较简洁来着……但至少就这个例子来说没有比中国版简洁到哪里去。主要是因为 SAPI 的获取物品栏和生成物品都是比较麻烦的。

对比一下可以发现,中国版返回的 ID 只是一个实体的数字 ID,所以并不能像国际版那样快捷地调用实体、方块、物品堆叠的方法,而必须通过接口(旧称组件)来获取这些数据。有很多开发者都强调,尽可能用事件而非接口,因为接口用多了会卡……唉。

接口是通过接口工厂工作的,可以通过返回的数据(比如 ID)经过这个工厂加工之后返回需要的数据,其实按我的理解的话,就类似于国际版的类的方法吧。相比于国际版来说,它是直接返回一个类的实例,然后可以调用这个方法或它的属性。

在后面的实践中,我发现了一个很不便的地方,就是获取它的实体类型。SAPI 有一个属性可以直接获取类型:

entity.typeId === "minecraft:player"

这个非常简洁,而中国版的话就必须得

serverApi.GetEngineCompFactory().CreateEngineType(entity).GetEngineTypeStr() == "minecraft:player"

而且还得查文档。所以中国版的脚本确实是绕啊。算了,看在中国版发展早 + 功能多,我也就不多说什么咯。

不管怎么说,现在应该是已经成功入了门了,这也意味着我以后有更多手段和工具可以更简单地实现更好的效果了,太棒了!现在就可以把《冒险小世界:剑之试炼》的一些遗憾全部通过脚本补齐了 >:)

初了解实体属性

· 阅读需 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。很明显就是因为这东西有坑

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