Skip to main content
Version: 1.21.4

进度(Advancements)

进度(Advancements)是类似任务的目标,玩家可以通过完成这些目标来获得成就。进度是基于特定的进度条件(advancement criteria)来授予的,并且在完成时可以触发特定行为。

要添加一个新的进度,只需在你的命名空间(namespace)下的 advancement 子文件夹中创建一个 JSON 文件。例如,如果我们想为一个 mod,mod id 为 examplemod,添加名为 example_name 的进度,那么它应位于 data/examplemod/advancement/example_name.json。进度的 ID 是相对于 advancement 目录的,所以在我们的例子中,它将是 examplemod:example_name。你可以选择任何名称,游戏会自动识别新添加的进度。只有在你需要添加新的条件(criteria)或想通过代码触发某个条件时,才需要编写 Java 代码(见下文)。

规范(Specification)

一个进度的 JSON 文件可以包含以下条目:

  • parent:此进度的父进度 ID(parent advancement ID)。如果出现循环引用,将会被检测到并导致加载失败。该字段为可选项;如果缺省,则该进度会被视为根进度(root advancement)。根进度指的是没有设置父进度的进度,它们会作为其进度树的根节点。
  • display:用于在进度 GUI 中显示该进度的多个属性对象。该字段为可选项;如果缺省,则该进度不会在界面中显示,但仍然可以被触发。
    • icon:一个 物品堆栈的 JSON 表示
    • text:用作进度标题的 文本组件
    • description:用作进度描述的 文本组件
    • frame:进度的框架类型。可选值有 challengegoaltask。可选,默认为 task
    • background:用于树形背景的纹理。该路径不是相对于 textures 目录的,也就是说必须包含 textures/ 文件夹前缀。可选,默认为缺失纹理。仅对根进度有效。
    • show_toast:完成时是否在右上角显示提示。可选,默认为 true。
    • announce_to_chat:完成进度时是否在聊天栏中公告。可选,默认为 true。
    • hidden:是否在进度 GUI 中隐藏该进度及其所有子进度,直到其被完成。对根进度本身无效,但仍会隐藏其所有子进度。可选,默认为 false。
  • criteria:该进度需要追踪的条件(criteria)映射。每个条件由其映射键唯一标识。Minecraft 内置的条件触发器(criteria triggers)列表可在 CriteriaTriggers 类中找到,JSON 规范可参见 Minecraft Wiki。如需自定义条件或在代码中触发条件,请参见下文。
  • requirements:用于决定哪些条件是必须的的列表嵌套列表(list of lists)。这是一个 “或” 列表的 “与” 组合,换句话说,每个子列表中至少有一个条件需要满足。可选,默认为所有条件都必须满足。
  • rewards:表示完成该进度后授予奖励的对象。可选,该对象下的所有字段也都是可选的。
    • experience:授予玩家的经验值数量。
    • recipes:要解锁的 配方 ID 列表。
    • loot:要结算并给予玩家的 战利品表 列表。
    • function:要执行的 函数。如果需要运行多个函数,请创建一个包裹函数来依次运行所有其他函数。
  • sends_telemetry_event:决定在完成该进度时是否收集遥测数据。仅在 minecraft 命名空间下有效。可选,默认为 false。
  • neoforge:conditions:NeoForge 新增。一个 条件 列表,只有全部通过时该进度(advancement)才会被加载。可选项。

进度树(Advancement Trees)

进度文件可以分组存放在不同的目录中,这样游戏会为每个目录创建一个独立的进度标签页(advancement tab)。一个进度标签页可以包含一棵或多棵进度树,具体取决于根进度(root advancement)的数量。空的进度标签页会被自动隐藏。

tip

Minecraft 每个标签页始终只包含一个根进度,并且总是将该进度命名为 root。建议你也遵循这一规范。

条件触发器(Criteria Triggers)

