ianpaschal/aurora

View on GitHub
src/core/System.ts

Summary

Maintainability
A
1 hr
Test Coverage
// Aurora is distributed under the MIT license.

import Engine from "./Engine"; // Typing
import Entity from "./Entity"; // Typing
import { SystemConfig } from "../utils/interfaces"; // Typing

/**
 * @module core
 * @classdesc Class representing a system.
 */
export default class System {

    private _accumulator:    number;
    private _componentTypes: string[];
    private _engine:         Engine;
    private _entityUUIDs:    string[];
    private _fixed:          boolean;
    private _frozen:         boolean;
    private _methods:        {};
    private _name:           string;
    private _onAddEntity:    ( entity: Entity ) => void;
    private _onInit:         () => void;
    private _onRemoveEntity: ( entity: Entity ) => void;
    private _onUpdate:       ( delta: number ) => void;
    private _step:           number;

    /**
     * @description Create a System.
     * @param {Object} config - Configuration object
     * @param {string} config.name - System name
     * @param {boolean} config.fixed - Fixed step size or update as often as possible
     * @param {number} config.step - Step size in milliseconds (only used if `fixed` is `false`)
     * @param {array} config.componentTypes - Types to watch
     * @param {Function} config.onInit - Function to run when first connecting the system to the
     * engine
     * @param {Function} config.onAddEntity - Function to run on an entity when adding it to the
     * system's watchlist
     * @param {Function} config.onRemoveEntity - Function to run on an entity when removing it from
     * the system's watchlist
     * @param {Function} config.onUpdate - Function to run each time the engine updates the main loop
     */
    constructor( config: SystemConfig ) {

        // Define defaults
        this._accumulator    = 0;
        this._componentTypes = [];
        this._engine         = undefined;
        this._entityUUIDs    = [];
        this._fixed          = false;
        this._frozen         = false;
        this._methods        = {};
        this._name           = "no-name";
        this._onAddEntity    = ( entity: Entity ) => {};
        this._onInit         = () => {};
        this._onRemoveEntity = ( entity: Entity ) => {};
        this._onUpdate       = ( delta: number ) => {};
        this._step           = 100;

        // Apply config values
        Object.keys( config ).forEach( ( key ) => {

            // Handle component types and methods slightly differently, otherwise simply overwite props
            // with config values
            const specialCases = [ "componentTypes", "methods", "entityUUIDs" ];

            // If not a special case
            if ( specialCases.indexOf( key ) > -1 ) {
                switch( key ) {
                    case "methods":
                        Object.keys( config.methods ).forEach( ( key ) => {
                            this.addMethod( key, config.methods[ key ] );
                        });
                        break;
                    case "componentTypes":
                        Object.keys( config.componentTypes ).forEach( ( key ) => {
                            this.watchComponentType( config.componentTypes[ key ] );
                        });
                        break;
                }
            } else {
                this[ "_" + key ] = config[ key ];
            }
        });
    }

    /**
     * @description Get the accumulated time of the system.
     * @readonly
     * @returns {number} - Time in milliseconds
     */
    get accumulator(): number {
        return this._accumulator;
    }

    /**
     * @description Get whether or not the system uses a fixed step.
     * @readonly
     * @returns {boolean} - True if the system uses a fixed step
     */
    get fixed(): boolean {
        return this._fixed;
    }

    /**
     * @description Get the step size of the system in milliseconds.
     * @readonly
     * @returns {number} - Time in milliseconds
     */
    get step(): number {
        return this._step;
    }

    /**
     * @description Get the entity's name.
     * @readonly
     * @returns {string} - Name string
     */
    get name(): string {
        return this._name;
    }

    /**
     * @description Get all of the component types the system is watching.
     * @readonly
     * @returns {string[]} - Array of component types
     */
    get watchedComponentTypes(): string[] {
        return this._componentTypes;
    }

    /**
     * @description Get all of the entity UUIDs the system is watching.
     * @readonly
     * @returns {string[]} - Array of UUID strings
     */
    get watchedEntityUUIDs(): string[] {
        return this._entityUUIDs;
    }

    /**
     * @description Add an extra method to the system. Cannot be modified after the system is
     * registered with the engine.
     * @param {string} key - Method identifier
     * @param {function} fn - Method to be called by user in the future
     */
    addMethod( key: string, fn: Function ): void {
        // TODO: Error handling
        this._methods[ key ] = fn.bind( this );
    }

    /**
     * @description Check if the system can watch a given entity.
     * @readonly
     * @param {Entity} entity - Entity to check
     * @returns {boolean} - True if the given entity is watchable
     */
    canWatch( entity: Entity ): boolean {
        // TODO: Error handling
        // Faster to loop through search criteria vs. all components on entity
        for ( const type of this._componentTypes ) {

            // Return early if any required component is missing on entity
            if ( !entity.hasComponent( type ) ) {
                return false;
            }
        }
        return true;
    }

