Source: SceneManager.js

SceneManager.js

//-----------------------------------------------------------------------------
// SceneManager
//
// The static class that manages scene transitions.
/**
 * The static class that manages scene transitions.
 *
 * @namespace
 */
function SceneManager() {
    throw new Error("This is a static class");
}

SceneManager._scene = null;
SceneManager._nextScene = null;
SceneManager._stack = [];
SceneManager._exiting = false;
SceneManager._previousScene = null;
SceneManager._previousClass = null;
SceneManager._backgroundBitmap = null;
SceneManager._smoothDeltaTime = 1;
SceneManager._elapsedTime = 0;

/**
 * Starts the main game loop with the given scene class as the base stage
 *
 * @static
 * @param {Stage} sceneClass - The first scene when game starts, default is Scene_Boot
 */
SceneManager.run = function(sceneClass) {
    try {
        this.initialize();
        this.goto(sceneClass);
        Graphics.startGameLoop();
    } catch (e) {
        this.catchException(e);
    }
};

/**
 * Initialize the SceneManager
 *
 * @static
 */
SceneManager.initialize = function() {
    this.checkBrowser();
    this.checkPluginErrors();
    this.initGraphics();
    this.initAudio();
    this.initVideo();
    this.initInput();
    this.setupEventHandlers();
};

/**
 * Check if the player's browser supports necessary technologies to run an RPG Maker game
 *
 * @static
 * @throws Errors if browser does not support WebGL, Web Audio API, CSS Font Loading, or IndexedDB
 */
SceneManager.checkBrowser = function() {
    if (!Utils.canUseWebGL()) {
        throw new Error("Your browser does not support WebGL.");
    }
    if (!Utils.canUseWebAudioAPI()) {
        throw new Error("Your browser does not support Web Audio API.");
    }
    if (!Utils.canUseCssFontLoading()) {
        throw new Error("Your browser does not support CSS Font Loading.");
    }
    if (!Utils.canUseIndexedDB()) {
        throw new Error("Your browser does not support IndexedDB.");
    }
};

/**
 * Check if any plugin errors exist
 *
 * @static
 */
SceneManager.checkPluginErrors = function() {
    PluginManager.checkErrors();
};

/**
 * Attempts to initialize Graphics
 *
 * @static
 * @throws Error if Graphics could not be initialized
 */
SceneManager.initGraphics = function() {
    if (!Graphics.initialize()) {
        throw new Error("Failed to initialize graphics.");
    }
    Graphics.setTickHandler(this.update.bind(this));
};

/**
 * Initializes WebAudio
 *
 * @static
 */
SceneManager.initAudio = function() {
    WebAudio.initialize();
};

/**
 * Initializes {@link Video}
 *
 * @static
 */
SceneManager.initVideo = function() {
    Video.initialize(Graphics.width, Graphics.height);
};

/**
 * Initializes {@link Input} and {@link TouchInput}
 *
 * @static
 */
SceneManager.initInput = function() {
    Input.initialize();
    TouchInput.initialize();
};

/**
 * Sets JS event listeners for error, unhandledrejection, unload, and keydown
 *
 * @static
 */
SceneManager.setupEventHandlers = function() {
    window.addEventListener("error", this.onError.bind(this));
    window.addEventListener("unhandledrejection", this.onReject.bind(this));
    window.addEventListener("unload", this.onUnload.bind(this));
    document.addEventListener("keydown", this.onKeyDown.bind(this));
};

/**
 * Updates the SceneManager
 *
 * @static
 * @param {number} deltaTime - Scalar time value from last frame to this frame.
 */
SceneManager.update = function(deltaTime) {
    try {
        const n = this.determineRepeatNumber(deltaTime);
        for (let i = 0; i < n; i++) {
            this.updateMain();
        }
    } catch (e) {
        this.catchException(e);
    }
};

/**
 * Check how many times to update based on time since last frame
 *
 * @static
 * @param {number} deltaTime - Scalar time value from last frame to this frame.
 */
SceneManager.determineRepeatNumber = function(deltaTime) {
    // [Note] We consider environments where the refresh rate is higher than
    //   60Hz, but ignore sudden irregular deltaTime.
    this._smoothDeltaTime *= 0.8;
    this._smoothDeltaTime += Math.min(deltaTime, 2) * 0.2;
    if (this._smoothDeltaTime >= 0.9) {
        this._elapsedTime = 0;
        return Math.round(this._smoothDeltaTime);
    } else {
        this._elapsedTime += deltaTime;
        if (this._elapsedTime >= 1) {
            this._elapsedTime -= 1;
            return 1;
        }
        return 0;
    }
};

/**
 * Exit the game (if nwjs)
 *
 * @static
 */
SceneManager.terminate = function() {
    if (Utils.isNwjs()) {
        nw.App.quit();
    }
};

