Create your own
Lesson illustration

Scene Management for Game Transitions

Hello! Welcome back.

In our last lesson, we laid the architectural groundwork for managing your game's flow. We designed a SceneManager using the State Pattern and a state stack, which is perfect for handling the nested states common in JRPGs, like opening a menu over the map.

Today, we move from blueprint to reality. This lesson directly addresses the learning outcome: Implement the scene manager to transition between the map, a main menu, and back. We will write the JavaScript code to bring last lesson's design to life. This is the foundational implementation that will allow us to seamlessly switch between different parts of your game.

1. Recap of the State Stack Architecture

As a quick refresher, our architecture relies on a few key components:

  • A Scene base class that defines a common interface (enter, exit, update, draw).
  • Concrete scene classes (MapScene, MenuScene) that inherit from the base class and implement their own specific logic.
  • A SceneManager that maintains a stack of active scenes.
  • The SceneManager uses push() to add a new scene to the top of the stack (e.g., opening a menu) and pop() to remove it, resuming the scene below.

This is a powerful and flexible pattern used across the game industry. Now, let's code it.

2. Implementing the Core Components in JavaScript

We'll follow a structure very similar to the one demonstrated in the Pygame tutorial from the resources, but adapted for our JavaScript engine. The logic is language-agnostic and translates perfectly.

The Scene Base Class

First, we need the "interface" or abstract base class for all our scenes. This ensures the SceneManager can interact with any scene in a predictable way. In JavaScript, we can create a base class with empty methods that child classes will override.

// In a new file, e.g., 'src/Scene.js'
class Scene {
    constructor(game) {
        this.game = game;
    }

    /**
     * Called once when the scene is pushed onto the stack.
     * Use this for initialization.
     * @param {object} [params] - Optional parameters passed during the transition.
     */
    enter(params) {}

    /**
     * Called once when the scene is popped from the stack.
     * Use this for cleanup.
     */
    exit() {}

    /**
     * Called every frame by the game loop.
     * @param {number} deltaTime - The time elapsed since the last frame.
     */
    update(deltaTime) {}

    /**
     * Called every frame after update.
     * @param {CanvasRenderingContext2D} context - The canvas rendering context.
     */
    draw(context) {}
}

This class doesn't do anything on its own, but it establishes the contract that all future scenes—MapScene, BattleScene, TitleScene—must follow.

The SceneManager and the State Stack

Next is the conductor of the orchestra: the SceneManager. This class will manage our sceneStack array.

Pygame Game States Tutorial: Creating an In-game Menu using States

The fundamental concept of using a stack to manage states is beautifully explained in this video. While it uses Python, the logic for pushing and popping states is what we're about to implement.

Watch the sections 'Understanding the State Stack Data Structure' (09:49 - 11:04) and 'Implementing enter_state and exit_state Methods' (11:37 - 12:18). This will give you a clear visual and conceptual model for the code we're writing next.

Now, let's implement that logic in JavaScript.

// In a new file, e.g., 'src/SceneManager.js'
class SceneManager {
    constructor(game) {
        this.game = game;
        this.sceneStack = [];
    }

    /**
     * Returns the currently active scene (the one on top of the stack).
     */
    get currentScene() {
        return this.sceneStack[this.sceneStack.length - 1];
    }

    /**
     * Pushes a new scene onto the stack, making it the active scene.
     * @param {Scene} scene - The scene instance to push.
     * @param {object} [params] - Optional parameters for the scene's enter method.
     */
    push(scene, params) {
        this.sceneStack.push(scene);
        scene.enter(params);
    }

    /**
     * Pops the current scene from the stack, resuming the one below.
     */
    pop() {
        if (this.sceneStack.length === 0) return;

        const scene = this.sceneStack.pop();
        scene.exit();
        return scene; // Return the popped scene in case it's needed.
    }

    update(deltaTime) {
        // Only the top scene gets updated.
        this.currentScene?.update(deltaTime);
    }

    draw(context) {
        // Draw all scenes in the stack, from bottom to top.
        // This allows for overlay effects, like a semi-transparent menu over the map.
        for (const scene of this.sceneStack) {
            scene.draw(context);
        }
    }
}

Notice the draw method. By iterating through the whole stack, we can render the map and then render the menu on top of it. This is a simple way to achieve the classic JRPG effect of a menu appearing over the game world.

3. Creating Concrete Scenes: Map and Menu

With the management system in place, let's create our first two scenes. For now, they will be very simple placeholders.

The MapScene

This will be our default game state. It will handle map exploration logic. For this lesson, it will simply draw a background and listen for an input to open the menu.

// In a new file, e.g., 'src/scenes/MapScene.js'
// Assume you have an InputManager that tracks key presses
class MapScene extends Scene {
    update(deltaTime) {
        // For now, our map "logic" is just checking for menu input.
        // In your real game, character movement updates would go here.
        if (this.game.inputManager.isKeyPressed('m')) {
            // Create and push the MenuScene onto the stack
            this.game.sceneManager.push(new MenuScene(this.game));
        }
    }

    draw(context) {
        // Draw the map background
        context.fillStyle = 'darkgreen';
        context.fillRect(0, 0, context.canvas.width, context.canvas.height);

        // In the future, you'd call your tilemap renderer here.
        context.fillStyle = 'white';
        context.font = '20px sans-serif';
        context.textAlign = 'center';
        context.fillText("Map Scene", context.canvas.width / 2, 50);
        context.fillText("Press 'M' to open the menu", context.canvas.width / 2, 100);
    }
}

