A walk through building this classic game with a new twist

Tamas Szikszai
Sep 22 · 9 min read

Today I’m going to recreate another classic game in React Native: Whack-A-Mole. The mechanical version of this game was an arcade fan-favourite in the late ‘70s and early ’80s. Since then numerous clones on various devices have been made.

TL;DR #1: Watch the video:

TL;DR #2: Grab the source code from GitHub

The gameplay is super simple. On the screen we display 12 slots. Moles will pop out of these slots and then disappear again at a rapid pace — the players’ goal is to hit as many of them on the head as possible. As a twist to the original, I want to introduce three different kinds of moles:

  • “Neutral”: this mole deals no damage if not “whacked”, but when hit the player will still receive points.
  • “Feisty”: This mole attacks the player if not whacked in time, dealing damage.
  • “Healer”: If whacked it will restore the player’s health a little bit.

The original game is more aimed at children — I think the introduction of different moles will spice things up just enough to broaden its appeal!


The Setup

We’ll require some animations. In my opinion, the best way to animate things in React Native is by using the rn-sprite-sheet library. You can install it by running the following:

npm install --save rn-sprite-sheet

This library is amazing at defining and running small animations from a single image file containing equal-sized frames.

For example, if you have six image frames for an animation, each of them 40x50 pixels, you can create a single 240x50 image, paste your frames and you’re ready to go.

However, in real-life your frames won’t be equal-sized. I spent a good couple of hours creating my sprite sheet by hand and got annoyed — to the point that I just had to create a python script to do the work for me. You can grab the script from my GitHub.

Anyway, I ended up with these image assets for our game: https://github.com/lepunk/react-native-videos/tree/whack-a-mole/WhackAMole/assets/img

As per usual, let’s pop them into an Image.js file for convenience:

export default Images = {
background: require('./img/background.png'),
sprites: require('./img/sprites.png'),
healthIcon: require('./img/icon_health.png'),
pauseIcon: require('./img/icon_pause.png'),
timeIcon: require('./img/icon_time.png'),
scoreIcon: require('./img/icon_score.png'),
restartIcon: require('./img/icon_restart.png'),
playIcon: require('./img/icon_play.png')
}

Setting up the base design is long and boring. So instead of writing hundreds of lines of explanation, just click here and see our starting position. There’s nothing special about it. The only thing I would highlight is two lines from Constants.js

XR: Dimensions.get("screen").width / 650,
YR: Dimensions.get("screen").height / 1024,

Our background.png image is a static 650x1024px png. We are stretching this image to cover the full screen of the user’s device. The chances are the device’s screen size won’t be 650x1024, so the image will be slightly distorted, which is not a big deal. The problem, however, is that later we’ll need to position elements on this background at exact locations. This is why we have to set up the XR and YR constants. They represent the X and Y ratio between the background image size and the user’s screen size. For example, if the screen’s width is 1300, the XR value will be 2. If we want to position something 20px from the left on the original background we can just say Constants.XR * 20

This is our clean slate

Mole.js

Time to add our moles to the screen. Our base Mole.js will look something like this:

import React, { Component } from 'react';
import { View, StyleSheet, Button, Image, TouchableWithoutFeedback } from 'react-native';
import Images from './assets/Images';
import SpriteSheet from 'rn-sprite-sheet';
import Constants from './Constants';

export default class Mole extends Component {
constructor(props){
super(props);

this.mole = null;
}

whack = () => {

}

render(){
return (
<View style={{ flex: 1 }}>
<SpriteSheet
ref={ref => (this.mole = ref)}
source={Images.sprites}
columns={6}
rows={8}
width={100}
animations={{
idle: [0],
appear: [1, 2, 3, 4],
hide: [4, 3, 2, 1, 0],
dizzy: [36, 37, 38],
faint: [42, 43, 44, 0],
attack: [11, 12, 13, 14, 15, 16],
heal: [24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
}}
/>
<TouchableWithoutFeedback onPress={this.whack} style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0}}>
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }} />
</TouchableWithoutFeedback>
</View>
)
}
}

The interesting part is the <SpriteSheet> component. Since our sprites.png has six images in a row, and we have eight rows, we pass in columns={6} and rows={8}

Then we need to define the animations. Each animation has a name (for example “appear”) and a list of frame indexes. Each frame in our sprite is assigned an index, starting from zero at the top left and increasing by one as it goes left to right and top to bottom.


Adding the Moles to the Screen

