Introduction
Snake game is probably one of the most known video games in the world and I have built it multiple times, why? Because it’s a good intro project to any new language or framework. I have built it with Python, C++, Javascript, and Dart (Flutter). I frequently use it as an example when I’m teaching someone the basics of a language/framework.
In this tutorial, we’ll build the Snake Game with Flutter. Some interesting things we’ll cover:
- Object-Oriented Design
- No frameworks - No game engines or third party libraries
- Flaws and Future Follow Ups
Things that aren’t covered well:
Structuring a Flutter project
- We’ll do it in one single file (executable on dartpad.dev)UI/UX
- No menu, UI, glyphs, etc.Scoring
- Because it doesn’t add value to project.
Let’s start:
For any simple single-player game, I usually start by thinking the following, step by step:
Objects
: What are the moving parts of the game, and which objects matter for building the MPG (Minimum Playable Game). In our game, they are the Snake and it’s food.State
: The state of the game and connecting the objects. In the snake game, the state is a grid, wherein the snake and food will be. We’ll draw these using simple containers.Update State
: The snake has to continually move forward which is a periodic update, and based on this (and the user input) the objects will have to respond to different states like snake eating the food, snake biting itself, snake hitting the wall, changing direction, etc.Controls
: User controls; In our case, change the direction of the snake based on input (arrow keys to set the direction).Score
: Score calculation and reset - number of food eaten. (not implemented in this tutorial though)
So having these things sorted, let’s code!
Foundation
We start with a stateless widget SnakeGame
to define our MaterialApp
. Followed by a stateful widget Game
which would render our game and manage the State
.
|
|
Since we’ll need a few constants, let’s define a Constants
class to access/modify them easily. Right now, we need 2 constants, canvasSize
(height and width of the canvas) and blockSize
(size of 1 block) so that we can imagine our canvas as a grid of side canvasSize/blockSize
elements.
|
|
After replacing height and width in the GameState
with Constants.canvasSize
we have a 50x50 imaginary grid to work with.
Models/Objects
Let’s start with our Snake; To represent the snake we’ll use a List of 2D points, and the number of points increases when the snake eats food. The first element of the list will be the location of the head and the last element, the tail. A point is just x, y values on a grid and for that, we’ll use dart’s Point
class (imported from dart:math
).
We also need a direction
to represent in which direction the snake is moving, this can also be represented using a Point
object since we just need to update the head
of the snake along a certain axis, i.e. certain x, y values. We can add these directions to the constants and define our snake:
|
|
Now, we can add a snake object to our GameState
along with the food, which also can be just a Point
. In the below code, we introduce some things:
random
: To generate random numbers (random food locations).initGame
: To initialize the Snake object and food location.foodUpdate
: To update food location.
|
|
Now we are more or less done with the foundation code, we just need to paint the snake and food on the canvas. For that, we’ll use Stack
and Positioned
Widgets. Positioned
puts a widget to provided locations in a Stack
. We’ll use the properties, top
and left
to set y
and x
of the widget respectively.
|
|
In the above code, we basically created a Positioned Widget for each part of the snake and added a food object as well to that list.
Update State
Let’s make the game alive now. We want to periodically update the state of the canvas and for that, we can use Timer.periodic
(imported from dart:async
) which calls a function after certain Duration
periodically.
We can add it to the initState
which gets called once our game starts. We also need an update function, which is passed as a callback to Timer.periodic
and is responsible for updating the game state.
Before implementing this gameUpdate
function, let’s see what we actually need to update:
- Move the snake in a particular direction
- Check if the snake’s head overlaps with food
- if yes, eat food, add length to the snake, and call
foodUpdate
- if yes, eat food, add length to the snake, and call
- Check if the snake’s head overlapped with any other body part, if yes, reset the game.
Implementation (added to GameState):
|
|
Added to Snake Class:
|
|
At this point, our game starts working (not controllable though). One bug I would like to address is that our foodUpdate
might add the food on the snake’s body, so let’s add a check for that.
|
|
Another interesting thing in the above code is snake[0] = Point((head.x + direction.x) % 50, (head.y + direction.y) % 50);
this line is really interesting on how it handles negative x, y cases (read about mod with negative numbers).
Controls
We are pretty much done now, we just need to be able to control the snake, that’s it. I understand the chronology of this is a bit weird and we should have done this before the whole eat food logic, but having implemented the snake game quite a bit, I usually do this at the end.
This part doesn’t need much explanation, just two things are sufficient I guess:
- We are using
RawKeyboardListener
widget withautofocus
set to true (so that we don’t have to tap it to take control). We’ll also define akeyHandler
function to handle the events by the listener. (LogicalKeyboardKey
is imported fromflutter/services.dart
) - Two edge/additional cases I would like to address:
- We don’t allow the snake to go in the direct opposite direction.
- We don’t call
setState
because regardless, it would be called within max50 ms
; We CAN call it but it won’t make much of a difference.
|
|
Voila! Our game is ready. Try it below:
Follow Ups
- This implementation still has a few IO bugs which can lead to snake dying (hint: the way we load frames and update direction). We can use an input queue to fix that.
- Haven’t implemented score because it’s trivial for this game.
- A good follow up project would be to implement a multiplayer version of this.
This project is quite simple and probaly not blog worthy, but I wrote it anyways because, it was fun and I wanted an easy writeup to get out of my writing rut:)