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 2 of 4 of this article. Click here to view the first page.

Creating the Snake

You’ll use Piece to create the snake. First, you’ll create List based on the position of the snake on the screen. Then, you’ll store the positions of all the pieces that make up the snake in a List called positions. You’ll do this using getPieces(), which reads positions and returns a List of Pieces.

Start by replacing getPieces(), located in lib/game.dart, with:

 List<Piece> getPieces() {
    final pieces = <Piece>[];
    draw();
    drawFood();

    // 1
    for (var i = 0; i < length; ++i) {
      // 2
      if (i >= positions.length) {
        continue;
      }

      // 3
      pieces.add(
        Piece(
          posX: positions[i].dx.toInt(),
          posY: positions[i].dy.toInt(),
          // 4
          size: step,
          color: Colors.red,
        ),
      );
    }

    return pieces;
}

Here’s what’s going on in the code above:

  1. You use a for loop that runs until it covers the entire length of the snake.
  2. The if block inside the for loop handles an edge case when the length of the snake doesn’t match the length of the positions list. This happens when the length of the snake increases after it eats some food.
  3. For each iteration, it creates a Piece with the correct position and adds it to the pieces list it returns.
  4. Along with the position, you also pass size and color to Piece. The size is step, in this case, which ensures that the Snake moves along a grid where each grid cell has the size step. The color value is a personal preference. Feel free to use your own colors.

Save the file and let the app hot reload. So far, nothing happens and you will not notice any changes in the UI.

Starter project on iOS Simulator

Filling the Positions List

You need to implement draw() to actually fill the positions list. So replace draw() in the same file with the following:

  void draw() async {
    // 1
    if (positions.length == 0) {
      positions.add(getRandomPositionWithinRange());
    }

    // 2
    while (length > positions.length) {
      positions.add(positions[positions.length - 1]);
    }

    // 3
    for (var i = positions.length - 1; i > 0; i--) {
      positions[i] = positions[i - 1];
    }

    // 4
    positions[0] = await getNextPosition(positions[0]);
  }

Here’s how the function above works:

  1. If positions is empty, getRandomPositionWithinRange() generates a random position and starts the process.
  2. If the snake has just eaten the food, its length increases. The while loop adds a new position to positions so that length and positions are always in sync.
  3. It checks positions‘s length and shifts each position. This creates the illusion that the snake is moving.
  4. Finally, getNextPosition() moves the first piece, the head of the snake, to a new position.

The last thing you need to do here is to implementat getNextPosition().

Moving the Snake to the Next Position

Add the following code to the function in lib/game.dart:

  Future<Offset> getNextPosition(Offset position) async {
    Offset nextPosition;

    if (direction == Direction.right) {
      nextPosition = Offset(position.dx + step, position.dy);
    } else if (direction == Direction.left) {
      nextPosition = Offset(position.dx - step, position.dy);
    } else if (direction == Direction.up) {
      nextPosition = Offset(position.dx, position.dy - step);
    } else if (direction == Direction.down) {
      nextPosition = Offset(position.dx, position.dy + step);
    }

    return nextPosition;
  }

Here’s what the code above does:

  1. Creates a new position for the object based on the object’s current position and the value of its direction. Changing the direction causes the object to move in a different direction. You’ll use control buttons to do this later.
  2. Increases the value of the x-coordinate if the direction is set to right and decreases the value if the direction is set to left.
  3. Similarly, increases the value of the y-coordinate if the direction is set to up decreases it if the direction is set to down.

Finally, change [] to getPieces() in the inner Stack():

  return Scaffold(
    body: Container(
      color: Color(0XFFF5BB00),
      child: Stack(
        children: [
          Stack(
            children: getPieces(),
          ),
        ],
      ),
    ),
  );

In the code snippet above, we are just adding the getPieces() method that returns a widget to the Stack so we can see some UI on the screen. Note that if you do not add the widget to build(), nothing will change on the screen.

Save everything and restart the app. You’ll see:

Rendering of a two-piece snake

You can see a snake… that does nothing. That’s because you haven’t added anything to rebuild the UI. However, save the files again and again and let hot reload do its job and you’ll see the snake move.

Adding Movement and Speed

Now, all you need to make the snake move is a way to rebuild the UI. Every time build is called, you need to calculate the new positions and render the new list of Pieces onscreen. To do this, you’ll use Timer.

Add the following definition to changeSpeed() in lib/game.dart:

  void changeSpeed() {
    if (timer != null && timer.isActive) timer.cancel();

    timer = Timer.periodic(Duration(milliseconds: 200 ~/ speed), (timer) {
      setState(() {});
    });
  }

The code above simply resets the timer with a duration that factors in speed. You control speed and increase it every time the snake eats the food. Finally, on every tick of the timer, you call setState(), which rebuilds the whole UI. This happens at a rate you control using speed.

Next, invoke changeSpeed() from within restart() in the same file:

  void restart() {
    changeSpeed();
  }

This reinitializes timer every time the user restarts the game.

Save all the files and restart the app. Now, the snake moves in a random direction every time you restart the app.

Snake moving

Adding Controls

Now that you have a moving snake, you need to add a few more knobs and dials so the user can control the snake’s direction and speed. Keep the following things in mind:

  • You’ll control the movement and the direction using ControlPanel.
  • The speed must increase every time the snake eats food.
  • restart() resets the speed, length and direction when the snake collides with the bounding box.
  • After a collision, you’ll display a Game Over alert to the user.

Changing the Direction

ControlPanel has everything you need to give the user control over the snake’s direction. It consists of four circular buttons, where each button changes the direction of the snake’s movement. The widget lives inside lib/control_panel.dart. Feel free to dig in and look at the implementation.

Now, add the following code to getControls() in lib/game.dart:

  Widget getControls() {
    return ControlPanel( // 1
      onTapped: (Direction newDirection) { // 2
        direction = newDirection; // 3
      },
    );
  }

In the code snippet above:

  1. We are using the ControlPanel widget which is already created in the starter project for you. The ControlPanel widget renders 4 buttons that you will use to control the snake’s movements.
  2. We are also using the onTapped method which recieves the new direction for the snake to move in as an argument.
  3. We update the direction variable with the new direction newDirection. This will cause the snake to change direction.

Also, add the following import at the top of the document:

  import 'control_panel.dart';

Next, add getControls() as the second child of the outer Stack in build():

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

The code above adds the widgets returned by the getControls method to the UI on the screen within the Stack but above the rest of the UI.

Save the files and restart the app. Now, you’ll see a set of buttons that control the snake’s direction.

The ControlPanel widget in action

Tapping the buttons changes the snake’s direction. By now, the game works on a simple level, but you still need to add something the player should strive for. Just moving a colorful snake around on the screen isn’t a lot of fun, right? So your next step is to give the snake some food to eat that speeds it up.