The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Today we are going to learn how to make a game using React Native. Because we are using React Native, this game will be cross-platform, meaning you can play the same game on Android, iOS, and on the web, too. Today, however, we are going to focus only on mobile devices. So let’s begin.
To make any game, we need a loop that updates our game while we play. This loop is optimized to run the game smoothly, and for this purpose we are going to use React Native Game Engine.
First let’s create a new React Native app with the following command:
npx react-native init ReactNativeGame
After creating the project we need to add a dependency in order to add a game engine:
npm i -S react-native-game-engine
This command will add React Native Game Engine to our project.
So what kind of game are we going to make? For the sake of simplicity, let’s make a game with a snake that eats pieces of food and grows in length.
React Native Game Engine is a lightweight game engine. It includes a component that allows us to add arrays of objects as entities so that we can manipulate them. To write our logic for the game, we use an array of system props that allow us to manipulate entities (game objects), detect touches, and many other awesome details that help us to make a simple, functional game.
To make a game we need a canvas or container where we will add game objects. To make a canvas we simply add a view component with styling like so:
// App.js
<View style={styles.canvas}>
</View>
And we can add our styling like this:
const styles = StyleSheet.create({
canvas: {
flex: 1,
backgroundColor: "#000000",
alignItems: "center",
justifyContent: "center",
}
});
In the canvas we will use the GameEngine component with some styling from React Native Game Engine:
import { GameEngine } from "react-native-game-engine";
import React, { useRef } from "react";
import Constants from "./Constants";
export default function App() {
const BoardSize = Constants.GRID_SIZE * Constants.CELL_SIZE;
const engine = useRef(null);
return (
<View style={styles.canvas}>
<GameEngine
ref={engine}
style={{
width: BoardSize,
height: BoardSize,
flex: null,
backgroundColor: "white",
}}
/>
</View>
);
We also added a ref to the game engine using the useRef() React Hook for later use.
We also created a Constants.js file at the root of the project to store our constant values:
// Constants.js
import { Dimensions } from "react-native";
export default {
MAX_WIDTH: Dimensions.get("screen").width,
MAX_HEIGHT: Dimensions.get("screen").height,
GRID_SIZE: 15,
CELL_SIZE: 20
};
You will notice we are making a 15-by-15 grid where our snake will move.
At this time our game engine is set up to show the snake and its food. We need to add entities and props to the GameEngine component, but before that we need to create a snake and food component that will render on the device.
Let’s first make the snake. The snake is divided into two parts, the head and the body (or tail). For now we will make the snake’s head and we will add the snake’s tail later in this tutorial.
To make the snake’s head we will make a Head component in the components folder:

As you can see we have three components: Head, Food, and Tail. We will see what’s inside of these files one by one in this tutorial.
In the Head component, we will create a view with some styling:
import React from "react";
import { View } from "react-native";
export default function Head({ position, size }) {
return (
<View
style={{
width: size,
height: size,
backgroundColor: "red",
position: "absolute",
left: position[0] * size,
top: position[1] * size,
}}
></View>
);
}
We will pass some props to set the size and position of the head.
We are using position: "absolute" property to easily move the head.
This will render a square, and we are not going to use anything more complex; a square or rectangle shape for the snake body, and a circle shape for the food.
Now let’s add this snake’s head to GameEngine.
To add any entity we need to pass an object in the entities props in GameEngine:
//App.js
import Head from "./components/Head";
<GameEngine
ref={engine}
style={{
width: BoardSize,
height: BoardSize,
flex: null,
backgroundColor: "white",
}}
entities={{
head: {
position: [0, 0],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Head />,
}
}}
/>
We have passed an object in to the entities prop with the head key. These are the properties that it defines:
position is a set of coordinates in which to place the snake headsize is the value to set the size of the snake headxspeed and yspeed are the values that determine the snake movement and direction, and can be 1, 0, or -1. Note that when xspeed is set to 1 or -1, then the value of yspeed must be 0 and vice versarenderer is responsible for rendering the componentupdateFrequency and nextMove will be discussed later.After adding the Head component, let’s add other components, too:
// commponets/Food/index.js
import React from "react";
import { View } from "react-native";
export default function Food({ position, size }) {
return (
<View
style={{
width: size,
height: size,
backgroundColor: "green",
position: "absolute",
left: position[0] * size,
top: position[1] * size,
borderRadius: 50
}}
></View>
);
}
The Food component is similar to the Head component, but we have changed the background color and border radius to make it a circle.
Now create a Tail component. This one can be tricky:
// components/Tail/index.js
import React from "react";
import { View } from "react-native";
import Constants from "../../Constants";
export default function Tail({ elements, position, size }) {
const tailList = elements.map((el, idx) => (
<View
key={idx}
style={{
width: size,
height: size,
position: "absolute",
left: el[0] * size,
top: el[1] * size,
backgroundColor: "red",
}}
/>
));
return (
<View
style={{
width: Constants.GRID_SIZE * size,
height: Constants.GRID_SIZE * size,
}}
>
{tailList}
</View>
);
}
When the snake eats the food, we will add an element in the snake body so that our snake will grow. These elements will pass into the Tail component, which will indicate that it must get larger.
We will loop through all of the elements to create the whole snake body, append it, and then render.
After making all of the required components, let’s add these two components as GameEngine entities:
// App.js
import Food from "./components/Food";
import Tail from "./components/Tail";
// App.js
const randomPositions = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
// App.js
<GameEngine
ref={engine}
style={{
width: BoardSize,
height: BoardSize,
flex: null,
backgroundColor: "white",
}}
entities={{
head: {
position: [0, 0],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Head />,
},
food: {
position: [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
],
size: Constants.CELL_SIZE,
renderer: <Food />,
},
tail: {
size: Constants.CELL_SIZE,
elements: [],
renderer: <Tail />,
},
}}
/>
To ensure randomness of food position, we have made a randomPositions function with minimum and maximum parameters.
In the tail, we have added an empty array in the initial state, so when the snake eats the food it will store each tail length in the elements: space.
At this point we have successfully created our game components. Now it’s time to add game logic in the game loop.
To make a game loop, the GameEngine component has a prop called systems that accepts an array of functions.
To keep everything structured, I am creating a folder called systems and inserting a file called GameLoop.js:
![]()
In this file we are exporting a function with certain parameters:
// GameLoop.js
export default function (entities, { events, dispatch }) {
...
return entities;
}
The first parameter is entities, which contains all of the entities we passed to the GameEngine component so we can manipulate them. Another parameter is an object with properties, i.e. events and dispatch.
Let’s write the code to move the snake head in the right direction.
In the GameLoop.js function we will update the head position, as this function will be called in every frame:
// GameLoop.js
export default function (entities, { events, dispatch }) {
const head = entities.head;
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
}
We are accessing the head using the entities parameter, and on every frame we are updating snake head’s position.
If you play the game now, nothing will happen because we have set our xspeed and yspeed to 0. If you set the xspeed or yspeed to 1, the snake’s head will move very fast.
To slow down the speed of the snake we will use the nextMove and updateFrequency values like this:
const head = entities.head;
head.nextMove -= 1;
if (head.nextMove === 0) {
head.nextMove = head.updateFrequency;
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
}
We are updating the value of nextMove to 0 by subtracting by 1 on every frame. When the value is 0, the if condition is set to true and the nextMove value is updated back to the initial value, thereby moving the snake’s head.
Now the speed of the snake should be slower than before.
At this point we haven’t added a “Game over!” condition. The first “Game over!” condition is when the snake touches the wall, and the game stops running and shows the user a message to indicate that the game is over.
To add this condition we are using this code:
if (head.nextMove === 0) {
head.nextMove = head.updateFrequency;
if (
head.position[0] + head.xspeed < 0 ||
head.position[0] + head.xspeed >= Constants.GRID_SIZE ||
head.position[1] + head.yspeed < 0 ||
head.position[1] + head.yspeed >= Constants.GRID_SIZE
) {
dispatch("game-over");
} else {
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
}
The second if condition is checking whether the snake head touched the walls or not. If that condition is true, then we are dispatching a "game-over" event using the dispatch function.
With the else, we are updating the snake’s head position.
Now let’s add the “Game over!” functionality.
Whenever we dispatch a "game-over" event, we will stop the game and show an alert which will say “Game over!” Let’s implement it.
To listen for the "game-over" event we need to pass the onEvent prop to the GameEngine component. To stop the game, we need to add a running prop and pass in useState.
Our GameEngine should look like this:
// App.js
import React, { useRef, useState } from "react";
import GameLoop from "./systems/GameLoop";
....
....
const [isGameRunning, setIsGameRunning] = useState(true);
....
....
<GameEngine
ref={engine}
style={{
width: BoardSize,
height: BoardSize,
flex: null,
backgroundColor: "white",
}}
entities={{
head: {
position: [0, 0],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Head />,
},
food: {
position: [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
],
size: Constants.CELL_SIZE,
renderer: <Food />,
},
tail: {
size: Constants.CELL_SIZE,
elements: [],
renderer: <Tail />,
},
}}
systems={[GameLoop]}
running={isGameRunning}
onEvent={(e) => {
switch (e) {
case "game-over":
alert("Game over!");
setIsGameRunning(false);
return;
}
}}
/>
In the GameEngine we have added the systems prop and passed in an array with our GameLoop function, along with a running prop with an isGameRunning state. Finally, we have added the onEvent prop, which accepts a function with an event parameter so that we can listen for our events.
In this case we are listening for the "game-over" event in the switch statement, so when we receive the event we show the "Game over!" alert and set the isGameRunning state to false to stop the game.
We have written the “Game over!” logic, now let’s work on the logic of making the snake eat the food.
When the snake eats the food, then the position of food should change randomly.
Open GameLoop.js and write the below code:
// GameLoop.js
const randomPositions = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
export default function (entities, { events, dispatch }) {
const head = entities.head;
const food = entities.food;
....
....
....
if (
head.position[0] + head.xspeed < 0 ||
head.position[0] + head.xspeed >= Constants.GRID_SIZE ||
head.position[1] + head.yspeed < 0 ||
head.position[1] + head.yspeed >= Constants.GRID_SIZE
) {
dispatch("game-over");
} else {
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
if (
head.position[0] == food.position[0] &&
head.position[1] == food.position[1]
) {
food.position = [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
];
}
}
We have added an if condition to check if the snake head and the food’s position is the same (which would indicate the snake has “eaten” the food). Then, we are updating the food position using the randomPositions function as we did above in App.js. Notice that we are accessing the food from entities parameter.
Now let’s add controls of the snake. We are going to use buttons to control where the snake moves.
To do that we need to add buttons in the screen below the canvas:
// App.js
import React, { useRef, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import { GameEngine } from "react-native-game-engine";
import { TouchableOpacity } from "react-native-gesture-handler";
import Food from "./components/Food";
import Head from "./components/Head";
import Tail from "./components/Tail";
import Constants from "./Constants";
import GameLoop from "./systems/GameLoop";
export default function App() {
const BoardSize = Constants.GRID_SIZE * Constants.CELL_SIZE;
const engine = useRef(null);
const [isGameRunning, setIsGameRunning] = useState(true);
const randomPositions = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
const resetGame = () => {
engine.current.swap({
head: {
position: [0, 0],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Head />,
},
food: {
position: [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Food />,
},
tail: {
size: Constants.CELL_SIZE,
elements: [],
renderer: <Tail />,
},
});
setIsGameRunning(true);
};
return (
<View style={styles.canvas}>
<GameEngine
ref={engine}
style={{
width: BoardSize,
height: BoardSize,
flex: null,
backgroundColor: "white",
}}
entities={{
head: {
position: [0, 0],
size: Constants.CELL_SIZE,
updateFrequency: 10,
nextMove: 10,
xspeed: 0,
yspeed: 0,
renderer: <Head />,
},
food: {
position: [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
],
size: Constants.CELL_SIZE,
renderer: <Food />,
},
tail: {
size: Constants.CELL_SIZE,
elements: [],
renderer: <Tail />,
},
}}
systems={[GameLoop]}
running={isGameRunning}
onEvent={(e) => {
switch (e) {
case "game-over":
alert("Game over!");
setIsGameRunning(false);
return;
}
}}
/>
<View style={styles.controlContainer}>
<View style={styles.controllerRow}>
<TouchableOpacity onPress={() => engine.current.dispatch("move-up")}>
<View style={styles.controlBtn} />
</TouchableOpacity>
</View>
<View style={styles.controllerRow}>
<TouchableOpacity
onPress={() => engine.current.dispatch("move-left")}
>
<View style={styles.controlBtn} />
</TouchableOpacity>
<View style={[styles.controlBtn, { backgroundColor: null }]} />
<TouchableOpacity
onPress={() => engine.current.dispatch("move-right")}
>
<View style={styles.controlBtn} />
</TouchableOpacity>
</View>
<View style={styles.controllerRow}>
<TouchableOpacity
onPress={() => engine.current.dispatch("move-down")}
>
<View style={styles.controlBtn} />
</TouchableOpacity>
</View>
</View>
{!isGameRunning && (
<TouchableOpacity onPress={resetGame}>
<Text
style={{
color: "white",
marginTop: 15,
fontSize: 22,
padding: 10,
backgroundColor: "grey",
borderRadius: 10
}}
>
Start New Game
</Text>
</TouchableOpacity>
)}
</View>
);
}
const styles = StyleSheet.create({
canvas: {
flex: 1,
backgroundColor: "#000000",
alignItems: "center",
justifyContent: "center",
},
controlContainer: {
marginTop: 10,
},
controllerRow: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
controlBtn: {
backgroundColor: "yellow",
width: 100,
height: 100,
},
});
Other than controls, we have also added a button to start a new game when the previous game is over. This button only appears when the game is not running. On the click of that button, we are resetting the game by using the swap function from the game engine, passing in the entity’s initial object, and updating the game’s running state.
Now to the controls. We have added touchables, which, when pressed, dispatch the events that will be handled in the game loop:
// GameLoop.js
....
....
export default function (entities, { events, dispatch }) {
const head = entities.head;
const food = entities.food;
if (events.length) {
events.forEach((e) => {
switch (e) {
case "move-up":
if (head.yspeed === 1) return;
head.yspeed = -1;
head.xspeed = 0;
return;
case "move-right":
if (head.xspeed === -1) return;
head.xspeed = 1;
head.yspeed = 0;
return;
case "move-down":
if (head.yspeed === -1) return;
head.yspeed = 1;
head.xspeed = 0;
return;
case "move-left":
if (head.xspeed === 1) return;
head.xspeed = -1;
head.yspeed = 0;
return;
}
});
}
....
....
});
In the above code we have added a switch statement to identify the events and update the snake direction.
Still with me? Great! The only thing left is the tail.
When the snake eats the food, we want its tail to grow. We also want to dispatch a “Game over!” event when the snake bites its own tail or body.
Let’s add tail logic:
// GameLoop.js
const tail = entities.tail;
....
....
....
else {
tail.elements = [[head.position[0], head.position[1]], ...tail.elements];
tail.elements.pop();
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
tail.elements.forEach((el, idx) => {
if (
head.position[0] === el[0] &&
head.position[1] === el[1]
)
dispatch("game-over");
});
if (
head.position[0] == food.position[0] &&
head.position[1] == food.position[1]
) {
tail.elements = [
[head.position[0], head.position[1]],
...tail.elements,
];
food.position = [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
];
}
}
To make the tail follow the snake’s head, we are updating the tail’s elements. We do this by adding the head’s position to the beginning of the element array and then removing the last element on the tail’s elements array.
Now after this, we write a condition that if the snake bites its own body, we dispatch the "game-over" event.
And finally, whenever the snake eats the food, we are appending the tail elements by the head’s current position to grow the tail’s length.
Here is the full code of GameLoop.js:
// GameLoop.js
import Constants from "../Constants";
const randomPositions = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
export default function (entities, { events, dispatch }) {
const head = entities.head;
const food = entities.food;
const tail = entities.tail;
if (events.length) {
events.forEach((e) => {
switch (e) {
case "move-up":
if (head.yspeed === 1) return;
head.yspeed = -1;
head.xspeed = 0;
return;
case "move-right":
if (head.xspeed === -1) return;
head.xspeed = 1;
head.yspeed = 0;
// ToastAndroid.show("move right", ToastAndroid.SHORT);
return;
case "move-down":
if (head.yspeed === -1) return;
// ToastAndroid.show("move down", ToastAndroid.SHORT);
head.yspeed = 1;
head.xspeed = 0;
return;
case "move-left":
if (head.xspeed === 1) return;
head.xspeed = -1;
head.yspeed = 0;
// ToastAndroid.show("move left", ToastAndroid.SHORT);
return;
}
});
}
head.nextMove -= 1;
if (head.nextMove === 0) {
head.nextMove = head.updateFrequency;
if (
head.position[0] + head.xspeed < 0 ||
head.position[0] + head.xspeed >= Constants.GRID_SIZE ||
head.position[1] + head.yspeed < 0 ||
head.position[1] + head.yspeed >= Constants.GRID_SIZE
) {
dispatch("game-over");
} else {
tail.elements = [[head.position[0], head.position[1]], ...tail.elements];
tail.elements.pop();
head.position[0] += head.xspeed;
head.position[1] += head.yspeed;
tail.elements.forEach((el, idx) => {
console.log({ el, idx });
if (
head.position[0] === el[0] &&
head.position[1] === el[1]
)
dispatch("game-over");
});
if (
head.position[0] == food.position[0] &&
head.position[1] == food.position[1]
) {
tail.elements = [
[head.position[0], head.position[1]],
...tail.elements,
];
food.position = [
randomPositions(0, Constants.GRID_SIZE - 1),
randomPositions(0, Constants.GRID_SIZE - 1),
];
}
}
}
return entities;
}
Now your first game in React Native is done! You can run this game on your own device to play it. I hope you learned something new, and that you can share with your friends as well.
Thanks for reading and have a great day.

LogRocket's Galileo AI watches sessions for you and and surfaces the technical and usability issues holding back your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.

line-clamp to trim lines of textMaster the CSS line-clamp property. Learn how to truncate text lines, ensure cross-browser compatibility, and avoid hidden UX pitfalls when designing modern web layouts.

Discover seven custom React Hooks that will simplify your web development process and make you a faster, better, more efficient developer.

Promise.all still relevant in 2025?In 2025, async JavaScript looks very different. With tools like Promise.any, Promise.allSettled, and Array.fromAsync, many developers wonder if Promise.all is still worth it. The short answer is yes — but only if you know when and why to use it.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 29th issue.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now