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