粒子

粒子

粒子是通常通过对应粒子类型生成的视觉效果。客户端和服务端两侧都可以生成粒子,但由于它主要用于视觉表现,关键部分只存在于物理(以及逻辑)客户端。

本页介绍粒子类型与粒子描述的构建和使用。渲染方面的详细信息,见配套的客户端粒子文档。

注册 ParticleType

粒子通过 ParticleType 注册,与 EntityType、BlockEntityType 类似:每个生成的粒子是 Particle 类的实例,而 ParticleType 保存公共信息,用于注册。粒子类型属于注册表对象,因此应像其他对象一样使用 DeferredRegister:

public class MyParticleTypes {
    // Assuming that your mod id is examplemod
    public static final DeferredRegister<ParticleType<?>> PARTICLE_TYPES =
        DeferredRegister.create(BuiltInRegistries.PARTICLE_TYPE, "examplemod");
    
    // The easiest way to add new particle types is reusing vanilla's SimpleParticleType.
    // Implementing a custom ParticleType is also possible, see below.
    public static final Supplier<SimpleParticleType> MY_QUAD_PARTICLE = PARTICLE_TYPES.register(
        // The name of the particle type.
        "my_quad_particle",
        // The supplier. The boolean parameter denotes whether setting the Particles option in the
        // video settings to Minimal will affect this particle type or not; this is false for
        // most vanilla particles, but true for e.g. explosions, campfire smoke, or squid ink.
        () -> new SimpleParticleType(false)
    );
}

说明 只有需要在服务端处理粒子时才必须提供 ParticleType。客户端也可以直接使用 Particle。

自定义 ParticleType

SimpleParticleType 能满足大多数需求,但有时需要在服务端为粒子附加数据。这时需要自定义 ParticleType 及对应的 ParticleOptions。先创建实际存储信息的 ParticleOptions:

public class MyParticleOptions implements ParticleOptions {
    
    // A map codec defining additional information for the particle, used e.g. in commands.
    // Since there is no information in our type, use a unit map codec;
    // this corresponds to using an empty string in a command.
    public static final MapCodec<MyParticleOptions> CODEC = MapCodec.unit(new MyParticleOptions());

    // Read and write information to the network buffer.
    public static final StreamCodec<ByteBuf, MyParticleOptions> STREAM_CODEC = StreamCodec.unit(new MyParticleOptions());

    // Does not need any parameters, but may define any fields necessary for the particle to work.
    public MyParticleOptions() {}

    @Override
    public ParticleType<?> getType() {
        // Return the registered particle type
    }
}

然后在自定义 ParticleType 中使用该实现:

public class MyParticleType extends ParticleType<MyParticleOptions> {
    // The boolean parameter again determines whether to limit particles at lower particle settings.
    // See implementation of the MyParticleTypes class near the top of the article for more information.
    public MyParticleType(boolean overrideLimiter) {
        // Pass the deserializer to super.
        super(overrideLimiter);
    }

    @Override
    public MapCodec<MyParticleOptions> codec() {
        return MyParticleOptions.CODEC;
    }

    @Override
    public StreamCodec<? super RegistryFriendlyByteBuf, MyParticleOptions> streamCodec() {
        return MyParticleOptions.STREAM_CODEC;
    }
}

并在注册时引用它:

public static final Supplier<MyParticleType> MY_CUSTOM_PARTICLE = PARTICLE_TYPES.register(
    "my_custom_particle",
    () -> new MyParticleType(false)
);

最后在 ParticleOptions#getType 中返回已注册的类型:

public class MyParticleOptions implements ParticleOptions {
    
    // ...

    @Override
    public ParticleType<?> getType() {
        return MY_CUSTOM_PARTICLE.get();
    }
}

粒子描述

粒子描述是位于 assets/<namespace>/particles 下的 JSON 文件,与对应的粒子类型同名,内容为相对于 assets/<namespace>/textures/particles 的纹理列表。

示例如下:

