Create your own
Lesson illustration

Reusable UI Architecture for Canvas Games

Hello! Welcome back to our JRPG development journey.

In our last lesson, we finalized the core logic for our equipment system, implementing the functions to equip items and dynamically calculate a character's final stats. We now have a robust data model and the business logic to handle gear changes. However, data and logic are invisible to the player without an interface.

This lesson marks our transition into Module 8: User Interface and Menus. We'll tackle the first and most fundamental outcome of this module: designing a reusable UI component architecture for our canvas-based game. Before we can build an inventory screen or a status menu, we need to design the foundational "Lego bricks"—like windows, lists, and cursors—that we'll use to construct every UI screen in our game.

Given your background in front-end development, you're deeply familiar with component-based architectures like React or Vue. We'll leverage those concepts but adapt them to the unique constraints of an HTML5 canvas, where we don't have the luxury of the DOM and CSS. Instead, we are in full control of rendering every single pixel.

1. The Challenge: UI on a Canvas

In web development, the browser's DOM provides a structured, hierarchical tree of elements. You create a <button>, and the browser handles rendering it, detecting clicks, and managing its state (like hover or focus).

On an HTML5 canvas, we have a blank slate. We are responsible for:

  • Drawing: Manually drawing shapes, text, and images to form our UI elements.
  • State: Tracking which element is active, selected, or hidden.
  • Input: Capturing raw mouse or keyboard events and determining which UI element they affect.
  • Layout: Calculating the position and size of every element.

To manage this complexity, we need a strong architecture.

2. A Layered Architecture for Canvas UI

A robust way to structure a canvas-based application is to separate its concerns into distinct layers. This is a common pattern in both complex web apps and game engines.

How to Develop a Canvas-Based UI like Figma

The article 'How to Develop a Canvas-Based UI like Figma' provides an excellent overview of a modern layered architecture for canvas applications. While its focus is on design tools, the architectural principles are directly applicable to building a game UI. It will help connect your existing web development knowledge to our new context.

Please read the section titled 'Architectural Overview'. Focus on the breakdown of the five layers. We will adapt a simplified version of this for our JRPG.

Drawing from that article and standard game architecture, we can define three core layers for our JRPG's UI system:

  1. The Rendering Layer: This is the "Visual Layer." It contains our UI components (Window, List, Cursor). Each component will be an object with a draw(context) method responsible for rendering itself to the canvas. This layer only knows what to draw and where, based on the current state.
  2. The State Management Layer: This is our "single source of truth." It holds the state of the UI, such as which menu is currently open, the list of items to display, and the currently selected item index. This is analogous to a Redux store or a Zustand state manager. When the state changes, the rendering layer is notified to redraw the UI.
  3. The Interaction Layer: This layer acts as the bridge. It listens for raw player input (e.g., the 'ArrowDown' key press) and translates it into meaningful actions that modify the state (e.g., dispatching a MOVE_CURSOR_DOWN action). This updates the State Management Layer, which in turn causes the Rendering Layer to update the view.

Diagram showing Interaction Layer sending commands to State Management Layer, which updates the Rendering Layer that draws to the canvas.

This diagram from the 'Enjoyable Game Architecture' article illustrates a similar separation. The 'Visual Layer' is our Rendering Layer, driven by the 'GameLogic Layer', which encompasses our State Management and Interaction logic.

This separation ensures our rendering code is "dumb"—it just draws what the state tells it to. Our interaction logic is clean—it just dispatches actions. And our state is predictable and centralized.

3. Designing for Reusability: Beyond the Basics

Now that we have a layered structure, let's think about the components themselves. A JRPG menu might be made of a Window component, which contains a List of items, which is navigated by a Cursor. To avoid rebuilding these for every new menu (inventory, skills, shop), we must design them to be reusable.

This is where we can learn from decades of game development wisdom on API and component design.

Designing and Evaluating Reusable Components - 2004

Casey Muratori's talk, 'Designing and Evaluating Reusable Components', is a masterclass in the theory behind creating flexible, reusable code. It's dense, but the principles are timeless and directly address the challenge of avoiding 'integration discontinuities'—situations where a small change in requirements leads to a massive amount of rework. This is exactly what we want to avoid with our UI components.

This is a deeper, more theoretical video. We'll focus on the core concepts. Please watch the following segments: Introduction & The Problem (00:00 - 08:00): Understand the challenge of component reuse and the concept of 'integration discontinuities'. The Five Characteristics (14:46 - 16:57): Get introduced to the five metrics for evaluating an API: Granularity, Redundancy, Coupling, Retention, and Flow Control. Key Takeaways (44:27 - 49:54): This is a fantastic summary of actionable design rules. Pay close attention to the points about immediate vs. retained mode, avoiding required data types, and making functions atomic.

