diff --git a/CHANGELOG.md b/CHANGELOG.md
index 212a50b2..5c3acf1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/sandbox/index.html b/sandbox/index.html
index 867436c8..e9c5ce1c 100644
--- a/sandbox/index.html
+++ b/sandbox/index.html
@@ -12,6 +12,7 @@
Sprite Tint
Parallel Actions
Isometric Map
+ Particle Emitter
Many Text instances without failure (Chrome)
Decode Many Images without failure (Chrome)
Arcade: Sliding on Floor
diff --git a/sandbox/tests/emitter/index.html b/sandbox/tests/emitter/index.html
new file mode 100644
index 00000000..e55a8a14
--- /dev/null
+++ b/sandbox/tests/emitter/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Emitter
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sandbox/tests/emitter/index.ts b/sandbox/tests/emitter/index.ts
new file mode 100644
index 00000000..a4be03c8
--- /dev/null
+++ b/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);
diff --git a/src/engine/Particles.ts b/src/engine/Particles.ts
index a716f0c4..f4d26c03 100644
--- a/src/engine/Particles.ts
+++ b/src/engine/Particles.ts
@@ -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;
@@ -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;
@@ -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;
@@ -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
*/
@@ -446,6 +483,7 @@ export class ParticleEmitter extends Actor {
emitterType,
radius,
particleRotationalVelocity,
+ particleTransform,
randomRotation,
random
} = { ...config };
@@ -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;
@@ -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);
+ }
}
}
}
diff --git a/src/engine/index.ts b/src/engine/index.ts
index a4abe03c..23f583d7 100644
--- a/src/engine/index.ts
+++ b/src/engine/index.ts
@@ -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';
diff --git a/src/spec/ParticleSpec.ts b/src/spec/ParticleSpec.ts
index baf80225..8de11fab 100644
--- a/src/spec/ParticleSpec.ts
+++ b/src/spec/ParticleSpec.ts
@@ -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);
+ });
});
diff --git a/src/spec/images/ParticleSpec/parented.png b/src/spec/images/ParticleSpec/parented.png
new file mode 100644
index 0000000000000000000000000000000000000000..380e38c7e7b018077b7a6b58e857fdcc4c95136c
GIT binary patch
literal 15186
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
literal 0
HcmV?d00001