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