Skip to content

Commit

Permalink
fix: [#2437] Respect particle emitter parenting (#2438)
Browse files Browse the repository at this point in the history
Closes #2437

![particles2](https://user-images.githubusercontent.com/612071/181403232-9accae6d-c2e7-44ee-9fc6-fe3b082a2bfd.gif)


## Changes:

- Adds a `particleTransform` parameter either `Global` or `Local`, `Global` was the implicit behavioral default before these changes and is not the explicit default. `Local` generates particles as children of the emitter which can be useful.
- Uses the global position when generating particles by default
  • Loading branch information
eonarheim committed Aug 6, 2022
1 parent fa65eb8 commit 2b91698
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 5 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Expand Up @@ -16,12 +16,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

-
- Added the emitted particle transform style as part of `ex.ParticleEmitter({particleTransform: ex.ParticleTransform.Global})`, [[ParticleTransform.Global]] is the default and emits particles as if they were world space objects, useful for most effects. If set to [[ParticleTransform.Local]] particles are children of the emitter and move relative to the emitter as they would in a parent/child actor relationship.

### Fixed

- Fixed issue where context opacity was not respected when set in a `preDraw`
- Fixed issue where `ex.Sound.loop` was not working, and switching tab visibility would cause odd behavior with looping `ex.Sound`
- Fixed issue where adding a `ex.ParticleEmitter` as a child did not position particles according to the parent
- Fixed issue where screenshots from `ex.Engine.screenshot()` did not match the smoothing set on the engine.
- Fixed incorrect event type returned when `ex.Actor.on('postupdate', (event) => {...})`.
- Fixed issue where using numerous `ex.Text` instances would cause Excalibur to crash webgl by implementing a global font cache.
Expand Down
1 change: 1 addition & 0 deletions sandbox/index.html
Expand Up @@ -12,6 +12,7 @@
<li><a href="tests/sprite-tint/">Sprite Tint</a></li>
<li><a href="tests/parallel/">Parallel Actions</a></li>
<li><a href="tests/isometric/">Isometric Map</a></li>
<li><a href="tests/emitter/">Particle Emitter</a></li>
<li><a href="tests/textcrash/">Many Text instances without failure (Chrome)</a></li>
<li><a href="tests/decode-many/">Decode Many Images without failure (Chrome)</a></li>
<li><a href="tests/side-collision/">Arcade: Sliding on Floor</a></li>
Expand Down
13 changes: 13 additions & 0 deletions sandbox/tests/emitter/index.html
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emitter</title>
</head>
<body>
<script src="../../lib/excalibur.js"></script>
<script src="./index.js"></script>
</body>
</html>
49 changes: 49 additions & 0 deletions sandbox/tests/emitter/index.ts
@@ -0,0 +1,49 @@

var game = new ex.Engine({
width: 400,
height: 400
});

var actor = new ex.Actor({
anchor: ex.vec(0.5, 0.5),
pos: game.screen.center,
color: ex.Color.Red,
radius: 5,
});

actor.actions.repeatForever(ctx => {
ctx.moveBy(ex.vec(100, 100), 100);
ctx.moveBy(ex.vec(-100, -100), 100);
});

var emitter = new ex.ParticleEmitter({
width: 10,
height: 10,
emitterType: ex.EmitterType.Rectangle,
particleTransform: ex.ParticleTransform.Global,
radius: 5,
minVel: 100,
maxVel: 200,
minAngle: 5.1,
maxAngle: 6.2,
emitRate: 300,
opacity: 0.5,
fadeFlag: true,
particleLife: 1000,
maxSize: 10,
minSize: 1,
startSize: 5,
endSize: 100,
acceleration: ex.vec(10, 80),
beginColor: ex.Color.Chartreuse,
endColor: ex.Color.Magenta
});
emitter.isEmitting = true;

game.start();
game.add(actor);

actor.angularVelocity = 2;

// doesn't work
actor.addChild(emitter);
49 changes: 46 additions & 3 deletions src/engine/Particles.ts
Expand Up @@ -102,8 +102,15 @@ export class ParticleImpl extends Entity {
this.endColor = endColor || this.endColor.clone();
this.beginColor = beginColor || this.beginColor.clone();
this._currentColor = this.beginColor.clone();
this.position = (position || this.position).add(this.emitter.pos);
this.velocity = velocity || this.velocity;

if (this.emitter.particleTransform === ParticleTransform.Global) {
const globalPos = this.emitter.transform.globalPos;
this.position = (position || this.position).add(globalPos);
this.velocity = (velocity || this.velocity).rotate(this.emitter.transform.globalRotation);
} else {
this.velocity = velocity || this.velocity;
this.position = (position || this.position);
}
this.acceleration = acceleration || this.acceleration;
this._rRate = (this.endColor.r - this.beginColor.r) / this.life;
this._gRate = (this.endColor.g - this.beginColor.g) / this.life;
Expand Down Expand Up @@ -236,6 +243,19 @@ export class Particle extends Configurable(ParticleImpl) {
}
}

export enum ParticleTransform {
/**
* [[ParticleTransform.Global]] is the default and emits particles as if
* they were world space objects, useful for most effects.
*/
Global = 'global',
/**
* [[ParticleTransform.Local]] particles are children of the emitter and move relative to the emitter
* as they would in a parent/child actor relationship.
*/
Local = 'local'
}

export interface ParticleEmitterArgs {
x?: number;
y?: number;
Expand All @@ -250,6 +270,14 @@ export interface ParticleEmitterArgs {
maxAngle?: number;
emitRate?: number;
particleLife?: number;
/**
* Optionally set the emitted particle transform style, [[ParticleTransform.Global]] is the default and emits particles as if
* they were world space objects, useful for most effects.
*
* If set to [[ParticleTransform.Local]] particles are children of the emitter and move relative to the emitter
* as they would in a parent/child actor relationship.
*/
particleTransform?: ParticleTransform;
opacity?: number;
fadeFlag?: boolean;
focus?: Vector;
Expand Down Expand Up @@ -414,6 +442,15 @@ export class ParticleEmitter extends Actor {
*/
public randomRotation: boolean = false;

/**
* Gets or sets the emitted particle transform style, [[ParticleTransform.Global]] is the default and emits particles as if
* they were world space objects, useful for most effects.
*
* If set to [[ParticleTransform.Local]] particles are children of the emitter and move relative to the emitter
* as they would in a parent/child actor relationship.
*/
public particleTransform: ParticleTransform = ParticleTransform.Global;

/**
* @param config particle emitter options bag
*/
Expand Down Expand Up @@ -446,6 +483,7 @@ export class ParticleEmitter extends Actor {
emitterType,
radius,
particleRotationalVelocity,
particleTransform,
randomRotation,
random
} = { ...config };
Expand Down Expand Up @@ -474,6 +512,7 @@ export class ParticleEmitter extends Actor {
this.radius = radius ?? this.radius;
this.particleRotationalVelocity = particleRotationalVelocity ?? this.particleRotationalVelocity;
this.randomRotation = randomRotation ?? this.randomRotation;
this.particleTransform = particleTransform ?? this.particleTransform;

this.body.collisionType = CollisionType.PreventCollision;

Expand All @@ -493,7 +532,11 @@ export class ParticleEmitter extends Actor {
const p = this._createParticle();
this.particles.push(p);
if (this?.scene?.world) {
this.scene.world.add(p);
if (this.particleTransform === ParticleTransform.Global) {
this.scene.world.add(p);
} else {
this.addChild(p);
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/engine/index.ts
Expand Up @@ -24,7 +24,7 @@ export * from './Events';
export * from './Label';
export { FontStyle, FontUnit, TextAlign, BaseAlign } from './Graphics/FontCommon';
export * from './Loader';
export { Particle, ParticleEmitter, ParticleArgs, ParticleEmitterArgs, EmitterType } from './Particles';
export { Particle, ParticleTransform, ParticleEmitter, ParticleArgs, ParticleEmitterArgs, EmitterType } from './Particles';
export * from './Collision/Physics';
export * from './Scene';

Expand Down
120 changes: 120 additions & 0 deletions src/spec/ParticleSpec.ts
Expand Up @@ -141,4 +141,124 @@ describe('A particle', () => {
engine.graphicsContext.flush();
await expectAsync(flushWebGLCanvasTo2D(engine.canvas)).toEqualImage('src/spec/images/ParticleSpec/Particles.png');
});

it('can be parented', async () => {
const emitter = new ex.ParticleEmitter({
pos: new ex.Vector(0, 0),
width: 20,
height: 30,
isEmitting: true,
minVel: 100,
maxVel: 200,
acceleration: ex.Vector.Zero.clone(),
minAngle: 0,
maxAngle: Math.PI / 2,
emitRate: 5,
particleLife: 4000,
opacity: 0.5,
fadeFlag: false,
focus: null,
focusAccel: null,
startSize: 30,
endSize: 40,
beginColor: ex.Color.Red.clone(),
endColor: ex.Color.Blue.clone(),
particleSprite: null,
emitterType: ex.EmitterType.Circle,
radius: 20,
particleRotationalVelocity: 3,
randomRotation: false,
random: new ex.Random(1337)
});

const parent = new ex.Actor({
pos: ex.vec(100, 50),
width: 10,
height: 10
});
parent.addChild(emitter);

engine.backgroundColor = ex.Color.Transparent;
engine.add(emitter);

emitter.emitParticles(20);
engine.currentScene.update(engine, 100);
engine.currentScene.update(engine, 100);
engine.currentScene.update(engine, 100);
engine.currentScene.draw(engine.graphicsContext, 100);
engine.graphicsContext.flush();
await expectAsync(flushWebGLCanvasTo2D(engine.canvas)).toEqualImage('src/spec/images/ParticleSpec/parented.png');

});

it('can set the particle transform to local making particles children of the emitter', () => {
const emitter = new ex.ParticleEmitter({
particleTransform: ex.ParticleTransform.Local,
pos: new ex.Vector(0, 0),
width: 20,
height: 30,
isEmitting: true,
minVel: 100,
maxVel: 200,
acceleration: ex.Vector.Zero.clone(),
minAngle: 0,
maxAngle: Math.PI / 2,
emitRate: 5,
particleLife: 4000,
opacity: 0.5,
fadeFlag: false,
focus: null,
focusAccel: null,
startSize: 30,
endSize: 40,
beginColor: ex.Color.Red.clone(),
endColor: ex.Color.Blue.clone(),
particleSprite: null,
emitterType: ex.EmitterType.Circle,
radius: 20,
particleRotationalVelocity: 3,
randomRotation: false,
random: new ex.Random(1337)
});
engine.add(emitter);
emitter.emitParticles(20);
expect(emitter.children.length).toBe(20);
expect(engine.currentScene.actors.length).toBe(1);
});

it('can set the particle transform to global adding particles directly to the scene', () => {
const emitter = new ex.ParticleEmitter({
particleTransform: ex.ParticleTransform.Global,
pos: new ex.Vector(0, 0),
width: 20,
height: 30,
isEmitting: true,
minVel: 100,
maxVel: 200,
acceleration: ex.Vector.Zero.clone(),
minAngle: 0,
maxAngle: Math.PI / 2,
emitRate: 5,
particleLife: 4000,
opacity: 0.5,
fadeFlag: false,
focus: null,
focusAccel: null,
startSize: 30,
endSize: 40,
beginColor: ex.Color.Red.clone(),
endColor: ex.Color.Blue.clone(),
particleSprite: null,
emitterType: ex.EmitterType.Circle,
radius: 20,
particleRotationalVelocity: 3,
randomRotation: false,
random: new ex.Random(1337)
});
engine.add(emitter);
emitter.emitParticles(20);
expect(emitter.children.length).toBe(0);
expect(engine.currentScene.actors.length).toBe(1);
expect(engine.currentScene.world.entityManager.entities.length).toBe(21);
});
});
Binary file added src/spec/images/ParticleSpec/parented.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 2b91698

Please sign in to comment.