UI Implementation


UI has always been an interesting aspect of game development to me, probably because I spend so much of my professional time building user interfaces as a software developer. I've found that it's surprisingly difficult to move between games and applications when it comes to UI because you're building fundamentally different things with different requirements, priorities and technical constraints.

I've been building my user interfaces with React (and other similar declarative component frameworks) for years now, and that's something I always miss when I have to switch back into imperative UI models. However React & friends don't play very well with games that run and render in realtime.

To have a component re-render in React, you need to convince it that something has changed. For traditional applications it's easy to wrap stateful transactions up in React's own state model and have controls and events that update that state. Click a button, read the current state, produce a new state, React sees the new state and re-renders.

Games are inherently mutable and you expect things to change on a frame-by-frame basis. It's not so easy to just put your state inside a component and make sure all updates go through a `setState` call.

So we've effectively got two models.

  1. The game's simulation is running and rendering at 60FPS drawing the entire viewport to screen every frame
  2. React which rebuilds the changed portions of HTML every time something asks a component to re-render

If we want to use React for the UI then the simplest way to keep the two in sync is to force React to re-render every frame. Unfortunately this is also pretty inefficient and it's going to add a lot of extra CPU work to each frame.

Another option would be to have the UI update once per turn. Although Midas looks and plays like a turn based game, there's a lot going on in between those turns that might require UI updates. For example Midas doesn't actually land on a tile and acquire gold for transmuting it until the movement animation has finished. That means the UI would end up lagging behind.

Another popular option is to completely decouple the UI from the game with events. The game emits events that the UI listens out for and uses to update itself. This model actually works quite well with React, especially if you put a Redux style reducer at the top of your component tree. You can handle the events as actions and have a pure function which calculates the new state of the UI in response. This kind of decoupling is powerful, but it also takes a lot of extra work. Updates to the UI usually end up being a multi-step process where you have to think about how data moves through the different layers of responsibilities. That's not the kind of stuff I want to think about with this game yet, so I came up with a simpler solution.

It revolves around a custom `useSync` hook. Here's an example of a component that renders the player's current coin count.

function HUDCoinCounter() {
  useSync(() => [game.player.coins]);
  return (
    <div>
      <img src="/sprites/coins.png" />
      {game.player.coins}
    </div>
  );
}

The useSync is a sort of hybrid of useEffect and useState. Don't worry if this means nothing at all, these are 'special' Preact functions that hook into the internal component model to do things like triggering updates and creating component state.

The useEffect hook takes an array of dependencies values and says "if any of these dependencies have changed since last time the hook was called, then run this effect" again. The effect is just a callback that you can use to do other fancy things like network requests or state updates.

The useSync hooks adds a callback to list of update callbacks that are called once per frame. Then we compare the dependency values that it returned (in this case just `game.player.coins`) to the previous versions. If the dependencies have changed, then we force the component to update.

No state involved, we don't have to mess around with events either, just a simple kind of brute force based reactivity. This allows me to keep the game state completely separate from React, and write UI components that are properly synced to the game state.

It's not a perfect solution yet though. The game state is fully mutable which means that you can't compare objects in the dependency array. For example, if a component adds `game.player` to their `useSync` hook, then it will only update if the player property is switched to point at another object, and not if a property inside the player object changes.

It doesn't make a huge difference to the technical content of this post, but I'm actually using Preact rather than React for this game. The API is very close to identical and it keeps the overall size of the game smaller.

I'll post another devlog that breaks down the actual design of the UI soon!

Leave a comment

Log in with itch.io to leave a comment.