# How I Rebuilt the OG Snake Game using Flutter

## With code snippets, work-in-progress screenshots and a complete app demonstration, this blog can be your go-to guide if you want to build a snake game using Flutter that works on almost all platforms.

10 min readOct 4, 2022

--

# Getting Started

The challenge is to recreate the nostalgic snake game — the one we played on our Nokia phones for hours! This Snake II app in Flutter is versatile and can be run on pretty much any platform.

Let’s learn and understand the steps I followed along the way -

• Approach: To determine the snake’s moveable area and position
• Movement: Automatically move the snake
• A Growing Snake: When the snake eats food it expands its length
• Food: Randomly appears on the screen

# Approach:

Here I’m taking the Matrix approach to solve this and determine the snake position by (x, y) coordinates. Firstly, let’s decide the number of columns and rows in the matrix and the size of each cell which is a square.

`int xcount = 22, ycount = 35;double cellSize = 16;`

So, our width and height of the snake moveable area would be,

width = xcount * cellSize and height = ycount * cellSize

totalCells = xcount * ycount

For instance, I can generate food position by getting a random number between 0 to totalCells, then I can use that position to convert into offset. We are going to use Widget Stack and Position to Rendering our Snake and Food on the screen.

# Snake:

The snake is just a list of co-ordinates, the length of this list will determine the length of the snake, we can define each coordinate as one snake’s body, then we can simply render the snake according to their coordinate using the Position widget.

Let’s define the snake body:

`class SnakeBody {  int position = 0;  Direction direction = Direction.right;  Offset offset = Offset.zero;  SnakeBody({    required this.position,    required this.direction,required this.offset,});}`

Here direction is used to save the current direction of each snake body moving at the moment. We can define the initial snake:

`snakeBodys = [SnakeBody(position: 100,direction: Direction.right,offset: getOffsetforPos(100),),SnakeBody(position: 101,direction: Direction.right,offset: getOffsetforPos(101),),SnakeBody(position: 102,direction: Direction.right,offset: getOffsetforPos(102),),SnakeBody(position: 103,direction: Direction.right,offset: getOffsetforPos(103),),SnakeBody(position: 104,direction: Direction.right,offset: getOffsetforPos(104),),SnakeBody(position: 105,direction: Direction.right,offset: getOffsetforPos(105),),];`

Here we are using the getOffsetforPos function to get offset by position:

`Offset getOffsetforPos(int pos) {return Offset(((pos % xcount) * 16) + 8, ((pos ~/ xcount % ycount) * 16)+ 8);}`

This step will give us the center offset for the given position in the matrix.

Moving ahead, we can now render this:

`Container(decoration: BoxDecoration(border: Border.all(width: 2)),constraints: const BoxConstraints(    maxWidth: 352 + 16,    minWidth: 352 + 16,    maxHeight: 560 + 16,    minHeight: 560 + 16),    child: Center(      child: Stack(        children: [          ...List.generate(              snakeBodys.length,              (index) => Positioned(                        left: snakeBodys[index].offset.dx,                        top: snakeBodys[index].offset.dy,                        child: getSnakeBody(                            snakeBodys[index],                            index,                            snakeBodys.length)),                  ),      ],    ),  ),),`

Here getSnakeBody will get an appropriate snake body based on where they are, we know the front of the snake should be the head and the end should be the tail, hence it looks more like a snake than just a square.

Therefore, in order to make it seem right, we rotate the head and tail in the appropriate directions.

NOTE: you can skip this part and simply retain it as a square container with a width and height of

`Widget getSnakeBody(SnakeBody snake, int index, int length) {index = index + 1;if (index == 1) {return RotatedBox(quarterTurns: snake.direction == Direction.down? 1            : snake.direction == Direction.up? -1                : snake.direction == Direction.left                    ? 2                    : 0,        child: tail);  } else if (index == length) {    return RotatedBox(        quarterTurns: snake.direction == Direction.down            ? 1            : snake.direction == Direction.up                ? -1                : snake.direction == Direction.left                    ? 2                    : 0,        child: openMouth(snake.position, food.position, snake.direction)            ? eatingHead            : snakeHead);  } else {    return normalBody;  }}`

