diff --git a/CHANGELOG.md b/CHANGELOG.md index d7c9de937..7be4fabf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,42 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Breaking Changes -- +- `ex.Particle` and `ex.ParticleEmitter` now have an API that looks like modern Excalibur APIs + * `particleSprite` is renamed to `graphic` + * `particleRotationalVelocity` is renamed to `angularVelocity` + * `fadeFlag` is renamed to `fade` + * `acceleration` is renamed to `acc` + * `particleLife` is renamed to `life` + * `ParticleEmitter` now takes a separate `particle: ParticleConfig` parameter to disambiguate between particles parameters and emitter ones + ```typescript + const emitter = new ex.ParticleEmitter({ + width: 10, + height: 10, + radius: 5, + emitterType: ex.EmitterType.Rectangle, + emitRate: 300, + isEmitting: true, + particle: { + transform: ex.ParticleTransform.Global, + opacity: 0.5, + life: 1000, + acc: ex.vec(10, 80), + beginColor: ex.Color.Chartreuse, + endColor: ex.Color.Magenta, + startSize: 5, + endSize: 100, + minVel: 100, + maxVel: 200, + minAngle: 5.1, + maxAngle: 6.2, + fade: true, + maxSize: 10, + graphic: swordImg.toSprite(), + randomRotation: true, + minSize: 1 + } + }); + ``` ### Deprecated @@ -44,6 +79,9 @@ are doing mtv adjustments during precollision. ### Updates +- Perf improvements to `ex.ParticleEmitter` + * Use the same integrator as the MotionSystem in the tight loop + * Leverage object pools to increase performance and reduce allocations - Perf improvements to collision narrowphase and solver steps * Working in the local polygon space as much as possible speeds things up * Add another pair filtering condition on the `SparseHashGridCollisionProcessor` which reduces pairs passed to narrowphase diff --git a/sandbox/src/game.ts b/sandbox/src/game.ts index 6bfa3fff6..306e18d52 100644 --- a/sandbox/src/game.ts +++ b/sandbox/src/game.ts @@ -915,33 +915,29 @@ game.add(player); // Add particle emitter // sprite.anchor = new ex.Vector(0.5, 0.5); var emitter = new ex.ParticleEmitter({ + isEmitting: false, + emitRate: 494, pos: new ex.Vector(100, 300), width: 2, height: 2, - minVel: 417, - maxVel: 589, - minAngle: Math.PI, - maxAngle: Math.PI * 2, - isEmitting: false, - emitRate: 494, - opacity: 0.84, - fadeFlag: true, - particleLife: 2465, - maxSize: 20.5, - minSize: 10, - acceleration: new ex.Vector(0, 460), - beginColor: ex.Color.Red, - endColor: ex.Color.Yellow, - particleSprite: blockSprite, - particleRotationalVelocity: Math.PI / 10, - randomRotation: true -}); -const original = (ex.ParticleEmitter.prototype as any)._createParticle; -(ex.ParticleEmitter.prototype as any)._createParticle = function () { - const particle = original.call(this); - particle.graphics.onPostDraw = () => {}; - return particle; -}; + particle: { + minVel: 417, + maxVel: 589, + minAngle: Math.PI, + maxAngle: Math.PI * 2, + opacity: 0.84, + fade: true, + life: 2465, + maxSize: 20.5, + minSize: 10, + beginColor: ex.Color.Red, + endColor: ex.Color.Yellow, + graphic: blockSprite, + angularVelocity: Math.PI / 10, + acc: new ex.Vector(0, 460), + randomRotation: true + } +}); game.add(emitter); var exploding = false; diff --git a/sandbox/tests/emitter/index.ts b/sandbox/tests/emitter/index.ts index 737f864d0..c9832f74e 100644 --- a/sandbox/tests/emitter/index.ts +++ b/sandbox/tests/emitter/index.ts @@ -1,8 +1,11 @@ var game = new ex.Engine({ width: 400, - height: 400 + height: 400, + displayMode: ex.DisplayMode.FitScreenAndFill }); +var swordImg = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png'); + var actor = new ex.Actor({ anchor: ex.vec(0.5, 0.5), pos: game.screen.center, @@ -18,28 +21,32 @@ actor.actions.repeatForever((ctx) => { 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, + emitterType: ex.EmitterType.Rectangle, 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 + isEmitting: true, + particle: { + transform: ex.ParticleTransform.Global, + opacity: 0.5, + life: 1000, + acc: ex.vec(10, 80), + beginColor: ex.Color.Chartreuse, + endColor: ex.Color.Magenta, + startSize: 5, + endSize: 100, + minVel: 100, + maxVel: 200, + minAngle: 5.1, + maxAngle: 6.2, + fade: true, + maxSize: 10, + graphic: swordImg.toSprite(), + randomRotation: true, + minSize: 1 + } }); -emitter.isEmitting = true; -game.start(); +game.start(new ex.Loader([swordImg])); game.add(actor); actor.angularVelocity = 2; diff --git a/src/engine/Debug/DebugSystem.ts b/src/engine/Debug/DebugSystem.ts index 2208db538..84c678bd2 100644 --- a/src/engine/Debug/DebugSystem.ts +++ b/src/engine/Debug/DebugSystem.ts @@ -12,7 +12,7 @@ import { BodyComponent } from '../Collision/BodyComponent'; import { CollisionSystem } from '../Collision/CollisionSystem'; import { CompositeCollider } from '../Collision/Colliders/CompositeCollider'; import { GraphicsComponent } from '../Graphics/GraphicsComponent'; -import { Particle } from '../Particles'; +import { Particle } from '../Particles/Particles'; import { DebugGraphicsComponent } from '../Graphics/DebugGraphicsComponent'; import { CoordPlane } from '../Math/coord-plane'; import { Debug } from '../Graphics/Debug'; diff --git a/src/engine/Graphics/GraphicsSystem.ts b/src/engine/Graphics/GraphicsSystem.ts index 04e3fd67a..61f9925fe 100644 --- a/src/engine/Graphics/GraphicsSystem.ts +++ b/src/engine/Graphics/GraphicsSystem.ts @@ -8,7 +8,6 @@ import { Camera } from '../Camera'; import { Query, System, SystemPriority, SystemType, World } from '../EntityComponentSystem'; import { Engine } from '../Engine'; import { GraphicsGroup } from './GraphicsGroup'; -import { Particle } from '../Particles'; // this import seems to bomb wallaby import { ParallaxComponent } from './ParallaxComponent'; import { CoordPlane } from '../Math/coord-plane'; import { BodyComponent } from '../Collision/BodyComponent'; @@ -142,10 +141,7 @@ export class GraphicsSystem extends System { } entity.events.emit('predraw', new PreDrawEvent(this._graphicsContext, delta, entity)); - // TODO remove this hack on the particle redo - // Remove this line after removing the wallaby import - const particleOpacity = entity instanceof Particle ? entity.opacity : 1; - this._graphicsContext.opacity *= graphics.opacity * particleOpacity; + this._graphicsContext.opacity *= graphics.opacity; // Draw the graphics component this._drawGraphicsComponent(graphics, transform); diff --git a/src/engine/ParticleEmitter.ts b/src/engine/ParticleEmitter.ts deleted file mode 100644 index ac60681a8..000000000 --- a/src/engine/ParticleEmitter.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { Engine } from './Engine'; -import { Actor } from './Actor'; -import { Color } from './Color'; -import { Vector, vec } from './Math/vector'; -import * as Util from './Util/Util'; -import { Random } from './Math/Random'; -import { CollisionType } from './Collision/CollisionType'; -import { randomInRange } from './Math/util'; -import { Graphic } from './Graphics'; -import { EmitterType } from './EmitterType'; -import { Particle, ParticleTransform, ParticleEmitterArgs } from './Particles'; - -/** - * Using a particle emitter is a great way to create interesting effects - * in your game, like smoke, fire, water, explosions, etc. `ParticleEmitter` - * extend [[Actor]] allowing you to use all of the features that come with. - */ -export class ParticleEmitter extends Actor { - private _particlesToEmit: number = 0; - - public numParticles: number = 0; - - /** - * Random number generator - */ - public random: Random; - - /** - * Gets or sets the isEmitting flag - */ - public isEmitting: boolean = true; - /** - * Gets or sets the backing particle collection - */ - public particles: Particle[] = []; - - /** - * Gets or sets the backing deadParticle collection - */ - public deadParticles: Particle[] = []; - - /** - * Gets or sets the minimum particle velocity - */ - public minVel: number = 0; - /** - * Gets or sets the maximum particle velocity - */ - public maxVel: number = 0; - - /** - * Gets or sets the acceleration vector for all particles - */ - public acceleration: Vector = new Vector(0, 0); - - /** - * Gets or sets the minimum angle in radians - */ - public minAngle: number = 0; - /** - * Gets or sets the maximum angle in radians - */ - public maxAngle: number = 0; - - /** - * Gets or sets the emission rate for particles (particles/sec) - */ - public emitRate: number = 1; //particles/sec - /** - * Gets or sets the life of each particle in milliseconds - */ - public particleLife: number = 2000; - /** - * Gets the opacity of each particle from 0 to 1.0 - */ - public get opacity(): number { - return this.graphics.opacity; - } - /** - * Gets the opacity of each particle from 0 to 1.0 - */ - public set opacity(opacity: number) { - this.graphics.opacity = opacity; - } - /** - * Gets or sets the fade flag which causes particles to gradually fade out over the course of their life. - */ - public fadeFlag: boolean = false; - - /** - * Gets or sets the optional focus where all particles should accelerate towards - */ - public focus: Vector = null; - /** - * Gets or sets the acceleration for focusing particles if a focus has been specified - */ - public focusAccel: number = null; - /** - * Gets or sets the optional starting size for the particles - */ - public startSize: number = null; - /** - * Gets or sets the optional ending size for the particles - */ - public endSize: number = null; - - /** - * Gets or sets the minimum size of all particles - */ - public minSize: number = 5; - /** - * Gets or sets the maximum size of all particles - */ - public maxSize: number = 5; - - /** - * Gets or sets the beginning color of all particles - */ - public beginColor: Color = Color.White; - /** - * Gets or sets the ending color of all particles - */ - public endColor: Color = Color.White; - - private _sprite: Graphic = null; - /** - * Gets or sets the sprite that a particle should use - */ - public get particleSprite(): Graphic { - return this._sprite; - } - - public set particleSprite(val: Graphic) { - if (val) { - this._sprite = val; - } - } - - /** - * Gets or sets the emitter type for the particle emitter - */ - public emitterType: EmitterType = EmitterType.Rectangle; - - /** - * Gets or sets the emitter radius, only takes effect when the [[emitterType]] is [[EmitterType.Circle]] - */ - public radius: number = 0; - - /** - * Gets or sets the particle rotational speed velocity - */ - public particleRotationalVelocity: number = 0; - - /** - * Indicates whether particles should start with a random rotation - */ - 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 - */ - constructor(config: ParticleEmitterArgs) { - super({ width: config.width ?? 0, height: config.height ?? 0 }); - - const { - x, - y, - z, - pos, - isEmitting, - minVel, - maxVel, - acceleration, - minAngle, - maxAngle, - emitRate, - particleLife, - opacity, - fadeFlag, - focus, - focusAccel, - startSize, - endSize, - minSize, - maxSize, - beginColor, - endColor, - particleSprite, - emitterType, - radius, - particleRotationalVelocity, - particleTransform, - randomRotation, - random - } = { ...config }; - - this.pos = pos ?? vec(x ?? 0, y ?? 0); - this.z = z ?? 0; - this.isEmitting = isEmitting ?? this.isEmitting; - this.minVel = minVel ?? this.minVel; - this.maxVel = maxVel ?? this.maxVel; - this.acceleration = acceleration ?? this.acceleration; - this.minAngle = minAngle ?? this.minAngle; - this.maxAngle = maxAngle ?? this.maxAngle; - this.emitRate = emitRate ?? this.emitRate; - this.particleLife = particleLife ?? this.particleLife; - this.opacity = opacity ?? this.opacity; - this.fadeFlag = fadeFlag ?? this.fadeFlag; - this.focus = focus ?? this.focus; - this.focusAccel = focusAccel ?? this.focusAccel; - this.startSize = startSize ?? this.startSize; - this.endSize = endSize ?? this.endSize; - this.minSize = minSize ?? this.minSize; - this.maxSize = maxSize ?? this.maxSize; - this.beginColor = beginColor ?? this.beginColor; - this.endColor = endColor ?? this.endColor; - this.particleSprite = particleSprite ?? this.particleSprite; - this.emitterType = emitterType ?? this.emitterType; - 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; - - this.random = random ?? new Random(); - } - - public removeParticle(particle: Particle) { - this.deadParticles.push(particle); - } - - /** - * Causes the emitter to emit particles - * @param particleCount Number of particles to emit right now - */ - public emitParticles(particleCount: number) { - for (let i = 0; i < particleCount; i++) { - const p = this._createParticle(); - this.particles.push(p); - if (this?.scene?.world) { - if (this.particleTransform === ParticleTransform.Global) { - this.scene.world.add(p); - } else { - this.addChild(p); - } - } - } - } - - public clearParticles() { - this.particles.length = 0; - } - - // Creates a new particle given the constraints of the emitter - private _createParticle(): Particle { - // todo implement emitter constraints; - let ranX = 0; - let ranY = 0; - - const angle = randomInRange(this.minAngle, this.maxAngle, this.random); - const vel = randomInRange(this.minVel, this.maxVel, this.random); - const size = this.startSize || randomInRange(this.minSize, this.maxSize, this.random); - const dx = vel * Math.cos(angle); - const dy = vel * Math.sin(angle); - - if (this.emitterType === EmitterType.Rectangle) { - ranX = randomInRange(0, this.width, this.random); - ranY = randomInRange(0, this.height, this.random); - } else if (this.emitterType === EmitterType.Circle) { - const radius = randomInRange(0, this.radius, this.random); - ranX = radius * Math.cos(angle); - ranY = radius * Math.sin(angle); - } - - const p = new Particle( - this, - this.particleLife, - this.opacity, - this.beginColor, - this.endColor, - new Vector(ranX, ranY), - new Vector(dx, dy), - this.acceleration, - this.startSize, - this.endSize, - this.particleSprite - ); - p.fadeFlag = this.fadeFlag; - p.particleSize = size; - p.particleRotationalVelocity = this.particleRotationalVelocity; - if (this.randomRotation) { - p.currentRotation = randomInRange(0, Math.PI * 2, this.random); - } - if (this.focus) { - p.focus = this.focus.add(new Vector(this.pos.x, this.pos.y)); - p.focusAccel = this.focusAccel; - } - return p; - } - - public update(engine: Engine, delta: number) { - super.update(engine, delta); - - if (this.isEmitting) { - this._particlesToEmit += this.emitRate * (delta / 1000); - if (this._particlesToEmit > 1.0) { - this.emitParticles(Math.floor(this._particlesToEmit)); - this._particlesToEmit = this._particlesToEmit - Math.floor(this._particlesToEmit); - } - } - - // deferred removal - for (let i = 0; i < this.deadParticles.length; i++) { - Util.removeItemFromArray(this.deadParticles[i], this.particles); - if (this?.scene?.world) { - this.scene.world.remove(this.deadParticles[i], false); - } - } - this.deadParticles.length = 0; - } -} diff --git a/src/engine/Particles.ts b/src/engine/Particles.ts deleted file mode 100644 index 7cb05cdea..000000000 --- a/src/engine/Particles.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { Engine } from './Engine'; -import { Color } from './Color'; -import { Vector, vec } from './Math/vector'; -import { Configurable } from './Configurable'; -import { Random } from './Math/Random'; -import { TransformComponent } from './EntityComponentSystem/Components/TransformComponent'; -import { GraphicsComponent } from './Graphics/GraphicsComponent'; -import { Entity } from './EntityComponentSystem/Entity'; -import { BoundingBox } from './Collision/BoundingBox'; -import { clamp } from './Math/util'; -import { Graphic } from './Graphics'; -import { EmitterType } from './EmitterType'; -import type { ParticleEmitter } from './ParticleEmitter'; - -const isEmitterConfig = (emitterOrConfig: ParticleEmitter | ParticleArgs): emitterOrConfig is ParticleArgs => { - return emitterOrConfig && !emitterOrConfig.constructor; -}; - -/** - * @hidden - */ -export class ParticleImpl extends Entity { - public position: Vector = new Vector(0, 0); - public velocity: Vector = new Vector(0, 0); - public acceleration: Vector = new Vector(0, 0); - public particleRotationalVelocity: number = 0; - public currentRotation: number = 0; - - public focus: Vector = null; - public focusAccel: number = 0; - public opacity: number = 1; - public beginColor: Color = Color.White; - public endColor: Color = Color.White; - - // Life is counted in ms - public life: number = 300; - public fadeFlag: boolean = false; - - // Color transitions - private _rRate: number = 1; - private _gRate: number = 1; - private _bRate: number = 1; - private _aRate: number = 0; - private _currentColor: Color = Color.White; - - public emitter: ParticleEmitter = null; - public particleSize: number = 5; - public particleSprite: Graphic = null; - - public startSize: number; - public endSize: number; - public sizeRate: number = 0; - public elapsedMultiplier: number = 0; - - public visible = true; - public isOffscreen = false; - - public transform: TransformComponent; - public graphics: GraphicsComponent; - - constructor( - emitterOrConfig: ParticleEmitter | ParticleArgs, - life?: number, - opacity?: number, - beginColor?: Color, - endColor?: Color, - position?: Vector, - velocity?: Vector, - acceleration?: Vector, - startSize?: number, - endSize?: number, - particleSprite?: Graphic - ) { - super(); - let emitter = emitterOrConfig; - if (isEmitterConfig(emitterOrConfig)) { - const config = emitterOrConfig; - emitter = config.emitter; - life = config.life; - opacity = config.opacity; - endColor = config.endColor; - beginColor = config.beginColor; - position = config.position; - velocity = config.velocity; - acceleration = config.acceleration; - startSize = config.startSize; - endSize = config.endSize; - particleSprite = config.particleSprite; - } - this.emitter = emitter; - this.life = life || this.life; - this.opacity = opacity || this.opacity; - this.endColor = endColor || this.endColor.clone(); - this.beginColor = beginColor || this.beginColor.clone(); - this._currentColor = this.beginColor.clone(); - this.particleSprite = particleSprite; - - 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; - this._bRate = (this.endColor.b - this.beginColor.b) / this.life; - this._aRate = this.opacity / this.life; - - this.startSize = startSize || 0; - this.endSize = endSize || 0; - - if (this.endSize > 0 && this.startSize > 0) { - this.sizeRate = (this.endSize - this.startSize) / this.life; - this.particleSize = this.startSize; - } - - this.addComponent((this.transform = new TransformComponent())); - this.addComponent((this.graphics = new GraphicsComponent())); - - this.transform.pos = this.position; - this.transform.rotation = this.currentRotation; - this.transform.scale = vec(1, 1); // TODO wut - this.transform.z = this.emitter.z; - if (this.particleSprite) { - this.graphics.opacity = this.opacity; - this.graphics.use(this.particleSprite); - } else { - this.graphics.localBounds = BoundingBox.fromDimension(this.particleSize, this.particleSize, Vector.Half); - this.graphics.onPostDraw = (ctx) => { - ctx.save(); - this.graphics.opacity = this.opacity; - const tmpColor = this._currentColor.clone(); - tmpColor.a = 1; - ctx.debug.drawPoint(vec(0, 0), { color: tmpColor, size: this.particleSize }); - ctx.restore(); - }; - } - } - - public kill() { - this.emitter.removeParticle(this); - } - - public update(engine: Engine, delta: number) { - this.life = this.life - delta; - this.elapsedMultiplier = this.elapsedMultiplier + delta; - - if (this.life < 0) { - this.kill(); - } - - if (this.fadeFlag) { - this.opacity = clamp(this._aRate * this.life, 0.0001, 1); - } - - if (this.startSize > 0 && this.endSize > 0) { - this.particleSize = clamp( - this.sizeRate * delta + this.particleSize, - Math.min(this.startSize, this.endSize), - Math.max(this.startSize, this.endSize) - ); - } - - this._currentColor.r = clamp(this._currentColor.r + this._rRate * delta, 0, 255); - this._currentColor.g = clamp(this._currentColor.g + this._gRate * delta, 0, 255); - this._currentColor.b = clamp(this._currentColor.b + this._bRate * delta, 0, 255); - this._currentColor.a = clamp(this.opacity, 0.0001, 1); - - if (this.focus) { - const accel = this.focus - .sub(this.position) - .normalize() - .scale(this.focusAccel) - .scale(delta / 1000); - this.velocity = this.velocity.add(accel); - } else { - this.velocity = this.velocity.add(this.acceleration.scale(delta / 1000)); - } - this.position = this.position.add(this.velocity.scale(delta / 1000)); - - if (this.particleRotationalVelocity) { - this.currentRotation = (this.currentRotation + (this.particleRotationalVelocity * delta) / 1000) % (2 * Math.PI); - } - - this.transform.pos = this.position; - this.transform.rotation = this.currentRotation; - this.transform.scale = vec(1, 1); // todo wut - this.graphics.opacity = this.opacity; - } -} - -export interface ParticleArgs extends Partial { - emitter: ParticleEmitter; - position?: Vector; - velocity?: Vector; - acceleration?: Vector; - particleRotationalVelocity?: number; - currentRotation?: number; - particleSize?: number; - particleSprite?: Graphic; -} - -/** - * Particle is used in a [[ParticleEmitter]] - */ -export class Particle extends Configurable(ParticleImpl) { - constructor(config: ParticleArgs); - constructor( - emitter: ParticleEmitter, - life?: number, - opacity?: number, - beginColor?: Color, - endColor?: Color, - position?: Vector, - velocity?: Vector, - acceleration?: Vector, - startSize?: number, - endSize?: number, - particleSprite?: Graphic - ); - constructor( - emitterOrConfig: ParticleEmitter | ParticleArgs, - life?: number, - opacity?: number, - beginColor?: Color, - endColor?: Color, - position?: Vector, - velocity?: Vector, - acceleration?: Vector, - startSize?: number, - endSize?: number, - particleSprite?: Graphic - ) { - super(emitterOrConfig, life, opacity, beginColor, endColor, position, velocity, acceleration, startSize, endSize, particleSprite); - } -} - -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; - z?: number; - pos?: Vector; - width?: number; - height?: number; - isEmitting?: boolean; - minVel?: number; - maxVel?: number; - acceleration?: Vector; - minAngle?: number; - 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; - focusAccel?: number; - startSize?: number; - endSize?: number; - minSize?: number; - maxSize?: number; - beginColor?: Color; - endColor?: Color; - particleSprite?: Graphic; - emitterType?: EmitterType; - radius?: number; - particleRotationalVelocity?: number; - randomRotation?: boolean; - random?: Random; -} diff --git a/src/engine/Particles/ParticleEmitter.ts b/src/engine/Particles/ParticleEmitter.ts new file mode 100644 index 000000000..8f9f08343 --- /dev/null +++ b/src/engine/Particles/ParticleEmitter.ts @@ -0,0 +1,184 @@ +import { Engine } from '../Engine'; +import { Actor } from '../Actor'; +import { vec } from '../Math/vector'; +import { Random } from '../Math/Random'; +import { CollisionType } from '../Collision/CollisionType'; +import { randomInRange } from '../Math/util'; +import { EmitterType } from '../EmitterType'; +import { Particle, ParticleTransform, ParticleEmitterArgs, ParticleConfig } from './Particles'; +import { RentalPool } from '../Util/RentalPool'; + +/** + * Used internally by Excalibur to manage all particles in the engine + */ +export const ParticlePool = new RentalPool( + () => new Particle({}), + (p) => p, + 2000 +); + +/** + * Using a particle emitter is a great way to create interesting effects + * in your game, like smoke, fire, water, explosions, etc. `ParticleEmitter` + * extend [[Actor]] allowing you to use all of the features that come with. + */ +export class ParticleEmitter extends Actor { + private _particlesToEmit: number = 0; + + public numParticles: number = 0; + + /** + * Random number generator + */ + public random: Random; + + /** + * Gets or sets the isEmitting flag + */ + public isEmitting: boolean = true; + + /** + * Gets or sets the backing deadParticle collection + */ + public deadParticles: Particle[] = []; + + /** + * Gets or sets the emission rate for particles (particles/sec) + */ + public emitRate: number = 1; //particles/sec + + /** + * Gets or sets the emitter type for the particle emitter + */ + public emitterType: EmitterType = EmitterType.Rectangle; + + /** + * Gets or sets the emitter radius, only takes effect when the [[emitterType]] is [[EmitterType.Circle]] + */ + public radius: number = 0; + + public particle: ParticleConfig = { + /** + * Gets or sets the life of each particle in milliseconds + */ + life: 2000, + transform: ParticleTransform.Global, + graphic: undefined, + opacity: 1, + angularVelocity: 0, + focus: undefined, + focusAccel: undefined, + randomRotation: false + }; + + /** + * @param config particle emitter options bag + */ + constructor(config: ParticleEmitterArgs) { + super({ width: config.width ?? 0, height: config.height ?? 0 }); + + const { particle, x, y, z, pos, isEmitting, emitRate, emitterType, radius, random } = { ...config }; + + this.particle = { ...this.particle, ...particle }; + + this.pos = pos ?? vec(x ?? 0, y ?? 0); + this.z = z ?? 0; + this.isEmitting = isEmitting ?? this.isEmitting; + this.emitRate = emitRate ?? this.emitRate; + this.emitterType = emitterType ?? this.emitterType; + this.radius = radius ?? this.radius; + + this.body.collisionType = CollisionType.PreventCollision; + + this.random = random ?? new Random(); + } + + public removeParticle(particle: Particle) { + this.deadParticles.push(particle); + } + + /** + * Causes the emitter to emit particles + * @param particleCount Number of particles to emit right now + */ + public emitParticles(particleCount: number) { + for (let i = 0; i < particleCount; i++) { + const p = this._createParticle(); + if (this?.scene?.world) { + if (this.particle.transform === ParticleTransform.Global) { + this.scene.world.add(p); + } else { + this.addChild(p); + } + } + } + } + + // Creates a new particle given the constraints of the emitter + private _createParticle(): Particle { + let ranX = 0; + let ranY = 0; + + const angle = randomInRange(this.particle.minAngle || 0, this.particle.maxAngle || Math.PI * 2, this.random); + const vel = randomInRange(this.particle.minVel || 0, this.particle.maxVel || 0, this.random); + const size = this.particle.startSize || randomInRange(this.particle.minSize || 5, this.particle.maxSize || 5, this.random); + const dx = vel * Math.cos(angle); + const dy = vel * Math.sin(angle); + + if (this.emitterType === EmitterType.Rectangle) { + ranX = randomInRange(0, this.width, this.random); + ranY = randomInRange(0, this.height, this.random); + } else if (this.emitterType === EmitterType.Circle) { + const radius = randomInRange(0, this.radius, this.random); + ranX = radius * Math.cos(angle); + ranY = radius * Math.sin(angle); + } + + const p = ParticlePool.rent(); + p.configure({ + life: this.particle.life, + opacity: this.particle.opacity, + beginColor: this.particle.beginColor, + endColor: this.particle.endColor, + pos: vec(ranX, ranY), + vel: vec(dx, dy), + acc: this.particle.acc, + angularVelocity: this.particle.angularVelocity, + startSize: this.particle.startSize, + endSize: this.particle.endSize, + size: size, + graphic: this.particle.graphic, + fade: this.particle.fade + }); + p.registerEmitter(this); + if (this.particle.randomRotation) { + p.transform.rotation = randomInRange(0, Math.PI * 2, this.random); + } + if (this.particle.focus) { + p.focus = this.particle.focus.add(vec(this.pos.x, this.pos.y)); + p.focusAccel = this.particle.focusAccel; + } + return p; + } + + public update(engine: Engine, delta: number) { + super.update(engine, delta); + + if (this.isEmitting) { + this._particlesToEmit += this.emitRate * (delta / 1000); + if (this._particlesToEmit > 1.0) { + this.emitParticles(Math.floor(this._particlesToEmit)); + this._particlesToEmit = this._particlesToEmit - Math.floor(this._particlesToEmit); + } + } + + // deferred removal + for (let i = 0; i < this.deadParticles.length; i++) { + if (this?.scene?.world) { + this.scene.world.remove(this.deadParticles[i], false); + ParticlePool.return(this.deadParticles[i]); + } + } + this.deadParticles.length = 0; + } +} diff --git a/src/engine/Particles/Particles.ts b/src/engine/Particles/Particles.ts new file mode 100644 index 000000000..357179e72 --- /dev/null +++ b/src/engine/Particles/Particles.ts @@ -0,0 +1,300 @@ +import { Engine } from '../Engine'; +import { Color } from '../Color'; +import { Vector, vec } from '../Math/vector'; +import { Random } from '../Math/Random'; +import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent'; +import { GraphicsComponent } from '../Graphics/GraphicsComponent'; +import { Entity } from '../EntityComponentSystem/Entity'; +import { BoundingBox } from '../Collision/BoundingBox'; +import { clamp } from '../Math/util'; +import { Graphic } from '../Graphics'; +import { EmitterType } from '../EmitterType'; +import { MotionComponent } from '../EntityComponentSystem'; +import { EulerIntegrator } from '../Collision/Integrator'; +import type { ParticleEmitter } from './ParticleEmitter'; + +/** +/** + * Particle is used in a [[ParticleEmitter]] + */ +export class Particle extends Entity { + public static DefaultConfig: ParticleConfig = { + beginColor: Color.White, + endColor: Color.White, + life: 300, + fade: false, + size: 5, + graphic: undefined, + startSize: undefined, + endSize: undefined + }; + + public focus?: Vector; + public focusAccel?: number; + public beginColor: Color = Color.White; + public endColor: Color = Color.White; + + // Life is counted in ms + public life: number = 300; + public fade: boolean = false; + + // Color transitions + private _rRate: number = 1; + private _gRate: number = 1; + private _bRate: number = 1; + private _aRate: number = 0; + private _currentColor: Color = Color.White; + + public size: number = 5; + public graphic?: Graphic; + + public startSize?: number; + public endSize?: number; + public sizeRate: number = 0; + + public visible = true; + public isOffscreen = false; + + public transform: TransformComponent; + public motion: MotionComponent; + public graphics: GraphicsComponent; + public particleTransform = ParticleTransform.Global; + + constructor(options: ParticleConfig) { + super(); + this.addComponent((this.transform = new TransformComponent())); + this.addComponent((this.motion = new MotionComponent())); + this.addComponent((this.graphics = new GraphicsComponent())); + this.configure(options); + } + + private _emitter?: ParticleEmitter; + registerEmitter(emitter: ParticleEmitter) { + this._emitter = emitter; + if (this.particleTransform === ParticleTransform.Global) { + const globalPos = this._emitter.transform.globalPos; + this.transform.pos = this.transform.pos.add(globalPos); + this.motion.vel = this.motion.vel.rotate(this._emitter.transform.globalRotation); + } + } + + configure(options: ParticleConfig) { + this.particleTransform = options.transform ?? this.particleTransform; + this.life = options.life ?? this.life; + this.fade = options.fade ?? this.fade; + this.size = options.size ?? this.size; + this.endColor = options.endColor ?? this.endColor.clone(); + this.beginColor = options.beginColor ?? this.beginColor.clone(); + this._currentColor = this.beginColor.clone(); + + this.graphic = options.graphic; + this.graphics.opacity = options.opacity ?? this.graphics.opacity; + this.transform.pos = options.pos ?? this.transform.pos; + this.transform.rotation = options.rotation ?? 0; + this.transform.scale = vec(1, 1); + + this.motion.vel = options.vel ?? this.motion.vel; + this.motion.angularVelocity = options.angularVelocity ?? 0; + this.motion.acc = options.acc ?? this.motion.acc; + + this._rRate = (this.endColor.r - this.beginColor.r) / this.life; + this._gRate = (this.endColor.g - this.beginColor.g) / this.life; + this._bRate = (this.endColor.b - this.beginColor.b) / this.life; + this._aRate = this.graphics.opacity / this.life; + + this.startSize = options.startSize ?? 0; + this.endSize = options.endSize ?? 0; + + if (this.endSize > 0 && this.startSize > 0) { + this.sizeRate = (this.endSize - this.startSize) / this.life; + this.size = this.startSize; + } + if (this.graphic) { + this.graphics.use(this.graphic); + this.graphics.onPostDraw = undefined; + } else { + this.graphics.localBounds = BoundingBox.fromDimension(this.size, this.size, Vector.Half); + this.graphics.onPostDraw = (ctx) => { + ctx.save(); + ctx.debug.drawPoint(vec(0, 0), { color: this._currentColor, size: this.size }); + ctx.restore(); + }; + } + } + + public kill() { + if (this._emitter) { + this._emitter.removeParticle(this); + } + } + + public update(engine: Engine, delta: number) { + this.life = this.life - delta; + + if (this.life < 0) { + this.kill(); + } + + if (this.fade) { + this.graphics.opacity = clamp(this._aRate * this.life, 0.0001, 1); + } + + if (this.startSize && this.endSize && this.startSize > 0 && this.endSize > 0) { + this.size = clamp(this.sizeRate * delta + this.size, Math.min(this.startSize, this.endSize), Math.max(this.startSize, this.endSize)); + } + + this._currentColor.r = clamp(this._currentColor.r + this._rRate * delta, 0, 255); + this._currentColor.g = clamp(this._currentColor.g + this._gRate * delta, 0, 255); + this._currentColor.b = clamp(this._currentColor.b + this._bRate * delta, 0, 255); + this._currentColor.a = this.graphics.opacity; + + let accel = this.motion.acc; + if (this.focus) { + accel = this.focus + .sub(this.transform.pos) + .normalize() + .scale(this.focusAccel || 0) + .scale(delta / 1000); + } + // Update transform and motion based on Euler linear algebra + EulerIntegrator.integrate(this.transform, this.motion, accel, delta); + } +} + +export interface ParticleConfig { + /** + * 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. + */ + transform?: ParticleTransform; + /** + * Starting position of the particle + */ + pos?: Vector; + /** + * Starting velocity of the particle + */ + vel?: Vector; + /** + * Starting acceleration of the particle + */ + acc?: Vector; + /** + * Starting angular velocity of the particle + */ + angularVelocity?: number; + /** + * Starting rotation of the particle + */ + rotation?: number; + /** + * Size of the particle in pixels + */ + size?: number; + /** + * Optionally set a graphic + */ + graphic?: Graphic; + /** + * Totally life of the particle in milliseconds + */ + life?: number; + /** + * Starting opacity of the particle + */ + opacity?: number; + /** + * Should the particle fade out to fully transparent over their life + */ + fade?: boolean; + + /** + * Ending color of the particle over its life + */ + endColor?: Color; + /** + * Beginning color of the particle over its life + */ + beginColor?: Color; + + /** + * Set the start size when you want to change particle size over their life + */ + startSize?: number; + /** + * Set the end size when you want to change particle size over their life + */ + endSize?: number; + + /** + * Smallest possible starting size of the particle + */ + minSize?: number; + /** + * Largest possible starting size of the particle + */ + maxSize?: number; + /** + * Minimum magnitude of the particle starting vel + */ + minVel?: number; // TODO Change to speed! + /** + * Maximum magnitude of the particle starting vel + */ + maxVel?: number; + /** + * Minimum angle to use for the particles starting rotation + */ + minAngle?: number; + /** + * Maximum angle to use for the particles starting rotation + */ + maxAngle?: number; + + /** + * Gets or sets the optional focus where all particles should accelerate towards + */ + focus?: Vector; + /** + * Gets or sets the optional acceleration for focusing particles if a focus has been specified + */ + focusAccel?: number; + + /** + * Indicates whether particles should start with a random rotation + */ + randomRotation?: boolean; +} + +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 { + particle?: ParticleConfig; + x?: number; + y?: number; + z?: number; + pos?: Vector; + width?: number; + height?: number; + isEmitting?: boolean; + emitRate?: number; + focus?: Vector; + focusAccel?: number; + emitterType?: EmitterType; + radius?: number; + random?: Random; +} diff --git a/src/engine/Particles/tsconfig.json b/src/engine/Particles/tsconfig.json new file mode 100644 index 000000000..20d5e2e03 --- /dev/null +++ b/src/engine/Particles/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "strict": true + } +} diff --git a/src/engine/index.ts b/src/engine/index.ts index 234d5f796..81f2f8c89 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -24,8 +24,8 @@ export * from './Events'; export * from './Label'; export { FontStyle, FontUnit, TextAlign, BaseAlign } from './Graphics/FontCommon'; export * from './EmitterType'; -export { Particle, ParticleTransform, ParticleArgs, ParticleEmitterArgs } from './Particles'; -export * from './ParticleEmitter'; +export { Particle, ParticleTransform, ParticleConfig as ParticleArgs, ParticleEmitterArgs } from './Particles/Particles'; +export * from './Particles/ParticleEmitter'; export * from './Collision/Physics'; export * from './Scene'; diff --git a/src/spec/ParticleSpec.ts b/src/spec/ParticleSpec.ts index 4818c5e80..90098bb72 100644 --- a/src/spec/ParticleSpec.ts +++ b/src/spec/ParticleSpec.ts @@ -34,28 +34,28 @@ describe('A particle', () => { width: 20, height: 30, isEmitting: true, - minVel: 40, - maxVel: 50, - acceleration: ex.Vector.Zero.clone(), - minAngle: 0, - maxAngle: Math.PI / 2, + particle: { + minVel: 40, + maxVel: 50, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4, + opacity: 0.5, + fade: false, + startSize: 1, + endSize: 10, + minSize: 1, + maxSize: 3, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3, + randomRotation: false + }, emitRate: 3, - particleLife: 4, - opacity: 0.5, - fadeFlag: false, - focus: null, - focusAccel: null, - startSize: 1, - endSize: 10, - minSize: 1, - maxSize: 3, - 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) }); @@ -64,28 +64,28 @@ describe('A particle', () => { expect(emitter.width).toBe(20); expect(emitter.height).toBe(30); expect(emitter.isEmitting).toBe(true); - expect(emitter.minVel).toBe(40); - expect(emitter.maxVel).toBe(50); - expect(emitter.acceleration.toString()).toBe(ex.Vector.Zero.clone().toString()); - expect(emitter.minAngle).toBe(0); - expect(emitter.maxAngle).toBe(Math.PI / 2); + expect(emitter.particle.minVel).toBe(40); + expect(emitter.particle.maxVel).toBe(50); + expect(emitter.acc.toString()).toBe(ex.Vector.Zero.clone().toString()); + expect(emitter.particle.minAngle).toBe(0); + expect(emitter.particle.maxAngle).toBe(Math.PI / 2); expect(emitter.emitRate).toBe(3); - expect(emitter.particleLife).toBe(4); - expect(emitter.opacity).toBe(0.5); - expect(emitter.fadeFlag).toBe(false); - expect(emitter.focus).toBe(null); - expect(emitter.focusAccel).toBe(null); - expect(emitter.startSize).toBe(1); - expect(emitter.endSize).toBe(10); - expect(emitter.minSize).toBe(1); - expect(emitter.maxSize).toBe(3); - expect(emitter.beginColor.toString()).toBe(ex.Color.Red.clone().toString()); - expect(emitter.endColor.toString()).toBe(ex.Color.Blue.clone().toString()); - expect(emitter.particleSprite).toBe(null); + expect(emitter.particle.life).toBe(4); + expect(emitter.particle.opacity).toBe(0.5); + expect(emitter.particle.fade).toBe(false); + expect(emitter.particle.focus).toBe(undefined); + expect(emitter.particle.focusAccel).toBe(undefined); + expect(emitter.particle.startSize).toBe(1); + expect(emitter.particle.endSize).toBe(10); + expect(emitter.particle.minSize).toBe(1); + expect(emitter.particle.maxSize).toBe(3); + expect(emitter.particle.beginColor.toString()).toBe(ex.Color.Red.clone().toString()); + expect(emitter.particle.endColor.toString()).toBe(ex.Color.Blue.clone().toString()); + expect(emitter.particle.graphic).toBe(null); expect(emitter.emitterType).toBe(ex.EmitterType.Circle); expect(emitter.radius).toBe(20); - expect(emitter.particleRotationalVelocity).toBe(3); - expect(emitter.randomRotation).toBe(false); + expect(emitter.particle.angularVelocity).toBe(3); + expect(emitter.particle.randomRotation).toBe(false); expect(emitter.random.seed).toBe(1337); }); @@ -95,25 +95,27 @@ describe('A particle', () => { 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, + particle: { + minVel: 100, + maxVel: 200, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4000, + opacity: 0.5, + fade: false, + startSize: 30, + endSize: 40, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3 + }, 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) }); @@ -135,25 +137,27 @@ describe('A particle', () => { 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, + particle: { + minVel: 100, + maxVel: 200, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4000, + opacity: 0.5, + fade: false, + startSize: 30, + endSize: 40, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3 + }, 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) }); @@ -179,30 +183,32 @@ describe('A particle', () => { it('can set the particle transform to local making particles children of the emitter', () => { const emitter = new ex.ParticleEmitter({ - particleTransform: ex.ParticleTransform.Local, + particle: { + transform: ex.ParticleTransform.Local, + minVel: 100, + maxVel: 200, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4000, + fade: false, + opacity: 0.5, + startSize: 30, + endSize: 40, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3 + }, 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) }); @@ -214,30 +220,32 @@ describe('A particle', () => { it('can set the particle transform to global adding particles directly to the scene', () => { const emitter = new ex.ParticleEmitter({ - particleTransform: ex.ParticleTransform.Global, + particle: { + transform: ex.ParticleTransform.Global, + minVel: 100, + maxVel: 200, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4000, + opacity: 0.5, + fade: false, + startSize: 30, + endSize: 40, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3 + }, 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) }); diff --git a/src/spec/images/ParticleSpec/Particles.png b/src/spec/images/ParticleSpec/Particles.png index eb77885b9..be470677f 100644 Binary files a/src/spec/images/ParticleSpec/Particles.png and b/src/spec/images/ParticleSpec/Particles.png differ diff --git a/src/spec/images/ParticleSpec/parented.png b/src/spec/images/ParticleSpec/parented.png index 380e38c7e..0e1f08fe9 100644 Binary files a/src/spec/images/ParticleSpec/parented.png and b/src/spec/images/ParticleSpec/parented.png differ diff --git a/src/stories/ParticleEmitter.stories.ts b/src/stories/ParticleEmitter.stories.ts index 3b4a42de3..d977bcf30 100644 --- a/src/stories/ParticleEmitter.stories.ts +++ b/src/stories/ParticleEmitter.stories.ts @@ -22,8 +22,8 @@ export const Main: StoryObj = { isEmitting, emitRate, opacity, - fadeFlag, - particleLife, + fade, + life, minSize, maxSize, startSize, @@ -39,29 +39,31 @@ export const Main: StoryObj = { // Particle Emitter const emitter = new ParticleEmitter({ - x: game.currentScene.camera.x, - y: game.currentScene.camera.y, + x: game.halfDrawWidth, + y: game.halfDrawHeight, width, height, emitterType, radius, - minVel, - maxVel, - minAngle, - maxAngle, isEmitting, emitRate, - opacity, - fadeFlag, - particleLife, - minSize, - maxSize, - startSize, - endSize, - acceleration: new Vector(accelX, accelY), - beginColor: Color.fromRGBString(beginColor), - endColor: Color.fromRGBString(endColor), - focusAccel: 800 + focusAccel: 800, + particle: { + minVel, + maxVel, + minAngle, + maxAngle, + opacity, + fade, + life, + minSize, + maxSize, + startSize, + endSize, + acc: new Vector(accelX, accelY), + beginColor: Color.fromRGBString(beginColor), + endColor: Color.fromRGBString(endColor) + } }); game.add(emitter); @@ -104,8 +106,8 @@ export const Main: StoryObj = { isEmitting: true, emitRate: 300, opacity: 0.5, - fadeFlag: true, - particleLife: 1000, + fade: true, + life: 1000, minSize: 1, maxSize: 10, startSize: 5,