DEV Community

kanta13jp1
kanta13jp1

Posted on

Flutter Flame Game Engine Complete Guide — 2D Game Development from Scratch

Flutter Flame Game Engine Complete Guide — 2D Game Development from Scratch

Flame lets you build 2D games with your existing Flutter skills. Its component-based design and rich feature set make cross-platform mobile game development accessible to Flutter developers.

What is Flame?

Flame is a 2D game engine that runs on top of Flutter:

  • Component-based: Manage game objects as composable components
  • Flutter integration: Mix Widgets and Flame (menus in Flutter, game in Flame)
  • Cross-platform: iOS / Android / Web / Desktop
  • Forge2D support: Box2D-based physics engine included

Setup

# pubspec.yaml
dependencies:
  flame: ^1.18.0
  flame_audio: ^2.10.0
Enter fullscreen mode Exit fullscreen mode
// main.dart
import 'package:flame/game.dart';

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

FlameGame Basics

class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    await images.loadAll(['player.png', 'enemy.png', 'background.png']);
    add(Background());
    add(Player());
  }

  @override
  void update(double dt) {
    super.update(dt);
    // Game logic (dt = delta time in seconds)
  }
}
Enter fullscreen mode Exit fullscreen mode

Component System

class Player extends SpriteComponent
    with HasGameRef<MyGame>, KeyboardHandler, CollisionCallbacks {

  static const double speed = 200;
  late final Vector2 velocity = Vector2.zero();

  Player() : super(size: Vector2(64, 64));

  @override
  Future<void> onLoad() async {
    sprite = await gameRef.loadSprite('player.png');
    position = gameRef.size / 2;
    add(RectangleHitbox());
  }

  @override
  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    velocity.setZero();
    if (keysPressed.contains(LogicalKeyboardKey.arrowLeft)) velocity.x = -speed;
    if (keysPressed.contains(LogicalKeyboardKey.arrowRight)) velocity.x = speed;
    if (keysPressed.contains(LogicalKeyboardKey.arrowUp)) velocity.y = -speed;
    if (keysPressed.contains(LogicalKeyboardKey.arrowDown)) velocity.y = speed;
    return true;
  }

  @override
  void update(double dt) {
    position += velocity * dt;
    position.clamp(Vector2.zero(), gameRef.size - size);
  }

  @override
  void onCollisionStart(Set<Vector2> points, PositionComponent other) {
    if (other is Enemy) {
      gameRef.playerDamaged();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Sprite Animations

class AnimatedPlayer extends SpriteAnimationComponent {
  @override
  Future<void> onLoad() async {
    final spriteSheet = await gameRef.images.load('player_sheet.png');

    animation = SpriteAnimation.fromFrameData(
      spriteSheet,
      SpriteAnimationData.sequenced(
        amount: 8,
        stepTime: 0.1,
        textureSize: Vector2(64, 64),
      ),
    );
    size = Vector2(64, 64);
  }
}

// State-based animation switching
class CharacterWithStates extends SpriteAnimationGroupComponent<PlayerState> {
  @override
  Future<void> onLoad() async {
    animations = {
      PlayerState.idle: await _loadAnimation('idle', 4, 0.15),
      PlayerState.running: await _loadAnimation('run', 8, 0.1),
      PlayerState.jumping: await _loadAnimation('jump', 5, 0.12),
    };
    current = PlayerState.idle;
    size = Vector2(64, 64);
  }

  Future<SpriteAnimation> _loadAnimation(
    String name, int frames, double stepTime,
  ) async {
    return SpriteAnimation.fromFrameData(
      await gameRef.images.load('$name.png'),
      SpriteAnimationData.sequenced(
        amount: frames,
        stepTime: stepTime,
        textureSize: Vector2(64, 64),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Tiled Map Integration

class TiledMapComponent extends Component {
  late TiledComponent tiledMap;

  @override
  Future<void> onLoad() async {
    tiledMap = await TiledComponent.load('level1.tmx', Vector2.all(32));
    add(tiledMap);

    final objectLayer = tiledMap.tileMap.getLayer<ObjectGroup>('enemies');
    for (final obj in objectLayer?.objects ?? []) {
      add(Enemy(position: Vector2(obj.x, obj.y)));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Flutter UI Integration

class GamePage extends StatelessWidget {
  final MyGame game = MyGame();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          GameWidget(game: game),
          Positioned(
            top: 16, left: 16,
            child: ValueListenableBuilder<int>(
              valueListenable: game.score,
              builder: (_, score, __) => Text(
                'Score: $score',
                style: const TextStyle(color: Colors.white, fontSize: 24),
              ),
            ),
          ),
          Positioned(
            top: 16, right: 16,
            child: IconButton(
              icon: const Icon(Icons.pause, color: Colors.white),
              onPressed: () => game.pauseEngine(),
            ),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Saving Scores to Supabase

Future<void> saveScore(int score) async {
  await supabase.from('game_scores').insert({
    'user_id': supabase.auth.currentUser!.id,
    'score': score,
    'completed_at': DateTime.now().toIso8601String(),
  });
}

Future<List<Map<String, dynamic>>> getLeaderboard() async {
  return await supabase
      .from('game_scores')
      .select('score, profiles(username)')
      .order('score', ascending: false)
      .limit(10);
}
Enter fullscreen mode Exit fullscreen mode

Summary

With Flutter Flame you can:

  • Component architecture keeps complex game logic organized
  • SpriteAnimation delivers smooth character visuals
  • Flutter Widget integration gives full UI/UX flexibility
  • Supabase integration enables leaderboards and multiplayer features

Your existing Flutter skills transfer directly to game development.


Building an AI Life Management app with Flutter × Supabase at 自分株式会社. Sharing indie dev insights every week.

Top comments (0)