Building Games With React/Redux
Using the React Game Engine to Quickly Build Interactive Games
Recently, I discovered the React Game Engine. It is based upon a game engine that was written for React Native, but is a little simpler. This was really exciting for me, as I've been wanting to build games for a long time, but I've often found the bar of entry for writing games to be rather high. When I read how easy this engine was to use, I immediately decided to build a game that I had built previously in college, namely Anti-Chess.
So what is Anti-Chess? Well, it is just like regular chess (all the pieces have the same general moves) but instead of attempting to checkmate the opponent’s king, the goal is instead have your own king checkmated. There is one major twist in the rules that makes this possible — if you can take a piece, you must. With this rule, you can put your pieces in strategic positions in order to draw your opponent’s attackers across the board and hopefully end up in a place where your king cannot escape (because you still have to move your king out of check if possible).
I originally wrote the game with a team of four people at MIT. We split up the tasks — one of us was in charge of building the model for the pieces and determining legal moves, one of us was in charge of writing the logic to display the board, one was in charge of writing the AI for deciding the best moves, and the last was there to unleash havoc upon whatever we wrote (that is, find bugs).
I ended up being the guy writing the UI, and it was incredible fun when the board started appearing and I could move pieces around. It was all written in C++ code, so it took awhile to get to the point where we could see anything, but once it was working, I spent countless hours playing Anti-Chess. Our application could link up with other teams’ servers so that our AI could play theirs, and there was a contest to see who’s AI was the best. Some teams had sophisticated AIs, and others just picked random moves. It was a really fun competition (I don’t really remember who won).
Starting From the Top
Writing the same game in React has actually been incredibly easy, and the subject of this article is to walk through the game and show how easy it is to build a game like this using the React Game Engine.
First, you have to start with a good foundation. I generally start by just running create-react-app which gives me a lot of the basic boilerplate content. At some point, I’ll probably augment this script to set up the other things I like to start with, but I’m still kind of tweaking my setup. To the basic react app template I add webpack, react testing library, and eslint. This gives me a solid foundation upon which to build any React app.
At the top level of the app is a single index.js file that links to a div element in the index.html to the app. Most React apps are built this way. Inside, I just tell React to render the main App component:
I personally like to keep components small and tidy, and this is the only place I will call the render method of ReactDOM. Everything else is a child of this main App component.
The source for the App component is also fairly simple:
As mentioned in the title, I use Redux for state, and we’ll see later on how that makes writing game logic much simpler, especially when integrating with a game engine. The Provider component is a wrapper that allows functional components to perform Redux actions using hooks. Unfortunately, not all of the components can take advantage of this because they are rendered as children of the game engine (which doesn’t use functional components), but for the ones that can use hooks, the provider allows these components to use useSelector and useDispatch. All the components of the game, including the game engine, are children of the AntiChess component.
Now we come to the part where we integrate with the react game engine. With only 30 lines, I build the game of AntiChess and hook it into the engine:
I’m teasing a little, as obviously it takes more than 30 lines to write the game. However, this is all the integration that is needed between my logic and the game engine. The game engine is the main component that will draw my game elements on screen, and it uses objects called systems for updating the elements.
Near the top, I import two main state objects in my game. The first is called squares, and is basically an array with every square on the board. I do some array arithmetic in order to find the appropriate square given a square name (A1, C3, G6, etc). The other is simply called game, and contains general high level state logic like whether a piece has been selected and whether it is white’s move or black’s move.
The GameEngine component takes a style attribute that describes the overall appearance of the main board. It is absolutely positioned on the page and all elements within it will be absolutely positioned as well. This is pretty familiar for anyone who has written games where elements are painted onto a canvas using coordinates.
Currently I only have one system for managing the game entities, which I call the MovesSystem. We’ll get to that in a moment. As for the entities, I build a default set of entities given the game state and the squares.
The game engine works by calling all the systems every few milliseconds (in game speak, there is a clock that is ticking and each tick is a chance to update all of the elements in the game as they are moving around). In a game like Anti-Chess, there isn’t a whole lot of moving action, but if you had characters moving around, you would just update the game each time the system gets called. For my game, I listen for onMouseDown events:
Starting at the top, the first thing I do is import the same state objects, game and squares. I have a button that allows me to reset the game, so there is a flag that is turned on when the game has been reset. If this is the case, I start with brand new entities (wipe the board clean and rebuild). Otherwise, I take the entities that were there before.
Notice that I am returning a new object from this system, rather than just reusing the old one. This is good practice in stateful applications like games so that you ensure that nothing gets into the new state unless you carried it over from the old state or explicitly added it.
The offset, xMax, and yMax represent the bounds of my board — each square is 100 pixels wide by 100 pixels tall. If I wanted to put the board in the middle of the screen, I could set offset to something other than 0, but for now, I’m leaving it in the top left corner.
The input.find call searches through events that were received for this tick and if an onMouseDown event is found and is within the bounds of the board, then we have an action we need to respond to. First, I figure out which square was clicked, and then I dispatch a Redux action for the click.
The last step is to return the new entities. I haven’t really described yet what these entities are, but think of them as the elements on the board that require rendering — in this case, each square on the board. I created all the entities when I started the game, and they will remain active (getting returned from the moves system) each tick until the game is reset. To clarify that further, let’s look at how those entities are constructed.
The next file is just a utility file that builds the entities and returns them. As mentioned previously, we will only need to do this when a game starts (or restarts).
In this game, the entities are the squares and a game stats display. In most games, they would be characters moving around. Those characters can have stats and different states, but to the game engine, they are mostly just things that exist and need to be drawn into the world. The same is true with my squares, and for that, I have a component called Square that represents a square in the game.
Each Square gets a name, and for convenience, I give them names according to the usual Chess standard of the columns having letters and the rows having numbers (A through H in the columns, 1 through 8 in the rows, the letters go left to right, the numbers go bottom to top). I also give each Square an index that maps to the corresponding state object that holds what should be in that square.
The other entity is the stats display which simply displays a list of moves and which player is expected to move. As you can see, entities is an object with each property being an entity in our game. Each entity has some properties that will be passed to the component to be rendered, which is specified with the renderer property. I’ll cover what the components look like in a little bit.
All of the action in the game is powered by Redux. As you saw in the MovesSystem, the main action is clicking on a square. It is up to the Redux actions to figure out what should happen when I click on that square.
This action gets the game state, the squares state, and the row and col of the square that was clicked (I determined this square in the MovesSystem). The game state has variable called pieceSelected which indicates that a previous piece was selected, meaning that the click could be to place that piece in another spot. To move a piece, the destination must be an empty square or an enemy piece. If neither of these is the case, it is an invalid move.
If it appears we are trying to move, we must validate that the move is legal, so we dispatch an action to check the move and move the piece if it is a legal move. Once the piece has been moved, we dispatch an action to reset the highlights (square backgrounds that indicate what piece was selected and what legal moves could have been made).
If a piece wasn’t previously selected, then we are likely picking a piece to move. Therefore, we aren’t interested in empty squares. Since we might have picked a different piece before (I may click multiple pieces, trying to decide which one I want to move), I first clear out any highlights that were there previously and then highlight the legal moves for the square that was clicked (assuming it is a piece of mine — if it isn’t, no squares will be highlighted).
Finally, if neither of these is the case, I just dispatch to clear the highlights, leaving the board without highlights. This is all the action there is in chess, making it sound like a really easy game to build. It gets complicated, however, when you consider all the checks that have to be made to determine if a move is legal. Let’s take a look at the method that checks if the move is legal.
This game logic isn’t optimized yet, but it gets the job done. The action gets the game and squares state and the row and column of the destination square. I get the row and column of the square where the piece to move is by fetching it from the game state. Then I get the legal moves for that piece and if the selected square is in the list of legal moves, then an action to move the piece is dispatched. I also dispatch an action to add the move to the moves list that is in the stats display. Finally, I switch the players (white to black or black to white). Finally, whether the move was legal or not, I clear the selection of the piece (essentially any click that isn’t a legal move is dismissing the selection of the piece).
As you may have noticed, I need to optimize this logic so that it doesn’t have to find all the legal moves to verify this particular move is legal, but when I was writing this, I started by making the logic that generated all the legal moves for each piece. Clearly I can cut out some of that logic if I know what target square I’m checking for, so that optimization will be coming soon.
Most of the actions powered by Redux are this simple — fire the action with data from the state, and process that data to determine what updates to the state need to be made. Let’s take a look at one of the reducers — the method that updates the state itself.
I like to use the Redux Thunk style of updating state. This means that when I fire an action to be reduced into state, it has a type and it has some data the reducer uses to resolve what the updated state is. For example, if I move a piece, the action body will look like this:
The MOVE_PIECE part is a constant that I define elsewhere in a file named actionNames.js. The content of this constant is arbitrary, but I generally make it a string that is the same as the constant name. The data that comes along is two objects, both with row and col properties that represent the squares involved. This is passed to the squaresReducer:
As you can see, the reducer uses a switch to quickly jump to the logic that should be performed based on the type of action. The reducer takes two arguments, the state from the previous iteration (which happens to be the array of squares representing our board) and the action that was dispatched. All actions are passed through every reducer, so there is no restrictions on the actions that reducers can listen to. This is handy when you have actions that affect many components, such as RESET_GAME. The movePiece method is fairly simple:
This method gets the array of squares and the from and to locations which represent where the piece was and where it is moving to. When updating state in a reducer, it is always a good idea to return a new object because Redux tries to optimize by checking if the object returned by a reducer is the same object (checking whether the pointer is the same, really — it doesn’t do a deep check to see if anything inside the object has changed). If you return the same object, Redux will believe that nothing has changed and will not inform objects that expect to be notified when the state updates.
We create a new array filled with the same squares from before. Then we update the two squares involved in the move, changing the from location to an empty square and changing the to location to the type of piece we are moving. Then we return the array of squares which will be returned to Redux, indicating that the state has been updated.
Rendering the Board
I have talked a lot about the logic under the hood, but how does this all end up getting displayed on the screen? Earlier I talked about the GameEngine, which is the parent of all components that will be rendered. When the systems are called with the entities, the systems update the entities objects as necessary (in a lot of games, a character being killed would be removed from the entity list or perhaps reincarnated and added back to the list, but in my game, the entities stay the same — it is the redux state that changes).
Once all the entities have been updated, the engine renders all the components by iterating through the list of entities and the render method of each is called, just like a normal React app rendering buttons on a page. Let’s take a look at how a Square is rendered in my game.
This file is a little long, but the part where I return the HTML to be displayed is pretty simple:
The div that represents the component has a style that details the look of the square — the size (100px by 100px), the background colors, etc. Squares can have different background colors. The default is white or black, depending on its location (getSquareColor()), but it may be yellow if it is the square containing the piece selected to move (square.highlightedPieceToMove) or it may be green if it is highlighted as a legal move for a selected piece (square.highlightedSquareToMoveTo).
If the square is not empty, then the outer div contains an inner div with an image of the piece to be displayed. Which piece gets displayed in the square comes from Redux by simply getting the array of squares and using the index related this square to find what the current state has in the given index.
Some games obviously would have more complicated rendering logic, and I wish I could say that the react game engine could do all that for you too, but really it is just the skeleton upon which all the components are bound. You still are responsible for making it look good.
In this article, I talked about how I used the react game engine to build an Anti-Chess game. The game starts out as a simple react app that can be generated just like any other react application. I talked about the game engine itself, using a collection of entities and systems that update those entities to result in a view rendered for the user. I covered using Redux to update the state of the game, which then is used by the entities when they render themselves. It is a simple example, but there is really no limit to the types of games that could be built with this engine. As I do React app development for my day job, it’s really great that I can use the same technologies to build some fun things too.