In our App.js we need to import our Mole component:

import Mole from './Mole';

We probably want to keep a reference to each mole so let's modify our constructor as follows:

constructor(props){
super(props);
this.state = DEFAULT_STATE;
this.moles = [];
}

Finally, create a 3x4 grid of moles in our playArea:

<View style={styles.playArea}>
{Array.apply(null, Array(4)).map((el, rowIdx) => {
return (
<View style={styles.playRow} key={rowIdx}>
{Array.apply(null, Array(3)).map((el, colIdx) => {
let moleIdx = (rowIdx * 3) + colIdx;

return (
<View style={styles.playCell} key={colIdx}>
<Mole
index={moleIdx}
ref={(ref) => { this.moles[moleIdx] = ref }}
/>
</View>
)
})}
</View>
)
})}
</View>

Excellent, we now have 12 holes on our screen that do nothing.


Pop Those Moles

We would like to pop moles at a random position periodically. In order to achieve this, we need to expose a pop() method in our Mole.js

pop = () => {
this.isPopping = true;
this.mole.play({
type: "appear",
fps: 24,
onFinish: () => {
this.actionTimeout = setTimeout(() => {
this.mole.play({
type: "hide",
fps: 24,
onFinish: () => {
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}, 1000)
}
});
}

When we call this method plays the “appear” animation, waits a second then plays the “hide” animation. Notice the this.props.onFinishPopping call? This will get called when the whole sequence is finished.

In our App.js we should modify the Mole definition:

<Mole
index={moleIdx}
ref={(ref) => { this.moles[moleIdx] = ref }}
onFinishPopping={this.onFinishPopping}
/>

Then we add the logic that sets up two loops: one for popping moles and one more for decreasing the timer:

constructor(props){
super(props);
this.state = DEFAULT_STATE;
this.moles = [];
this.molesPopping = 0;
this.interval = null;
this.timeInterval = null;
}

componentDidMount = () => {
this.setState(DEFAULT_STATE, this.setupTicks);
}

setupTicks = () => {
let speed = 750 - (this.state.level * 50);
if (speed < 350){
speed = 350;
}
this.interval = setInterval(this.popRandomMole, speed);
this.timeInterval = setInterval(this.timerTick, 1000);
}

randomBetween = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}

onFinishPopping = (index) => {
this.molesPopping -= 1;
}

popRandomMole = () => {
if (this.moles.length != 12){
return;
}

let randomIndex = this.randomBetween(0, 11);
if (!this.moles[randomIndex].isPopping && this.molesPopping < 3){
this.molesPopping += 1;
this.moles[randomIndex].pop();
}
}

timerTick = () => {
if (this.state.time === 0){
clearInterval(this.interval);
clearInterval(this.timeInterval);
this.setState({
cleared: true
})
} else {
this.setState({
time: this.state.time - 1
})
}
}

OK, there’s a lot to unpack here:

  • On componentDidMount we call setupTicks (after setting some base state).
  • setupTick will set up the two intervals described above.
  • We keep track of how many moles are “popping” at any given moment in the molesPopping variable.
  • When a mole finishes popping onFinishPopping is called and we reduce this variable.
  • popRandomMole will select a random slot which is not currently popping, but only if we have less than three active moles at a time (or things would get too chaotic).

It’s looking good, but not exactly exciting…


Whack Those Moles

As mentioned in the first paragraph of this post, I’d like to add a twist to the original game and introduce different kinds of moles.

By default each mole will be “Neutral” but they will have a 40% chance to become “Feisty” and 5% chance to became “Healers”.

Based on their types we will play different animation sequences. We should also handle the whacking. A TouchableWithoutFeedback is already defined and pointed to our Mole’s whack method that we need to fill. Our Mole.js will look something like this:

import React, { Component } from 'react';
import { View, StyleSheet, Button, Image, TouchableWithoutFeedback } from 'react-native';
import Images from './assets/Images';
import SpriteSheet from 'rn-sprite-sheet';
import Constants from './Constants';

export default class Mole extends Component {
constructor(props){
super(props);

this.mole = null;
this.actionTimeout = null;
this.isPopping = false;
this.isFeisty = false;
this.isHealing = false;
this.isWhacked = false;
this.isAttacking = false;
}

pop = () => {
this.isWhacked = false;
this.isAttacking = false;
this.isPopping = true;

this.isFeisty = Math.random() < 0.4;
if (!this.isFeisty){
this.isHealing = Math.random() < 0.05;
}

if (this.isHealing){
this.mole.play({
type: "heal",
fps: 24,
onFinish: () => {
this.actionTimeout = setTimeout(() => {
this.mole.play({
type: "hide",
fps: 24,
onFinish: () => {
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}, 1000);
}
})
} else {
this.mole.play({
type: "appear",
fps: 24,
onFinish: () => {
if (this.isFeisty){
this.actionTimeout = setTimeout(() => {
this.isAttacking = true;
this.props.onDamage();
this.mole.play({
type: "attack",
fps: 12,
onFinish: () => {
this.mole.play({
type: "hide",
fps: 24,
onFinish: () => {
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}
})
}, 1000)
} else {
this.actionTimeout = setTimeout(() => {
this.mole.play({
type: "hide",
fps: 24,
onFinish: () => {
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}, 1000)
}
}
})
}
}

whack = () => {
if (!this.isPopping || this.isWhacked || this.isAttacking){
return;
}

if (this.actionTimeout){
clearTimeout(this.actionTimeout);
}

this.isWhacked = true;
this.isFeisty = false;

this.props.onScore();
if (this.isHealing){
this.props.onHeal();
}

this.mole.play({
type: "dizzy",
fps: 24,
onFinish: () => {
this.mole.play({
type: "faint",
fps: 24,
onFinish: () => {
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}
})
}

render(){
return (
<View style={{ flex: 1 }}>
<SpriteSheet
ref={ref => (this.mole = ref)}
source={Images.sprites}
columns={6}
rows={8}
width={100}
animations={{
idle: [0],
appear: [1, 2, 3, 4],
hide: [4, 3, 2, 1, 0],
dizzy: [36, 37, 38],
faint: [42, 43, 44, 0],
attack: [11, 12, 13, 14, 15, 16],
heal: [24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
}}
/>
<TouchableWithoutFeedback onPress={this.whack} style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0}}>
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }} />
</TouchableWithoutFeedback>
</View>
)
}
}

As you may notice we used three new callbacks:

  • this.props.onDamage
  • this.props.onHeal
  • this.props.onScore

We should define these in App.js. First, let’s change the mole’s definition:

<Mole
index={moleIdx}
ref={(ref) => { this.moles[moleIdx] = ref }}
onFinishPopping={this.onFinishPopping}
onDamage={this.onDamage}
onHeal={this.onHeal}
onScore={this.onScore}
/>

Next, we define these three methods:

gameOver = () => {
clearInterval(this.interval);
clearInterval(this.timeInterval);

this.setState({
gameover: true
});
}

onScore = () => {
this.setState({
score: this.state.score + 1
})
}

onDamage = () => {
if (this.state.cleared || this.state.gameOver || this.state.paused){
return;
}

let targetHealth = this.state.health - 10 < 0 ? 0 : this.state.health - 10;

this.setState({
health: targetHealth
});

if (targetHealth <= 0){
this.gameOver();
}
}

onHeal = () => {
let targetHealth = this.state.health + 10 > 100 ? 100 : this.state.health + 10;
this.setState({
health: targetHealth
});
}

None of these methods are rocket science:

  • onHeal adds ten health points to the state (if it’s not already full).
  • onScore adds one point to the state.
  • onDamage takes away 10 health points. If health falls below 0 it clears the intervals and displays the game over screen.

Finally, we have to hook up some methods for our interface. We need to define:

  • reset(), which is called when the user hits the reset button on any of the popup screens
  • nextLevel(), which is called when the user hits the “play” button on the Cleared screen
  • pause(), when the user hits the pause button in the top right corner
  • resume(), when the user hits the play button on the Pause screen.

They’re pretty straight-forward and should look something like this:

reset = () => {
this.molesPopping = 0;

this.setState(DEFAULT_STATE, this.setupTicks);
}

pause = () => {
if (this.interval) clearInterval(this.interval);
if (this.timeInterval) clearInterval(this.timeInterval);
this.setState({
paused: true
});
}

resume = () => {
this.molesPopping = 0;
this.setState({
paused: false
}, this.setupTicks);
}

nextLevel = () => {
this.molesPopping = 0;

this.setState({
level: this.state.level + 1,
cleared: false,
gameover: false,
time: DEFAULT_TIME
}, this.setupTicks)
}

And there you have it! We’ve successfully built a working Whack-A-Mole game in React Native, which is actually surprisingly fun to play!

I hope you enjoyed this tutorial.

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade