Skip to content

Particles

ZSolarDev edited this page Jan 25, 2025 · 7 revisions


The Particle System

The whole foundation of this particle system is to give all power to the user here. At first I thought I would need to bloat this system with as many features as possible, until I realized a much better solution; the behavior system. Basically, you have control over a given particle and its parent emitter when a particle is about to be spawned, after a particle was just spawned, and every frame the particle exists(you get deltaTime every frame, too.) This goes for every particle in an emitter. I then realized that you have no easy way of giving each particle its own local values, so I made a "values" variable for each particle(reference the LunarParticle docs just below to see how this variable is used, its super nice.) This variable essentially gives you an efficient way to assign each particle its own local values, and it is an essential feature to behaviors. With this behavior system, you can define if the particle will spawn or not when it's supposed to and what will happen in this event, what happens after a particle is spawned, and every frame a particle exists along with the dt. ALSO! To access deltaTime in preParticleSpawn/onParticleSpawn, use emitter.curDt. It is the most recent deltaTime given to the emitter.

I also made a LunarParticleSystem to go along with the LunarParticleEmitter. A particle system basically holds and lets you easily manage a group of emitters at once. You can set the variable of all emitters, like if they should all autoSpawn, or how long to wait if they do. You can also add emitters that don't autoSpawn and some that do, and use spawnParticleBatch to spawn a particle batch on the emitters that don't autoSpawn. I found that this could be a handy feature for a lot of cases, especially while making effects.

Here is an example on how to use a particle emitter on HaxeFlixel:

var emitter:LunarParticleEmitter;
var fadeBehavior:LunarFadeParticleBehavior = new LunarFadeParticleBehavior(1.5); // A behavior that fades the particle after 1.5 seconds.
// In create()
var renderer = new LunarRenderer({
	x: 0,
	y: 0,
	width: FlxG.width,
	height: FlxG.height
});
add(renderer);

emitter = new LunarParticleEmitter(60, 60, renderer, new LunarTexture(0xFFFFFFFF, 'assets/breh.png', 60, 60), null, {
	mainParticleBehavior: new LunarRandSpawnOffsetParticleBehavior(100, 100) // Spawns the particles at a random 100x100 x and y area
});
var fadeBehavior = new LunarFadeParticleBehavior(); // Making a new fade behavior
emitter.addBehavior('time is ticking!', fadeBehavior); // Adds our fade behavior into the emitter
fadeBehavior.fadeStartedCallback = (particle, emitter, dt) -> // When a fade starts on this particle...
{
	particle.behavior = new LunarGravityParticleBehavior(1); // Apply a gravity of 1 via the gravity particle behavior.
};

This code spawns a particle(a texture located at assets/breh.png with width and height of 60, 60. Color has no effect on textures currently.) that spawns every 0.1 seconds within a 100 x 100 pixel range of the emitter. Each particle gets a gravity force of 1 applied to it and starts fading out after 1 second. After the particle becomes invisible, it dies(sets dead to true for reuse when another particle needs to be spawned.)

Here is an example on how to make a custom particle behavior:

// Imports
import lunarps.LunarShape.LunarCircle;
import lunarps.math.*;
import lunarps.particles.*;

class GrowOnSpawnBehavior extends LunarParticleBehavior // Extends LunarParticleBehavior
{
    // When a particle spawns...
	override public function onParticleSpawn(particle:LunarParticle, emitter:LunarParticleEmitter)
	{
        if (particle.shape.shapeType == CIRCLE){ // Is the particle that just spawned a circle shape? If so...
		    var circ:LunarCircle = cast particle.shape; // get the shape as a circle.
		    particle.values.r = 0.0; // Set its ratio to 0 as a local value to the particle(Used for eased intopolation.)
		    particle.values.oldRadius = circ.radius; // Save its old radius as a local value to the particle.
		    circ.radius = 1; // Set the circles radius to 1 so it's almost invisible.
        }
	}

