Any help or advice would be appreciated. I'm trying to learn how to add particles to a mod, using a tutorial from TheGreyGhost.
Trying to get the particle to appear in the Entity's tick() method. The vanilla FLAME particle appears but not my particle.
@Override
public void tick() {
if(entity == null) return;
try {
// Adds a particle to every entity while in the overworld!
//this.level.addParticle(ParticleTypes.FLAME, this.getX(), this.getY() + 1, this.getZ(), 0, 0.04, 0);
// Try to add a custom particle to every entity in the overworld.
Color tint = Color.CYAN; // does this do anything if the particle texture is already colored?
double diameter = this.getEntityScale();
FlameParticleData flameParticleData = new FlameParticleData(tint, diameter);
this.level.addParticle(flameParticleData, this.getX(), this.getY() + 2, this.getZ(), 0, 0.05, 0);
} catch (Exception e) {
The other code I have setting up the Particle Registration, the FlameParticle, the FlameParticleData, the FlameParticleFactory, and the FlameParticleType:
ParticleRegistration.java.
I also have a flame_particle.json that references the flame.png that I want to use.
@Mod.EventBusSubscriber(modid = MyMod.MODID, bus = Bus.MOD, value = Dist.CLIENT)
public class ParticleRegistration {
public static final DeferredRegister<ParticleType<?>> PARTICLES_TYPES = DeferredRegister.create(
ForgeRegistries.PARTICLE_TYPES, MyMod.MODID);
// This sets the FlameParticleType to use the textures specified in flame_particle.json.
public static final RegistryObject<ParticleType<FlameParticleData>> FLAME_PARTICLE = PARTICLES_TYPES.register(
"flame_particle",
FlameParticleType::new);
// Is this necessary?
public static ParticleType<FlameParticleData> flameParticleType = new FlameParticleType();
@SuppressWarnings("resource")
@SubscribeEvent
public static void registerParticleFactory(ParticleFactoryRegisterEvent event) {
Minecraft.getInstance().particleEngine.register(ParticleRegistration.FLAME_PARTICLE.get(), FlameParticleFactory::new);
}
}
FlameParticle.java
/**
* Based on TheGreyGhost's MinecraftByExample
* Custom Particle to illustrate how to add a Particle with your own texture and movement/animation behaviour
* */
public class FlameParticle extends SpriteTexturedParticle
{
/**
* Construct a new FlameParticle at the given [x,y,z] position, with the given initial velocity, the given color, and the
* given diameter.
* We also supply sprites so that you can change the sprite texture in the tick() method (although not needed for this example)
*/
public FlameParticle(ClientWorld world, double x, double y, double z,
double velocityX, double velocityY, double velocityZ,
Color tint, double diameter,
IAnimatedSprite sprites)
{
super(world, x, y, z, velocityX, velocityY, velocityZ);
this.sprites = sprites;
setColor(tint.getRed()/255.0F, tint.getGreen()/255.0F, tint.getBlue()/255.0F);
setSize((float)diameter, (float)diameter); // the size (width, height) of the collision box.
final float PARTICLE_SCALE_FOR_ONE_METRE = 0.5F; // if the particleScale is 0.5, the texture will be rendered as 1 metre high
// sets the rendering size of the particle for a TexturedParticle.
this.scale(PARTICLE_SCALE_FOR_ONE_METRE * (float)diameter);
//maxAge = 100; // lifetime in ticks: 100 ticks = 5 seconds
this.lifetime = 100;
final float ALPHA_VALUE = 1.0F;
this.alpha = ALPHA_VALUE;
//the vanilla Particle constructor added random variation to our starting velocity. Undo it!
this.xd = velocityX;
this.yd = velocityY;
this.zd = velocityZ;
// the move() method will check for collisions with scenery
this.hasPhysics = true; // I think hasPhysics replaces canCollide
}
// Comments from TheGreyGhost
// ---- methods used by TexturedParticle.renderParticle() method to find out how to render your particle
// the base method just renders a quad, rotated to directly face the player
// can be used to change the skylight+blocklight brightness of the rendered Particle.
@Override
public int getLightColor(float partialTick) // previously protected int getBrightnessForRender(float partialTick)
{
final int BLOCK_LIGHT = 15; // maximum brightness
final int SKY_LIGHT = 15; // maximum brightness
final int FULL_BRIGHTNESS_VALUE = LightTexture.pack(BLOCK_LIGHT, SKY_LIGHT); // .pack replaces .packLight
return FULL_BRIGHTNESS_VALUE;
// if you want the brightness to be the local illumination (from block light and sky light) you can just use
// the Particle.getBrightnessForRender() base method, which contains:
// BlockPos blockPos = new BlockPos(this.posX, this.posY, this.posZ);
// return this.world.isBlockLoaded(blockPos) ? WorldRenderer.getCombinedLight(this.world, blockPos) : 0;
}
// Choose the appropriate render type for your particles:
// There are several useful predefined types:
// PARTICLE_SHEET_TRANSLUCENT semi-transparent (translucent) particles
// PARTICLE_SHEET_OPAQUE opaque particles
// TERRAIN_SHEET particles drawn from block or item textures
// PARTICLE_SHEET_LIT appears to be the same as OPAQUE. Not sure of the difference. In previous versions of minecraft,
// "lit" particles changed brightness depending on world lighting i.e. block light + sky light
public IParticleRenderType getRenderType() {
return IParticleRenderType.PARTICLE_SHEET_TRANSLUCENT;
}
/**
* call once per tick to update the Particle position, calculate collisions, remove when max lifetime is reached, etc
*/
@Override
public void tick()
{
// if you want to change the texture as the particle gets older, you can use
//setSpriteFromAge(sprites); // not sure whether this should be uncommented yet
this.xo = x; // previously prevPosX and posX
this.yo = y; // previously prevPosY and posY
this.xo = z; // previously prevPosZ and posZ
move(xd, yd, zd); // simple linear motion. You can change speed by changing xd, yd,
// zd every tick. For example - you can make the particle accelerate downwards due to gravity by
// final double GRAVITY_ACCELERATION_PER_TICK = -0.02;
// yd += GRAVITY_ACCELERATION_PER_TICK;
// calling move() also calculates collisions with other objects
// collision with a block makes the ball disappear. But does not collide with entities
if (onGround) { // onGround is only true if the particle collides while it is moving downwards...
this.remove(); // this.setExpired() is probably this.remove()
}
if (yo == y && yd > 0) { // detect a collision while moving upwards (can't move up at all)
this.remove();
}
if (this.age++ >= this.lifetime) { // this.maxAge becomes this.lifetime
this.remove();
}
}
private final IAnimatedSprite sprites; // contains a list of textures; choose one using either
// newParticle.selectSpriteRandomly(sprites); or newParticle.selectSpriteWithAge(sprites);
}
FlameParticleData.java
/**
* Based on TheGreyGhost's MinecraftByExample
* The particle has two pieces of information which are used to customise it:
*
* 1) The colour (tint) which is used to change the hue of the particle
* 2) The diameter of the particle
*
* This class is used to
* 1) store this information, and
* 2) transmit it between server and client (write and read methods), and
* 3) parse it from a command string i.e. the /particle params
*/
public class FlameParticleData implements IParticleData {
public FlameParticleData(Color tint, double diameter) {
this.tint = tint;
this.diameter = constrainDiameterToValidRange(diameter);
}
public Color getTint() {
return tint;
}
/**
* @return get diameter of particle in metres
*/
public double getDiameter() {
return diameter;
}
@Nonnull
@Override
public ParticleType<FlameParticleData> getType() {
return ParticleRegistration.flameParticleType;
}
// write the particle information to a PacketBuffer, ready for transmission to a client
@Override
public void writeToNetwork(PacketBuffer buf) {
buf.writeInt(tint.getRed());
buf.writeInt(tint.getGreen());
buf.writeInt(tint.getBlue());
buf.writeDouble(diameter);
}
// used for debugging I think; prints the data in human-readable format
@Nonnull
@Override
public String writeToString() {
return String.format(Locale.ROOT, "%s %.2f %i %i %i",
this.getType().getRegistryName(), diameter, tint.getRed(), tint.getGreen(), tint.getBlue());
}
private static double constrainDiameterToValidRange(double diameter) {
final double MIN_DIAMETER = 0.05;
final double MAX_DIAMETER = 1.0;
return MathHelper.clamp(diameter, MIN_DIAMETER, MAX_DIAMETER);
}
private Color tint;
private double diameter;
// Comments from the TheGreyGhost
// --------- these remaining methods are used to serialize the Particle Data.
// I'm not yet sure what the Codec is used for, given that the DESERIALIZER already deserializes using read.
// Perhaps it will be used to replace the manual read methods in the future.
// The CODEC is a convenience to make it much easier to serialise and deserialise your objects.
// Using the builder below, you construct a serialiser and deserialiser in one go, using lambda functions.
// eg for the FlameParticleData CODEC:
// a) In order to serialise it, it reads the 'tint' member variable (type: INT) and the 'diameter' member variable (type: DOUBLE)
// b) In order to deserialise it, call the matching constructor FlameParticleData(INT, DOUBLE)
public static final Codec<FlameParticleData> CODEC = RecordCodecBuilder.create(
instance -> instance.group(
Codec.INT.fieldOf("tint").forGetter(d -> d.tint.getRGB()),
Codec.DOUBLE.fieldOf("diameter").forGetter(d -> d.diameter)
).apply(instance, FlameParticleData::new)
);
private FlameParticleData(int tintRGB, double diameter) {
this.tint = new Color(tintRGB);
this.diameter = constrainDiameterToValidRange(diameter);
}
// The DESERIALIZER is used to construct FlameParticleData from either command line parameters or from a network packet
public static final IDeserializer<FlameParticleData> DESERIALIZER = new IDeserializer<FlameParticleData>() {
// parse the parameters for this particle from a /particle command
@Nonnull
@Override
public FlameParticleData fromCommand(@Nonnull ParticleType<FlameParticleData> type, @Nonnull StringReader reader) throws CommandSyntaxException {
reader.expect(' ');
double diameter = constrainDiameterToValidRange(reader.readDouble());
final int MIN_COLOUR = 0;
final int MAX_COLOUR = 255;
reader.expect(' ');
int red = MathHelper.clamp(reader.readInt(), MIN_COLOUR, MAX_COLOUR);
reader.expect(' ');
int green = MathHelper.clamp(reader.readInt(), MIN_COLOUR, MAX_COLOUR);
reader.expect(' ');
int blue = MathHelper.clamp(reader.readInt(), MIN_COLOUR, MAX_COLOUR);
Color color = new Color(red, green, blue);
return new FlameParticleData(color, diameter);
}
// read the particle information from a PacketBuffer after the client has received it from the server
@Override
public FlameParticleData fromNetwork(@Nonnull ParticleType<FlameParticleData> type, PacketBuffer buf) {
// warning! never trust the data read in from a packet buffer.
final int MIN_COLOUR = 0;
final int MAX_COLOUR = 255;
int red = MathHelper.clamp(buf.readInt(), MIN_COLOUR, MAX_COLOUR);
int green = MathHelper.clamp(buf.readInt(), MIN_COLOUR, MAX_COLOUR);
int blue = MathHelper.clamp(buf.readInt(), MIN_COLOUR, MAX_COLOUR);
Color color = new Color(red, green, blue);
double diameter = constrainDiameterToValidRange(buf.readDouble());
return new FlameParticleData(color, diameter);
}
};
}
FlameParticleFactory.java
/**
* Based on TheGreyGhost's MinecraftByExample
* On the client side:
* When the client wants to spawn a Particle, it gives the FlameParticleData to this factory method
* The factory selects an appropriate Particle class and instantiates it *
*/
public class FlameParticleFactory implements IParticleFactory<FlameParticleData> { //IParticleFactory
private final IAnimatedSprite sprites; // contains a list of textures; choose one using either
// not sure if i still need this
// newParticle.selectSpriteRandomly(sprites); or newParticle.selectSpriteWithAge(sprites);
// this method is needed for proper registration of your Factory:
// The ParticleManager.register method creates a Sprite and passes it to your factory for subsequent use when rendering, then
// populates it with the textures from your textures/particle/xxx.json
public FlameParticleFactory(IAnimatedSprite sprite) {
this.sprites = sprite;
}
@Nullable
@Override
public Particle createParticle(FlameParticleData flameParticleData, ClientWorld world, double xPos, double yPos, double zPos, double xVelocity, double yVelocity, double zVelocity) {
FlameParticle newParticle = new FlameParticle(world, xPos, yPos, zPos, xVelocity, yVelocity, zVelocity,
flameParticleData.getTint(), flameParticleData.getDiameter(), sprites);
newParticle.pickSprite(sprites); // not quite the newParticle.selectSpriteRandomly(sprites) that was used.
return newParticle;
}
// This is private to prevent you accidentally registering the Factory using the default constructor.
// ParticleManager has two register methods, and if you use the wrong one the game will enter an infinite loop
private FlameParticleFactory() {
throw new UnsupportedOperationException("Use the FlameParticleFactory(IAnimatedSprite sprite) constructor");
}
}
FlameParticleType.java
/**
* Based on TheGreyGhost's MinecraftByExample
* Simple class used to describe the Particle
*/
public class FlameParticleType extends ParticleType<FlameParticleData> {
private static boolean ALWAYS_SHOW_REGARDLESS_OF_DISTANCE_FROM_PLAYER = false;
public FlameParticleType() {
super(ALWAYS_SHOW_REGARDLESS_OF_DISTANCE_FROM_PLAYER, FlameParticleData.DESERIALIZER);
}
// get the Codec used to
// a) convert a FlameParticleData to a serialised format
// b) construct a FlameParticleData object from the serialised format
public Codec<FlameParticleData> codec() {
return FlameParticleData.CODEC;
}
}
The flame_particle.json, which is located in resources/asset.MyMod/particles, that references the flame.png, located in resources/asset.MyMod/particles/textures/particles
{
"textures": [
"MyMod:flame"
]
}