I have defined these body parts already in a different file, you can find them in the repo and openMouth. It basically determines if an open mouth snake head widget should be used in place of the head.

# Food:

Just like how we rendered the snake using position and offset, we can do the same for Food.

Let’s also define Food

`class Food {int position = 0;Offset offset = Offset.zero;int count = 0;Food({required this.position,required this.offset,});}`

Here count keeps track of how much food the snake has eaten.

We assign position zero initially because we are going to change that before the game starts

Food food = Food(position: 0, offset: Offset.zero);

Now we can render the Food on screen

`Stack(children: [...List.generate(snakeBodys.length,(index) => Positioned(left: snakeBodys[index].offset.dx,top: snakeBodys[index].offset.dy,child: getSnakeBody(snakeBodys[index],                    index,                    snakeBodys.length),          )),  Positioned(      left: food.offset.dx,      top: food.offset.dy,      child: snakeFood);],)`

# Automatic Movement:

We must determine the direction in which the snake is moving in order to determine its next position.

For example, if I take the initial snake positions have [1,2,3,4,5,6] that is moving in the right direction then we will add the next position to the last of our array because it’s moving in the right next position will be 7 so we add it at the end and remove the first so it becomes

[2,3,4,5,6,7]

Here it’s very important to calculate the next position properly depending on the direction it’s

moving.

We can have an enum for Direction:

`enum Direction { up, down, left, right }`

We can also have an initial function to initiate all our variables.

`/// Init Board or Reset board/// and variablesinitBoard() {snakeBodys = [    SnakeBody(      position: 100,      direction: Direction.right,      offset: getOffsetforPos(100),    ),    SnakeBody(      position: 101,      direction: Direction.right,      offset: getOffsetforPos(101),    ),    SnakeBody(      position: 102,      direction: Direction.right,      offset: getOffsetforPos(102),    ),    SnakeBody(      position: 103,      direction: Direction.right,      offset: getOffsetforPos(103),    ),    SnakeBody(      position: 104,      direction: Direction.right,      offset: getOffsetforPos(104),    ),    SnakeBody(      position: 105,      direction: Direction.right,      offset: getOffsetforPos(105),    ),  ];  do {    food.position = Random().nextInt(770);} while ([100, 101, 102, 103, 104, 105].contains(food.position));  direction = Direction.right;  food.offset = getOffsetforPos(food.position);  food.count = 0;  totalSpot = List.generate(770, (index) => index);  score = 0;}`

In order to actually move the snake, we must use the Timer.periodic, which takes a callback function and a duration. Here, we pass the updateSnake function with a time of 300 milliseconds. It, thus, calls the updateSnake function every 300 milliseconds.

updateSnake function calculates the next snake position depending on where the current snake’s last element in the list is also here snake can move one end to another end lets consider the matrix of 4*4 and the snake size is one and it is in the 15th cell and moving down

1 2 3 4

5 6 7 8

9 10 11 12

13 14 15 16

So, if it’s moving down then the next position of that snake should be 3, we can calculate this using xcount, ycount and totalcells

