How I created a Snake Game in React
Recently I was asked to create a Snake Game in React JS. So on a weekend, I attempted creating it. In a few hours, I created a simple version of the Snake Game, which I am going to explain to you how I created.
The Game Plan
First of all, I wanted to proceed in a step by step manner. For that, I created a simple to-do list in a proper sequence.
- Create the Snake Grid System
- Randomly show the food on the Snake Grid
- Create a single sized Snake and move it in the default direction
- Give directions to the snake using arrow keys
- Grow the Snake every time it eats food
- Game Over conditions — crossing the boundaries ends game
- Show score
I was told not to spend much time on creating visually very appealing things, so I will keep the design at the minimum. Also, I was instructed to not use any third-party libraries, so I will write my code for it.
The Snake Grid System
The Snake Grid System is a collection of squares formed by rows and columns. You can consider it as a combination of rows and columns just like the multiplication table. So each cell can be used to represent an item, either food or part of a snake.
The next question is, how to build such a grid system using HTML, CSS, and JS? I think, Flexbox
should be the right answer for this. So I designed one using CSS Flexbox.
/* CSS Code */.snake-container {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}.grid {
width: 500px;
height: 500px;
margin: auto;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}.grid-item {
outline: 1px solid grey;
width: 50px;
height: 50px;
}
With this CSS, I first created an outer div snake-container
and gave it height, without which the content inside won’t be aligned vertically. Inside this div, I have another div grid
which essentially will be our Snake Grid. It needs to be of fixed width. And finally, this div will be a collection of grid-item
divs which will represent each cell.
Now the calculations of widths and heights of these grid-item
and grid
should be proper. Note that this calculation must be done so that the future changes shall be possible, and grid appearance could be adjusted. For that I will use a simple formula:
gridWidth = numberOfGridCells * widthOfAGridItem;
class App extends React.Component {
// Initiate state
this.state = {
rows: 10,
cols: 10,
grid: [],
};const grid = [];
for (let row = 0; row < this.state.rows; row++) {
for (let col = 0; col < this.state.cols; col++) {
grid.push({
row,
col,
})
}
}
this.setState({ grid: grid })render () {
const gridItems = this.state.grid.map((grid) => {
return <div
key={grid.row.toString() + '-' + grid.col.toString()}
className="grid-item" ></div>
})
return (
<div className="snake-container">
<div className="grid">{gridItems}</div>
</div>
)
}
}
We created a react state variable grid
which contains all the cells (or grid-item) that will be inside the grid. Each cell has a row
and col
which represents its position.
Then we are using this variable to render the grid view by looping through this.grid
variable and injecting a grid-item
div for each cell. This will lead to a view something like this:
Displaying Food Item on the Snake Grid
The idea here is to use a special object key inside a grid cell to identify if the cell is a food item or not. Then this variable can be used in the render method to add a food item class name.
// CSS
.is-food {
background-color: red;
}// Gets random grid position
getRandomGrid() {
return {
row: Math.floor((Math.random() * this.state.rows)),
col: Math.floor((Math.random() * this.state.cols))
}
}// Use it inside componentDidMount()
const food = this.getRandomGrid();// Before pushing grid-cells into `this.grid`
const isFood = (food.row === row && food.col === col);
grid.push({
row,
col,
isFood,
})// In render() method
const gridItems = this.state.grid.map((grid) => {
return <div
key={grid.row.toString() + '-' + grid.col.toString()}
className={grid.isFood ? 'grid-item is-food' : 'grid-item'} ></div>
})
Above changes will show a red-colored cell randomly placed inside the snake grid.
A Moving Snake
Just like food, we can show a single length snake (or call it snake’s head) anywhere. But since the snake will have to move in any direction in pursuit of randomly appearing food. So the best position to keep the snake initially would a center of the grid. And similar to food-item, we can show snakehead in the grid, but with a different color.
// CSS
.is-head {
background-color: black;
}// this.state needs more variables
state = {
rows: 10,
cols: 10,
grid: [],
food: {},
snake: {
head: {},
},
currentDirection: 'right'
};// JS Function to get center of the grid system
getCenterOfGrid() {
return {
row: Math.floor((this.state.rows - 1) / 2),
col: Math.floor((this.state.cols - 1) / 2),
}
}// Need to identify cell as head
const isFood = (food.row === row && food.col === col);
const isHead = (snake.head.row === row && snake.head.col === col);
grid.push({
row,
col,
isFood,
isHead,
})
To give the snake a motion, we need to create a tick function that will be called every few milliseconds. This function will take the current direction of the motion, and then will move the snake’s head in that direction.
Based on the direction, the positions (row, col) of the head will increase or decrease.
// Set tick interval
window.fnInterval = setInterval(() => {
this.gameTick();
}, this.state.tickTime);// Tick function
gameTick() {
let { currentDirection } = state;
const { row, col } = state.snake.head;// Snake moves head
switch (currentDirection) {
case 'left':
head.col--;
break;case 'up':
head.row--;
break;case 'down':
head.row++;
break;case 'right':
default:
head.col++;
break;
}// Other things
}
Set Directions Using the Arrow Keys
// In constructor() functionthis.handleKeyPress = this.handleKeyPress.bind(this);// Inside componentDidMount() functiondocument.body.addEventListener('keydown', this.handleKeyPress);// Inside componentWillUnmount() functiondocument.body.removeEventListener('keydown', this.handleKeyPress);// function that thandles key PresshandleKeyPress(e) {
let { currentDirection } = this.state;switch (e.keyCode) {
case 37:
currentDirection = 'left';
break;case 38:
currentDirection = 'up';
break;case 39:
default:
currentDirection = 'right';
break;case 40:
currentDirection = 'down';
break;
}// Other things...
}
Snake Eats the Food and Grows Tail
Snake eating the food is a very simple condition when the snake’s head reaches exactly at the same position as the food. So whenever the row and column of the snake’s head and the food matches, it’s a snake eating the food condition.
This whole snake moving ahead, the snake eating the food and snake growing the tail is a part of logical sequence, which took me time to understand, just from the observation and thinking. But when I understood how it works, I found it is a very simple multi-step process to follow.
- Tail’s beginning moves to the head’s current position
- Tail’s end is removed from the tail, except when the snake eats the food
- Head moves to the next position
// Step 1: Tail will always take head's position
let { tail } = snake;
tail.unshift({
row: head.row,
col: head.col,
})// Step 2: shorten the tail, only when not eating
if (head.row === state.food.row && head.col === state.food.col) {
// Snake just ate the food, time to show new food
food = this.getRandomGrid();
} else {
tail.pop();
}// Step 3: use `currentDirection` for 'Moving a Snake' logic
The ‘Game is Over’ Condition
Here we will simply check if we are not crossing the boundaries, i.e. the maximum number of rows and columns that we decided.
// In new state, check if die conditions are met
let die = false;
if (newState.snake.head.row < 0
|| newState.snake.head.row >= this.state.rows
|| newState.snake.head.col < 0
|| newState.snake.head.col >= this.state.rows
) {
die = true;
}
Displaying Score
For displaying the score, we will use a simple scoring method based on how many food items have been eaten. It can be simple multiple, like 10. So eating 4 food items will make the score 40.
const score = newState.snake.tail.length * newState.scoreFactor;
Conclusion
Building a snake game can be very easy if you understand how to work on smaller logics. I have tried to simplify the logic here by taking out code. But if you would like to see the whole code, you can reach to my GitHub repository here: https://github.com/rakeshtembhurne/snake-game.