Flutter Tutorial

Flutter Beginner Lesson 119 of 135 5 min read

Snake Game in Flutter with Flame (Full Source Code)

Build the classic Snake game in Flutter with Flame: a grid, keyboard and swipe controls, food, growing and game over. Full code that runs in your browser.

On this page

Snake is the perfect first game: simple rules, a grid instead of free movement, and it teaches the core of every game (a loop, input, collisions and a score).

Controls: arrow keys or WASD on a computer, swipe on a phone. Eat the red food to grow. Do not hit the walls or yourself. Tap or press Space to restart.

The full game

import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

const cells = 20;          // the board is 20 × 20 cells
const cellSize = 20.0;     // each cell is 20 × 20 pixels
const boardTop = 40.0;     // space for the score at the top

void main() => runApp(GameWidget(game: SnakeGame()));

class SnakeGame extends FlameGame with KeyboardEvents, DragCallbacks, TapCallbacks {
  SnakeGame()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: cells * cellSize,
            height: cells * cellSize + boardTop,
          ),
        );

  final random = Random();
  final snake = <Point<int>>[];          // first item = the head
  Point<int> direction = const Point(1, 0);
  Point<int> nextDirection = const Point(1, 0);
  Point<int> food = const Point(10, 10);
  double timer = 0;
  double stepTime = 0.15;                // seconds per move
  int score = 0;
  int best = 0;
  bool gameOver = false;
  late final TextComponent scoreText;

  @override
  Future<void> onLoad() async {
    camera.viewfinder.anchor = Anchor.topLeft;
    world.add(Board());
    scoreText = TextComponent(
      position: Vector2(10, 8),
      textRenderer: TextPaint(style: const TextStyle(color: Colors.white, fontSize: 18)),
    );
    world.add(scoreText);
    reset();
  }

  void reset() {
    snake
      ..clear()
      ..addAll(const [Point(5, 10), Point(4, 10), Point(3, 10)]);
    direction = const Point(1, 0);
    nextDirection = direction;
    score = 0;
    stepTime = 0.15;
    timer = 0;
    gameOver = false;
    placeFood();
    updateText();
  }

  void placeFood() {
    do {
      food = Point(random.nextInt(cells), random.nextInt(cells));
    } while (snake.contains(food));
  }

  void updateText() {
    scoreText.text = gameOver
        ? 'Game over! Score $score. Tap or Space'
        : 'Score: $score    Best: $best';
  }

  void turn(Point<int> d) {
    // The snake cannot turn straight back into itself.
    if (d.x == -direction.x && d.y == -direction.y) return;
    nextDirection = d;
  }

  @override
  void update(double dt) {
    super.update(dt);
    if (gameOver) return;

    // Move one cell every stepTime seconds.
    timer += dt;
    if (timer < stepTime) return;
    timer = 0;

    direction = nextDirection;
    final head = snake.first;
    final newHead = Point(head.x + direction.x, head.y + direction.y);

    final hitWall = newHead.x < 0 || newHead.y < 0 || newHead.x >= cells || newHead.y >= cells;
    if (hitWall || snake.contains(newHead)) {
      gameOver = true;
      best = max(best, score);
      updateText();
      return;
    }

    snake.insert(0, newHead);
    if (newHead == food) {
      score++;
      stepTime = max(0.06, stepTime * 0.96); // a little faster each time
      placeFood();
      updateText();
    } else {
      snake.removeLast(); // not growing: drop the tail
    }
  }

  @override
  KeyEventResult onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    if (event is KeyDownEvent) {
      final key = event.logicalKey;
      if (key == LogicalKeyboardKey.arrowUp || key == LogicalKeyboardKey.keyW) turn(const Point(0, -1));
      if (key == LogicalKeyboardKey.arrowDown || key == LogicalKeyboardKey.keyS) turn(const Point(0, 1));
      if (key == LogicalKeyboardKey.arrowLeft || key == LogicalKeyboardKey.keyA) turn(const Point(-1, 0));
      if (key == LogicalKeyboardKey.arrowRight || key == LogicalKeyboardKey.keyD) turn(const Point(1, 0));
      if (key == LogicalKeyboardKey.space && gameOver) reset();
    }
    return KeyEventResult.handled;
  }

  @override
  void onDragEnd(DragEndEvent event) {
    super.onDragEnd(event);
    final v = event.velocity;
    if (v.x.abs() > v.y.abs()) {
      turn(Point(v.x > 0 ? 1 : -1, 0));
    } else {
      turn(Point(0, v.y > 0 ? 1 : -1));
    }
  }

  @override
  void onTapDown(TapDownEvent event) {
    if (gameOver) reset();
  }
}

/// Draws the board, the food and the snake.
class Board extends PositionComponent with HasGameReference<SnakeGame> {
  Board() : super(position: Vector2(0, boardTop), size: Vector2.all(cells * cellSize));

  final _background = Paint()..color = const Color(0xFF1B2B1B);
  final _body = Paint()..color = const Color(0xFF7CD34B);
  final _head = Paint()..color = const Color(0xFFC6F68D);
  final _food = Paint()..color = const Color(0xFFFF5252);

  Rect _cell(Point<int> p) =>
      Rect.fromLTWH(p.x * cellSize + 1, p.y * cellSize + 1, cellSize - 2, cellSize - 2);

  @override
  void render(Canvas canvas) {
    canvas.drawRect(size.toRect(), _background);
    canvas.drawOval(_cell(game.food), _food);
    for (var i = 0; i < game.snake.length; i++) {
      canvas.drawRRect(
        RRect.fromRectAndRadius(_cell(game.snake[i]), const Radius.circular(5)),
        i == 0 ? _head : _body,
      );
    }
  }
}

How it works

A grid, not pixels. The snake is a list of cells (Point<int>). The Board turns cells into pixels only when drawing: x * cellSize. This keeps the game logic simple.

Moving in steps. Snake does not move every frame. A timer adds up dt, and every stepTime seconds the snake moves one cell. Lowering stepTime speeds the game up.

Growing. Each step a new head is added at the front. If the snake did not eat, the last cell is removed, so the length stays the same. If it ate, the tail stays, so it grows by one.

No turning back. turn ignores a direction that is exactly opposite to the current one, otherwise the head would crash into the neck. nextDirection stores the turn until the next step, so two quick key presses cannot break this rule.

Collisions are just checks on the grid: outside the board, or a cell already in the list.

Input. Keys come from the KeyboardEvents mixin on the game. Swipes come from DragCallbacks: the drag’s velocity tells you which direction the finger moved.

Make it yours

  1. Wrap around instead of dying at walls: Point((x + cells) % cells, (y + cells) % cells).
  2. Golden food worth 5 points that appears for a few seconds.
  3. Save the best score with shared_preferences.
  4. Obstacles: a list of wall cells drawn in grey that end the game.
  5. Sound when eating (sound and music).
Practise in the playground Updated