pixijs / particle-emitter

A particle system for PixiJS
http://pixijs.io/particle-emitter/docs
MIT License
793 stars 125 forks source link

Add spawn strategies #109

Closed ilya-klindukhov closed 3 years ago

ilya-klindukhov commented 5 years ago

Hi again.

I made a little bit refactoring)

What was made:

1) create spawnFactory with registered spawn strategies for each spawnType. Spawn funcions and parsing spawn config moved from Emitter to strategies. SpawnFactory supporting add new strategies and override exists by extend from it. The Emitter has become easier.

2) removed _spawnFunc() from Emitter. Insteand using strategy.spawn(). At the start of refactoring I don't remove it and mark as deprecated and set _spawnFunc = strategy.spawn for legacy. But this solution has some limitation i.e. if strategy has other function which using in spawn than this functions not available because to _spawnFunc set link to function and strategy context(this) will be missing. Also was additional code for decide which function use _spawnFunc or strategy.spawn(). I decided to remove _spawnFunc() and using only strategy.spawn().

3) add public getter for rotation property and rename to _rotation protected property. Using in spawn functions on strategies.

I tried to do as less as possible breaking changes on this refactoring. Potential BC: 1) using _spawnFunc in extended classes. Early this function was private ( I made this function protected 3 days ago and think than no one had time to use it). Using hack emitter["_spawnFunc"] not in count. 2) rotation -> _rotation. Can affect if try to set value to 'rotation' property in extended from Emitter class. But to set rotation has rotate() function so this case must be quite rare.

Some examples how it help to customize emitter: I will use previos example with spawn rect with normal

1) register new spawn strategy 'rectNormal'

Emitter

import {Emitter, SpawnFactory} from "pixi-particles";
import {SpawnFactoryWithNew} from "./newfactory/SpawnFactoryWithNew";

export class ExtendedEmitterWithNewStrategies extends Emitter {

    protected getSpawnFactory(): SpawnFactory {
        return SpawnFactoryWithNew.instance;
    }
}

Factory

import {SpawnFactory} from "pixi-particles";
import {SpawnRectNormalStrategy} from "./strategy/SpawnRectNormalStrategy";

export class SpawnFactoryWithNew extends SpawnFactory {

    protected static _instance: SpawnFactoryWithNew;

    public static get instance(): SpawnFactoryWithNew {
        if (!this._instance) {
            this._instance = new SpawnFactoryWithNew();
        }

        return this._instance;
    }

    protected constructor() {
        super();

        this.strategies["rectNormal"] = new SpawnRectNormalStrategy();
    }
}

Strategy

import {Emitter, EmitterConfig, ISpawnStrategy, OldEmitterConfig, Particle, Temp} from "pixi-particles";
import Rectangle = PIXI.Rectangle;

export class SpawnRectNormalStrategy implements ISpawnStrategy {

    parseConfig(emitter: Emitter, config: EmitterConfig | OldEmitterConfig): void {
        emitter.spawnType = "rect";
        const spawnRect = config.spawnRect;
        emitter.spawnRect = new Rectangle(spawnRect.x, spawnRect.y, spawnRect.w, spawnRect.h);
    }

    spawn(p: Particle, emitPosX: number, emitPosY: number, i?: number): void {
        const emitter = p.emitter;
        //place the particle at a random point in the rectangle borders
        const isHorizontalBorder: boolean = !!Math.round(Math.random());
        if (isHorizontalBorder) {
            const isTop: boolean = !!Math.round(Math.random());
            Temp.point.x = Math.random() * emitter.spawnRect.width + emitter.spawnRect.x;
            Temp.point.y = (isTop ? 0 : 1) * emitter.spawnRect.height + emitter.spawnRect.y;
            p.rotation = isTop ? 270 : 90;
        } else {
            const isLeft: boolean = !!Math.round(Math.random());
            Temp.point.x = (isLeft ? 0 : 1) * emitter.spawnRect.width + emitter.spawnRect.x;
            Temp.point.y = Math.random() * emitter.spawnRect.height + emitter.spawnRect.y;
            p.rotation = isLeft ? 180 : 0;
        }

        p.position.x = emitPosX + Temp.point.x;
        p.position.y = emitPosY + Temp.point.y;
    }

}