/**
 * Handling for errors
 *
 * @static
 * @param {{message: string, filename: string, lineno: string}} event - The error event
 */
SceneManager.onError = function(event) {
    console.error(event.message);
    console.error(event.filename, event.lineno);
    try {
        this.stop();
        Graphics.printError("Error", event.message, event);
        AudioManager.stopAll();
    } catch (e) {
        //
    }
};

/**
 * Handling for an uncaught exception in Promise when unhandledrejection event is fired. See {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event Documentation}
 *
 * @static
 * @param {{reason: string, filename: string, lineno: string}} event - The error event
 */
SceneManager.onReject = function(event) {
    // Catch uncaught exception in Promise
    event.message = event.reason;
    this.onError(event);
};

/**
 * Handling for when an unload event is fired. See {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event Documentation}
 *
 * @static
 */
SceneManager.onUnload = function() {
    ImageManager.clear();
    EffectManager.clear();
    AudioManager.stopAll();
};

/**
 * Handling for when a keydown event is fired. See {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event Documentation}
 *
 * @static
 * @param {event} event - The keydown event
 */
SceneManager.onKeyDown = function(event) {
    if (!event.ctrlKey && !event.altKey) {
        switch (event.keyCode) {
            case 116: // F5
                this.reloadGame();
                break;
            case 119: // F8
                this.showDevTools();
                break;
        }
    }
};

/**
 * Reloads the game, if nwjs is available in the environment
 *
 * @static
 */
SceneManager.reloadGame = function() {
    if (Utils.isNwjs()) {
        chrome.runtime.reload();
    }
};

/**
 * Shows the chrome dev tools, if nwjs is available in the environment and is a test play
 *
 * @static
 */
SceneManager.showDevTools = function() {
    if (Utils.isNwjs() && Utils.isOptionValid("test")) {
        nw.Window.get().showDevTools();
    }
};

/**
 * Catching for exceptions
 *
 * @static
 * @param {(Error|Array|*)} e - The error encountered
 */
SceneManager.catchException = function(e) {
    if (e instanceof Error) {
        this.catchNormalError(e);
    } else if (e instanceof Array && e[0] === "LoadError") {
        this.catchLoadError(e);
    } else {
        this.catchUnknownError(e);
    }
    this.stop();
};

/**
 * Catching for normal Error type errors
 *
 * @static
 * @param {Error} e - The error encountered
 */
SceneManager.catchNormalError = function(e) {
    Graphics.printError(e.name, e.message, e);
    AudioManager.stopAll();
    console.error(e.stack);
};

/**
 * Catching for load Errors
 *
 * @static
 * @param {Array} e - Array of error information for the error encountered
 */
SceneManager.catchLoadError = function(e) {
    const url = e[1];
    const retry = e[2];
    Graphics.printError("Failed to load", url);
    if (retry) {
        Graphics.showRetryButton(() => {
            retry();
            SceneManager.resume();
        });
    } else {
        AudioManager.stopAll();
    }
};

/**
 * Catching for all other Errors
 *
 * @static
 * @param {*} e - An object of any type with error information
 */
SceneManager.catchUnknownError = function(e) {
    Graphics.printError("UnknownError", String(e));
    AudioManager.stopAll();
};

/**
 * Update the game
 *
 * @static
 */
SceneManager.updateMain = function() {
    this.updateFrameCount();
    this.updateInputData();
    this.updateEffekseer();
    this.changeScene();
    this.updateScene();
};

/**
 * Add one to the frame count
 *
 * @static
 */
SceneManager.updateFrameCount = function() {
    Graphics.frameCount++;
};

/**
 * Update {@link Input} and {@link TouchInput}
 *
 * @static
 */
SceneManager.updateInputData = function() {
    Input.update();
    TouchInput.update();
};

/**
 * Update Effekseer
 *
 * @static
 */
SceneManager.updateEffekseer = function() {
    if (Graphics.effekseer && this.isGameActive()) {
        Graphics.effekseer.update();
    }
};

/**
 * Changes the scene, if needed
 *
 * @static
 */
SceneManager.changeScene = function() {
    if (this.isSceneChanging() && !this.isCurrentSceneBusy()) {
        if (this._scene) {
            this._scene.terminate();
            this.onSceneTerminate();
        }
        this._scene = this._nextScene;
        this._nextScene = null;
        if (this._scene) {
            this._scene.create();
            this.onSceneCreate();
        }
        if (this._exiting) {
            this.terminate();
        }
    }
};

/**
 * Updates the current scene
 *
 * @static
 */
SceneManager.updateScene = function() {
    if (this._scene) {
        if (this._scene.isStarted()) {
            if (this.isGameActive()) {
                this._scene.update();
            }
        } else if (this._scene.isReady()) {
            this.onBeforeSceneStart();
            this._scene.start();
            this.onSceneStart();
        }
    }
};

