Flame Engine, the Game Engine built on top of Flutter

Flame Engine, the Game Engine built on top of Flutter

Apr 5, 2022·

6 min read

Flutter, a modern UI toolkit with which you can build cross-platform beautiful UIs. But, do you know that it can also be used to develop games?

Introducing Flame Engine, a 2D game framework built on top of Flutter with which you can create any kind of 2D game and run it in all Flutter-supported platforms like mobile, the web and desktop.

📽 Video version available on YouTube and Odysee

In this article I will show you 3 quick samples to get started with Flame. The samples can be found here.

Simple 2D movement

To start, add the flame dependency to pubspec.yaml:

dependencies:
  flame: 1.1.0

Use the following snippet (in-code comments):

import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

// Create a class that extends from FlameGame, use the 
// KeyboardEvents mixin to receive keyboard input events
class Basic2DMovement extends FlameGame with KeyboardEvents {
  // This will be the size of the moving object
  static const _size = 100.0;

  // With this paint we will give it a color
  final paint = Paint()..color = Colors.lightGreen;

  // Use this variables to store the position of the object
  double _x = 0.0;
  double _y = 0.0;

  // This render function will be called in 
  // each frame to paint the object
  @override
  void render(Canvas canvas) {
    super.render(canvas);

    // This rect represents our object (a square)
    final rect = Rect.fromLTWH(_x, _y, _size, _size);
    // Draw the object with the provided Canvas
    canvas.drawRect(rect, paint);
  }

  @override
  KeyEventResult onKeyEvent(
    RawKeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    // Store if the key is down
    final isKeyDown = event is RawKeyDownEvent;

    // Alter the x, y values according to the current keys pressed
    if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) && isKeyDown) {
      _x -= 10.0;
    } else if (keysPressed.contains(LogicalKeyboardKey.arrowRight) &&
        isKeyDown) {
      _x += 10.0;
    }

    if (keysPressed.contains(LogicalKeyboardKey.arrowUp) && isKeyDown) {
      _y -= 10.0;
    } else if (keysPressed.contains(LogicalKeyboardKey.arrowDown) &&
        isKeyDown) {
      _y += 10.0;
    }

    // Return this value to acknowledge that the input 
    // has been managed
    return KeyEventResult.handled;
  }
}

To execute this sample, this class must be wrapped within a GameObject called in runApp():

void main() {
  runApp(GameWidget(game: Basic2DMovement()));
}

Advanced 2D movement

import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class Advanced2DMovement extends FlameGame with KeyboardEvents {
  static const _size = 100.0;

  final paint = Paint()..color = Colors.lightGreen;

  // This will be the speed of the object
  static const double _speed = 100.0;
  // This friction will apply if there are no input so the object
  // does not stop immediately
  static const double _friction = 0.9;

  // Store the position in a Vector2
  Vector2 _position = Vector2.zero();
  // This vector is the current momentum of the object
  Vector2 _movementVector = Vector2.zero();

  // This booleans will indicate us the current pressed keys
  bool _isPressingLeft = false;
  bool _isPressingRight = false;
  bool _isPressingUp = false;
  bool _isPressingDown = false;

  // The update function will be called in each frame
  // with the time from the last frame as the delta value, 
  // this way we can ensure a framerate
  // independent movement
  @override
  void update(double delta) {
    super.update(delta);

    // Create a vector to store the current input
    final Vector2 inputVector = Vector2.zero();

    // Alter the input vector according to the current pressed keys
    if (_isPressingLeft) {
      inputVector.x -= 1.0;
    } else if (_isPressingRight) {
      inputVector.x += 1.0;
    }

    if (_isPressingUp) {
      inputVector.y -= 1.0;
    } else if (_isPressingDown) {
      inputVector.y += 1.0;
    }

    // If there are some input...
    if (!inputVector.isZero()) {
      // Assign the input vector to the movement vector
      _movementVector = inputVector;

      // Normalize the movement vector so the speed 
      // will be always the same in all directions
      _movementVector.normalize();
      // Apply the speed and the delta time for a 
      // framerate independent movement
      _movementVector *= _speed * delta;
    } else {
      // If no keys are pressed, apply a friction to the vector to make
      // the object stop gradually
      _movementVector *= _friction;
    }

    // Apply movement vector to the current position
    _position += _movementVector;
  }

  @override
  void render(Canvas canvas) {
    super.render(canvas);

    final rect = Rect.fromLTWH(_position.x, _position.y, _size, _size);
    canvas.drawRect(rect, paint);
  }

  @override
  KeyEventResult onKeyEvent(
    RawKeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) &&
        event is RawKeyDownEvent) {
      _isPressingLeft = true;
    } else if (event is RawKeyUpEvent &&
        event.data.logicalKey == LogicalKeyboardKey.arrowLeft) {
      _isPressingLeft = false;
    }

    if (keysPressed.contains(LogicalKeyboardKey.arrowRight) &&
        event is RawKeyDownEvent) {
      _isPressingRight = true;
    } else if (event is RawKeyUpEvent &&
        event.data.logicalKey == LogicalKeyboardKey.arrowRight) {
      _isPressingRight = false;
    }

    if (keysPressed.contains(LogicalKeyboardKey.arrowUp) &&
        event is RawKeyDownEvent) {
      _isPressingUp = true;
    } else if (event is RawKeyUpEvent &&
        event.data.logicalKey == LogicalKeyboardKey.arrowUp) {
      _isPressingUp = false;
    }

    if (keysPressed.contains(LogicalKeyboardKey.arrowDown) &&
        event is RawKeyDownEvent) {
      _isPressingDown = true;
    } else if (event is RawKeyUpEvent &&
        event.data.logicalKey == LogicalKeyboardKey.arrowDown) {
      _isPressingDown = false;
    }

    return KeyEventResult.handled;
  }
}

