DEV Community

Mario
Mario

Posted on • Originally published at mariokandut.com on

Create the classic Snake game - React II

This article was originally published on mariokandut.com.

This is the second part of the tutorial Create the classic Snake game - React. The first part can be found on dev.to.

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 },
Enter fullscreen mode Exit fullscreen mode

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]);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
  };
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Add setPoints to startGame to reset score:

setPoints(0);
Enter fullscreen mode Exit fullscreen mode

Increase points if apple is eaten, add this to checkAppleCollision:

setPoints(points + 1);
Enter fullscreen mode Exit fullscreen mode

Add points to game wrapper:

<p className="points">{points}</p>
Enter fullscreen mode Exit fullscreen mode

Add some styling for the points:

.points {
  position: absolute;
  bottom: 0;
  right: 1rem;
  font-size: 2rem;
}
Enter fullscreen mode Exit fullscreen mode

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,
);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

We add a variable for maxPoints and import it into App.tsx:

export const maxPoints = 1600;
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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();
  };
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)