要解锁一个进度,必须满足指定的条件(criteria)。条件的检测由触发器(trigger)完成,当相关事件发生时会由代码触发(例如,当玩家击杀指定 实体 时会触发 player_killed_entity 触发器)。每当一个进度被加载进游戏时,定义好的条件会被读取并作为监听器(listener)添加到相应的触发器上。当某个触发器被执行时,所有监听该条件的进度都会重新检查是否已完成。如果进度完成,则会移除对应的监听器。

自定义条件触发器通常由两部分组成:触发器(trigger)本身——通过调用 #trigger 方法在代码中激活;以及实例(instance)——用于定义在什么条件下该触发器会判定条件达成。触发器需要继承 SimpleCriterionTrigger<T>,而实例则需要实现 SimpleCriterionTrigger.SimpleInstance 接口。这里的泛型 T 代表触发器实例的类型。

SimpleCriterionTrigger.SimpleInstance

SimpleCriterionTrigger.SimpleInstance 表示在 criteria 对象中定义的某一个具体条件。触发器实例负责保存已定义的条件,并在需要时判断输入是否满足这些条件。

这些条件通常通过构造函数传递进来。SimpleCriterionTrigger.SimpleInstance 接口只要求实现一个函数 #player,该函数返回玩家需要满足的条件,类型为 Optional<ContextAwarePredicate>。如果你的子类是一个带有同类型 player 参数的 record(如下例所示),那么自动生成的 #player 方法就已经足够了。

public record ExampleTriggerInstance(Optional<ContextAwarePredicate> player/*, 这里可以有其他参数*/)
implements SimpleCriterionTrigger.SimpleInstance {}

通常,触发器实例会提供一些静态辅助方法,用于根据参数构建完整的 Criterion<T> 对象。这让你在数据生成阶段可以方便地创建这些实例,但这些方法并不是必须的。

// 在这个例子中,EXAMPLE_TRIGGER 是一个 DeferredHolder<CriterionTrigger<?>, ExampleTrigger>。
// 关于如何注册触发器,请参考下文。
public static Criterion<ExampleTriggerInstance> instance(ContextAwarePredicate player, ItemPredicate item) {
return EXAMPLE_TRIGGER.get().createCriterion(new ExampleTriggerInstance(Optional.of(player), item));
}

最后,你需要添加一个方法,该方法接收当前数据状态,并返回用户是否满足必要条件。玩家的条件已经通过 SimpleCriterionTrigger#trigger(ServerPlayer, Predicate) 进行了检查。大多数触发器实例会将这个方法命名为 #matches

// 假设我们有一个额外的 ItemPredicate 参数。你可以根据需要传递任何类型。
// 例如,这里也可以是 Predicate<LivingEntity>。
// 这是一个记录类(record)定义。
public record ExampleTriggerInstance(Optional<ContextAwarePredicate> player, ItemPredicate predicate)
implements SimpleCriterionTrigger.SimpleInstance {
// 这个方法对每个实例来说都是唯一的,因此不会被重写(override)。
// 参数类型可以根据需要自定义,比如也可以是 LivingEntity。
// 如果除了 player 之外不需要其他上下文,也可以不带参数。
public boolean matches(ItemStack stack) {
// 由于 ItemPredicate 检查的是物品堆(stack),所以这里输入参数也是 stack。
return this.predicate.test(stack);
}
}

SimpleCriterionTrigger

SimpleCriterionTrigger<T> 的实现有两个主要目的:一是提供一个方法,用于检查触发器实例并在条件达成时运行关联的监听器(listener);二是指定一个 编解码器(codec),用于序列化和反序列化触发器实例(T)。

首先,我们需要添加一个方法,接收所需的输入,并调用 SimpleCriterionTrigger#trigger,以正确地处理所有监听器的检查。大多数触发器实例也将这个方法命名为 #trigger。继续使用上面示例中的触发器实例,我们的触发器类大致如下:

public class ExampleCriterionTrigger extends SimpleCriterionTrigger<ExampleTriggerInstance> {
// 这个方法对每个触发器来说都是唯一的,因此不会被重写(override)
public void trigger(ServerPlayer player, ItemStack stack) {
this.trigger(player,
// 这里调用 SimpleCriterionTrigger.SimpleInstance 子类中的条件检查方法
triggerInstance -> triggerInstance.matches(stack)
);
}
}

