Phaser game with a React UI

I find it interesting to consider the idea of using React as a UI and Phaser as a headless, UI-less game engine. In this blog post, I’ll explore this concept in more detail.

The Concept

The idea behind this approach is to use React to handle the UI elements of a game, while using Phaser as a headless game engine to handle the gameplay mechanics. This approach can help to alleviate the burden of handling the UI in Canvas. RequestAnimationFrame can be expensive and should be used for the game only. The canvas game shouldn’t be wasting valuable updates for the UI.

The Game

The game we’ll be developing is using Next.js (a React framework) and Phaser (JavaScript game engine). Next.js compiles a static HTML version of the game that can be used on iOS/Android webview for mobile, NW.js/Electron for desktop (similar to what Discord does).

The game is a Next.js application that builds the Phaser game and starts up normally. The Next.js application is handling the UI elements like the merchant dialog, player bag, player armory, and other dialogs like the death dialog.

This is a similar approach to older console games that used Scaleform - which under the hood was using Flash and overlaying the UI created by a Flash developer. From there, Scaleform would sit on top of the c++ game engine.

Writing the Bridge between Canvas and React

To create the bridge between Phaser and React, we’ll use an event listener:

  • addEventListener on the document object to send objects back and forth.
  • The implementation of addEventListener is contained within a hook that implements an Effect Hook: useEffect so that state can be passed in.

The performance issue that was solved here was to convert the class components (Bag, Armory, DeathDialog, MerchantDialog) to functional components and then use hooks to manage the add/remove listeners.

Types of Hooks

  • State Hooks: useState - called inside a function component to add some local state (e.g., a counter).
  • Effect Hooks: useEffect - perform side effects from a function component (e.g., listening to an event fire from the DOM).
  • Context Hooks: useContext - subscribe to React context without introducing nesting (e.g., dark & light themes).
  • Reducer Hooks: useReducer - lets you manage the local state of complex components with a reducer (e.g., dealing with complex state logic).

Rules of Using Hooks

Take the time to learn hooks and using functional components.

  • Call Hooks at the top level only: Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions.
  • Functional Components only: Only call Hooks from React function components. Don’t call Hooks from regular JavaScript functions (see Custom Hooks).

If you are using class components, don’t worry - porting class components to functional components is trivial. Simply follow the pattern of functional components when porting classes.

Pay close attention to useState when rewriting your new functional components. Once you spend time writing functional components, you’ll enjoy using them, and paired with hooks, they make the component design simple to implement and iterate on the design of the interface.

class to functional component

An example of the class component. It’s a Cat that holds state for agility, balance, and sight:

// Class component
import React, { Component } from 'react';

class Cat extends Component {
  state = {
    agility: 10,
    balance: 8,
    sight: 12,
  }

  render() {
    return (
      <div>
        <h1>Cat Stats</h1>
        <p>Agility: {this.state.agility}</p>
        <p>Balance: {this.state.balance}</p>
        <p>Sight: {this.state.sight}</p>
      </div>
    );
  }
}

export default Cat;

Now, let’s see how we can convert this class component into a functional component.

// Functional component
import React, { useState } from 'react';

const Cat = ({ agility, balance, sight }) => {
  return (
    <div>
      <h1>Cat Stats</h1>
      <p>Agility: {agility}</p>
      <p>Balance: {balance}</p>
      <p>Sight: {sight}</p>
    </div>
  );
}

export default Cat;

In the above example, we have a Cat component that is originally written as a class component. It has a state object that holds the cat’s abilities: agility, balance, and sight. The render method displays the cat’s stats using JSX.

To convert this component to a functional component, we can replace the class declaration with a function declaration. We also need to remove the state object and replace it with parameters passed into the function. In this example, the parameters are agility, balance, and sight.

In the functional component version of the Cat component, we use destructuring to extract the parameters from the props object. We then use those parameters to display the cat’s stats using JSX.

📄 Note that the functional component does not have a this object or a render method. Instead, we simply return JSX directly from the function.

Have a solid understanding of Hooks

Taking the time to understand hooks and why we are using them is the first important building block to building the bridge between the game engine and UI. Pick the areas that you need side effects - the Effect Hook, which allows for data fetching, DOM updates, and for our example for the bridge: we use addEventListener within useEffect. In the callback for useEffect, pass in removeEventListener to clean up that instance of the event listener.

Resources and frameworks

Here’s all the resources and frameworks used in the video: