render/Renderer.js

let singleton = null;

const TIME_RESET_THRESHOLD = 100;

/**
 * The Renderer is the component which must *draw* the game on the client.
 * It will be instantiated once on each client, and must implement the draw
 * method.  The draw method will be invoked on every iteration of the browser's
 * render loop.
 */
class Renderer {

    static getInstance() {
        return singleton;
    }

    /**
    * Constructor of the Renderer singleton.
    * @param {GameEngine} gameEngine - Reference to the GameEngine instance.
    * @param {ClientEngine} clientEngine - Reference to the ClientEngine instance.
    */
    constructor(gameEngine, clientEngine) {
        this.gameEngine = gameEngine;
        this.clientEngine = clientEngine;
        this.gameEngine.on('client__stepReset', () => { this.doReset = true; });
        gameEngine.on('objectAdded', this.addObject.bind(this));
        gameEngine.on('objectDestroyed', this.removeObject.bind(this));

        // the singleton renderer has been created
        singleton = this;
    }

    /**
     * Initialize the renderer.
     * @return {Promise} Resolves when renderer is ready.
    */
    init() {
        if ((typeof window === 'undefined') || !document) {
            console.log('renderer invoked on server side.');
        }
        this.gameEngine.emit('client__rendererReady');
        return Promise.resolve(); // eslint-disable-line new-cap
    }

    reportSlowFrameRate() {
        this.gameEngine.emit('client__slowFrameRate');
    }

    /**
     * The main draw function.  This method is called at high frequency,
     * at the rate of the render loop.  Typically this is 60Hz, in WebVR 90Hz.
     * If the client engine has been configured to render-schedule, then this
     * method must call the clientEngine's step method.
     *
     * @param {Number} t - current time (only required in render-schedule mode)
     * @param {Number} dt - time elapsed since last draw
     */
    draw(t, dt) {
        this.gameEngine.emit('client__draw');

        if (this.clientEngine.options.scheduler === 'render-schedule')
            this.runClientStep(t);
    }

    /**
     * The main draw function.  This method is called at high frequency,
     * at the rate of the render loop.  Typically this is 60Hz, in WebVR 90Hz.
     *
     * @param {Number} t - current time
     * @param {Number} dt - time elapsed since last draw
     */
    runClientStep(t) {
        let p = this.clientEngine.options.stepPeriod;
        let dt = 0;

        // reset step time if we passed a threshold
        if (this.doReset || t > this.clientEngine.lastStepTime + TIME_RESET_THRESHOLD) {
            this.doReset = false;
            this.clientEngine.lastStepTime = t - p / 2;
            this.clientEngine.correction = p / 2;
        }

        // catch-up missed steps
        while (t > this.clientEngine.lastStepTime + p) {
            this.clientEngine.step(this.clientEngine.lastStepTime + p, p + this.clientEngine.correction);
            this.clientEngine.lastStepTime += p;
            this.clientEngine.correction = 0;
        }

        // if not ready for a real step yet, return
        // this might happen after catch up above
        if (t < this.clientEngine.lastStepTime) {
            dt = t - this.clientEngine.lastStepTime + this.clientEngine.correction;
            if (dt < 0) dt = 0;
            this.clientEngine.correction = this.clientEngine.lastStepTime - t;
            this.clientEngine.step(t, dt, true);
            return;
        }

        // render-controlled step
        dt = t - this.clientEngine.lastStepTime + this.clientEngine.correction;
        this.clientEngine.lastStepTime += p;
        this.clientEngine.correction = this.clientEngine.lastStepTime - t;
        this.clientEngine.step(t, dt);
    }

    /**
     * Handle the addition of a new object to the world.
     * @param {Object} obj - The object to be added.
     */
    addObject(obj) {}

    /**
     * Handle the removal of an old object from the world.
     * @param {Object} obj - The object to be removed.
     */
    removeObject(obj) {}

    /**
     * Called when clientEngine has stopped, time to clean up
     */
    stop() {}
}

export default Renderer;