触发器(trigger)必须注册到 Registries.TRIGGER_TYPE 注册表(registry) 中:

public static final DeferredRegister<CriterionTrigger<?>> TRIGGER_TYPES =
DeferredRegister.create(Registries.TRIGGER_TYPE, ExampleMod.MOD_ID);

public static final Supplier<ExampleCriterionTrigger> EXAMPLE_TRIGGER =
TRIGGER_TYPES.register("example", ExampleCriterionTrigger::new);

随后,触发器还必须通过重写 #codec 方法,定义一个 编解码器(codec),用于序列化和反序列化触发器实例。这个编解码器通常会作为实例实现中的常量定义。

public record ExampleTriggerInstance(Optional<ContextAwarePredicate> player/*, other parameters here*/)
implements SimpleCriterionTrigger.SimpleInstance {
public static final Codec<ExampleTriggerInstance> CODEC = ...;

// ...
}

public class ExampleTrigger extends SimpleCriterionTrigger<ExampleTriggerInstance> {
@Override
public Codec<ExampleTriggerInstance> codec() {
return ExampleTriggerInstance.CODEC;
}

// ...
}

对于前面提到的同时包含 ContextAwarePredicateItemPredicate 的 record(记录类型),其 codec 可以这样写:

public static final Codec<ExampleTriggerInstace> CODEC = RecordCodecBuilder.create(instance -> instance.group(
EntityPredicate.ADVANCEMENT_CODEC.optionalFieldOf("player").forGetter(ExampleTriggerInstance::player),
ItemPredicate.CODEC.fieldOf("item").forGetter(ExampleTriggerInstance::item)
).apply(instance, ExampleTriggerInstance::new));

调用条件触发器(Criterion Triggers)

每当你需要检测的动作被执行时,都应该调用我们自定义的 SimpleCriterionTrigger 子类中定义的 #trigger 方法。当然,你也可以调用原版(vanilla)的触发器,这些触发器可以在 CriteriaTriggers 中找到。

// 在某些执行该动作的代码中
// 这里,EXAMPLE_TRIGGER 是你注册的自定义条件触发器的实例供应器(supplier)
public void performExampleAction(ServerPlayer player, additionalContextParametersHere) {
// 这里执行具体动作的代码
EXAMPLE_TRIGGER.get().trigger(player, additionalContextParametersHere);
}

数据生成(Data Generation)

进度(Advancements)可以通过 数据生成 的方式,使用 AdvancementProvider 自动生成。AdvancementProvider 接收一个 AdvancementSubProviders 的列表,这些子生成器会用 Advancement.Builder 实际生成进度内容。

首先,需要在某个 GatherDataEvent 事件中创建 AdvancementProvider 的实例:

@SubscribeEvent
public static void gatherData(GatherDataEvent.Client event) {
// 如果要添加数据包对象,先调用 event.createDatapackRegistryObjects(...)

event.createProvider((output, lookupProvider) -> new AdvancementProvider(
output, lookupProvider,
// 在这里添加生成器
List.of(...)
));

// 其他生成器
}

接下来,需要用我们自己的生成器填充这个列表。具体做法可以是:将生成器作为类或 lambda 表达式添加,然后将每个生成器的实例加入构造函数参数中当前为空的列表。

// 类示例
public class MyAdvancementGenerator implements AdvancementSubProvider {

@Override
public void generate(HolderLookup.Provider registries, Consumer<AdvancementHolder> saver) {
// 在这里生成你的进度(advancements)。
}
}

// 方法示例
public class ExampleClass {

// 参数与 AdvancementSubProvider#generate 方法一致
public static void generateExampleAdvancements(HolderLookup.Provider registries, Consumer<AdvancementHolder> saver) {
// 在这里生成你的进度(advancements)。
}
}