/**
 * Check if the game window is active. Will also return true if this check could not be completed
 *
 * @static
 * @return {boolean} True if game window has focus. 
 */
SceneManager.isGameActive = function() {
    // [Note] We use "window.top" to support an iframe.
    try {
        return window.top.document.hasFocus();
    } catch (e) {
        // SecurityError
        return true;
    }
};

/**
 * Handling for when a scene is terminated
 *
 * @static
 */
SceneManager.onSceneTerminate = function() {
    this._previousScene = this._scene;
    this._previousClass = this._scene.constructor;
    Graphics.setStage(null);
};

/**
 * Handling for when a scene is created
 *
 * @static
 */
SceneManager.onSceneCreate = function() {
    Graphics.startLoading();
};

/**
 * Handling before a scene is started
 *
 * @static
 */
SceneManager.onBeforeSceneStart = function() {
    if (this._previousScene) {
        this._previousScene.destroy();
        this._previousScene = null;
    }
    if (Graphics.effekseer) {
        Graphics.effekseer.stopAll();
    }
};

/**
 * Handling when a scene is started
 *
 * @static
 */
SceneManager.onSceneStart = function() {
    Graphics.endLoading();
    Graphics.setStage(this._scene);
};

/**
 * Check if the scene is changing
 *
 * @static
 * @return {boolean} True if game is exiting or there is a next scene to go to
 */
SceneManager.isSceneChanging = function() {
    return this._exiting || !!this._nextScene;
};

/**
 * Check if the current scene is busy
 *
 * @static
 * @return {boolean} True if the scene is busy
 */
SceneManager.isCurrentSceneBusy = function() {
    return this._scene && this._scene.isBusy();
};

/**
 * Check if the provided scene is the next scene
 *
 * @static
 * @param {Stage} sceneClass - The scene to check
 * @return {boolean} True if the passed scene class is the next scene
 */
SceneManager.isNextScene = function(sceneClass) {
    return this._nextScene && this._nextScene.constructor === sceneClass;
};

/**
 * Check if the provided scene is the previous scene
 *
 * @static
 * @param {Stage} sceneClass - The scene to check
 * @return {boolean} True if the passed scene class is the previous scene
 */
SceneManager.isPreviousScene = function(sceneClass) {
    return this._previousClass === sceneClass;
};

/**
 * Goes directly to the given scene
 *
 * @static
 * @param {Stage} sceneClass - The scene to go to
 */
SceneManager.goto = function(sceneClass) {
    if (sceneClass) {
        this._nextScene = new sceneClass();
    }
    if (this._scene) {
        this._scene.stop();
    }
};

/**
 * Pushes the given scene onto the end of the scene stack
 *
 * @static
 * @param {Stage} sceneClass - The scene to push to the stack
 */
SceneManager.push = function(sceneClass) {
    this._stack.push(this._scene.constructor);
    this.goto(sceneClass);
};

/**
 * Pops the last scene on the end of the scene stack
 *
 * @static
 */
SceneManager.pop = function() {
    if (this._stack.length > 0) {
        this.goto(this._stack.pop());
    } else {
        this.exit();
    }
};

/**
 * Exits the game
 *
 * @static
 */
SceneManager.exit = function() {
    this.goto(null);
    this._exiting = true;
};

/**
 * Clears the scene stack
 *
 * @static
 */
SceneManager.clearStack = function() {
    this._stack = [];
};

/**
 * Stops the main game loop
 *
 * @static
 */
SceneManager.stop = function() {
    Graphics.stopGameLoop();
};

/**
 * Prepares the next scene with the given arguments
 *
 * @static
 * @param {*} arguments - Arguments to be passed to the next scene's prepare function
 */
SceneManager.prepareNextScene = function() {
    this._nextScene.prepare(...arguments);
};

/**
 * Snaps a bitmap of the current scene
 *
 * @static
 * @return {Bitmap} A bitmap of the current scene as it appears when called
 */
SceneManager.snap = function() {
    return Bitmap.snap(this._scene);
};

/**
 * Snaps a bitmap of the current scene for use in the background
 *
 * @static
 */
SceneManager.snapForBackground = function() {
    if (this._backgroundBitmap) {
        this._backgroundBitmap.destroy();
    }
    this._backgroundBitmap = this.snap();
};

/**
 * Gets the background bitmap
 *
 * @static
 * @return {Bitmap} The background bitmap
 */
SceneManager.backgroundBitmap = function() {
    return this._backgroundBitmap;
};

/**
 * Resumes the main game loop
 *
 * @static
 */
SceneManager.resume = function() {
    TouchInput.update();
    Graphics.startGameLoop();
};