`updateSnake() {/// There are no walls in this game we need to make sure thesnake moves through/// and comes from the other sidesetState(() {switch (direction) {case Direction.down:// If Snake is already on the Last Row of the matrix// we need to make it come from top on the same columnif (snakeBodys.last.position > 748) {snakeBodys.add(SnakeBody(position: snakeBodys.last.position - 770 + xcount,direction: direction,offset:getOffsetforPos(snakeBodys.last.position - 770 +xcount)));} else {// else we just move to the next row of matrixsnakeBodys.add(SnakeBody(position: snakeBodys.last.position + xcount,direction: direction,              offset: getOffsetforPos(snakeBodys.last.position + xcount)));        }        break;      case Direction.up:        // If the snake is already at the Top row of the matrix        // we need to make it come from the bottom on the same column        if (snakeBodys.last.position < xcount) {          snakeBodys.add(SnakeBody(              position: snakeBodys.last.position + 770 - xcount,              direction: direction,              offset:                  getOffsetforPos(snakeBodys.last.position + 770 -xcount)));        } else {          // else we just move to the next row of the matrix          snakeBodys.add(SnakeBody(              position: snakeBodys.last.position - xcount,              direction: direction,              offset: getOffsetforPos(snakeBodys.last.position - xcount)));        }        break;      case Direction.right:        // If the snake is already at the last column of the matrix        // we need to make it come from the first column that is left        if ((snakeBodys.last.position + 1) % xcount == 0) {          snakeBodys.add(SnakeBody(              position: snakeBodys.last.position + 1 - xcount,              direction: direction,              offset:                  getOffsetforPos(snakeBodys.last.position + 1 - xcount)));        } else {          snakeBodys.add(SnakeBody(              position: snakeBodys.last.position + 1,              direction: direction,              offset: getOffsetforPos(snakeBodys.last.position + 1)));        }break;      case Direction.left:        // If the snake is already at the first column of the matrix        // we need to make it come from the first column that is left        if (snakeBodys.last.position % xcount == 0) {          snakeBodys.add(SnakeBody(              position: snakeBodys.last.position - 1 + xcount,              direction: direction,              offset:                  getOffsetforPos(snakeBodys.last.position - 1 + xcount)));        } else {          snakeBodys.add(SnakeBody(              position: snakeBodys.last.position - 1,              direction: direction,              offset: getOffsetforPos(snakeBodys.last.position - 1)));        }        break;      default:    }    // If the Snake’s last position is head is the same as the Food position    // and get a new food position    if (snakeBodys.last.position == food.position) {      /// We can't have food generated on positions of the snake body      totalSpot.removeWhere((element) =>          snakeBodys.map((e) => e.position).toList().contains(element));      /// get new position for food once it’s eaten by snake      food.position = totalSpot[Random().nextInt(totalSpot.length - 1)];      food.offset = getOffsetforPos(food.position);      // increase count      food.count += 1;      // Add score      score = score + 5;      /// This is to repopulate Total available spots to also increases randrom postions      if (totalSpot.length < (770 / 2)) {        totalSpot = List.generate(770, (index) => index);      }    }    else {      /// If Snake didn't eat any food we need to remove it first      /// element to keep snake length same      snakeBodys.removeAt(0);    }});}`

Now that we have a method to update the snake’s next position, we can use Timer.periodic so that snakes start moving every 300 milliseconds.

Putting it all together in one method,

`/// Starts the Snake movingstartGame() {// indicates that the game has startedstart = true;// Reset BoardinitBoard();// Cancel Timer if the old timer is runningif (timer != null) {timer!.cancel();timer == null;}// Assign new timertimer = Timer.periodic(Duration(milliseconds: speeds[Provider.of<MenuAndSettingsProvider>(context,listen: false).level]!), (timer) {/// This will be called every duration// Call update snake to update to a new positionupdateSnake();// after the snake’s new position is updated get the currentpositions of// snakes list and check whether positions are repeated// if it is game over else continue    final copyList = List.from(snakeBodys.map((e) => e.position)).toList();    if (snakeBodys.length > copyList.toSet().length) {      start = false;      this.timer!.cancel();      this.timer = null;    }  });}`

Voila, the snake can now move! But the snakes keep moving forever in the same Right direction we initialized in but we can change this by incorporating the use of touch drag and keyboard arrows to change the snake direction.

# Changing Snake direction:

We can change the direction of the snake using GestureDetector widget, which we can used to detect drag directions of both touch and the mouse, although we can use the mouse on the web we can use the keyboard for input using RawKeyboardListener widget and change the direction by ARROWS or AWSD keys. Now let’s wrap our Snake rendering widget

