Creating an HTML5 game with Typescript and Redux

I spent a few months creating a short point&click adventure game that runs in the browser with HTML5 and canvas.

Two of the key technologies I decided to use are the Typescript language and the Redux library. I want to share some details of how I used them.

Functional programming

I really wanted to use ideas from functional programming style as much as possible / reasonable. Specifically, I love immutability and composition. These are the reasons:

  1. It is harder to introduce bugs. The hardest bugs I had to debug in my career were almost always caused by mutations and indirectly, data dependencies.
  2. Even when you have bugs, it is easier to debug.
  3. Functional style favors composition over inheritance, which makes dependencies explicit.
  4. It seemed like a fun challenge to implement a game engine with this style.

Programming in a functional style with dynamic types is certainly possible, as demonstrated by Erlang over the years. However, I feel more comfortable when a compiler can spot my stupid mistakes, rather than the players, so I went for Typescript with the compiler in a very strict mode.

Proponents of dynamic typing usually claim that such stupid mistakes can be detected with proper unit tests. I agree, and I like to think about strong static types as a really compact way of writing some of those tests 😉

As I mention, I wanted to use functional programming as much as possible but also only as much as reasonable. When a loop is the best tool to solve a particular problem, I just use a loop. Also, I keep using classes and interfaces. I just avoid inheritance.

Typescript

If you don’t know Typescript, here is a simple example written first in Javascript and then in Typescript to spot the differences:

Javascript:

function sum(a, b) {
return a + b;
}

Typescript:

function sum(a: number, b: number) {
return a + b;
}

As you can see, you can be explicit about the contract of the sum function with Typescript, and if you try to call it with a string or something else, the code will fail at compile time, before you even deploy it to your servers.

Also, note that the language can do type inference. We are not declaring the return type of sum, since the language is smart enough to figure out that it returns a number.

This is only scratching the surface, the language has much more, like generics, visibility of class members, default parameter values, and so on.

Redux

Since I love functional programming, immutability and composition, Redux was a perfect match for state management. In this case, I won’t go over the list of reasons I chose this library, because to be honest, I wanted the challenge of writing a game with it.

However, after having completed the game, I don’t regret anything. I didn’t have that many bugs related to state management, and the ones I had were really easy to debug and fix. Plus, I got a few things for free:

  1. Saving and restoring the game is trivial, I already have all the state in a JSON tree.
  2. I implemented a time machine debug tool in a matter of minutes. I could drag a slider and move back and forward in time to debug animations, rendering glitches, etc.

It is not easy to explain redux in a single paragraph for those of you not familiar with it, but I will try. The basic idea is that you have a huge JSON tree with the state of your application, and write functions that are responsible for updating branches of that tree as a response to events called actions. However, the tree is never mutated. Instead, whenever an action is dispatched, a new tree is produced (often reusing a bunch of branches that don’t care about that action and weren’t modified).

Redux is often used in web applications in which you have a DOM with callbacks, and those callbacks dispatch actions that are essentially a reinterpretation of some event. For example, when a button is clicked, a click event is triggered, but your callback dispatches an action called delete-user, so that redux doesn’t need to know about the DOM to interpret the events.

I use Redux in an unconventional way, because I wasn’t prepared to depart completely from the traditional scenegraph model for games.

I still use classes. Specifically, I still have a Sprite class, a Background class, and so on. However, those classes don’t contain their dynamic state. They only contain static configuration.

For example, the Sprite class contains the sprite sheet with all the frames in it, and information about movement between frames, intrinsic playback speed, etc. Those are things that should never change during gameplay.

In a traditional procedural engine, that Sprite class would also include the current frame being played, and timing information to decide when to switch to the next frame.

However, since I use redux, that information is already somewhere in the redux tree. Therefore, key methods of my Sprite class get that branch of the state tree as a parameter.

Another way in which my usage of Redux departs from the conventional way is the reducers. Reducers are those functions I mentioned earlier that update a branch of the state. They have the following signature:

function myReducer(previousState, currentAction) {
// Compute nextState
return nextState;
}

I have all the reducers inside the classes. So, for example, the Sprite class has a reduce method that has the same signature as above.

In practice, my Sprite class looks pretty much like any other Sprite class you might have seen before, except that the state that can change is outsourced to Redux.

Now, the last question is how the Redux tree maps to these classes? They map 1:1 in terms of structure. The redux tree has the same shape as my scenegraph. For example, if one of the rooms in the game has the following structure:

[
playerWalkingSprite (instance of Sprite),
background (instance of Background),
foreground (instance of Background)
]

Then the Redux state might look something like this:

[
{
currentFrame: 1,
currentFrameStartedAt: 123456,
},
{}, // Background doesn't have dynamic state
{} // Background doesn't have dynamic state
]

Whenever the reducer of the room class runs, it will delegate on the reducers of each of its contents, passing the right state to it.

Likewise, whenever the render(canvas, roomState) method of the room class runs, it will also delegate on the render methods of each of its contents, and pass the right state to them.

Outsourcing the state like this is what made possible the time machine debug tool. You can make game render any state you want, just by passing it to the render method. Therefore, by keeping around the last N states, I can replay any of them.

User input

Similarly to the conventional usage of Redux, in a game you also have to handle events coming from the user and send actions to Redux.

However, in a canvas game there is a twist. There is just one HTML Element producing all the events, and you need a way to figure out what a click on the coordinate X, Y of the canvas really means for your game before talking to redux. You need that raw event converted into a domain specific one before proceeding.