The MenuScene

This scene will overlay the map. It will listen for an input to close itself.

// In a new file, e.g., 'src/scenes/MenuScene.js'
class MenuScene extends Scene {
    enter() {
        console.log("Entering Menu Scene.");
        // We could pause map music here, for example.
    }

    exit() {
        console.log("Exiting Menu Scene.");
        // And resume it here.
    }

    update(deltaTime) {
        if (this.game.inputManager.isKeyPressed('Escape')) {
            // Pop this scene from the stack to return to the one below (MapScene).
            this.game.sceneManager.pop();
        }
    }

    draw(context) {
        // Draw a semi-transparent overlay to darken the map behind the menu
        context.fillStyle = 'rgba(0, 0, 0, 0.5)';
        context.fillRect(0, 0, context.canvas.width, context.canvas.height);

        // Draw the menu "window"
        context.fillStyle = 'blue';
        context.fillRect(100, 100, context.canvas.width - 200, context.canvas.height - 200);

        context.fillStyle = 'white';
        context.font = '24px sans-serif';
        context.textAlign = 'center';
        context.fillText("Main Menu", context.canvas.width / 2, 150);
        context.fillText("Press 'Escape' to close", context.canvas.width / 2, 200);
    }
}

4. Integrating into the Game Loop

The final step is to wire the SceneManager into our main Game class.

// In your main Game class file, e.g., 'src/Game.js'

// Import the necessary classes
import SceneManager from './SceneManager.js';
import MapScene from './scenes/MapScene.js';
// (Don't need to import MenuScene here, as MapScene creates it)

class Game {
    constructor(canvas) {
        this.canvas = canvas;
        this.context = canvas.getContext('2d');
        // ...other managers like InputManager...
        
        // Create and initialize the SceneManager
        this.sceneManager = new SceneManager(this);
        
        // Push the initial scene to start the game
        this.sceneManager.push(new MapScene(this));

        this.lastTime = 0;
        this.gameLoop = this.gameLoop.bind(this);
    }

    start() {
        this.gameLoop(0);
    }

    gameLoop(currentTime) {
        const deltaTime = (currentTime - this.lastTime) / 1000;
        this.lastTime = currentTime;

        // Delegate update and draw calls to the SceneManager
        this.sceneManager.update(deltaTime);
        this.sceneManager.draw(this.context);

        // Reset input manager for next frame
        this.inputManager.clearKeys();

        requestAnimationFrame(this.gameLoop);
    }
}

And that's it! When you run the game, it will start in the MapScene. Pressing 'M' will push the MenuScene on top, which will appear as an overlay. Pressing 'Escape' will pop the MenuScene, and you'll be right back in the MapScene as if you never left.

Test your understanding!

In our SceneManager, the draw method renders every scene in the stack. This is great for an overlay menu. However, when transitioning from the map to a full-screen battle, you typically don't want to see the map rendered in the background.

How would you modify the draw method to only render the top-most scene on the stack?

Show answer

You would simply get the currentScene (the top of the stack) and call its draw method, instead of looping through the entire stack.

// Inside the SceneManager class
draw(context) {
    // Only draw the top scene.
    this.currentScene?.draw(context);
}

A more advanced solution could involve a flag on each scene, like isOverlay, to let the SceneManager decide whether to draw the scenes underneath it.

5. The RPG Maker Connection

You might be wondering how this low-level implementation relates to your goal of understanding tools like RPG Maker. The answer is: it's the exact same principle.

When you use an "Event Command" in RPG Maker to open the Item menu, the engine is executing a script call behind the scenes.

Eventing a Picture-Based Menu

This article from the official RPG Maker blog discusses eventing a custom menu. While it focuses on the visual editor, it reveals the underlying script calls used for scene transitions.

Read the section that begins 'Now we can open our menu and navigate around...' Focus on the list of script calls provided, such as SceneManager.push(Scene_Item);.

That line, SceneManager.push(Scene_Item), is precisely what we've built. RPG Maker's SceneManager is also a state stack. By building it yourself, you now understand the core architectural pattern that powers the tools you want to master. This knowledge demystifies the "magic" and empowers you to extend or even create your own engine logic.

Conclusion

Congratulations! You have successfully implemented a robust and scalable SceneManager. This is a huge step forward in building a full-fledged JRPG.

Key Takeaways:

  • You implemented a Scene base class to create a consistent interface for all game states.
  • You built a SceneManager with a sceneStack that uses push and pop to handle transitions.
  • You created two distinct scenes, MapScene and MenuScene, and implemented the logic to switch between them based on player input.
  • You integrated the SceneManager into the main game loop, delegating all update and draw calls to the currently active scene(s).
  • You saw how this custom implementation directly mirrors the SceneManager used in professional tools like RPG Maker.

Preview of the next lesson:

Our MapScene is currently static. To make the world feel alive, we need a way to script events: talking to an NPC, opening a treasure chest, or triggering a cutscene. In the next lesson, "Design the architecture for a data-driven event system," we will design a system that allows us to create these interactions without writing custom code for every single one. This system will be the heart of your game's storytelling capabilities.

Can't find a good explanation? Sign up and we'll make it for you

Sign up