How I Created a Simple Game with Expo, React Native & Redux

Ali Alaa

Mobile Engineer
React Native Developer
Expo
React
React Native
Recently I have been working on a couple of react native projects. For the first project I worked on, I used a plain react native set-up without Expo. Having to deal with Xcode and Android Studio was a bit of a hassle, however it wasn't a bad experience overall. On the other hand, using Expo made things even easier. Expo lets you completely abandon Xcode and Android studio. There are however some limitation to what you can do with expo.
Having discovered how easy it was to start and develop a project using expo, I decided to create a little game. React native is of course not made for developing games, however, I thought of a simple idea that requires no physics, 3D graphics or any fancy stuff, just simple user interactions.
The idea of the game is to simply kill all of the randomly appearing animals on the screen by tapping on them. The pandas are the only animals you shouldn't kill. Killing a panda means game over. You also can’t miss the target more than 4 times.
In this article, I will shed some light on how I developed the game without getting into too much of technical details. I uploaded the game on both Android’s play store and Apple’s app store, you can try them out here:
Or just watch a video demo:

Collecting & Loading Assets

Graphics

Since I am not much of an artist myself, the first thing I needed to create the game was to find some good animal sprites. After spending a substantial amount of time scouring the internet, I found the perfect set of sprites. They were however offered only in a 20x20 pixels resolution, but I then discovered that you can resize an image using photoshop with preserving hard edges which was perfect in my situation.
I also found this irresistibly cute panda character so I used it for the logo and the app icon.

Fonts

After a bit of searching I found a font called Pequena by Rodrigo Araya Salas that seemed perfect to me. I used it for the logo and the game itself.
Using a font in the app is made ridiculously easy by Expo, all you have to do is just import the Font object from expo and add a line of code:
Font.loadAsync('open-sans', 'http://url/to/open-sans.ttf');
then use it in a component like so:
<Text style={{fontFamily: ‘open-sans’}}>Hello world</Text>

Music & Sounds

All audio assets used were found on audiojungle marketplace. After listening to multiple music tracks I instantly fell in love with this 8 Bit track by LynxStudio. The angry characters pack is used when animals get shot. Other sounds include buzz, pop, laser gun and shot gun sounds.
I used Expo’s audio api to play sounds. It wasn't a particularly pleasant experience. I found some troubles when playing multiple successive sounds where after a while the sounds start to get really laggy and sometime the app crashes. I think the problem was that each time you create an audio object and play a sound it takes a place in memory and by the time all memory resources are consumed.
I reached Expo through their forum, but they were not that helpful. I finally searched in an example expo provided how the sound object is unloaded and it was as simple as setting the object to null after it’s played.

Loading Assets

Expo does not bundle your assets inside of the standalone build. Instead it uploads your assets to Amazon CloudFront. This has the benefit of allowing you to update any asset over the air, without re-submitting to app stores. This approach can also lead to an undesirable behaviour where assets are loaded after the app is started. Users might see the default font initially until our desired font is loaded or experience a delay before images appear.
For this reason, Expo provides a way to preload and cach all your desired assets. You can use the AppLoading component to show a loading screen until all assets are downloaded. You can also configure how the loading screen looks like using a file called app.json.

JavaScript Dependencies

In addition to Expo, I used a few other libraries in development.
Navigation: I used React Navigation for routing in this project. I used to use React Native Router Flux before but react’s official website recommends React Navigation so I tried it out and found out that it’s really much more superior; easier to use with an excellent documentation. Routing in this game was very simple anyways, I didn't need more than the StackNavigator.
State Management: I used Redux for state management, I have used it before in multiple projects and had no problems with it. Some alternatives however are mobX and Alt. In addition to that, Redux Persist was used to save some parts of the state to the device’s local storage like the settings. Aslo Redux Thunk for asynchronous actions.

Defining Constants and Utility Functions

To keep things clean and easy to change for future adjustments, it’s always a good idea to export all constant data like font names, api urls, api keys, etc.. in one file. In my case, I needed to store things like array of animals, sounds, points per kill and so on. Here is a sample of my file:
In this file, you will find data like the height of the bar that contains the score, timer, etc.., the height of the game area, scores per kill or escape, list of animals and sounds and so on.
Another example of things to define here is helper functions. For instance in the code below, getRandomAnimal() returns a random animal from the array of animals defined above with an option to add a number of extra pandas to the array to increase their chance of appearing, this will be used later in the game. generateRandomPosition() returns a random left and top positions relative to the game screen, this will be used when generating new animals and adding them on the screen. randomFromArray() simply returns a random item from an array, this will be used to randomise how often animals appear and how long they stay on the screen.

State, Actions and Reducers

The game consists of three main reducers controlling three different state pieces; the settings state, statistics state and game state. These pieces are combined together into one main reducer using combineReducers that can be passed to createStore. If you are not familiar with these terms you can refer to redux’s documentation.

The Settings State

The simplest reducer of the three, it only contains an object of sounds, music and gun settings and persisted using redux persist’s whitelist feature so that the values are not gone when the app is closed.
The only action creator for this reducer takes a settings object as an argument and passes it to the reducer where the settings state piece gets updated:
calling updateSettings({ music: false }); would only update the music key in the state using a reducer like this:

The Game State

