粒子效果(Particles)
粒子效果是游戏中的二维特效,用于美化画面并增强沉浸感。粒子可以在客户端和服务端 端 生成,但由于它们主要是视觉效果,其关键部分只存在于物理(和逻辑)客户端。
注册粒子效果(Registering Particles)
ParticleType
粒子效果通过 ParticleType 进行注册。这与 EntityType 或 BlockEntityType 的注册方式类似:有一个 Particle 类——每一个被生成的粒子都是该类的实例——同时还有一个 ParticleType 类,用于保存一些通用信息,并在注册时使用。ParticleType 属于注册表(Registry)registry,因此我们需要像注册其他对象一样,使用 DeferredRegister 进行注册:
public class MyParticleTypes {
// 假设你的 mod id 是 examplemod
public static final DeferredRegister<ParticleType<?>> PARTICLE_TYPES =
DeferredRegister.create(BuiltInRegistries.PARTICLE_TYPE, "examplemod");
// 添加新粒子类型最简单的方法是复用原版的 SimpleParticleType。
// 当然也可以实现自定义的 ParticleType,具体见下文。
public static final Supplier<SimpleParticleType> MY_PARTICLE = PARTICLE_TYPES.register(
// 粒子类型的名称
"my_particle",
// 提供者。布尔参数表示在视频设置中将粒子选项设为“最少”时,
// 是否会影响该粒子类型;大多数原版粒子的该参数为 false,
// 但如爆炸、篝火烟雾或鱿鱼墨汁等粒子的该参数为 true。
() -> new SimpleParticleType(false)
);
}
只有当你需要在服务端处理粒子时,才必须使用 ParticleType。客户端也可以直接使用 Particle。
Particle
Particle 就是最终被生成到世界中并展示给玩家的对象。虽然你可以直接继承 Particle 并自行实现相关逻辑,但在大多数情况下,更推荐继承 TextureSheetParticle,因为该类提供了动画、缩放等辅助功能,并且帮你实现了实际的渲染(如果直接继承 Particle,这些都需要你自己实现)。
Particle 的大多数属性通过如 gravity(重力)、lifetime(生命周期)、hasPhysics(是否受物理影响)、friction(摩擦力)等字段控制。你通常只需要自己实现 tick 和 move 这两个方法,它们的作用 正如其名。因此,自定义粒子类通常很简洁,往往只包含一个构造函数用于设置字段,剩下的交由父类处理。一个基础实现大致如下:
public class MyParticle extends TextureSheetParticle {
private final SpriteSet spriteSet;
// 前四个参数很好理解。SpriteSet 参数由 ParticleProvider(粒子提供者)提供,见下文。
// 你也可以根据需要添加额外的参数,比如 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; // 现在我们的粒子会漂浮在空中,为什么不呢。
// 我们在这里设置初始的 sprite,因为 tick 时不一定能保证在 render 方法调用前设置好 sprite。
this.setSpriteFromAge(spriteSet);
}
@Override
public void tick() {
// 根据当前粒子的 age 设置 sprite,也就是推进动画帧。
this.setSpriteFromAge(spriteSet);
// 让父类处理后续的移动逻辑。如果需要,你可以用自己的移动逻辑替换它。
// 如果你只想修改内置的移动方式,也可以重写 move() 方法。
super.tick();
}
}
ParticleProvider
接下来,粒子类型必须注册一个 ParticleProvider(粒子提供者)。ParticleProvider 是一个仅在客户端使用的类,负责通过 createParticle 方法实际创建我们的 Particle(粒子)对象。虽然这里可以写更复杂的代码,但很多粒子提供者其实很简单,就像这样:
// ParticleProvider 的泛型类型必须与你要为其提供的粒子类型一致。
public class MyParticleProvider implements ParticleProvider<SimpleParticleType> {
// 一组粒子用到的 sprite。
private final SpriteSet spriteSet;
// 注册函数会传入一个 SpriteSet,所以我们接受它并保存起来以便后续使用。
public MyParticleProvider(SpriteSet spriteSet) {
this.spriteSet = spriteSet;
}
// 魔法就发生在这里。每次调用该方法时,我们都会返回一个新的粒子对象!
// 第一个参数的类型要和父接口的泛型类型一致。
@Override
public Particle createParticle(SimpleParticleType type, ClientLevel level,
double x, double y, double z, double xSpeed, double ySpeed, double zSpeed) {
// 这里我们没有用到 type 和速度参数,但你可以根据需要使用它们。
return new MyParticle(level, x, y, z, spriteSet);
}
}
然后,你需要在 客户端 的 mod 总线 [事件] RegisterParticleProvidersEvent 中将你的粒 子提供者与粒子类型关联起来:
@SubscribeEvent
public static void registerParticleProviders(RegisterParticleProvidersEvent event) {
// 注册粒子提供器(provider)有多种方式,主要区别在于第二个参数所使用的函数式类型。
// 例如 ,#registerSpriteSet 代表 Function<SpriteSet, ParticleProvider<?>>:
event.registerSpriteSet(MyParticleTypes.MY_PARTICLE.get(), MyParticleProvider::new);
// 其他方法还包括 #registerSprite,本质上是 Supplier<TextureSheetParticle>,
// 以及 #registerSpecial,对应 Supplier<Particle>。更多 细节可参考该事件的源码。
}
粒子描述(Particle Descriptions)
最后,我们需要为自定义的粒子类型关联一张纹理。类似于物品(item)需要与物品模型(item model)关联,粒子类型也需要与所谓的粒子描述(particle description)关联。粒子描述是一个位于 assets/<namespace>/particles 目录下的 JSON 文件,文件名与粒子类型相同(例如上述例子中为 my_particle.json)。粒子定义的 JSON 文件格式如下:
{
// 依次播放的纹理列表。如有需要会循环播放。
// 纹理路径是相对于 textures/particle 文件夹的。
"textures": [
"examplemod:my_particle_0",
"examplemod:my_particle_1",
"examplemod:my_particle_2",
"examplemod:my_particle_3"
]
}
当你的粒子使用了 SpriteSet(通过 registerSpriteSet 或 registerSprite 注册粒子提供器时会用到),那么必须提供粒子定义文件。而如果粒子提供器是通过 #registerSpecial 注册的,则不需要提供粒子定义文件。
如果 sprite set 粒子工厂(factory)与粒子定义文件不匹配,比如有粒子描述但没有对应的粒子工厂,或者反之,都会抛出异常!
虽然粒子描述文件必须以特定方式注册 provider,但只有当 ParticleRenderType 的 RenderType(通过 Particle#getRenderType 设置)为 shader 纹理使用了 TextureAtlas#LOCATION_PARTICLES 时,这些描述文件才会被实际使用。对于原版粒子渲染类型来说,只有 PARTICLE_SHEET_OPAQUE 和 PARTICLE_SHEET_TRANSLUCENT 会用到它们。
数据生成(Datagen)
粒子定义文件也可以通过继承 ParticleDescriptionProvider 并重写 #addDescriptions() 方法来进行 数据生成。
public class MyParticleDescriptionProvider extends ParticleDescriptionProvider {
// 从 `GatherDataEvent.Client` 获取参数。
public MyParticleDescriptionProvider(PackOutput output) {
super(output);
}
// 假设所有引用的粒子都已存在。请将 "examplemod" 替换为你的 mod id。
@Override
protected void addDescriptions() {
// 添加一个单独的 sprite 粒子定义,文件位于
// assets/examplemod/textures/particle/my_single_particle.png。
sprite(MyParticleTypes.MY_SINGLE_PARTICLE.get(), ResourceLocation.fromNamespaceAndPath("examplemod", "my_single_particle"));
// 添加一个多 sprite 粒子定义,使用可变参数。也可以接受一个列表。
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")
);
// 上述方法的另一种写法,将为给定数量的贴图在基础名称后自动追加 "_<index>"。
spriteSet(MyParticleTypes.MY_ALT_MULTI_PARTICLE.get(),
// 基础名称。
ResourceLocation.fromNamespaceAndPath("examplemod", "my_multi_particle"),
// 贴图数量。
3,
// 是否反转列表,例如从最后一个元素开始而不是第一个。
false
);
}
}
不要忘记将该 provider 添加到 GatherDataEvent.Client:
@SubscribeEvent
public static void gatherData(GatherDataEvent.Client event) {
event.createProvider(MyParticleDescriptionProvider::new);
}
自定义 ParticleType(Custom ParticleTypes)
虽然在大多数情况下 SimpleParticleType 已经足够,但有时你需要在服务端为粒子附加额外的数据。这时你就需要自定义的 ParticleType 和相应的自定义 ParticleOptions。我们先从 ParticleOptions 开始,因为实际的信息就存储在这里:
public class MyParticleOptions implements ParticleOptions {
// 读写信息,通常用于命令
// 由于此类型没有额外信息,这里会是一个空字符串
public static final MapCodec<MyParticleOptions> CODEC = MapCodec.unit(new MyParticleOptions());
// 读写信息到网络缓冲区。
public static final StreamCodec<ByteBuf, MyParticleOptions> STREAM_CODEC = StreamCodec.unit(new MyParticleOptions());
// 不需要任何参数,但你可以定义粒子需要的任何字段。
public MyParticleOptions() {}
@Override
public ParticleType<?> getType() {
// 返回已注册的粒子类型
}
}
我们接下来在自定义的 ParticleType 中使用这个 ParticleOptions 实现类……
public class MyParticleType extends ParticleType<MyParticleOptions> {
// 这个布尔参数依然决定是否在较低粒子设置下限制粒子数量。
// 更多信息请参见本文开头附近 MyParticleTypes 类的实现说明。
public MyParticleType(boolean overrideLimiter) {
// 将反序列化器传递给父类。
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();
}
}
生成粒子(Spawning Particles)
如前所述,服务端只了解 ParticleType 和 ParticleOption,而客户端则直接处理由与 ParticleType 关联的 ParticleProvider 提供的 Particle。因此,根据你所处的端(side),生成粒子的方式有很大不同。
- 通用代码:调用
Level#addParticle或Level#addAlwaysVisibleParticle。这是创建对所有人可见粒子的推荐方式。 - 客户端代码:可以使用通用代码方式。或者,也可以用你选择的粒子类直接
new Particle(),然后通过Minecraft.getInstance().particleEngine#add(Particle)添加该粒子。注意,这种方式添加的粒子只会在本客户端显示,其他玩家不可见。 - 服务端代码:调用
ServerLevel#sendParticles。原版/particle指令就是用这种方式。