Styling, Improvements and Features
Posted
Updated
Europe’s developer-focused job platform
Let companies apply to you
Developer-focused, salary and tech stack upfront.
Just one profile, no job applications!
This is the second part of the tutorial Create the classic Snake game - React. The first part can be found here.
If you have followed the first part of the tutorial, you should have:
💰 The Pragmatic Programmer: journey to mastery. 💰 One of the best books in software development, sold over 200,000 times.
Let's continue with the missing parts and features.
Currently, the collision detection return true if we press the opposite key of the current direction the snake is moving.
For example: The key ArrowRight
is pressed, so the snake moves right, and then the key ArrowLeft
is pressed. This would trigger a collision,
which is a wrong behaviour. We have to fix this.
An easy way to fix this, is to filter out keys which are in the opposite direction. Since we have a state for direction and coordinates for arrow keys, we can simply sum up the current direction and the arrow direction.
The sum of x-coordinates for ArrowLeft and ArrowRight equal 0 and return a falsy value, hence this can be filtered.
ArrowLeft: {
x: -1,
y: 0
},
ArrowRight: {
x: 1,
y: 0
}
Update the moveSnake with the following code:
const moveSnake = (event: React.KeyboardEvent) => {
const { key } = event;
// Check if key is arrow key
if (
key === 'ArrowUp' ||
key === 'ArrowDown' ||
key === 'ArrowRight' ||
key === 'ArrowLeft'
) {
// disable backwards key, this means no collision when going right, and then pressing ArrowLeft
if (
direction.x + directions[key].x &&
direction.y + directions[key].y
) {
setDirection(directions[key]);
}
}
};
The styling of the game needs some improvement, and we have to add an overlay, if we lost the game, and autofocus. The styling will be made in the App.css
,
there are plenty of other ways to do styling in a React application. What styling method do you prefer? Leave a comment.
The game wrapper should be autofocussed, after the start button is clicked. We have access to the focus()
method, when we use the useRef
hook.
Add the wrapperRef and a state for isPlaying:
// add wrapper ref and isPlaying flag for showing start button
const wrapperRef = useRef<HTMLDivElement>(null);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
Now we have to update the startGame and endGame function:
// update startGame
const startGame = () => {
setIsPlaying(true);
setSnake(SNAKE_START);
setApple(APPLE_START);
setDirection(DIRECTION_START);
setSpeed(SPEED);
setGameOver(false);
wrapperRef.current?.focus();
};
// update endGame
const endGame = () => {
setIsPlaying(false);
setSpeed(null);
setGameOver(true);
};
Now we update the wrapper with some classNames and some condition for an overlay and the reference.
// Update div with classes and flag for showing buttons, conditional styles
return (
<div className="wrapper">
<div
ref={wrapperRef}
className="canvas"
role="button"
tabIndex={0}
onKeyDown={(event: React.KeyboardEvent) => moveSnake(event)}
>
<canvas
style={
gameOver
? { border: '1px solid black', opacity: 0.5 }
: { border: '1px solid black' }
}
ref={canvasRef}
width={CANVAS_SIZE.x}
height={CANVAS_SIZE.y}
/>
{gameOver && <div className="game-over">Game Over</div>}
{!isPlaying && (
<button className="start" onClick={startGame}>
Start Game
</button>
)}
</div>
</div>
);
Now we can update our styling.
.wrapper {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100vh;
}
.canvas {
display: flex;
justify-content: center;
align-items: center;
background: rgb(151, 216, 148);
position: relative;
}
.start {
font-size: 1rem;
position: absolute;
border: 1px solid black;
background: none;
border-radius: 1rem;
padding: 1rem;
outline: none;
}
.start:hover {
border: none;
background: white;
}
.game-over {
position: absolute;
font-size: 5rem;
margin-bottom: 10rem;
}
The fillStyle should be updated as well from red
and green
to #1C1B17
, so we have this retro feeling/styling of the game.
We have now a working and styled version of the Classic Snake Game. Well, done. 😎
What's next?
Add state for points:
const [points, setPoints] = useState<number>(0);
Add setPoints to startGame to reset score:
setPoints(0);
Increase points if apple is eaten, add this to checkAppleCollision
:
setPoints(points + 1);
Add points to game wrapper:
<p className="points">{points}</p>
Add some styling for the points:
.points {
position: absolute;
bottom: 0;
right: 1rem;
font-size: 2rem;
}
We have to define a condition, when somebody has finished the game, which is unlikely, though to be feature-complete. The game end, besides a collision, would be the reaching of the maximum of available points. With the current scaling, the calculation is 40x40 = 1600points.
So we just add a condition to check if the maxPoints are reached and update the state and show some message.
We add the state to track if the game hasFinished
const [hasFinishedGame, setHasFinishedGame] = useState<boolean>(
false,
);
We add some condition to show the hasFinished message.
{
hasFinishedGame && <p className="finished-game">Congratulations</p>;
}
.finished-game {
position: absolute;
top: 60px;
font-size: 5rem;
color: red;
text-decoration: underline;
}
We add a variable for maxPoints and import it into App.tsx
:
export const maxPoints = 1600;
We add the check if maxPoints have been reached:
const checkAppleCollision = (newSnake: ICoords[]) => {
if (newSnake[0].x === apple.x && newSnake[0].y === apple.y) {
let newApple = createRandomApple();
while (checkCollision(newApple, newSnake)) {
newApple = createRandomApple();
}
setPoints(points + 1);
if (points === maxPoints) {
setHasFinishedGame(true);
endGame();
}
setApple(newApple);
return true;
}
return false;
};
In case hasFinishedGame has been set to true and we start a new game, the value has to be resetted.
const startGame = () => {
setHasFinishedGame(false);
setPoints(0);
setIsPlaying(true);
setSnake(snake_start);
setApple(apple_start);
setDirection(direction_start);
setSpeed(initial_speed);
setGameOver(false);
wrapperRef.current?.focus();
};
That's it. We can always come back and add more features, like sound effects or saving the score in localStorage or ...
Yay, ✨✨ Congratulations ✨✨. Well, done.
Your Game should look like this.
Thanks for reading and if you have any questions, use the comment function or send me a message @mariokandut.
References (and Big thanks): Maksim and Weibenfalk.
Never miss an article.