Not quite as simple as the settings reducer. This piece of state holds all the information about the current game. Amongst the roles of this state are keeping track of the score, the time, animals appearing and disappearing and much more. The game state structure looks like this:
animals is an object that holds the animals currently appearing, it’s meant to be an object and not an array so it can be easily manipulated using the key. The key is generated using the current time stamp added to a random number just in case multiple animals appeared at the same time.
The action creator for generating an animal will utilize functions that we defined above. The action will receive a lifetime and extra pandas parameter so that we can randomize these values when generating animals as we will discuss later.
The action generated from this function will pass through the game reducer as so:
And in a similar fashion, removing an animal:
The next step now is to increment the timer in the state, and while doing so, we can generate animals using the actions that we just created. Also we can use the current time in the state to maybe speed up how often animals appear as time goes by.
Incrementing the timer in the reducer is quite simple, other simple actions like toggling the playing flag and resetting the state are demonstrated here as we will use them later:
To increment the timer every second, we will use javascript’s setInterval(). Note that when using setInterval() you might forget to clear it once you don’t need it any more and that will lead to disastrous results.
As seen above, clearGame function clears the timer, dipatches GAME_END which will set the playing flag to false and also dispatches CLEAR_GAME which will reset the state to its original values.
startGame will clear any previous games, dispatch GAME_START and set an interval that calls incrementTimer() every second.
incrementTimer() will dispatch INCREMENT_TIMER in addition to generating animals at different pace depending on the current time as so:
There are a bunch of other trivial actions here that for example increments the score, trigger a gameover modal and so on.
In addition to that, the stats object will contain information about appeared, killed, escaped and saved animals. These statistics are shown at the end of every game and also used to aggregate statistics using the stats reducer which we will discuss next.

The Statistics State

There were two approaches on my mind to calculate statistics. The first approach was to store these big game objects that we get from the game reducer in an array and then iterate it to calculate the statistics. The second one was to just have a single statistics object with all the values and use the game object just to update the statistics object without storing the game object itself. I chose the latter for various reasons.
Although storing games would give us more information about each game, most of these information we don’t care about anyway. Since as mentioned before we use redux-persist to store the statistics state in the device’s memory, it would be more rational to store just the aggregated statistics rather than storing each game. That way we can save memory in addition to not having to process a large array of games when showing the values to the user.

Rendering The UI

There are a bunch of components in this game mainly used to render each screen, however, we are only concerned about two components; the game screen and the animal component.
In the game screen component, there are two main parts. The upper bar that shows the score, time, etc… and this one is easy to implement by connecting the component to the game state using redux’s connect function. The part that we are concerned about, is the game itself:
Clicking on the game surface means a miss. The function miss() will handle decrementing the score, ending the game if the maximum misses is reached, animating the background to red using react native’s Animated module and finally play a buzz sound.
Using javascrip’s map and Object.keys, we iterate the animals object that is passed down from the redux state and rendering an Animal component. The animal component is positioned using the random position that we generated in the redux action. It will additionally receive the key, the animal object itself and also a callback function onShoot() that will be triggered when tapping on an Animal.
The Animal component on the other hand might seem quite simple judging by its render method:
However, a lot of work has to be done in the handleClick() function and also in the component’s lifecycle methods.

handleClick() will handle these tasks in order:

Check if the playing flag is falsy, if yes do nothing!
Define a Class variable called killed and set it to true, this will decide if the animal is killed or escaped in componentWillUnmount().
Call a redux action animalKilled(species), this is used for statistics.
If the animal is a panda, end the game and return.
Call the onShoot() callback that is passed in the animal’s props. This will remove the animal from the state.
Increment the score using redux actions.
Check if the sounds are enabled using the settings piece of state and play the gun and groan sound if it’s true.

componentDidMount() will do the following:

Check if the sounds are enabled using the settings piece of state and play a pop sound if it’s true.
Call a redux action animalAppeared(species), this is used for statistics.
Set a time out that will automatically call removeAnimal(id) after the animals lifetime. (Note that the id and the lifetime are passed to the animal through the props)

componentWillUnmount() will:

Check if the playing flag is falsy, if yes do nothing!
If the unmounting animal is not a panda and is not killed, decrement the score and call animalEscaped() for statistics.
If it’s a panda call pandaSaved() for statistics.
Otherwise it’s a killed animal and we already handled that in handleClick().
And finally using componentWillReceiveProps() we will check if the playing flag is set to false and clear the animal’s life timeout to avaid componentWillUnmount() being called after the game has ended.

Monetizing the App

Expo comes integrated with Facebook Audience Network and google’s AdMob. Both are great platforms for ad integration on mobile apps. Expo has made the integration very simple. Here are the javascript APIs for both Facebook Audience Network and AdMob.
In my case I used Facebook Audience Network, it was pretty easy to setup however it took about two days for facebook to review the android app while only a couple of hours for the iOS app.

Publishing to the Stores

Again expo does a great job here. Without bothering yourself with Android Studio or Xcode’s building processes, Expo handles everything on their servers including generating the required certificates and app signing and all that stuff. You can build your app into an .apk or .ipa files with just a single command. You will however have to use Xcode or Application Loader only if you want to upload the ipa build to the app store or test flight. Using Application Loader for this task was quite simple for me though.

Conclusion

Expo is a great tool to get an application started with react native in no time. The main pros for me was being able to develop on an iOS device using a windows machine. Over the Air updates is another great feature of expo, where you can just republish your app using expo’s command line tool without having to rebuild and reupload to the stores.
There are a lot more advantages to using expo, however, the downsides shouldn't be ignored. Some of these downsides maybe crucial to your case, this includes relatively large app size, the inability to bundle assets inside your app, using other push notifications service other than expo’s and more.
Partner With Ali
View Services

More Projects by Ali