Let's apply Muratori's key principles to the design of our UI components:

  • Low Coupling: Our components should know as little about each other as possible.

    • Bad Design: A ShopMenu component that has hardcoded logic for buying and selling. It's not reusable.
    • Good Design: A generic List component that accepts an array of objects and displays a chosen property (e.g., name). It doesn't know if it's displaying items, skills, or magic spells. It just emits an onConfirm event with the selected item's index. The parent scene (ShopScene) then decides what to do with that selection.
  • High Granularity: We should be able to break down large components into smaller, independent parts.

    • Bad Design: A single, monolithic MenuWindow component that always includes a title, a list, and a description box.
    • Good Design: Separate Window, List, and TextBox components. We can then compose them to build our MenuWindow. If we need a menu with no description box, we simply compose a Window and a List. This provides maximum flexibility.
  • Clear State Ownership (Retention): A component shouldn't manage state that it doesn't need to.

    • Retained Mode: Our UIManager or a specific MenuScene holds the state (e.g., selectedItemIndex). It passes this state down to the List component each frame. The List component is stateless; it just renders based on the props it receives. This is very similar to how React works.
    • Immediate Mode: A drawList(items, selectedIndex) function is called every frame. There's no persistent List object at all.
    • For complex UIs like menus, a retained mode approach is generally better. We'll create instances of our components (new Window(), new List()) and have a higher-level manager orchestrate their state and rendering.
Test your understanding!

Imagine you're designing a DialogueBox component. Your game needs simple dialogue boxes (just text) and more complex ones that also show a character's portrait next to the text.

Using the principle of granularity, how would you design this system to be flexible and reusable?

Show answer

Instead of one monolithic DialogueBox component, you would create smaller, composable components:

  1. A Window component: Just draws a 9-slice window border and background.
  2. A TextBox component: Manages drawing text, potentially with a typewriter effect. It fits inside the Window.
  3. A Sprite or Image component: Draws a static image (the character portrait).

To create a simple dialogue, you would compose a Window and a TextBox. To create the version with a portrait, you would compose a Window, a TextBox, and a Sprite, arranging them as needed. This way, you don't duplicate the window-drawing or text-rendering logic.

4. A Concrete Architectural Plan

Let's synthesize these ideas into a practical architecture for our JRPG.

Component Hierarchy

We will use a scene graph or component tree. Every UI element, from a whole menu screen down to a single piece of text, will be a "component."

  1. UIComponent (Base Class/Interface):
    All components will share a common structure. This could be a base class they extend.

    class UIComponent {
        constructor({ x, y, width, height, visible = true }) {
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
            this.visible = visible;
            this.children = [];
        }
    
        // Handles component-specific logic (e.g., cursor movement)
        update(input) {
            if (!this.visible) return;
            this.children.forEach(child => child.update(input));
        }
    
        // Draws the component and its children to the canvas
        draw(context) {
            if (!this.visible) return;
            // Draw self first...
            // Then draw children relative to parent position
            context.save();
            context.translate(this.x, this.y);
            this.children.forEach(child => child.draw(context));
            context.restore();
        }
    }
    
  2. Concrete Components:
    We'll create specific components that extend UIComponent:

    • Window: Knows how to draw a classic JRPG 9-slice window frame.
    • List: Manages an array of data and a selectedIndex. It will contain Text components as children. Handles input for moving a cursor.
    • Cursor: A simple Sprite that positions itself next to the selected item in a List.
    • Text: Renders text with a specific font and color.
  3. Scene Components:
    A whole screen, like the MainMenu, will also be a component. It will act as the root of a UI tree, composing other components.

    class MainMenuScene extends UIComponent {
        constructor() {
            super({ x: 0, y: 0, width: canvas.width, height: canvas.height });
    
            const menuWindow = new Window({ x: 100, y: 50, width: 200, height: 150 });
            const menuList = new List({
                items: ['New Game', 'Load Game', 'Options'],
                x: 10, y: 10 // Relative to menuWindow
            });
            
            menuWindow.children.push(menuList);
            this.children.push(menuWindow);
        }
    }
    

UI Manager

Finally, a global UIManager will manage the active UI scene.

const UIManager = {
    activeScene: null,

    pushScene(scene) {
        this.activeScene = scene;
    },

    update(input) {
        this.activeScene?.update(input);
    },

    draw(context) {
        this.activeScene?.draw(context);
    }
};

// In the main game loop:
function gameLoop() {
    // ... update game world ...
    // ... draw game world ...
    
    UIManager.update(playerInput);
    UIManager.draw(canvasContext);
}

This isolates the UI rendering and logic from the main game world, just as suggested by the Godot scene management pattern you saw.

Conclusion

Today, we've laid the complete architectural blueprint for a robust, reusable, and scalable UI system. You didn't write a single line of rendering code, but you did the most important work: designing the system correctly from the start.

Key Takeaways:

  • Canvas UI Requires Architecture: Unlike the DOM, canvas requires you to manage rendering, state, and input explicitly. A layered architecture is essential.
  • Separate Concerns: Our UI architecture is split into a Rendering Layer (components that draw), a State Layer (the source of truth), and an Interaction Layer (input handling).
  • Design for Reusability: By focusing on low coupling and high granularity, we can create generic components (Window, List) that can be composed to build any menu screen we need.
  • Use a Component Tree: Structuring the UI as a scene graph of nested components provides a powerful and scalable model for layout and rendering, much like the DOM or a modern game engine's scene editor.

Preview of the next lesson:
With this solid architecture in hand, we're ready to start implementing. In our next lesson, we will build the main menu scene. We'll create our first concrete components—the Window and the List—and bring our architectural design to life on the canvas.

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

Sign up