Episode 1: Building and Debugging a Simple Snake Game with Flutter using AI
Season 2: Chat GPT Experiments #chatGPT #Flutter
The Game Logic
The Snake game consists of a snake that moves around a grid. The objective of the game is to eat food that appears randomly on the grid. Each time the snake eats the food, it grows in length. The game ends if the snake collides with the edge of the grid or with itself.
The game's state changes over time. Thus, we need to use StatefulWidget
to hold the game's state. The game's state includes the positions of the snake and the food, the direction the snake is moving, whether the game is over, and a timer to update the game's state every few milliseconds.
The game grid is created using Flutter's GridView.builder
widget, which creates a scrollable, 2D array of widgets that are created on demand.
The SnakeGame Widget
In Flutter, everything is a widget. We'll create a SnakeGame
widget, which is a StatelessWidget
that only contains a Snake
widget. It's a good practice to separate widgets like this to keep the code modular and maintainable.
The SnakeGame
widget uses a MaterialApp
widget which provides many material design features. The home
of the MaterialApp
is a Scaffold
widget which provides a framework to implement the basic material design layout.
The Snake Widget
The Snake
widget is a StatefulWidget
since it needs to maintain the state of the game.
In the Snake
widget's state, we define a few variables:
snakePosition
is a list that holds the positions of the snake's body parts.snakeDirection
is an integer that represents the direction in which the snake is moving.foodPosition
is an integer that represents the position of the food.timer
is a timer that updates the game state every few milliseconds.isGameOver
is a boolean that represents whether the game is over or not.
In the initState
method, we call setupGame
method which initializes these variables.
In the build
method of the Snake
widget's state, we use a Column
widget to place the game grid and the game score. The game grid is a GridView.builder
widget and it's wrapped in a GestureDetector
widget to handle swipe gestures.
The GridView.builder
creates the game grid, where each cell can be a part of the snake's body, food, or just empty. It uses a builder function to generate each cell. The builder function checks if the current index is in snakePosition
(i.e., it's part of the snake's body), or if it's equal to foodPosition
(i.e., it's food), or else it's an empty cell.
Handling Game Updates
We use a Timer
to update the game state every few milliseconds. The updateGame
method is called in each timer tick. In the updateGame
method, we update the snake's position and check if the snake has eaten the food or if a collision has occurred.
When the snake eats food, we add the food's position to the snake's position and generate new food. When the snake moves, we add a new cell in the direction of movement and remove the tail cell.
Collision detection is done in the collisionDetected
method. If the snake hits the grid's edge or collides with its body, we end the game by setting isGameOver
to true
and canceling the timer.
Handling User Input
We use the GestureDetector
widget to handle swipe gestures. When the user swipes, we change the snake's direction accordingly. However, we don't allow the snake to move in the opposite direction it's currently moving to prevent it from colliding with its body.
Wrapping Up
When you're finished with the coding, you can run the game with the flutter run
command.
The game should work as follows: The snake moves around the grid, and when it eats the food, it grows in length. The game ends if the snake hits the wall or its body.
This is a basic version of the Snake game, and there are a lot of things you can improve. You can add more controls, improve the UI, keep high scores, and more. Despite its simplicity, it demonstrates the power and flexibility of Flutter. You can take this basic game and extend it as much as you want, learning more about Flutter in the process.
Complete Code
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(const SnakeGame());
class SnakeGame extends StatelessWidget {
const SnakeGame({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const Scaffold(
backgroundColor: Colors.black,
body: SafeArea(child: Snake()),
),
);
}
}
class Snake extends StatefulWidget {
const Snake({Key? key}) : super(key: key);
@override
_SnakeState createState() => _SnakeState();
}
class _SnakeState extends State<Snake> {
final int squaresPerRow = 20;
final int squaresPerCol = 40;
final textStyle = const TextStyle(color: Colors.white, fontSize: 20);
final randomGen = Random();
late List<int> snakePosition;
late int snakeDirection;
late int foodPosition;
late Timer timer;
late bool isGameOver;
@override
void initState() {
super.initState();
setupGame();
}
void setupGame() {
snakePosition = [45, 65, 85, 105, 125];
snakeDirection = 20; // up
isGameOver = false;
generateNewFood();
timer = Timer.periodic(const Duration(milliseconds: 400), (Timer timer) {
updateGame();
});
}
void updateGame() {
setState(() {
if (snakePosition.last == foodPosition) {
// If the snake eats the food, grow the snake & generate new food.
eatFood();
}
// Move the snake
moveSnake();
// Check collision after moving the snake.
if (collisionDetected()) {
endGame();
}
});
}
bool collisionDetected() {
if (snakeDirection == -1 && snakePosition.last % squaresPerRow == 0) {
return true; // left
} else if (snakeDirection == 1 &&
snakePosition.last % squaresPerRow == squaresPerRow - 1) {
return true; // right
} else if (snakeDirection == -squaresPerRow &&
snakePosition.last < squaresPerRow) {
return true; // up
} else if (snakeDirection == squaresPerRow &&
snakePosition.last > squaresPerRow * (squaresPerCol - 1)) {
return true; // down
} else if (snakePosition
.sublist(0, snakePosition.length - 1)
.contains(snakePosition.last)) {
return true; // collision with itself
}
return false;
}
void generateNewFood() {
foodPosition = randomGen.nextInt(squaresPerRow * squaresPerCol);
while (snakePosition.contains(foodPosition)) {
foodPosition = randomGen.nextInt(squaresPerRow * squaresPerCol);
}
}
void endGame() {
isGameOver = true;
timer.cancel();
}
void eatFood() {
// Add new head based on current direction but do not remove the tail.
snakePosition.add(snakePosition.last + snakeDirection);
generateNewFood();
}
void moveSnake() {
// Add a new head and remove the tail.
snakePosition.add(snakePosition.last + snakeDirection);
snakePosition.removeAt(0);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: GestureDetector(
onVerticalDragUpdate: (details) {
if (snakeDirection != -20 && details.delta.dy > 0) {
snakeDirection = squaresPerRow;
} else if (snakeDirection != 20 && details.delta.dy < 0) {
snakeDirection = -squaresPerRow;
}
},
onHorizontalDragUpdate: (details) {
if (snakeDirection != 1 && details.delta.dx < 0) {
snakeDirection = -1;
} else if (snakeDirection != -1 && details.delta.dx > 0) {
snakeDirection = 1;
}
},
child: AspectRatio(
aspectRatio: squaresPerRow / (squaresPerCol + 5),
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: squaresPerRow * squaresPerCol,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: squaresPerRow,
),
itemBuilder: (BuildContext context, int index) {
if (snakePosition.contains(index)) {
return Container(
padding: const EdgeInsets.all(2),
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Container(
color: Colors.white,
),
),
);
} else if (index == foodPosition) {
return Container(
padding: const EdgeInsets.all(2),
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Container(
color: Colors.green,
),
),
);
} else {
return Container(
padding: const EdgeInsets.all(2),
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Container(
color: Colors.grey[900],
),
),
);
}
},
),
),
),
),
Padding(
padding: const EdgeInsets.all(20),
child: isGameOver
? Text(
'Game Over',
style: textStyle,
)
: SizedBox.shrink(),
),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text('Score: ${snakePosition.length}', style: textStyle),
ElevatedButton(
child: Text(
'New Game',
style: TextStyle(color: Colors.black),
),
onPressed: isGameOver ? () => setupGame() : null,
),
],
),
),
],
);
}
}
Conclusion
In this article, we built a simple Snake game using Flutter. We learned how to use several Flutter concepts such as StatefulWidget
, GridView
, GestureDetector
, and Timer
. I hope you find this article useful and it helps you to build more complex apps using Flutter.
If you have any questions or feedback, feel free to leave a comment. Happy coding!