Again, in this case I use the classes I mentioned before. I ask them about that click and let them translate that raw event into a domain specific one. For example, if there is a sprite under the mouse, the click event could be translated into a sprite-clicked event by the Sprite class under the mouse. Then, I have the game logic (code specific to one game) listen to these domain specific events and send actions to Redux.

Again, this is non conventional, and also not really pretty. In Redux, actions should represent what happened, not what to do about it. However, using the conventional approach made the game logic much harder to write, and be more tightly coupled with the engine, and that’s something I really wanted to avoid.

In future games I might explore other solutions, because I wasn’t super happy about that problem mentioned in the previous paragraph. But I had a game to launch.

I could also talk about asynchronous chains of actions (e.g. protagonist walks to the mouse coordinate, and when it reaches that, open the actions menu), but this is another situation in which I’m not super happy with the solution I designed. I designed an alternative to redux-saga, and even though I managed to keep the game logic simple to read and write, it made the engine code a bit more convoluted than I’d like, and it complicated savegames, because I’m unable to reliably save in the middle of a chain of actions. Once I figure out a cleaner way to do this, I will write about it.

Declarative programming

I wanted to use a declarative style as much as possible, and beyond the use of functional ideas. I wanted the game logic to be really simple and declare the intent rather than the code to achieve such intent.

Again, the reasons:

  1. This decision aligns very well with a core principle I had to make the engine as generic as needed but not more generic.
  2. The logic of an adventure game grows really quickly. Even with a declarative style, the specific logic of my game has about 2000 lines of code. With a more generic engine, that could easily explode much larger because it would need to be much more explicit about how to do things using engine primitives.
  3. Unit testing the engine is generally much easier than unit testing the game logic. The former can be tested with simple nodejs-based tests while the latter typically requires a browser and a webdriver. When the logic maps 1:1 to the intent, there isn’t much to test.

To give a specific example, here is a snippet of a conversation:

entranceConversation: conversation({
'entry': [
{
sentence: `I want in.`,
reply: `And why should I let you in?`,
next_tag: 'want-in',
},
{
sentence: `I'm here to repair the cable.`,
reply: `We don't have TV here.`,
next_tag: 'cable',
},
],
'want-in': [
{
sentence: `Because I am the police!`,
reply: `I don't like you, go away!`,
next_tag: 'police',
},
...
],
...
}),

Notice how there is no imperative code anywhere. It is just configuration declaring how the conversation flows.

I recently had the need to add sentences that can only be chosen once. I could make the game logic handle that, but instead, I extended the configuration language to support specifying just that. Again, solved it in a declarative way, to keep the game logic simple and put the burden on the engine.

This is probably not the right choice for general-purpose engines such as Unity, but in my case it makes total sense, and makes my life easier to finish the game.

Automated testing

There was a recent blog post by Ron Gilbert claiming that he doesn't unit test games, with very good reasons. It was unclear to me though whether Ron referred just to unit tests or automated tests in general.

Still, I did some automated testing on this game and it has proven super valuable:

I do unit testing for critical parts of the engine. Precisely because I was changing it so much, it was useful to make sure that my tweaks didn't break anything. For example, I unit test the conversation engine, and those tests caught real bugs when I extended the engine to support new use cases like the sentences that can only be said once.

I don't unit test the game-specific logic. As I mentioned, I exposed a declarative API from the engine so that the logic expresses the intent, not the control flow to achieve it.

I do have a webdriver test that plays the whole game from start to end. I wrote it in a way that was hard to break unless there is a real bug. For example, I don't use raw pixel coordinates, and instead use the names of the objects, and indices in some cases. This is a snippet from the test:

clickOn('door');
clickAction('knock');
clickConversation(1);
clickConversation(0);
clickOn('door');

I have a debug mode that exposes a few methods on window that can be called from the webdriver test to figure out where door is, or where is the first conversation sentence, to do the translation from the symbol to mouse coordinates to click the canvas.

That test has limited value for the game-specific logic, because the game is not linear, so it is only testing one of the possible ways of finishing the game. But the whole point is not to test the logic. This is also testing the engine, exercising all the engine objects in a more realistic setup, running in a browser, clicking on a canvas. And I made sure that it exercises all the important engine features.

I run it before pushing new beta releases, and it has caught already a few engine bugs, stopping me from publicly breaking the beta.

So in the end, I don’t have any sort of automated testing for the game-specific logic, but:

  1. I mostly don’t need it because the logic is written in a way that expresses the intent, not code to achieve it.
  2. Only a human can properly test the logic, because the bugs that can happen there are most likely an issue with the design of the game (story, puzzles, etc), and not the code. That’s what playtesting and betas are for.

That said, thanks to redux, it is possible to compute a state graph and use graph analysis to find dead ends in the puzzles. That still requires smart filtering of the state tree, because otherwise, for example, each position of the player would be a new state, and the graph would be unmanageable.

About quantum derail

The game is available since Feburary 15 2018 on itch.io.

Anuncios

Acerca de Rubén L.

Software Engineer
Esta entrada fue publicada en engineering, English y etiquetada , , , , , , , , . Guarda el enlace permanente.

Una respuesta a Creating an HTML5 game with Typescript and Redux

  1. Pingback: Creating a 2D point&click adventure with Blender | Rubén López

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s