// 在某个 `GatherDataEvent` 事件中
event.createProvider((output, lookupProvider) -> new AdvancementProvider(
output, lookupProvider,
// 在这里添加生成器
List.of(
// 将我们自定义的生成器实例添加到列表参数中。你可以根据需要添加多次。
// 拥有多个生成器只是为了便于组织,所有功能也可以用单个生成器实现。
new MyAdvancementGenerator(),
ExampleClass::generateExampleAdvancements
)
));

要生成一个进度(advancement),你需要使用 Advancement.Builder

// 所有方法都遵循构建器模式(builder pattern),这意味着可以链式调用,并且推荐这样做。
// 为了让说明更易读,这里不进行链式调用。

// 使用静态方法 #advancement() 创建一个进度(advancement)构建器。
// 使用 #advancement() 会自动启用遥测事件(telemetry events)。如果你不需要这个功能,
// 可以使用 #recipeAdvancement(),两者在功能上没有其他区别。
Advancement.Builder builder = Advancement.Builder.advancement();

// 设置进度的父级(parent)。你可以使用已经生成的其他进度,
// 或者通过静态方法 AdvancementSubProvider#createPlaceholder 创建一个占位进度(placeholder advancement)。
builder.parent(AdvancementSubProvider.createPlaceholder("minecraft:story/root"));

// 设置进度的显示属性(display properties)。可以传入一个 DisplayInfo 对象,
// 或者直接传入各个参数。如果直接传参,会自动为你创建一个 DisplayInfo 对象。
builder.display(
// 进度的图标(icon)。可以是 ItemStack 或 ItemLike。
new ItemStack(Items.GRASS_BLOCK),
// 进度的标题和描述(title 和 description)。别忘了为这些添加本地化翻译!
Component.translatable("advancements.examplemod.example_advancement.title"),
Component.translatable("advancements.examplemod.example_advancement.description"),
// 背景纹理(background texture)。如果不需要背景纹理(非根进度),传 null 即可。
null,
// 框架类型(frame type)。可用值有 AdvancementType.TASK、CHALLENGE 或 GOAL。
AdvancementType.GOAL,
// 是否显示进度弹窗(toast)。
true,
// 是否在聊天栏中公告进度(announce)。
true,
// 是否隐藏该进度(hidden)。
false
);

// 一个进度奖励(advancement reward)构建器。可以通过四种奖励类型中的任意一种创建,
// 并可通过以 add 开头的方法添加更多奖励。这也可以提前构建,
// 然后将生成的 AdvancementRewards 在多个进度构建器间复用。
builder.rewards(
// 也可以使用 addExperience() 向已有构建器添加经验奖励。
AdvancementRewards.Builder.experience(100)
// 也可以使用 loot() 创建一个新的构建器。
.addLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.fromNamespaceAndPath("minecraft", "chests/igloo")))
// 也可以使用 recipe() 创建一个新的构建器。
.addRecipe(ResourceKey.create(Registries.RECIPE, ResourceLocation.fromNamespaceAndPath("minecraft", "iron_ingot")))
// 也可以使用 function() 创建一个新的构建器。
.runs(ResourceLocation.fromNamespaceAndPath("examplemod", "example_function"))
);

// 为进度添加一个指定名称的条件(criterion)。请使用对应触发器(trigger)实例的静态方法。
builder.addCriterion("pickup_dirt", InventoryChangeTrigger.TriggerInstance.hasItems(Items.DIRT));

// 添加一个条件要求处理器(requirements handler)。Minecraft 原生提供了 allOf() 和 anyOf(),
// 更复杂的要求需要手动实现。仅当有两个或以上条件时才有效。
builder.requirements(AdvancementRequirements.allOf(List.of("pickup_dirt")));

// 使用指定的资源位置(resource location)将进度保存到磁盘。
// 这会返回一个 AdvancementHolder,可以存储到变量中,并作为其他进度构建器的父级使用。
builder.save(saver, ResourceLocation.fromNamespaceAndPath("examplemod", "example_advancement"));