	override public function onParticleFrame(particle:LunarParticle, emitter:LunarParticleEmitter, dt:Float)
	{
        if (particle.shape.shapeType == CIRCLE){ // Is this particle a circle shape? If so...
		    var circ:LunarCircle = cast particle.shape; // get the shape as a circle.
		    particle.values.r += dt / 2; // Increase the ratio to increase ease progress
            /* 
                Smoothly interpolate between the current size(super small as I set earlier when the particle spawned) 
                and to the size it was supposed to be when it originally spawned at.
            */
		    circ.radius = LunarInterp.easedInterp(circ.radius, particle.values.oldRadius, particle.values.r, 'smootherStepInOut');
        }
	}
}

This is a simple behavior that makes a circle smoothly grow to appear. If you need more references, look at the preset behaviors.





LunarParticle

A LunarParticle is mainly only used in a LunarParticleEmitter. They are used in a very interesting way. A particle comes with its own behavior, and a "values" variable:

holds all custom values for this particle. These values are defined manually. eg:

// Lifetime isn't defined yet, but by setting it to a value, it now exists.
trace(values.lifetime); // null, not defined yet
values.lifetime = 50;
trace(values.lifetime); // 50

// You can also store functions in this value as well. eg:
values.startDeathAnimation = (length:Int):String -> { 
	trace('Starting death animation with length: $length');
};
trace(values.startDeathAnimation(30)); // Starting death animation with length 30

This is useful for storing custom values for particles that are defined by a particle behavior. You can make a whole state system if you really wanted to, you can define your own functions, and variables.



LunarParticleEmitter

A LunarParticleEmitter is an extremely flexible particle emitter.

Functions:

  • new(x:Float = 0, y:Float = 0, renderer:LunarRenderer, particleConfig:LunarShape, ?emitterLayer:LunarRenderLayer, miscProps:MiscProps): makes a new emitter.
  • remove(base:LunarBase): removes a Base added to this renderer.
  • spawnParticle(shape:LunarShape): spawns a particle with the given shape.
  • addBehavior(name:String, behavior:LunarParticleBehavior): Adds a behavior to the sideParticleBehaviors map.
  • removeBehavior(name:String): Removes a behavior from the sideParticleBehaviors map.
  • getBehavior(name:String): Gets a behavior from the sideParticleBehaviors map.
  • spawnParticleBatch(shape:LunarShape, amount:Int): Spawns a batch of particles with the given shape.