2. Change already exists strategy

Emitter

import {Emitter, SpawnFactory} from "pixi-particles";
import {SpawnFactoryWithChanged} from "./changefactory/SpawnFactoryWithChanged";

export class ExtendedEmitterWithChangedStrategies extends Emitter {

    public spawnByNormal: boolean = false;

    protected getSpawnFactory(): SpawnFactory {
        return SpawnFactoryWithChanged.instance;
    }
}

Factory

import {SpawnFactory} from "pixi-particles";
import {SpawnRectExtendedStrategy} from "./strategy/SpawnRectExtendedStrategy";

export class SpawnFactoryWithChanged extends SpawnFactory {
    protected static _instance: SpawnFactoryWithChanged;

    public static get instance(): SpawnFactoryWithChanged {
        if (!this._instance) {
            this._instance = new SpawnFactoryWithChanged();
        }

        return this._instance;
    }

    protected constructor() {
        super();

        this.strategies["rect"] = new SpawnRectExtendedStrategy();
    }
}

Strategy

import {Emitter, EmitterConfig, OldEmitterConfig, Particle, SpawnRectStrategy, Temp} from "pixi-particles";
import {ExtendedEmitterWithChangedStrategies} from "../../ExtendedEmitterWithChangedStrategies";
import {EmitterExtendedConfig} from "../config/EmitterExtendedConfig";

export class SpawnRectExtendedStrategy extends SpawnRectStrategy {

    parseConfig(emitter: Emitter, config: EmitterConfig | OldEmitterConfig): void {
        super.parseConfig(emitter, config);

        const extendedEmitter: ExtendedEmitterWithChangedStrategies = emitter as ExtendedEmitterWithChangedStrategies;
        const extendedConfig: EmitterExtendedConfig = config as EmitterExtendedConfig;
        extendedEmitter.spawnByNormal = extendedConfig.spawnByNormal;
    }

    spawn(p: Particle, emitPosX: number, emitPosY: number): void {
        const emitter: ExtendedEmitterWithChangedStrategies = p.emitter as ExtendedEmitterWithChangedStrategies;
        if ( emitter.spawnByNormal) {
            this.spawnByNormal(p, emitPosX, emitPosY);
        } else {
            super.spawn(p, emitPosX, emitPosY);
        }
    }

    protected spawnByNormal(p: Particle, emitPosX: number, emitPosY: number, i?: number): void {
        const emitter = p.emitter;
        //place the particle at a random point in the rectangle borders
        const isHorizontalBorder: boolean = !!Math.round(Math.random());
        if (isHorizontalBorder) {
            const isTop: boolean = !!Math.round(Math.random());
            Temp.point.x = Math.random() * emitter.spawnRect.width + emitter.spawnRect.x;
            Temp.point.y = (isTop ? 0 : 1) * emitter.spawnRect.height + emitter.spawnRect.y;
            p.rotation = isTop ? 270 : 90;
        } else {
            const isLeft: boolean = !!Math.round(Math.random());
            Temp.point.x = (isLeft ? 0 : 1) * emitter.spawnRect.width + emitter.spawnRect.x;
            Temp.point.y = Math.random() * emitter.spawnRect.height + emitter.spawnRect.y;
            p.rotation = isLeft ? 180 : 0;
        }

        p.position.x = emitPosX + Temp.point.x;
        p.position.y = emitPosY + Temp.point.y;
    }
}

Config

import {EmitterConfig} from "pixi-particles";

export interface EmitterExtendedConfig extends EmitterConfig {
    spawnByNormal?: boolean;
}
ilya-klindukhov commented 4 years ago

Hi. I made some changes.

andrewstart commented 4 years ago

Ah, I missed that spawn strategies were global, and was thinking that they were one per emitter. In that case, I would suggest that parseConfig() on each strategy returns an object containing the parsed data, and the emitter saves that object and gives it back during spawn(). That way, the Emitter and its properties don't need to know about any of the strategy specifics, only that the strategy exists.

andrewstart commented 3 years ago

With release 5.0.0, the emitter now uses configurable behaviors for everything, including spawn strategies, which should cover the use cases here.