UNPKG

pxt-common-packages

Version:
427 lines (372 loc) 13 kB
namespace particles { enum Flag { enabled = 1 << 0, destroyed = 1 << 1, relativeToCamera = 1 << 2 } // maximum count of sources before removing previous sources //% whenUsed const MAX_SOURCES = (() => { const sz = control.ramSize(); if (sz <= 1024 * 100) { return 8; } else if (sz <= 1024 * 200) { return 16; } else { return 50; } })(); const TIME_PRECISION = 10; // time goes down to down to the 1<<10 seconds let lastUpdate: number; /** * A single particle */ //% maxBgInstances=200 export class Particle { _x: Fx8; _y: Fx8; vx: Fx8; vy: Fx8; lifespan: number; next: Particle; data?: number; color?: number; } /** * An anchor for a Particle to originate from */ export interface ParticleAnchor { x: number; y: number; vx?: number; vy?: number; width?: number; height?: number; image?: Image; flags?: number; setImage?: (i: Image) => void; } /** * A source of particles */ export class ParticleSource extends sprites.BaseSprite { /** * A relative ranking of this sources priority * When necessary, a source with a lower priority will * be culled before a source with a higher priority. */ priority: number; _dt: number; /** * The anchor this source is currently attached to */ anchor: ParticleAnchor; /** * Time to live in milliseconds. The lifespan decreases by 1 on each millisecond * and the source gets destroyed when it reaches 0. */ lifespan: number; protected pFlags: number; protected head: Particle; protected timer: number; protected period: number; protected _factory: ParticleFactory; protected ax: Fx8; protected ay: Fx8; /** * @param anchor to emit particles from * @param particlesPerSecond rate at which particles are emitted * @param factory [optional] factory to generate particles with; otherwise, */ constructor(anchor: ParticleAnchor, particlesPerSecond: number, factory?: ParticleFactory) { super(scene.SPRITE_Z) init(); const sources = particleSources(); // remove and immediately destroy oldest source if over MAX_SOURCES if (sources.length >= MAX_SOURCES) { sortSources(sources); const removedSource = sources.shift(); removedSource.clear(); removedSource.destroy(); } this.pFlags = 0; this.setRate(particlesPerSecond); this.setAcceleration(0, 0); this.setAnchor(anchor); this.lifespan = undefined; this._dt = 0; this.priority = 0; this.setFactory(factory || particles.defaultFactory); sources.push(this); this.enabled = true; } __draw(camera: scene.Camera) { let current = this.head; const left = (this.pFlags & Flag.relativeToCamera) ? Fx.zeroFx8 : Fx8(camera.drawOffsetX); const top = (this.pFlags & Flag.relativeToCamera) ? Fx.zeroFx8 : Fx8(camera.drawOffsetY); while (current) { if (current.lifespan > 0) this.drawParticle(current, left, top); current = current.next; } } _update(dt: number) { this.timer -= dt; if (this.lifespan !== undefined) { this.lifespan -= dt; if (this.lifespan <= 0) { this.lifespan = undefined; this.destroy(); } } else if (this.anchor && this.anchor.flags !== undefined && (this.anchor.flags & sprites.Flag.Destroyed)) { this.lifespan = 750; } while (this.timer < 0 && this.enabled) { this.timer += this.period; const p = this._factory.createParticle(this.anchor); if (!p) continue; // some factories can decide to not produce a particle p.next = this.head; this.head = p; } if (!this.head) return; let current = this.head; this._dt += dt; let fixedDt = Fx8(this._dt); if (fixedDt) { do { if (current.lifespan > 0) { current.lifespan -= dt; this.updateParticle(current, fixedDt) } } while (current = current.next); this._dt = 0; } else { do { current.lifespan -= dt; } while (current = current.next); } } _prune() { while (this.head && this.head.lifespan <= 0) { this.head = this.head.next; } if ((this.pFlags & Flag.destroyed) && !this.head) { const scene = game.currentScene(); if (scene) scene.allSprites.removeElement(this); const sources = particleSources(); if (sources && sources.length) sources.removeElement(this); this.anchor == undefined; } let current = this.head; while (current && current.next) { if (current.next.lifespan <= 0) { current.next = current.next.next; } else { current = current.next; } } } /** * Sets the acceleration applied to the particles */ setAcceleration(ax: number, ay: number) { this.ax = Fx8(ax); this.ay = Fx8(ay); } /** * Enables or disables particles * @param on */ setEnabled(on: boolean) { this.enabled = on; } /** * Sets whether the particle source is drawn relative to the camera or not * @param on */ setRelativeToCamera(on: boolean) { if (on) this.pFlags |= Flag.relativeToCamera else this.pFlags = ~(~this.pFlags | Flag.relativeToCamera); } get enabled() { return !!(this.pFlags & Flag.enabled); } /** * Set whether this source is currently enabled (emitting particles) or not */ set enabled(v: boolean) { if (v !== this.enabled) { this.pFlags = v ? (this.pFlags | Flag.enabled) : (this.pFlags ^ Flag.enabled); this.timer = 0; } } /** * Destroy the source */ destroy() { // The `_prune` step will finishing destroying this Source once all emitted particles finish rendering this.enabled = false; this.pFlags |= Flag.destroyed; this._prune(); } /** * Clear all particles emitted from this source */ clear() { this.head = undefined; } /** * Set a anchor for particles to be emitted from * @param anchor */ setAnchor(anchor: ParticleAnchor) { this.anchor = anchor; } /** * Sets the number of particle created per second * @param particlesPerSecond */ setRate(particlesPerSecond: number) { this.period = Math.ceil(1000 / particlesPerSecond); this.timer = 0; } get factory(): ParticleFactory { return this._factory; } /** * Sets the particle factory * @param factory */ setFactory(factory: ParticleFactory) { if (factory) this._factory = factory; } protected updateParticle(p: Particle, fixedDt: Fx8) { fixedDt = Fx.rightShift(fixedDt, TIME_PRECISION); p.vx = Fx.add(p.vx, Fx.mul(this.ax, fixedDt)); p.vy = Fx.add(p.vy, Fx.mul(this.ay, fixedDt)); p._x = Fx.add(p._x, Fx.mul(p.vx, fixedDt)); p._y = Fx.add(p._y, Fx.mul(p.vy, fixedDt)); } protected drawParticle(p: Particle, screenLeft: Fx8, screenTop: Fx8) { this._factory.drawParticle(p, Fx.sub(p._x, screenLeft), Fx.sub(p._y, screenTop)); } } //% whenUsed export const defaultFactory = new particles.SprayFactory(20, 0, 60); /** * Creates a new source of particles attached to a sprite * @param sprite * @param particlesPerSecond number of particles created per second */ export function createParticleSource(sprite: Sprite, particlesPerSecond: number): ParticleSource { return new ParticleSource(sprite, particlesPerSecond); } function init() { const scene = game.currentScene(); if (scene.particleSources) return; scene.particleSources = []; lastUpdate = control.millis(); game.onUpdate(updateParticles); game.onUpdateInterval(250, pruneParticles); } function updateParticles() { const sources = particleSources(); if (!sources) return; sortSources(sources); const time = control.millis(); const dt = time - lastUpdate; lastUpdate = time; for (let i = 0; i < sources.length; i++) { sources[i]._update(dt); } } function pruneParticles() { const sources = particleSources(); if (sources) sources.slice(0, sources.length).forEach(s => s._prune()); } function sortSources(sources: ParticleSource[]) { sources.sort((a, b) => (a.priority - b.priority || a.id - b.id)); } /** * A source of particles where particles will occasionally change speed based off of each other */ export class FireSource extends ParticleSource { protected galois: Math.FastRandom; constructor(anchor: ParticleAnchor, particlesPerSecond: number, factory?: ParticleFactory) { super(anchor, particlesPerSecond, factory); this.galois = new Math.FastRandom(); this.z = 20; } updateParticle(p: Particle, fixedDt: Fx8) { super.updateParticle(p, fixedDt); if (p.next && this.galois.percentChance(30)) { p.vx = p.next.vx; p.vy = p.next.vy; } } } /** * A source of particles where the particles oscillate horizontally, and occasionally change * between a given number of defined states */ export class BubbleSource extends ParticleSource { protected maxState: number; protected galois: Math.FastRandom; stateChangePercentage: number; oscillationPercentage: number constructor(anchor: ParticleAnchor, particlesPerSecond: number, maxState: number, factory?: ParticleFactory) { super(anchor, particlesPerSecond, factory); this.galois = new Math.FastRandom(); this.maxState = maxState; this.stateChangePercentage = 3; this.oscillationPercentage = 4; } updateParticle(p: Particle, fixedDt: Fx8) { super.updateParticle(p, fixedDt); if (this.galois.percentChance(this.stateChangePercentage)) { if (p.data < this.maxState) { p.data++; } else if (p.data > 0) { p.data--; } } if (this.galois.percentChance(this.oscillationPercentage)) { p.vx = Fx.neg(p.vx); } } } export function clearAll() { const sources = particleSources(); if (sources) { sources.forEach(s => s.clear()); pruneParticles(); } } /** * Stop all particle sources from creating any new particles */ export function disableAll() { const sources = particleSources(); if (sources) { sources.forEach(s => s.enabled = false); pruneParticles(); } } /** * Allow all particle sources to create any new particles */ export function enableAll() { const sources = particleSources(); if (sources) { sources.forEach(s => s.enabled = true); pruneParticles(); } } function particleSources() { const sources = game.currentScene().particleSources; return sources; } }