    /**
     * @description Call a user-added method from outside the system. Cannot be modified after the
     * system is registered with the engine.
     * @param {string} key - Method identifier
     * @param {any} payload - Any data which should be passed to the method
     * @returns {any} - Any data which the method returns
     */
    dispatch( key: string, payload?: any ): any {
        if ( !this._methods[ key ] ) {
            throw Error( `Method ${ key } does not exist!` );
        }
        return this._methods[ key ]( payload );
    }

    /**
     * @description Initialize the system (as a part of linking to the engine). After linking the
     * engine, the system will run its stored init hook method. Cannot be modified after the system is
     * registered with the engine.
     * @param {Engine} engine - Engine instance to link to
     */
    init( engine: Engine ): void {
        console.log( "Initializing a new system: " + this._name + "." );
        this._engine = engine;

        // Run the actual init behavior:
        if ( this._onInit ) {
            this._onInit();
        }

        // Freeze the system to make it immutable:
        this._frozen = true;
    }

    /**
     * @description Check if the system is watching a given component type.
     * @readonly
     * @param {Entity} entity - Component type to check
     * @returns {boolean} - True if the given component type is being watched
     */
    isWatchingComponentType( componentType: string ): boolean {
        if ( this._componentTypes.indexOf( componentType ) > -1 ) {
            return true;
        }
        return false;
    }

    /**
     * @description Check if the system is watching a given entity.
     * @readonly
     * @param {Entity} entity - Entity instance to check
     * @returns {boolean} - True if the given entity instance is being watched
     */
    isWatchingEntity( entity: Entity ): boolean {
        if ( this._entityUUIDs.indexOf( entity.uuid ) > -1 ) {
            return true;
        }
        return false;
    }

    /**
     * @description Remove a user-added method from the system. Cannot be modified after the system is
     * registered with the
     * engine.
     * @param {string} key - Method identifier
     */
    removeMethod( key: string ): void {
        if ( !this._methods[ key ] ) {
            throw Error( `Method ${ key } does not exist!` );
        }
        delete this._methods[ key ];
    }

    /**
     * @description Remove a component type to the system's watch list. Cannot be modified after the
     * system is registered
     * with the engine.
     * @param {string} componentType - Component type to stop watching
     * @returns {array} - Array of watched component types
     */
    unwatchComponentType( componentType: string ): string[] {
        const index = this._componentTypes.indexOf( componentType );
        if ( this._componentTypes.length < 2 ) {
            throw Error( "Cannot remove component type, this system will be left with 0." );
        }
        if ( index == -1 ) {
            throw Error( "Component type not found on system." );
        }
        this._componentTypes.splice( index, 1 );
        return this._componentTypes;
    }

    /**
     * @description Remove an entity UUID to the system's watch list.
     * @param {Entity} entity - Entity instance to stop watching
     * @returns {array} - Array of watched entity UUIDs
     */
    unwatchEntity( entity: Entity ): string[] {
        const index = this._entityUUIDs.indexOf( entity.uuid );
        if ( index < 0 ) {
            throw Error( `Could not unwatch entity ${ entity.uuid }; not watched.` );
        }
        this._entityUUIDs.splice( index, 1 );
        return this._entityUUIDs;
    }

    /**
     * @description Update the system with a given amount of time to simulate. The system will run its
     * stored update function using either a fixed step or variable step (specified at creation) and
     * the supplied delta time. Cannot be modified after the system is registered with the engine.
     * @param {number} delta - Time in milliseconds to simulate
     */
    update( delta: number ): void {
        if ( this._fixed ) {
            // Add time to the accumulator & simulate if greater than the step size:
            this._accumulator += delta;
            while ( this._accumulator >= this._step ) {
                this._onUpdate( this._step );
                this._accumulator -= this._step;
            }
        } else {
            this._onUpdate( delta );
        }
    }

    /**
     * @description Add a single component type to the system's watch list. Cannot be modified after
     * the system is registered with the engine.
     * @param {string} componentType - Component type to watch
     * @returns {array} - Array of watched component types
     */
    watchComponentType( componentType: string ): string[] {

        // Early return if frozen; this avoids updating the entity watch list during
        // execution.
        if ( this._frozen ) {
            throw Error( "Cannot modify watchedComponentTypes after adding to engine." );
        }

        // Check if this component type is already present
        if ( this._componentTypes.indexOf( componentType ) > -1 ) {
            throw Error( `Component type ${ componentType } is already being watched!` );
        }

        // If not, add it to the system
        this._componentTypes.push( componentType );
        return this._componentTypes;
    }

    /**
     * @description Watch an entity by adding its UUID to to the system. After adding, the system will
     * run the entity through the internal add function to do any additional processing.
     * @param {Entity} entity - Entity instance to watch
     * @returns {array} - Array of watched entity UUIDs
     */
    watchEntity( entity: Entity ): string[] {

        // Check if this entity is already being watched
        if ( this._entityUUIDs.indexOf( entity.uuid ) >= 0 ) {
            throw Error( `Entity ${ entity.uuid } is already being watched!` );
        }
        this._entityUUIDs.push( entity.uuid );
        this._onAddEntity( entity );
        return this._entityUUIDs;
    }

}