API/Utilities for Particle creation and registration
See original GitHub issueAs it currently stands, creating a custom Particle
is… quite the ordeal. There are several steps involved in doing so:
- Creating the
Particle
subclass (client) - Registering any required sprites (client)
- Registering the appropriate
ParticleType
(client/server) - Registering a
ParticleFactory
(client) - Actually spawning the particle (client)
Of these 5, only registering the ParticleType
is straightforward, and even that can’t be easily understood by looking at how Vanilla handles particles. Thus, I’d like to propose a Fabric wrapper around the particle system to simplify this process.
Simple Steps - Particle
and ParticleType
Creating a custom Particle
is almost as straightforward and easy as a Block
or Item
as long as you don’t need to do anything particularly fancy with it. The trouble only really comes when you try to apply a custom texture. It’s almost as simple as this:
@Environment(EnvType.CLIENT)
public class TestParticle extends SpriteBillboardParticle {
static final Identifier sprite = new Identifier("particletest:particle/test_particle");
public TestParticle(World world, double x, double y, double z) { this(world, x, y, z, 0, 0, 0); }
public TestParticle(World world, double x, double y, double z, double vx, double vy, double vz) {
super(world, x, y, z, vx, vy, vz);
this.setSprite(MinecraftClient.getInstance().getSpriteAtlas().getSprite(sprite));
}
public ParticleTextureSheet getType() { return ParticleTextureSheet.PARTICLE_SHEET_OPAQUE; }
}
Registering the ParticleType
is similarly straightforward, albeit extremely obtuse:
public class ParticleTestCommon implements ModInitializer {
public static DefaultParticleType testParticleType = new DefaultParticleType(false){};
public void onInitialize() {
Registry.register(Registry.PARTICLE_TYPE, "particletest:test_particle", testParticleType);
}
}
Problem 1 - Registering ParticleFactory
s
ParticleFactory
registry, on the other hand, is quite the ordeal. There is no Registry
type for ParticleFactory
s, and all the relevant methods on ParticleManager
are private. The only solution I’ve found is a client-side mixin:
@Mixin(ParticleManager.class)
public abstract class MixinParticleManager {
@Shadow
private <T extends ParticleEffect> void registerFactory(ParticleType<T> pt, ParticleFactory<T> pf) {}
@Inject(method = "net/minecraft/client/particle/ParticleManager.registerDefaultFactories()V", at = @At("RETURN"))
private void registerCustomFactories(CallbackInfo cbi) {
this.registerFactory(ParticleTestCommon.testParticleType, (pt, world, x, y, z, vx, vy, vz) -> {
return new TestParticle(world, x, y, z, vx, vy, vz);
});
}
}
We can then spawn our particle as follows:
world.addParticle(testParticleType, x, y, z, 0, 0, 0);
Or, if we need to pass custom properties of some kind, we can bypass the ParticleFactory
altogether (keep in mind that this will make your particle incompatible with or feature-limited when spawned via the /particle
command - this should really be done through a custom ParticleType
rather than bypassing ParticleFactory
but I haven’t messed with that yet so I can’t give examples):
MinecraftClient.getInstance().particleManager.addParticle(new TestParticle(world, x, y, z, vx, vy, vz, ...));
This works passably, but as we’ll see in just a moment, it comes with an inherent and deadly problem.
Problem 2 - Registering Textures
Let’s get the biggest problem out of the way now - registering custom particle textures is nearly if not impossible to do “Vanilla-esque”. There are two separate levels involved; the client texture registry, and a modification to how your ParticleFactory
is registered. Registering the texture with the client is (theoretically) simple, via ClientSpriteRegistryCallback
:
@Environment(EnvType.CLIENT)
public class ParticleTestClient implements ClientModInitializer {
public void onInitializeClient() {
ClientSpriteRegistryCallback.EVENT.register((atlas, registry) -> {
if(atlas == MinecraftClient.getInstance().getSpriteAtlas()) {
registry.register(TestParticle.sprite);
}
});
}
}
However, the problem comes when we try to update our ParticleFactory
to properly use this texture. Registering a ParticleFactory
that can use this texture requires a SpriteProvider
, which uses a different version of ParticleManager#registerFactory
. Unfortunately, ParticleManager
uses two package-private inner classes in the process of this version of the registry method: SimpleSpriteProvider
and class_4091
(henceforth called ParticleFactoryFactory
for simplicity, because that’s literally what it is). It’s impossible to shadow the proper version of registerFactory
, because it uses ParticleFactoryFactory
in its signature. It’s also impossible to shadow the maps that registerFactory
saves the registered information to and create our own registerFactory
method, since one of those maps has SimpleSpriteProvider
in its signature. As of the time of writing, I have been unable to find any alternative method that uses the built-in particle sprite registry.
The Workaround
I have, however, found one workaround - skipping the vanilla particle texture registry altogether. This reduces the process to only 4 steps:
- Creating the
Particle
subclass (client) - Registering the appropriate
ParticleType
(client/server) - Registering a
ParticleFactory
(client) - Actually spawning the particle (client)
Since we no longer need to use the vanilla sprite registry, there’s no need to use the more complex version of ParticleManager#registerFactory
, and we bypass the issue of the package-private inner classes altogether. There are a couple of other changes that come along with this:
- The client-side entrypoint is no longer needed
- Our custom
Particle
needs to extendBillboardParticle
instead ofSpriteBillboardParticle
(cutting one link out of the inheritance chain), which will require a couple of extra abstract method implementations - We need to manually acquire and bind our particle texture when we’re ready to draw our particle
This means the following changes to our Particle
class:
@Environment(EnvType.CLIENT)
public class TestParticle extends /*Sprite*/BillboardParticle { // <-- HERE
static final Identifier sprite = new Identifier("particletest:textures/particle/test_particle.png"); // <-- HERE
public TestParticle(World world, double x, double y, double z) { this(world, x, y, z, 0, 0, 0); }
public TestParticle(World world, double x, double y, double z, double vx, double vy, double vz) {
super(world, x, y, z, vx, vy, vz);
// this.setSprite(MinecraftClient.getInstance().getSpriteAtlas().getSprite(sprite)); // <-- HERE
}
public ParticleTextureSheet getType() { return ParticleTextureSheet.PARTICLE_SHEET_OPAQUE; }
// v ADDED METHODS v
public void buildGeometry(BufferBuilder var1, Camera var2, float var3, float var4, float var5, float var6, float var7, float var8) {
MinecraftClient.getInstance().getTextureManager().bindTexture(texture);
super.buildGeometry(var1, var2, var3, var4, var5, var6, var7, var8);
}
protected float getMinU() { return 0; }
protected float getMaxU() { return 1; }
protected float getMinV() { return 0; }
protected float getMaxV() { return 1; }
}
This works perfectly fine. We can revert our MixinParticleManager
to before we tried to shadow the more complex overload of registerFactory
, and spawn our particles into the world the same way.
Proposed API
I would personally propose the following additions to Fabric’s API:
- A
FabricParticle
class which can be extended rather than extendingBillboardParticle
directly, which implements the basic method changes likegetMin/MaxU/V
andbuildGeometry
and makes the texture location a superctor parameter (potentially simplified from the full path needed?)- Any particles which are more complicated than
BillboardParticle
is set up for will need to do this for themselves, but currently all Vanilla particles except the following extendBillboardParticle
at least indirectly, so these cases should be rare.ElderGuardianAppearanceParticle
EmitterParticle
ExplosionEmitterParticle
FireworksSparkParticle
andFireworkParticle
ItemPickupParticle
- Some of the base “extend me” particle classes that aren’t actual ingame particles on their own
- Any particles which are more complicated than
- A
FabricParticleTypeRegistry
and/orFabricParticleFactoryRegistry
which can be publicly accessed- The former would be for registering
ParticleTypes
-DefaultParticleType
is almost always sufficient for this, but it would be trivial to add an overload that accepts a manually-constructedParticleType
. The registry method would take an identifier and optional customParticleType
, callRegistry.register(PARTICLE_TYPE, ...)
, and return the registeredParticleType
so it could be saved for later use.- Note that while this is not strictly necessary for the API to function, it would smooth the process of understanding how to register a particle significantly, as it cuts out the middle man of understanding where you’re supposed to get a
ParticleType
and how to construct a trivial one for simple particles.
- Note that while this is not strictly necessary for the API to function, it would smooth the process of understanding how to register a particle significantly, as it cuts out the middle man of understanding where you’re supposed to get a
- The latter would be for registering
ParticleFactory
s. The registry method would take aParticleType
(usually from the above method) and a lambda for the factory body, and store them in a temporary map.
- The former would be for registering
- A mixin to
ParticleManager#registerDefaultFactories
, as done manually above, that accesses the temporary map fromFabricParticleFactoryRegistry
and registers all of the contained factories withParticleManager#registerFactory
. - Potentially a simplified way of creating custom
ParticleType
s?
These additions would drastically simplify the process of creating a custom particle, allowing modders to bypass potential hours of digging through Vanilla’s code before realizing that they can’t actually register a particle the same way Vanilla does.
Issue Analytics
- State:
- Created 4 years ago
- Comments:15 (7 by maintainers)
Top GitHub Comments
I’d say leave this open until it’s merged
@ShadowHunter22 This issue is extremely outdated and none of the code in it is relevant to current Fabric development. It was replaced by a PR in #264, which itself was superceded by #341, which has been merged for almost 2 years now. If you’re trying to use the code from this issue, don’t. If you’re not, you’re in the wrong place.