`/// For detecting touch dragGestureDetector(  // for checking up and down  onVerticalDragUpdate: (details) {    if (dead) {      return;    }    if (start) {      if (direction != Direction.up && details.delta.dy > 0) {        if (direction != Direction.down &&            direction != Direction.up) {          direction = Direction.down;        }      }      if (direction != Direction.down && details.delta.dy < 0) {        if (direction != Direction.up &&            direction != Direction.down) {          direction = Direction.up;        }      }    } else {      /// If the game is not started and did drag action      /// this starts the game      startGame();    }  },  // for checking left or right  onHorizontalDragUpdate: (details) {    if (dead) {      return;    }    if (start) {      if (direction != Direction.left && details.delta.dx > 0) {        if (direction != Direction.right &&            direction != Direction.left) {          direction = Direction.right;        }      }      if (direction != Direction.right && details.delta.dx < 0) {        if (direction != Direction.left &&            direction != Direction.right) {          direction = Direction.left;        }      }    } else {      /// If the game is not started and did drag action      /// this starts the game      startGame();    }  },  // For listen to key events  child: RawKeyboardListener(    autofocus: true,    focusNode: FocusNode(),    // We will check which key is pressed and change the direction    // accordingly    onKey: (value) {      if (dead) {        return;      }      if (value.isKeyPressed(LogicalKeyboardKey.escape)) {        Navigator.of(context).push(MaterialPageRoute(          builder: (context) => const WelcomeScreen(),        ));      }      if (start) {        if (value.isKeyPressed(LogicalKeyboardKey.arrowUp) ||            value.isKeyPressed(LogicalKeyboardKey.keyW)) {          if (direction != Direction.up &&              direction != Direction.down) {            direction = Direction.up;          }        } else if (value.isKeyPressed(LogicalKeyboardKey.arrowDown) ||            value.isKeyPressed(LogicalKeyboardKey.keyS)) {          if (direction != Direction.down &&              direction != Direction.up) {            direction = Direction.down;          }        } else if (value.isKeyPressed(LogicalKeyboardKey.arrowLeft) ||            value.isKeyPressed(LogicalKeyboardKey.keyA)) {          if (direction != Direction.left &&              direction != Direction.right) {            direction = Direction.left;          }        } else if (value                .isKeyPressed(LogicalKeyboardKey.arrowRight) ||            value.isKeyPressed(LogicalKeyboardKey.keyD)) {          if (direction != Direction.right &&              direction != Direction.left) {            direction = Direction.right;          }        }      } else if ([        LogicalKeyboardKey.arrowUp,        LogicalKeyboardKey.keyW,        LogicalKeyboardKey.arrowDown,        LogicalKeyboardKey.keyS,        LogicalKeyboardKey.arrowLeft,        LogicalKeyboardKey.keyA,        LogicalKeyboardKey.arrowRight,        LogicalKeyboardKey.keyD      ].contains(value.logicalKey)) {        /// If the game is not started and pressed on the key        /// this starts the gamestartGame();      }    },    child: SizedBox(      width: 450,      child: Container(        decoration: BoxDecoration(border: Border.all(width: 2)),        constraints: const BoxConstraints(            maxWidth: 352 + 16,            minWidth: 352 + 16,            maxHeight: 560 + 16,            minHeight: 560 + 16),        child: Center(          child: Stack(            children: [              ...List.generate(                  snakeBodys.length,                  (index) => Visibility(                        visible: !deathFlicker,                        replacement: const SizedBox(),                        child: Positioned(                            left: snakeBodys[index].offset.dx,                            top: snakeBodys[index].offset.dy,                            child: getSnakeBody(snakeBodys[index],                                index, snakeBodys.length)),                      )),              Positioned(                  left: food.offset.dx,                  top: food.offset.dy,                  child: snakeFood),              if (bigFood.show)                Positioned(                    left: bigFood.offset.dx,                    top: bigFood.offset.dy,                    child: geekyFood),            ],          ),        ),      ),    ),  ),)`

Perfect! The snake is now able to change directions when the arrow keys are pressed or when we drag. The point of the game is to eat as much food as possible, so we simply display the score using the Text widget.

Combining all of the pieces with the automated movement, arrow keys, touch or mouse drag, food, and score, we now have a fully working snake game. The game is now fully operational!

You can play this game here Live: https://clasicsnakegame.web.app/#/

You find the code here: https://github.com/hemanthkb97/snake_game_clasic

This article was originally written by Hemanth Kumar B. He is a Software engineer at Geekyants, currently working as a Flutter developer and constantly learning and exploring new technologies.

--

--