Add a sprite and move the logic to a Player class

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

// We are going to move all the logic regarding 
// the player to this class, the KeyboardHandler 
// will allow us to capture keyboard events from within.
// All the previous logic has been moved to this class 
// almost without changes.
//
// This time we are going to extend from SpriteComponent, 
// which will allow us to add a sprite to this element.
class Player extends SpriteComponent with KeyboardHandler {
  static const _size = 128.0;

  static const double _speed = 100.0;
  static const double _friction = 0.9;

  Vector2 _movementVector = Vector2.zero();

  bool _isPressingLeft = false;
  bool _isPressingRight = false;
  bool _isPressingUp = false;
  bool _isPressingDown = false;

  // The onLoad() function is called at the beginning 
  // to make an initial load of resources
  @override
  Future<void> onLoad() async {
    await super.onLoad();
    size = Vector2(_size, _size);
    // Calling Sprite.load() we can asign to this 
    // component the given image
    sprite = await Sprite.load('flutter.png');
  }

  @override
  void update(double delta) {
    super.update(delta);

    final Vector2 inputVector = Vector2.zero();

    if (_isPressingLeft) {
      inputVector.x -= 1.0;
    } else if (_isPressingRight) {
      inputVector.x += 1.0;
    }

    if (_isPressingUp) {
      inputVector.y -= 1.0;
    } else if (_isPressingDown) {
      inputVector.y += 1.0;
    }

    if (!inputVector.isZero()) {
      _movementVector = inputVector;

      _movementVector.normalize();
      _movementVector *= _speed * delta;
    } else {
      _movementVector *= _friction;
    }

    position += _movementVector;
  }

  @override
  bool onKeyEvent(
    RawKeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) &&
        event is RawKeyDownEvent) {
      _isPressingLeft = true;
    } else if (event is RawKeyUpEvent &&
        event.data.logicalKey == LogicalKeyboardKey.arrowLeft) {
      _isPressingLeft = false;
    }

    if (keysPressed.contains(LogicalKeyboardKey.arrowRight) &&
        event is RawKeyDownEvent) {
      _isPressingRight = true;
    } else if (event is RawKeyUpEvent &&
        event.data.logicalKey == LogicalKeyboardKey.arrowRight) {
      _isPressingRight = false;
    }

    if (keysPressed.contains(LogicalKeyboardKey.arrowUp) &&
        event is RawKeyDownEvent) {
      _isPressingUp = true;
    } else if (event is RawKeyUpEvent &&
        event.data.logicalKey == LogicalKeyboardKey.arrowUp) {
      _isPressingUp = false;
    }

    if (keysPressed.contains(LogicalKeyboardKey.arrowDown) &&
        event is RawKeyDownEvent) {
      _isPressingDown = true;
    } else if (event is RawKeyUpEvent &&
        event.data.logicalKey == LogicalKeyboardKey.arrowDown) {
      _isPressingDown = false;
    }

    return true;
  }
}

class SpriteExample extends FlameGame with HasKeyboardHandlerComponents {
  // Declare the player as a variable
  late final Player _player;

  // Let's modify the background color
  @override
  Color backgroundColor() => const Color(0xFF353935);

  @override
  Future<void> onLoad() async {
    await super.onLoad();

    // Add the player to this game
    _player = Player();
    await add(_player);
  }
}

Did you find this article valuable?

Support David Serrano by becoming a sponsor. Any amount is appreciated!