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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Create a 2D Snake Game in Flutter
25 mins
- Getting Started
- Using Flutter as a Game Engine
- Drawing the Snake
- The Basics of 2D Rendering
- Creating the Snake
- Filling the Positions List
- Moving the Snake to the Next Position
- Adding Movement and Speed
- Adding Controls
- Changing the Direction
- Eating Food and Increasing Speed
- Displaying the Food Onscreen
- Consuming and Regenerating the Food
- Detecting Collisions and Showing the Game Over Dialog
- Adding Some Finishing Touches
- Restarting the Game
- Displaying the Score
- Where to Go From Here?
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 Piece
s.
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:
- You use a
for loop
that runs until it covers the entire length of the snake. - The
if
block inside thefor loop
handles an edge case when thelength
of the snake doesn’t match the length of thepositions
list. This happens when the length of the snake increases after it eats some food. - For each iteration, it creates a
Piece
with the correct position and adds it to thepieces
list it returns. - Along with the position, you also pass
size
andcolor
toPiece
. The size isstep
, in this case, which ensures that the Snake moves along a grid where each grid cell has the sizestep
. 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.
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:
- If
positions
is empty,getRandomPositionWithinRange()
generates a random position and starts the process. - If the snake has just eaten the food, its
length
increases. Thewhile loop
adds a new position topositions
so thatlength
andpositions
are always in sync. - It checks
positions
‘s length and shifts each position. This creates the illusion that the snake is moving. - 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:
- 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.
- 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.
- 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:
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 Piece
s 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.
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:
- 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. - We are also using the
onTapped
method which recieves the new direction for the snake to move in as an argument. - We update the
direction
variable with the new directionnewDirection
. 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.
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.