Variables:

  • x:Float
  • y:Float
  • emitterLayer:LunarRenderLayer: The render layer used for this particle emitter(Makes its own in the constructor if you don't give it one)
  • particleConfig:LunarShape: The Shape used for each particle
  • renderer:LunarRenderer: This emitters renderer(if you don't give it one in the constructor)
  • particles:LunarPool: The current particles in this emitter.
  • mainParticleBehavior:LunarParticleBehavior: The particle behavior that gets preParticleSpawn called on it. (It's basically the same as a sideParticleBehavior.)
  • sideParticleBehaviors:Map<String, LunarParticleBehavior>: every other behavior ran on the particles.
  • waitingSecs:Float: How long in seconds between each particle spawn if autoSpawning is enabled.
  • particlesPerWaitingSecs:Int: How many particles spawn every time curSecs is 0.
  • maxParticles:Int: The maximum number of particles that can exist at once. 0 = infinite particles.
  • curSecs:Float: An internal variable used for counting time for autoSpawning.
  • autoSpawning:Bool: If particles should be spawn automatically every seconds.
  • curDt:Float: The current deltaTime the emitter has.

I should say this so you don't get confused. Think of a main-behavior and a side-behavior as the same thing; Only in a few unser-made edge cases will you need to worry about the difference between these. The only difference is that the main-behavior gets to run code when a particle is about to get spawned(getting access to the config of the particle about to be spawned and the emitter), and gets to choose if the particle will truly be spawned by the emitter based on your return. If you make the function return true, the emitter will go on and spawn the particle. If false, the emitter will not spawn the particle. The emitter will also not call onParticleSpawn for any side-behaviors, as the emitter didn't create the particle to give to the side-behaviors for the onParticleSpawnFunction. beware of that.



LunarParticleBehavior

A LunarParticleBehavior is a class that can be extended to add custom movement, rules, basically your whole particle system. there are many presets inside of the behaviors folder you can use for reference or just to use in general.

Functions:

  • preParticleSpawn(config:LunarShape, emitter:LunarParticleEmitter):Bool: This function is called before a particle is spawned. If false is returned, the particle emitter will not spawn a particle. This is useful for spawning a modified particle instead of a normal one spawned by the emitter.
  • onParticleSpawn(particle:LunarParticle, emitter:LunarParticleEmitter): This function is called when a particle is spawned.
  • onParticleFrame(particle:LunarParticle, emitter:LunarParticleEmitter, dt:Float): This function is called on each particle every frame.


LunarParticleBehaviorPack

A LunarParticleBehaviorPack is an hx that contains behaviors and a class extending LunarParticleBehaviorPack. This class will include a main behavior and side behaviors using either lunarps preset behaviors or behaviors inside the hx with your custom behavior pack.

This was made so that someone can make something like a lunarps explosion behavior pack which other people can use. All you would have to do is give someone the hx, then run emitter.addBehaviorPack(new LunarExplosionBehaviorPack);. Here is an example:

import lunarps.LunarShape.LunarCircle;
import lunarps.math.*;
import lunarps.particles.*;
import lunarps.particles.behaviors.*;

class CustomBehaviorPack extends LunarParticleBehaviorPack
{
	var fadeBehavior:LunarFadeParticleBehavior = new LunarFadeParticleBehavior();
	var growBehavior:GrowOnSpawnBehavior = new GrowOnSpawnBehavior();

	public function new()
	{
		super();
		sideBehaviors.set('time is ticking...!', fadeBehavior);
		sideBehaviors.set('let the particles grow', growBehavior);
		fadeBehavior.fadeStartedCallback = (particle, emitter, dt) ->
		{
			particle.behavior = new LunarGravityParticleBehavior(1);
		};
	}
}

class GrowOnSpawnBehavior extends LunarParticleBehavior
{
	override public function onParticleSpawn(particle:LunarParticle, emitter:LunarParticleEmitter)
	{
		if (particle.shape.shapeType == CIRCLE)
		{
			var circ:LunarCircle = cast particle.shape;
			particle.values.r = 0.0;
			particle.values.oldWidth = circ.width;
			circ.width = 1;
			particle.values.oldHeight = circ.height;
			circ.height = 1;
		}
	}

	override public function onParticleFrame(particle:LunarParticle, emitter:LunarParticleEmitter, dt:Float)
	{
		if (particle.shape.shapeType == CIRCLE)
		{
			var circ:LunarCircle = cast particle.shape;
			particle.values.r += dt / 2;
			circ.width = cast LunarInterp.easedInterp(circ.width, particle.values.oldWidth, particle.values.r, 'smootherStepInOut');
			circ.height = cast LunarInterp.easedInterp(circ.height, particle.values.oldHeight, particle.values.r, 'smootherStepInOut');
		}
	}
}

Variables:

  • mainBehavior:LunarParticleBehavior: The main behavior of this pack
  • sideBehaviors:Map<String, LunarParticleBehavior>: The side behaviors of this pack


LunarParticleSystem

-# This is only partially documented! everything else is obvious or irrelevant.

A LunarParticleSystem is a class that handles multiple emitters for effects that require them.

Variables:

  • emitters:Map<String, LunarParticleEmitter>: This store all of your emitters.

Functions:

  • spawnParticleBatch(data:Array): This function spawns a particle batch. Here's how it works: LunarParticleBatchData contains the name of the emitter to spawn the batch on, the amount of particles to spawn in that batch, and the LunarShape of each particle spawned in that batch. you don't need to spawn a batch on every emitter due to you being able to have an array with only 2 emitters or something.
  • setEmitterProperty(prop:String, val:Dynamic): This function sets a variable name to a set value in all emitters so you don't have to do it manually.
  • addEmitter(name:String, emitter:LunarParticleEmitter): Adds an emitter to this particle system.
  • removeEmitter(name:String): Removes an emitter from this particle system.
  • getEmitter(name:String): Gets an emitter from this particle system.

Welcome, Welcome. Read up, and have a good day!

Clone this wiki locally