src/core/System.js
import EventDispatcher, {
EMITTER_ADDED,
EMITTER_REMOVED,
SYSTEM_UPDATE,
SYSTEM_UPDATE_AFTER,
} from '../events';
import { DEFAULT_SYSTEM_DELTA } from './constants';
import Emitter from '../emitter/Emitter';
import { INTEGRATION_TYPE_EULER } from '../math/constants';
import { POOL_MAX } from '../constants';
import Pool from './Pool';
import fromJSON from './fromJSON';
import fromJSONAsync from './fromJSONAsync';
import { CORE_TYPE_SYSTEM as type } from './types';
/**
* The core of the three-system particle engine.
* A System instance can contain multiple emitters, each with their own initializers
* and behaviours.
*
*/
export default class System {
/**
* Constructs a System instance.
*
* @param {object} THREE - ThreeJs
* @param {number} [preParticles=POOL_MAX] - The number of particles to start with
* @param {string} [integrationType=INTEGRATION_TYPE_EULER] - The integration type to use
* @return void
*/
constructor(
preParticles = POOL_MAX,
integrationType = INTEGRATION_TYPE_EULER
) {
/**
* @desc The class type.
* @type {string}
*/
this.type = type;
/**
* @desc Determines if the system can update or not. Set to false when destroying
* to ensure that external calls to update do not throw errors.
* @type {boolean}
*/
this.canUpdate = true;
/**
* @desc The number of particles to start with.
* @type {number}
*/
this.preParticles = preParticles;
/**
* @desc The integration algorithm type to use.
* @param {string}
*/
this.integrationType = integrationType;
/**
* @desc The emitters in the particle system.
* @type {array<Emitter>}
*/
this.emitters = [];
/**
* @desc The renderers for the system.
* @type {array<Renderer>}
*/
this.renderers = [];
/**
* @desc A pool used to manage the internal system cache of objects
* @type {Pool}
*/
this.pool = new Pool();
/**
* @desc Internal event dispatcher
* @type {EventDispatcher}
*/
this.eventDispatcher = new EventDispatcher();
}
/**
* Creates a System instance from a JSON object.
*
* @param {object} json - The JSON to create the System instance from
* @param {object} THREE - The Web GL Api to use eg., THREE
* @return {System}
*
* @deprecated use fromJSONAsync instead
*/
static fromJSON(json, THREE) {
return fromJSON(json, THREE, System, Emitter);
}
/**
* Loads a System instance from JSON asynchronously. Ensures all textures are
* fully loaded before resolving with the instantiated System instance.
*
* @param {object} json - The JSON to create the System instance from
* @param {object} THREE - The Web GL Api to use eg., THREE
* @param {?object} options - Optional config options
* @return {Promise<System>}
*/
static fromJSONAsync(json, THREE, options) {
return fromJSONAsync(json, THREE, System, Emitter, options);
}
/**
* Proxy method for the internal event dispatcher's dispatchEvent method.
*
* @param {string} event - The event to dispatch
* @param {object<System|Emitter|Particle>} [target=this] - The event target
*/
dispatch(event, target = this) {
this.eventDispatcher.dispatchEvent(event, target);
}
/**
* Adds a renderer to the System instance and initializes it.
*
* @param {Renderer} renderer - The renderer to add
* @return {System}
*/
addRenderer(renderer) {
this.renderers.push(renderer);
renderer.init(this);
return this;
}
/**
* Removes a renderer from the System instance.
*
* @param {Renderer} renderer
* @return {System}
*/
removeRenderer(renderer) {
this.renderers.splice(this.renderers.indexOf(renderer), 1);
renderer.remove(this);
return this;
}
/**
* Adds an emitter to the System instance.
* Dispatches the EMITTER_ADDED event.
*
* @param {Emitter} emitter - The emitter to add
* @return {System}
*/
addEmitter(emitter) {
const index = this.emitters.length;
emitter.parent = this;
emitter.index = index;
this.emitters.push(emitter);
this.dispatch(EMITTER_ADDED, emitter);
return this;
}
/**
* Removes an emitter from the System instance.
* Dispatches the EMITTER_REMOVED event.
*
* @param {Emitter} emitter - The emitter to remove
* @return {System}
*/
removeEmitter(emitter) {
if (emitter.parent !== this) {
return this;
}
emitter.parent = null;
emitter.index = undefined;
this.emitters.splice(this.emitters.indexOf(emitter), 1);
this.dispatch(EMITTER_REMOVED, emitter);
return this;
}
/**
* Wires up life cycle methods and causes a system's emitters to emit particles.
* Expects emitters to have their totalEmitTimes and life set already.
* Inifnite systems will resolve immediately.
*
* @param {object} hooks - Functions to hook into the life cycle API
* @param {function} hooks.onStart - Called when the system starts to emit particles
* @param {function} hooks.onUpdate - Called each time the system updates
* @param {function} hooks.onEnd - Called when the system's emitters have all died
* @return {Promise}
*/
emit({ onStart, onUpdate, onEnd }) {
if (onStart) {
onStart();
}
if (onUpdate) {
this.eventDispatcher.addEventListener(SYSTEM_UPDATE, onUpdate);
}
const emitters = this.emitters.map(emitter => {
const { life } = emitter;
if (life === Infinity) {
if (onEnd) {
onEnd();
}
emitter.experimental_emit();
return Promise.resolve();
}
return new Promise(resolve => {
emitter.addOnEmitterDeadEventListener(() => {
if (onEnd) {
onEnd();
}
resolve();
});
emitter.experimental_emit();
});
});
try {
return Promise.all(emitters);
} catch (e) {
console.warn(e);
}
}
/**
* Updates the particle system based on the delta passed.
*
* @example
* animate = () => {
* threeRenderer.render(threeScene, threeCamera);
* system.update();
* requestAnimationFrame(animate);
* }
* animate();
*
* @param {number} delta - Delta time
* @return {Promise}
*/
update(delta = DEFAULT_SYSTEM_DELTA) {
const d = delta || DEFAULT_SYSTEM_DELTA;
if (this.canUpdate) {
if (d > 0) {
let i = this.emitters.length;
while (i--) {
const emitter = this.emitters[i];
emitter.update(d);
emitter.particles.length && this.dispatch(SYSTEM_UPDATE);
}
}
this.dispatch(SYSTEM_UPDATE_AFTER);
}
return Promise.resolve();
}
/**
* Gets a count of the total number of particles in the system.
*
* @return {integer}
*/
getCount() {
const length = this.emitters.length;
let total = 0;
let i;
for (i = 0; i < length; i++) {
total += this.emitters[i].particles.length;
}
return total;
}
/**
* Destroys all emitters, renderers and the Nebula pool.
* Ensures that this.update will not perform any operations while the system
* is being destroyed.
*
* @return void
*/
destroy() {
const length = this.emitters.length;
this.canUpdate = false;
for (let e = 0; e < length; e++) {
this.emitters[e] && this.emitters[e].destroy();
delete this.emitters[e];
}
for (let r = 0; r < length; r++) {
if (this.renderers[r] && this.renderers[r].destroy) {
this.renderers[r].destroy();
delete this.renderers[r];
}
}
this.emitters.length = 0;
this.pool.destroy();
this.canUpdate = true;
}
}