Particles
Particles are 2D effects that polish the game and add immersion. They can be spawned both client and server side, but being mostly visual in nature, critical parts exist only on the physical (and logical) client side.
Registering Particles
ParticleType
Particles are registered using ParticleType
s. These work similar to EntityType
s or BlockEntityType
s, in that there's a Particle
class - every spawned particle is an instance of that class -, and then there's the ParticleType
class, holding some common information, that is used for registration. ParticleType
s are a registry, which means that we want to register them using a DeferredRegister
like all other registered objects:
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)
);
}
A ParticleType
is only necessary if you need to work with particles on the server side. The client can also use Particle
s directly.
Particle
A Particle
is what is later spawned into the world and displayed to the player. While you may extend Particle
and implement things yourself, in many cases it will be better to extend TextureSheetParticle
instead, as this class provides helpers for things such as animating and scaling, and also does the actual rendering for you (all of which you'd need to implement yourself if extending Particle
directly).
Most properties of Particle
s are controlled by fields such as gravity
, lifetime
, hasPhysics
, friction
, etc. The only two methods that make sense to implement yourself are tick
and move
, both of which do exactly what you'd expect. As such, custom particle classes are often short, consisting e.g. only of a constructor that sets some fields and lets the superclass handle the rest. A basic implementation would look somewhat like this:
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
Next, particle types must register a ParticleProvider
. ParticleProvider
is a client-only class responsible for actually creating our Particle
s through the createParticle
method. While more elaborate code can be included here, many particle providers are as simple as this:
// 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);
}
}
Your particle provider must then be associated with the particle type in the client-side mod bus event 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.
}