How to Create a 2D Snake Game in Flutter

Learn how to use Flutter as a simple game engine by creating a classic 2D Snake Game. Get to know the basics of 2D game graphics and how to control objects. By Samarth Agarwal.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Eating Food and Increasing Speed

To render the food onscreen, you’ll use Piece again, but you’ll change its color. Keep in mind that you don’t want the food to show up at just any arbitrary position. Instead, it should always render at a random location within the play area and it should always lie on the grid that the snake moves along.

Now, you’ll implement drawFood() to render the food Piece on the screen. Add the following code to drawFood() in the same file:

  void drawFood() {

    // 1
    if (foodPosition == null) {
      foodPosition = getRandomPositionWithinRange();
    }

    // 2
    food = Piece(
      posX: foodPosition.dx.toInt(),
      posY: foodPosition.dy.toInt(),
      size: step,
      color: Color(0XFF8EA604),
      isAnimated: true,
    );
  }

Here is what is happening above.

  1. The code above creates a Piece and stores it inside food.
  2. It stores the position of food inside an Offset object, foodPosition. Initially, this is null, so you use getRandomPositionWithinRange() to render the food randomly anywhere on the screen within the play area.

Displaying the Food Onscreen

Next, you need to add food to build within Stack to render it onscreen.

@override
Widget build(BuildContext context) {
  //...
  return Scaffold(
    body: Container(
      color: Color(0XFFF5BB00),
      child: Stack(
        children: [
          Stack(
            children: getPieces(),
          ),
          getControls(),
          food,
        ],
      ),
    ),
  );
}

Save all the files and restart the app to see the changes in action. Once the app restarts, you’ll see the green-colored food on the screen.

Food rendered on the screen

However, if you try to navigate the snake to the food, nothing happens — it just runs over the food icon. That’s because you haven’t done anything to make the snake eat the food. You’ll fix that next.

Consuming and Regenerating the Food

Now, you need to check if the snake and food are at the same coordinates in the 2D space. If they are, you’ll make some changes to the snake and render some new food at a new location.

Add the following code to drawFood(), right after the first if block ends:

  void drawFood() {

    // ...

    if (foodPosition == positions[0]) {
      length++;
      speed = speed + 0.25;
      score = score + 5;
      changeSpeed();

      foodPosition = getRandomPositionWithinRange();
    }

    // ...
  }

The code above simply checks if you the position you stored in foodPosition and the position of the snake’s first Piece widget are the same. If they match, you increase length by 1, speed by 0.25 and score by 5. Then you call changeSpeed(), which reinitializes timer using the new settings.

Finally, you update foodPosition with a new random position on the screen, thereby rendering a new food Piece.

Save the files and restart the app to let the changes take effect.

Eating the food

The snake can now eat the food. When it does, its length increases considerably.

Detecting Collisions and Showing the Game Over Dialog

As of now, the snake can freely move around — which causes a problem. There’s nothing preventing the snake from going outside the rectangular play area. You need to limit the snake’s movement to stay within the play area you’ve defined on the screen.

First, render that play area using getPlayAreaBorder(), which adds an outline to the play area. Simply add getPlayAreaBorder() to the outer Stack in build().

@override
Widget build(BuildContext context) {
  //...
  return Scaffold(
      body: Container(
        color: Color(0XFFF5BB00),
        child: Stack(
          children: [
            getPlayAreaBorder(),
            Stack(
              children: getPieces(),
            ),
            getControls(),
            food,
          ],
        ),
      ),
    );
}

The above code adds the food widget to the Stack in build() so now it will be rendered on the screen.

Next, add the following code to detectCollision():

  bool detectCollision(Offset position) {

    if (position.dx >= upperBoundX && direction == Direction.right) {
      return true;
    } else if (position.dx <= lowerBoundX && direction == Direction.left) {
      return true;
    } else if (position.dy >= upperBoundY && direction == Direction.down) {
      return true;
    } else if (position.dy <= lowerBoundY && direction == Direction.up) {
      return true;
    }

    return false;
  }

The function above checks if the snake has reached any of the four boundaries. If it has, then it returns true, otherwise, it returns false. It uses the lowerBoundX, upperBoundX, lowerBoundY and upperBoundY variables to check if the snake is still within the play area or not.

Next, you need to use detectCollision() inside getNextPosition() to check for a collision each time you generate the snake's next position. Add the following code to getNextPosition(), right after the declaration of nextPosition:

Future<Offset> getNextPosition(Offset position) async {
  //...
  if (detectCollision(position) == true) {
      if (timer != null && timer.isActive) timer.cancel();
      await Future.delayed(
          Duration(milliseconds: 500), () => showGameOverDialog());
      return position;
    }
  //...
}

The code above checks for a collision. If the snake collides with the bounding box, the code cancels the timer and displays the Game Over dialog to the user using showGameOverDialog().

The Game Over dialog

Save all the files and hot reload the app.

This time, if the snake touches the surrounding bounding box, you'll immediately see a dialog that informs the user that the game is over and displays their score. Tap on the Restart button in the dialog to dismiss the dialog and restart the game. You'll do that next.

Adding Some Finishing Touches

The game is starting to take shape. Your next steps are to write code to restart the game, then add a score to give the game a competitive element.

Restarting the Game

Next, you'll work on making the game restart when the user taps the Restart button. Replace the existing restart in lib/game.dart with:

  void restart() {

    score = 0;
    length = 5;
    positions = [];
    direction = getRandomDirection();
    speed = 1;

    changeSpeed();
  }

The code above simply resets everything to their initial values. It also clears positions so the snake loses its length and respawns from scratch.

Displaying the Score

Next, you need to add the code to display the score on the top-right corner of the screen. To do that, implement getScore():

  Widget getScore() {
    return Positioned(
      top: 50.0,
      right: 40.0,
      child: Text(
        "Score: " + score.toString(),
        style: TextStyle(fontSize: 24.0),
      ),
    );
  }

Add getScore() to build() as the last child of the outer Stack right after food within the Stack. Finally, your build() should look like this:

@override
Widget build(BuildContext context) {
  //...
  return Scaffold(
    body: Container(
      color: Color(0XFFF5BB00),
      child: Stack(
        children: [
          getPlayAreaBorder(),
          Stack(
            children: getPieces(),
          ),
          getControls(),
          food,
          getScore(),
        ],
      ),
    ),
  );
}

Save all the files and restart the app.

The final Snake Game with score

Now, you should see the score update in real-time.

Yay! You did it. Now it's time to build the app and share it with your friends to show off your developer skills.