{
    // A list of textures that will be played in order. Will loop if necessary.
    // Texture locations are relative to the textures/particle folder.
    "textures": [
        // Points to `assets/examplemod/textures/particle/my_particle_0.png`
        "examplemod:my_particle_0",
        "examplemod:my_particle_1",
        "examplemod:my_particle_2",
        "examplemod:my_particle_3"
    ]
}

资源重载时,ParticleResources 会读取所有粒子描述,并把纹理拼接到 TextureAtlas#LOCATION_PARTICLES 图集中,再为每个描述创建包含对应 TextureAtlasSprite 列表的 SpriteSet。

使用粒子描述

若要让粒子使用描述,必须通过客户端 Mod 事件总线上的 RegisterParticleProvidersEvent 事件,将 ParticleType 与接收 SpriteSet 的 ParticleProvider 关联:

public class MyParticleProvider implements ParticleProvider<SimpleParticleType> {

    private final SpriteSet spriteSet;

    // Take in the sprite set provided by the `ParticleResources`.
    public MyParticleProvider(SpriteSet spriteSet) {
        this.spriteSet = spriteSet;
    }

    // ...
}

// In some client-only event handler

@SubscribeEvent // on the mod event bus only on the physical client
public static void registerParticleProviders(RegisterParticleProvidersEvent event) {
    // #registerSpriteSet MUST be used when dealing with particle descriptions.
    event.registerSpriteSet(MyParticleTypes.MY_PARTICLE.get(), MyParticleProvider::new);
}

警告 如果某个粒子类型有描述文件,却没有通过 RegisterParticleProvidersEvent#registerSpriteSet 关联 ParticleProvider,日志会出现“Redundant texture list”(多余纹理列表)消息。

数据生成

继承 ParticleDescriptionProvider 并重写 #addDescriptions(),即可生成粒子定义文件:

public class MyParticleDescriptionProvider extends ParticleDescriptionProvider {
    // Get the parameters from `GatherDataEvent.Client`.
    public MyParticleDescriptionProvider(PackOutput output) {
        super(output);
    }

    // Assumes that all the referenced particles actually exists. Replace "examplemod" with your mod id.
    @Override
    protected void addDescriptions() {
        // Adds a single sprite particle definition with the file at
        // assets/examplemod/textures/particle/my_single_particle.png.
        spriteSet(MyParticleTypes.MY_SINGLE_PARTICLE.get(), Identifier.fromNamespaceAndPath("examplemod", "my_single_particle"));
        // Adds a multi sprite particle definition, with a vararg parameter. Alternatively accepts an iterable.
        spriteSet(MyParticleTypes.MY_MULTI_PARTICLE.get(),
            Identifier.fromNamespaceAndPath("examplemod", "my_multi_particle_0"),
            Identifier.fromNamespaceAndPath("examplemod", "my_multi_particle_1"),
            Identifier.fromNamespaceAndPath("examplemod", "my_multi_particle_2")
        );
        // Alternative for the above, appends "_<index>" to the base name given, for the given amount of textures.
        spriteSet(MyParticleTypes.MY_ALT_MULTI_PARTICLE.get(),
            // The base name.
            Identifier.fromNamespaceAndPath("examplemod", "my_multi_particle"),
            // The number of textures.
            3,
            // Whether to reverse the list, i.e. start at the last element instead of the first.
            false
        );
    }
}

别忘了将提供器添加到 GatherDataEvent.Client:

@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
    event.createProvider(MyParticleDescriptionProvider::new);
}

生成粒子

如前所述,服务端只认识 ParticleType 和 ParticleOptions,客户端则直接操作由与类型关联的 ParticleProvider 提供的 Particle。因此,不同侧的生成方式差异很大。

  • 通用代码:调用 Level#addParticle 或 Level#addAlwaysVisibleParticle,这是创建所有人都能看见的粒子的首选方式。
  • 客户端代码:使用通用方式;或者创建所需粒子类的 new Particle() 实例,再调用 Minecraft.getInstance().particleEngine#add(Particle)。这种方式仅对当前客户端显示,其他玩家看不到。
  • 服务端代码:调用 ServerLevel#sendParticles,原版的 /particle 命令使用此方法。

来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。