David Serrano Canales
David Serrano

David Serrano

Flame Engine, the Game Engine built on top of Flutter

Flame Engine, the Game Engine built on top of Flutter

David Serrano Canales
·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 Canales by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors