DEV Community

Cover image for Flame Engine, the Game Engine built on top of Flutter
David Serrano
David Serrano

Posted on • Originally published at davidserrano.io

Flame Engine, the Game Engine built on top of Flutter

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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

void main() {
  runApp(GameWidget(game: Basic2DMovement()));
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Latest comments (3)

Collapse
 
dukemagus profile image
Duke

i like the promises Flutter brings (native performance on any platform, write-once-run-everywhere, made for browsers and just recently will allow some desktop export without relying on electron, and Flame has a lot of potential if it can harness all these low level performance tweaks, but the 2d-only aspect makes it lag behind good game frameworks on almost every language.

Python, that's interpreted instead of compiled has 3d engines and frameworks (pygame 2 onwards).
Even javascript that's an ungodly mess to explore, have 3d gpu accelerated game engines or frameworks (three.js, babylon.js, etc.)

Wish google gave the flutter devs a money grant or some other kind of sponsorship to advance flame to be closer to the maturity of Raylib, LibGDX or other frameworks.

Collapse
 
spydon profile image
Lukas Klingsbo

At some point we'll most likely be able to make it a 3D engine, but right now we are lacking proper support for shaders from Flutter (it is underway though) and access to a Z-buffer.

Collapse
 
svprdga profile image
David Serrano

A 3D renderer would be awesome, indeed. But the problem is that Flutter is designed to create conventional 2D user interfaces, so Flame is constrained to that. Personally, I think that can be an interesting option if you plan to develop a 2D game, but if you are going after 3D you should look to other tools that can run on 3D canvases (Flutter uses Skia, which is optimized for 2D).