Create the classic Snake game - React II
© https://unsplash.com/@jxk

Create the classic Snake game - React II

Styling, Improvements and Features

By Mario Kandut

Posted 6 min read

📬 Subscribe to my monthly Newsletter and never miss an article. No spam. Promise. 🤞

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:

  • Moving, controllable snake
  • Border Collision detection
  • Random generated apples
  • Apple Collision Detection (snake eats apple)
  • Snake grows when the apple has been eaten

Let's continue with the missing parts and features.

Table of Contents - Part 2

  1. Fix collision detection for opposite direction keys
  2. Styling Updates and Autofocus
  3. Game Points
  4. Game End

1. Fix collision detection for opposite direction keys

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]);
    }
  }
};

2. Styling Updates and Autofocus

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 useRefhook.

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?

  • Adding points
  • Game End

3. Game Points

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;
}

4. Game End

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. React Snake

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.

Find this post useful?

Buy me a beerBuy me a beer

Newsletter Signup

Never miss an article about web development.

No spam. Promise. 🤞

Scroll to top ↑