粒子

粒子

粒子是用于完善游戏表现、增强沉浸感的二维效果。客户端与服务端都可以生成粒子,但由于它们主要用于视觉表现,关键部分仅存在于物理客户端(也就是逻辑客户端)。

注册粒子

ParticleType

粒子通过 ParticleType 注册。它与 EntityType、BlockEntityType 类似:每个生成的粒子都是 Particle 类的实例,而 ParticleType 保存共用信息并用于注册。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 DeferredHolder<ParticleType<?>, SimpleParticleType> MY_PARTICLE = PARTICLE_TYPES.register(
        // The name of the particle type.
        "my_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。

Particle

Particle 是实际生成到世界中并向玩家显示的对象。可以继承 Particle 自行实现,但许多情况下继承 TextureSheetParticle 更合适:它提供动画、缩放等辅助功能,并负责实际渲染;若直接继承 Particle,这些都需要自行实现。

粒子的大部分属性通过 gravity、lifetime、hasPhysics、friction 等字段控制。通常值得自行实现的方法只有 tick 与 move,它们分别处理刻更新和移动。因此,自定义粒子类往往很短,可能只在构造函数中设置一些字段,其余工作交给父类。基本实现如下:

public class MyParticle extends TextureSheetParticle {
    private final SpriteSet spriteSet;
    
    // First four parameters are self-explanatory. The SpriteSet parameter is provided by the
    // ParticleProvider, see below. You may also add additional parameters as needed, e.g. xSpeed/ySpeed/zSpeed.
    public MyParticle(ClientLevel level, double x, double y, double z, SpriteSet spriteSet) {
        super(level, x, y, z);
        this.spriteSet = spriteSet;
        this.gravity = 0; // Our particle floats in midair now, because why not.

        // We set the initial sprite here since ticking is not guaranteed to set the sprite
        // before the render method is called.
        this.setSpriteFromAge(spriteSet);
    }
    
    @Override
    public void tick() {
        // Set the sprite for the current particle age, i.e. advance the animation.
        this.setSpriteFromAge(spriteSet);
        // Let super handle further movement. You may replace this with your own movement if needed.
        // You may also override move() if you only want to modify the built-in movement.
        super.tick();
    }
}

ParticleProvider

接下来,粒子类型必须注册 ParticleProvider。它是仅限客户端的类,通过 createParticle 方法创建实际粒子。这里可以包含更复杂的逻辑,不过许多提供器都很简单:

// The generic type of ParticleProvider must match the type of the particle type this provider is for.
public class MyParticleProvider implements ParticleProvider<SimpleParticleType> {
    // A set of particle sprites.
    private final SpriteSet spriteSet;
    
    // The registration function passes a SpriteSet, so we accept that and store it for further use.
    public MyParticleProvider(SpriteSet spriteSet) {
        this.spriteSet = spriteSet;
    }
    
    // This is where the magic happens. We return a new particle each time this method is called!
    // The type of the first parameter matches the generic type passed to the super interface.
    @Override
    public Particle createParticle(SimpleParticleType type, ClientLevel level,
            double x, double y, double z, double xSpeed, double ySpeed, double zSpeed) {
        // We don't use the type and speed, and pass in everything else. You may of course use them if needed.
        return new MyParticle(level, x, y, z, spriteSet);
    }
}

然后在客户端的 Mod 总线事件 RegisterParticleProvidersEvent 中,将提供器与粒子类型关联起来:

@SubscribeEvent // on the mod event bus only on the physical client
public static void registerParticleProviders(RegisterParticleProvidersEvent event) {
    // There are multiple ways to register providers, all differing in the functional type they provide in the
    // second parameter. For example, #registerSpriteSet represents a Function<SpriteSet, ParticleProvider<?>>:
    event.registerSpriteSet(MyParticleTypes.MY_PARTICLE.get(), MyParticleProvider::new);
    // Other methods include #registerSprite, which is essentially a Supplier<TextureSheetParticle>,
    // and #registerSpecial, which maps to a Supplier<Particle>. See the source code of the event for further info.
}

粒子描述

最后需要为粒子类型关联纹理。与物品关联物品模型类似,粒子类型关联的是粒子描述。它是 assets/<namespace>/particles 目录中的 JSON 文件,文件名与粒子类型相同(上例对应 my_particle.json)。格式如下:

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

如果粒子接受 SpriteSet,即通过 registerSpriteSet 或 registerSprite 注册提供器,就必须提供粒子定义。通过 #registerSpecial 注册的提供器则不能提供粒子定义。

重要警告 使用精灵集合的粒子工厂与粒子定义文件必须一一对应。如果存在描述文件而没有对应工厂,或反之,会抛出异常!

提示 虽然粒子描述要求以特定方式注册提供器,但只有 ParticleRenderType(通过 Particle#getRenderType 设置)将 TextureAtlas#LOCATION_PARTICLES 用作着色器纹理时,才会实际使用这些描述。原版中符合条件的渲染类型是 PARTICLE_SHEET_OPAQUE、PARTICLE_SHEET_TRANSLUCENT 和 PARTICLE_SHEET_LIT。

数据生成

粒子定义文件也可以通过数据生成创建:继承 ParticleDescriptionProvider 并重写 #addDescriptions():

public class MyParticleDescriptionProvider extends ParticleDescriptionProvider {
    // Get the parameters from GatherDataEvent.
    public AMParticleDefinitionsProvider(PackOutput output, ExistingFileHelper existingFileHelper) {
        super(output, existingFileHelper);
    }

    // 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.
        sprite(MyParticleTypes.MY_SINGLE_PARTICLE.get(), ResourceLocation.fromNamespaceAndPath("examplemod", "my_single_particle"));
        // Adds a multi sprite particle definition, with a vararg parameter. Alternatively accepts a list.
        spriteSet(MyParticleTypes.MY_MULTI_PARTICLE.get(),
            ResourceLocation.fromNamespaceAndPath("examplemod", "my_multi_particle_0"),
            ResourceLocation.fromNamespaceAndPath("examplemod", "my_multi_particle_1"),
            ResourceLocation.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.
            ResourceLocation.fromNamespaceAndPath("examplemod", "my_multi_particle"),
            // The amount of textures.
            3,
            // Whether to reverse the list, i.e. start at the last element instead of the first.
            false
        );
    }
}

不要忘记在 GatherDataEvent 中添加提供器:

@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent event) {
    DataGenerator generator = event.getGenerator();
    PackOutput output = generator.getPackOutput();
    ExistingFileHelper existingFileHelper = event.getExistingFileHelper();

    // other providers here
    generator.addProvider(
        event.includeClient(),
        new MyParticleDescriptionProvider(output, existingFileHelper)
    );
}

自定义 ParticleType

大多数情况下 SimpleParticleType 已足够,但有时需要在服务端为粒子附加额外数据。这时就需要自定义 ParticleType 及对应的 ParticleOptions。先实现实际存储信息的 ParticleOptions:

public class MyParticleOptions implements ParticleOptions {
    
    // Read and write information, typically for use in commands
    // Since there is no information in this type, this will be an empty string
    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() {}
}

然后在自定义 ParticleType 中使用这个 ParticleOptions 实现:

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)
);

生成粒子

再强调一次:服务端只认识 ParticleType 与 ParticleOption,客户端则直接使用与粒子类型关联的 ParticleProvider 提供的 Particle。因此,不同端上生成粒子的方式有很大差别。

  • 共用代码:调用 Level#addParticle 或 Level#addAlwaysVisibleParticle。这是创建所有人都能看见的粒子的首选方式。
  • 客户端代码:可以使用共用代码的方式。也可以用目标粒子类创建 new Particle(),再将它传给 Minecraft.getInstance().particleEngine#add(Particle)。这样添加的粒子只在当前客户端显示,其他玩家看不到。
  • 服务端代码:调用 ServerLevel#sendParticles。原版的 /particle 命令使用此方法。

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