7. Adding Menus

To add menus to the game, we will leverage Flame’s built-in overlay system.

Game Over Menu

Next, create a file called lib/overlays/game_over.dart and add the following code:

import 'package:flutter/material.dart';

import '../ember_quest.dart';

class GameOver extends StatelessWidget {
  // Reference to parent game.
  final EmberQuestGame game;
  const GameOver({super.key, required this.game});

  @override
  Widget build(BuildContext context) {
    const blackTextColor = Color.fromRGBO(0, 0, 0, 1.0);
    const whiteTextColor = Color.fromRGBO(255, 255, 255, 1.0);

    return Material(
      color: Colors.transparent,
      child: Center(
        child: Container(
          padding: const EdgeInsets.all(10.0),
          height: 200,
          width: 300,
          decoration: const BoxDecoration(
            color: blackTextColor,
            borderRadius: const BorderRadius.all(
              Radius.circular(20),
            ),
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text(
                'Game Over',
                style: TextStyle(
                  color: whiteTextColor,
                  fontSize: 24,
                ),
              ),
              const SizedBox(height: 40),
              SizedBox(
                width: 200,
                height: 75,
                child: ElevatedButton(
                  onPressed: () {
                    game.reset();
                    game.overlays.remove('GameOver');
                  },
                  style: ElevatedButton.styleFrom(
                    backgroundColor: whiteTextColor,
                  ),
                  child: const Text(
                    'Play Again',
                    style: TextStyle(
                      fontSize: 28.0,
                      color: blackTextColor,
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

As with the Main Menu, this is all standard Flutter widgets except for the call to remove the overlay and also the call to game.reset() which we will create now.

Open lib/ember_quest.dart and add / update the following code:

@override
Future<void> onLoad() async {
  await images.loadAll([
      'block.png',
      'ember.png',
      'ground.png',
      'heart_half.png',
      'heart.png',
      'star.png',
      'water_enemy.png',
  ]);
  
  camera.viewfinder.anchor = Anchor.topLeft;
  initializeGame(true);
}

void initializeGame(bool loadHud) {
  // Assume that size.x < 3200
  final segmentsToLoad = (size.x / 640).ceil();
  segmentsToLoad.clamp(0, segments.length);

  for (var i = 0; i <= segmentsToLoad; i++) {
    loadGameSegments(i, (640 * i).toDouble());
  }

  _ember = EmberPlayer(
    position: Vector2(128, canvasSize.y - 128),
  );
  add(_ember);
  if (loadHud) {
    add(Hud());
  }
}

void reset() {
  starsCollected = 0;
  health = 3;
  initializeGame(false);
}

You may notice that we have added a parameter to the initializeGame method which allows us to bypass adding the HUD to the game. This is because in the coming section, when Ember’s health drops to 0, we will wipe the game, but we do not need to remove the HUD, as we just simply need to reset the values using reset().

Displaying the Menus

To display the menus, add the following code to lib/main.dart:

void main() {
  runApp(
    GameWidget<EmberQuestGame>.controlled(
      gameFactory: EmberQuestGame.new,
      overlayBuilderMap: {
        'MainMenu': (_, game) => MainMenu(game: game),
        'GameOver': (_, game) => GameOver(game: game),
      },
      initialActiveOverlays: const ['MainMenu'],
    ),
  );
}

If the menus did not auto-import, add the following:

import 'overlays/game_over.dart';
import 'overlays/main_menu.dart';

If you run the game now, you should be greeted with the Main Menu overlay. Pressing play will remove it and allow you to start playing the game.

Health Check for Game Over

Our last step to finish Ember Quest is to add a game-over mechanism. This is fairly simple but requires us to place similar code in all of our components. So let’s get started!

In lib/actors/ember.dart, in the update method, add the following:

// If ember fell in pit, then game over.
if (position.y > game.size.y + size.y) {
  game.health = 0;
}

if (game.health <= 0) {
  removeFromParent();
}

In lib/actors/water_enemy.dart, in the update method update the following code:

if (position.x < -size.x || game.health <= 0) {
  removeFromParent();
}

In lib/objects/ground_block.dart, in the update method update the following code:

if (game.health <= 0) {
  removeFromParent();
}

In lib/objects/platform_block.dart, in the update method update the following code:

if (position.x < -size.x || game.health <= 0) {
  removeFromParent();
}

In lib/objects/star.dart, in the update method update the following code:

if (position.x < -size.x || game.health <= 0) {
  removeFromParent();
}

Finally, in lib/ember_quest.dart, add the following update method:

@override
void update(double dt) {
  if (health <= 0) {
    overlays.add('GameOver');
  }
  super.update(dt);
}

Congratulations

You made it! You have a working Ember Quest. Press the button below to see what the resulting code looks like or to play it live.

actors/ember.dart
  1import 'package:flame/collisions.dart';
  2import 'package:flame/components.dart';
  3import 'package:flame/effects.dart';
  4import 'package:flutter/services.dart';
  5
  6import '../ember_quest.dart';
  7import '../objects/ground_block.dart';
  8import '../objects/platform_block.dart';
  9import '../objects/star.dart';
 10import 'water_enemy.dart';
 11
 12class EmberPlayer extends SpriteAnimationComponent
 13    with KeyboardHandler, CollisionCallbacks, HasGameReference<EmberQuestGame> {
 14  EmberPlayer({
 15    required super.position,
 16  }) : super(size: Vector2.all(64), anchor: Anchor.center);
 17
 18  final Vector2 velocity = Vector2.zero();
 19  final Vector2 fromAbove = Vector2(0, -1);
 20  final double gravity = 15;
 21  final double jumpSpeed = 600;
 22  final double moveSpeed = 200;
 23  final double terminalVelocity = 150;
 24  int horizontalDirection = 0;
 25
 26  bool hasJumped = false;
 27  bool isOnGround = false;
 28  bool hitByEnemy = false;
 29
 30  @override
 31  Future<void> onLoad() async {
 32    animation = SpriteAnimation.fromFrameData(
 33      game.images.fromCache('ember.png'),
 34      SpriteAnimationData.sequenced(
 35        amount: 4,
 36        textureSize: Vector2.all(16),
 37        stepTime: 0.12,
 38      ),
 39    );
 40
 41    add(
 42      CircleHitbox(),
 43    );
 44  }
 45
 46  @override
 47  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
 48    horizontalDirection = 0;
 49    horizontalDirection +=
 50        (keysPressed.contains(LogicalKeyboardKey.keyA) ||
 51            keysPressed.contains(LogicalKeyboardKey.arrowLeft))
 52        ? -1
 53        : 0;
 54    horizontalDirection +=
 55        (keysPressed.contains(LogicalKeyboardKey.keyD) ||
 56            keysPressed.contains(LogicalKeyboardKey.arrowRight))
 57        ? 1
 58        : 0;
 59
 60    hasJumped = keysPressed.contains(LogicalKeyboardKey.space);
 61    return true;
 62  }
 63
 64  @override
 65  void update(double dt) {
 66    velocity.x = horizontalDirection * moveSpeed;
 67    game.objectSpeed = 0;
 68    // Prevent ember from going backwards at screen edge.
 69    if (position.x - 36 <= 0 && horizontalDirection < 0) {
 70      velocity.x = 0;
 71    }
 72    // Prevent ember from going beyond half screen.
 73    if (position.x + 64 >= game.size.x / 2 && horizontalDirection > 0) {
 74      velocity.x = 0;
 75      game.objectSpeed = -moveSpeed;
 76    }
 77
 78    // Apply basic gravity.
 79    velocity.y += gravity;
 80
 81    // Determine if ember has jumped.
 82    if (hasJumped) {
 83      if (isOnGround) {
 84        velocity.y = -jumpSpeed;
 85        isOnGround = false;
 86      }
 87      hasJumped = false;
 88    }
 89
 90    // Prevent ember from jumping to crazy fast.
 91    velocity.y = velocity.y.clamp(-jumpSpeed, terminalVelocity);
 92
 93    // Adjust ember position.
 94    position += velocity * dt;
 95
 96    // If ember fell in pit, then game over.
 97    if (position.y > game.size.y + size.y) {
 98      game.health = 0;
 99    }
100
101    if (game.health <= 0) {
102      removeFromParent();
103    }
104
105    // Flip ember if needed.
106    if (horizontalDirection < 0 && scale.x > 0) {
107      flipHorizontally();
108    } else if (horizontalDirection > 0 && scale.x < 0) {
109      flipHorizontally();
110    }
111    super.update(dt);
112  }
113
114  @override
115  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
116    if (other is GroundBlock || other is PlatformBlock) {
117      if (intersectionPoints.length == 2) {
118        // Calculate the collision normal and separation distance.
119        final mid =
120            (intersectionPoints.elementAt(0) +
121                intersectionPoints.elementAt(1)) /
122            2;
123
124        final collisionNormal = absoluteCenter - mid;
125        final separationDistance = (size.x / 2) - collisionNormal.length;
126        collisionNormal.normalize();
127
128        // If collision normal is almost upwards,
129        // ember must be on ground.
130        if (fromAbove.dot(collisionNormal) > 0.9) {
131          isOnGround = true;
132        }
133
134        // Resolve collision by moving ember along
135        // collision normal by separation distance.
136        position += collisionNormal.scaled(separationDistance);
137      }
138    }
139
140    if (other is Star) {
141      other.removeFromParent();
142      game.starsCollected++;
143    }
144
145    if (other is WaterEnemy) {
146      hit();
147    }
148    super.onCollision(intersectionPoints, other);
149  }
150
151  // This method runs an opacity effect on ember
152  // to make it blink.
153  void hit() {
154    if (!hitByEnemy) {
155      game.health--;
156      hitByEnemy = true;
157    }
158    add(
159      OpacityEffect.fadeOut(
160          EffectController(
161            alternate: true,
162            duration: 0.1,
163            repeatCount: 5,
164          ),
165        )
166        ..onComplete = () {
167          hitByEnemy = false;
168        },
169    );
170  }
171}
actors/water_enemy.dart
 1import 'package:flame/collisions.dart';
 2import 'package:flame/components.dart';
 3import 'package:flame/effects.dart';
 4
 5import '../ember_quest.dart';
 6
 7class WaterEnemy extends SpriteAnimationComponent
 8    with HasGameReference<EmberQuestGame> {
 9  final Vector2 gridPosition;
10  double xOffset;
11
12  final Vector2 velocity = Vector2.zero();
13
14  WaterEnemy({
15    required this.gridPosition,
16    required this.xOffset,
17  }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);
18
19  @override
20  Future<void> onLoad() async {
21    animation = SpriteAnimation.fromFrameData(
22      game.images.fromCache('water_enemy.png'),
23      SpriteAnimationData.sequenced(
24        amount: 2,
25        textureSize: Vector2.all(16),
26        stepTime: 0.7,
27      ),
28    );
29    position = Vector2(
30      (gridPosition.x * size.x) + xOffset,
31      game.size.y - (gridPosition.y * size.y),
32    );
33    add(RectangleHitbox(collisionType: CollisionType.passive));
34    add(
35      MoveEffect.by(
36        Vector2(-2 * size.x, 0),
37        EffectController(
38          duration: 3,
39          alternate: true,
40          infinite: true,
41        ),
42      ),
43    );
44  }
45
46  @override
47  void update(double dt) {
48    velocity.x = game.objectSpeed;
49    position += velocity * dt;
50    if (position.x < -size.x || game.health <= 0) {
51      removeFromParent();
52    }
53    super.update(dt);
54  }
55}
ember_quest.dart
  1import 'package:flame/components.dart';
  2import 'package:flame/events.dart';
  3import 'package:flame/game.dart';
  4import 'package:flutter/material.dart';
  5
  6import 'actors/ember.dart';
  7import 'actors/water_enemy.dart';
  8import 'managers/segment_manager.dart';
  9import 'objects/ground_block.dart';
 10import 'objects/platform_block.dart';
 11import 'objects/star.dart';
 12import 'overlays/hud.dart';
 13
 14class EmberQuestGame extends FlameGame
 15    with HasCollisionDetection, HasKeyboardHandlerComponents {
 16  EmberQuestGame();
 17
 18  late EmberPlayer _ember;
 19  late double lastBlockXPosition = 0.0;
 20  late UniqueKey lastBlockKey;
 21
 22  int starsCollected = 0;
 23  int health = 3;
 24  double cloudSpeed = 0.0;
 25  double objectSpeed = 0.0;
 26
 27  @override
 28  Future<void> onLoad() async {
 29    //debugMode = true; // Uncomment to see the bounding boxes
 30    await images.loadAll([
 31      'block.png',
 32      'ember.png',
 33      'ground.png',
 34      'heart_half.png',
 35      'heart.png',
 36      'star.png',
 37      'water_enemy.png',
 38    ]);
 39    camera.viewfinder.anchor = Anchor.topLeft;
 40
 41    initializeGame(loadHud: true);
 42  }
 43
 44  @override
 45  void update(double dt) {
 46    if (health <= 0) {
 47      overlays.add('GameOver');
 48    }
 49    super.update(dt);
 50  }
 51
 52  @override
 53  Color backgroundColor() {
 54    return const Color.fromARGB(255, 173, 223, 247);
 55  }
 56
 57  void loadGameSegments(int segmentIndex, double xPositionOffset) {
 58    for (final block in segments[segmentIndex]) {
 59      final component = switch (block.blockType) {
 60        const (GroundBlock) => GroundBlock(
 61          gridPosition: block.gridPosition,
 62          xOffset: xPositionOffset,
 63        ),
 64        const (PlatformBlock) => PlatformBlock(
 65          gridPosition: block.gridPosition,
 66          xOffset: xPositionOffset,
 67        ),
 68        const (Star) => Star(
 69          gridPosition: block.gridPosition,
 70          xOffset: xPositionOffset,
 71        ),
 72        const (WaterEnemy) => WaterEnemy(
 73          gridPosition: block.gridPosition,
 74          xOffset: xPositionOffset,
 75        ),
 76        _ => throw UnimplementedError(),
 77      };
 78      world.add(component);
 79    }
 80  }
 81
 82  void initializeGame({required bool loadHud}) {
 83    // Assume that size.x < 3200
 84    final segmentsToLoad = (size.x / 640).ceil();
 85    segmentsToLoad.clamp(0, segments.length);
 86
 87    for (var i = 0; i <= segmentsToLoad; i++) {
 88      loadGameSegments(i, (640 * i).toDouble());
 89    }
 90
 91    _ember = EmberPlayer(
 92      position: Vector2(128, canvasSize.y - 128),
 93    );
 94    world.add(_ember);
 95    if (loadHud) {
 96      camera.viewport.add(Hud());
 97    }
 98  }
 99
100  void reset() {
101    starsCollected = 0;
102    health = 3;
103    initializeGame(loadHud: false);
104  }
105}
main.dart
 1import 'package:flame/game.dart';
 2import 'package:flutter/material.dart';
 3
 4import 'ember_quest.dart';
 5import 'overlays/game_over.dart';
 6import 'overlays/main_menu.dart';
 7
 8void main() {
 9  runApp(
10    GameWidget<EmberQuestGame>.controlled(
11      gameFactory: EmberQuestGame.new,
12      overlayBuilderMap: {
13        'MainMenu': (_, game) => MainMenu(game: game),
14        'GameOver': (_, game) => GameOver(game: game),
15      },
16      initialActiveOverlays: const ['MainMenu'],
17    ),
18  );
19}
managers/segment_manager.dart
  1import 'package:flame/components.dart';
  2
  3import '../actors/water_enemy.dart';
  4import '../objects/ground_block.dart';
  5import '../objects/platform_block.dart';
  6import '../objects/star.dart';
  7
  8class Block {
  9  // gridPosition position is always segment based X,Y.
 10  // 0,0 is the bottom left corner.
 11  // 10,10 is the upper right corner.
 12  final Vector2 gridPosition;
 13  final Type blockType;
 14  Block(this.gridPosition, this.blockType);
 15}
 16
 17final segments = [
 18  segment0,
 19  segment1,
 20  segment2,
 21  segment3,
 22  segment4,
 23];
 24
 25final segment0 = [
 26  Block(Vector2(0, 0), GroundBlock),
 27  Block(Vector2(1, 0), GroundBlock),
 28  Block(Vector2(2, 0), GroundBlock),
 29  Block(Vector2(3, 0), GroundBlock),
 30  Block(Vector2(4, 0), GroundBlock),
 31  Block(Vector2(5, 0), GroundBlock),
 32  Block(Vector2(5, 1), WaterEnemy),
 33  Block(Vector2(5, 3), PlatformBlock),
 34  Block(Vector2(6, 0), GroundBlock),
 35  Block(Vector2(6, 3), PlatformBlock),
 36  Block(Vector2(7, 0), GroundBlock),
 37  Block(Vector2(7, 3), PlatformBlock),
 38  Block(Vector2(8, 0), GroundBlock),
 39  Block(Vector2(8, 3), PlatformBlock),
 40  Block(Vector2(9, 0), GroundBlock),
 41];
 42
 43final segment1 = [
 44  Block(Vector2(0, 0), GroundBlock),
 45  Block(Vector2(1, 0), GroundBlock),
 46  Block(Vector2(1, 1), PlatformBlock),
 47  Block(Vector2(1, 2), PlatformBlock),
 48  Block(Vector2(1, 3), PlatformBlock),
 49  Block(Vector2(2, 6), PlatformBlock),
 50  Block(Vector2(3, 6), PlatformBlock),
 51  Block(Vector2(6, 5), PlatformBlock),
 52  Block(Vector2(7, 5), PlatformBlock),
 53  Block(Vector2(7, 7), Star),
 54  Block(Vector2(8, 0), GroundBlock),
 55  Block(Vector2(8, 1), PlatformBlock),
 56  Block(Vector2(8, 5), PlatformBlock),
 57  Block(Vector2(8, 6), WaterEnemy),
 58  Block(Vector2(9, 0), GroundBlock),
 59];
 60
 61final segment2 = [
 62  Block(Vector2(0, 0), GroundBlock),
 63  Block(Vector2(1, 0), GroundBlock),
 64  Block(Vector2(2, 0), GroundBlock),
 65  Block(Vector2(3, 0), GroundBlock),
 66  Block(Vector2(3, 3), PlatformBlock),
 67  Block(Vector2(4, 0), GroundBlock),
 68  Block(Vector2(4, 3), PlatformBlock),
 69  Block(Vector2(5, 0), GroundBlock),
 70  Block(Vector2(5, 3), PlatformBlock),
 71  Block(Vector2(5, 4), WaterEnemy),
 72  Block(Vector2(6, 0), GroundBlock),
 73  Block(Vector2(6, 3), PlatformBlock),
 74  Block(Vector2(6, 4), PlatformBlock),
 75  Block(Vector2(6, 5), PlatformBlock),
 76  Block(Vector2(6, 7), Star),
 77  Block(Vector2(7, 0), GroundBlock),
 78  Block(Vector2(8, 0), GroundBlock),
 79  Block(Vector2(9, 0), GroundBlock),
 80];
 81
 82final segment3 = [
 83  Block(Vector2(0, 0), GroundBlock),
 84  Block(Vector2(1, 0), GroundBlock),
 85  Block(Vector2(1, 1), WaterEnemy),
 86  Block(Vector2(2, 0), GroundBlock),
 87  Block(Vector2(2, 1), PlatformBlock),
 88  Block(Vector2(2, 2), PlatformBlock),
 89  Block(Vector2(4, 4), PlatformBlock),
 90  Block(Vector2(6, 6), PlatformBlock),
 91  Block(Vector2(7, 0), GroundBlock),
 92  Block(Vector2(7, 1), PlatformBlock),
 93  Block(Vector2(8, 0), GroundBlock),
 94  Block(Vector2(8, 8), Star),
 95  Block(Vector2(9, 0), GroundBlock),
 96];
 97
 98final segment4 = [
 99  Block(Vector2(0, 0), GroundBlock),
100  Block(Vector2(1, 0), GroundBlock),
101  Block(Vector2(2, 0), GroundBlock),
102  Block(Vector2(2, 3), PlatformBlock),
103  Block(Vector2(3, 0), GroundBlock),
104  Block(Vector2(3, 1), WaterEnemy),
105  Block(Vector2(3, 3), PlatformBlock),
106  Block(Vector2(4, 0), GroundBlock),
107  Block(Vector2(5, 0), GroundBlock),
108  Block(Vector2(5, 5), PlatformBlock),
109  Block(Vector2(6, 0), GroundBlock),
110  Block(Vector2(6, 5), PlatformBlock),
111  Block(Vector2(6, 7), Star),
112  Block(Vector2(7, 0), GroundBlock),
113  Block(Vector2(8, 0), GroundBlock),
114  Block(Vector2(8, 3), PlatformBlock),
115  Block(Vector2(9, 0), GroundBlock),
116  Block(Vector2(9, 1), WaterEnemy),
117  Block(Vector2(9, 3), PlatformBlock),
118];
objects/ground_block.dart
 1import 'dart:math';
 2
 3import 'package:flame/collisions.dart';
 4import 'package:flame/components.dart';
 5import 'package:flutter/material.dart';
 6
 7import '../ember_quest.dart';
 8import '../managers/segment_manager.dart';
 9
10class GroundBlock extends SpriteComponent
11    with HasGameReference<EmberQuestGame> {
12  final Vector2 gridPosition;
13  double xOffset;
14
15  final UniqueKey _blockKey = UniqueKey();
16  final Vector2 velocity = Vector2.zero();
17
18  GroundBlock({
19    required this.gridPosition,
20    required this.xOffset,
21  }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);
22
23  @override
24  Future<void> onLoad() async {
25    final groundImage = game.images.fromCache('ground.png');
26    sprite = Sprite(groundImage);
27    position = Vector2(
28      (gridPosition.x * size.x) + xOffset,
29      game.size.y - (gridPosition.y * size.y),
30    );
31    add(RectangleHitbox(collisionType: CollisionType.passive));
32    if (gridPosition.x == 9 && position.x > game.lastBlockXPosition) {
33      game.lastBlockKey = _blockKey;
34      game.lastBlockXPosition = position.x + size.x;
35    }
36  }
37
38  @override
39  void update(double dt) {
40    velocity.x = game.objectSpeed;
41    position += velocity * dt;
42
43    if (position.x < -size.x) {
44      removeFromParent();
45      if (gridPosition.x == 0) {
46        game.loadGameSegments(
47          Random().nextInt(segments.length),
48          game.lastBlockXPosition,
49        );
50      }
51    }
52    if (gridPosition.x == 9) {
53      if (game.lastBlockKey == _blockKey) {
54        game.lastBlockXPosition = position.x + size.x - 10;
55      }
56    }
57    if (game.health <= 0) {
58      removeFromParent();
59    }
60
61    super.update(dt);
62  }
63}
objects/platform_block.dart
 1import 'package:flame/collisions.dart';
 2import 'package:flame/components.dart';
 3
 4import '../ember_quest.dart';
 5
 6class PlatformBlock extends SpriteComponent
 7    with HasGameReference<EmberQuestGame> {
 8  final Vector2 gridPosition;
 9  double xOffset;
10
11  final Vector2 velocity = Vector2.zero();
12
13  PlatformBlock({
14    required this.gridPosition,
15    required this.xOffset,
16  }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);
17
18  @override
19  Future<void> onLoad() async {
20    final platformImage = game.images.fromCache('block.png');
21    sprite = Sprite(platformImage);
22    position = Vector2(
23      (gridPosition.x * size.x) + xOffset,
24      game.size.y - (gridPosition.y * size.y),
25    );
26    add(RectangleHitbox(collisionType: CollisionType.passive));
27  }
28
29  @override
30  void update(double dt) {
31    velocity.x = game.objectSpeed;
32    position += velocity * dt;
33    if (position.x < -size.x || game.health <= 0) {
34      removeFromParent();
35    }
36    super.update(dt);
37  }
38}
objects/star.dart
 1import 'package:flame/collisions.dart';
 2import 'package:flame/components.dart';
 3import 'package:flame/effects.dart';
 4import 'package:flutter/material.dart';
 5
 6import '../ember_quest.dart';
 7
 8class Star extends SpriteComponent with HasGameReference<EmberQuestGame> {
 9  final Vector2 gridPosition;
10  double xOffset;
11
12  final Vector2 velocity = Vector2.zero();
13
14  Star({
15    required this.gridPosition,
16    required this.xOffset,
17  }) : super(size: Vector2.all(64), anchor: Anchor.center);
18
19  @override
20  Future<void> onLoad() async {
21    final starImage = game.images.fromCache('star.png');
22    sprite = Sprite(starImage);
23    position = Vector2(
24      (gridPosition.x * size.x) + xOffset + (size.x / 2),
25      game.size.y - (gridPosition.y * size.y) - (size.y / 2),
26    );
27    add(RectangleHitbox(collisionType: CollisionType.passive));
28    add(
29      SizeEffect.by(
30        Vector2.all(-24),
31        EffectController(
32          duration: 0.75,
33          reverseDuration: 0.5,
34          infinite: true,
35          curve: Curves.easeOut,
36        ),
37      ),
38    );
39  }
40
41  @override
42  void update(double dt) {
43    velocity.x = game.objectSpeed;
44    position += velocity * dt;
45    if (position.x < -size.x || game.health <= 0) {
46      removeFromParent();
47    }
48    super.update(dt);
49  }
50}
overlays/game_over.dart
 1import 'package:flutter/material.dart';
 2
 3import '../ember_quest.dart';
 4
 5class GameOver extends StatelessWidget {
 6  // Reference to parent game.
 7  final EmberQuestGame game;
 8  const GameOver({required this.game, super.key});
 9
10  @override
11  Widget build(BuildContext context) {
12    const blackTextColor = Color.fromRGBO(0, 0, 0, 1.0);
13    const whiteTextColor = Color.fromRGBO(255, 255, 255, 1.0);
14
15    return Material(
16      color: Colors.transparent,
17      child: Center(
18        child: Container(
19          padding: const EdgeInsets.all(10.0),
20          height: 200,
21          width: 300,
22          decoration: const BoxDecoration(
23            color: blackTextColor,
24            borderRadius: BorderRadius.all(
25              Radius.circular(20),
26            ),
27          ),
28          child: Column(
29            mainAxisAlignment: MainAxisAlignment.center,
30            children: [
31              const Text(
32                'Game Over',
33                style: TextStyle(
34                  color: whiteTextColor,
35                  fontSize: 24,
36                ),
37              ),
38              const SizedBox(height: 40),
39              SizedBox(
40                width: 200,
41                height: 75,
42                child: ElevatedButton(
43                  onPressed: () {
44                    game.reset();
45                    game.overlays.remove('GameOver');
46                  },
47                  style: ElevatedButton.styleFrom(
48                    backgroundColor: whiteTextColor,
49                  ),
50                  child: const Text(
51                    'Play Again',
52                    style: TextStyle(
53                      fontSize: 28.0,
54                      color: blackTextColor,
55                    ),
56                  ),
57                ),
58              ),
59            ],
60          ),
61        ),
62      ),
63    );
64  }
65}
overlays/heart.dart
 1import 'package:flame/components.dart';
 2
 3import '../ember_quest.dart';
 4
 5enum HeartState {
 6  available,
 7  unavailable,
 8}
 9
10class HeartHealthComponent extends SpriteGroupComponent<HeartState>
11    with HasGameReference<EmberQuestGame> {
12  final int heartNumber;
13
14  HeartHealthComponent({
15    required this.heartNumber,
16    required super.position,
17    required super.size,
18    super.scale,
19    super.angle,
20    super.anchor,
21    super.priority,
22  });
23
24  @override
25  Future<void> onLoad() async {
26    await super.onLoad();
27    final availableSprite = await game.loadSprite(
28      'heart.png',
29      srcSize: Vector2.all(32),
30    );
31
32    final unavailableSprite = await game.loadSprite(
33      'heart_half.png',
34      srcSize: Vector2.all(32),
35    );
36
37    sprites = {
38      HeartState.available: availableSprite,
39      HeartState.unavailable: unavailableSprite,
40    };
41
42    current = HeartState.available;
43  }
44
45  @override
46  void update(double dt) {
47    if (game.health < heartNumber) {
48      current = HeartState.unavailable;
49    } else {
50      current = HeartState.available;
51    }
52    super.update(dt);
53  }
54}
overlays/hud.dart
 1import 'package:flame/components.dart';
 2import 'package:flutter/material.dart';
 3
 4import '../ember_quest.dart';
 5import 'heart.dart';
 6
 7class Hud extends PositionComponent with HasGameReference<EmberQuestGame> {
 8  Hud({
 9    super.position,
10    super.size,
11    super.scale,
12    super.angle,
13    super.anchor,
14    super.children,
15    super.priority = 5,
16  });
17
18  late TextComponent _scoreTextComponent;
19
20  @override
21  Future<void>? onLoad() async {
22    _scoreTextComponent = TextComponent(
23      text: '${game.starsCollected}',
24      textRenderer: TextPaint(
25        style: const TextStyle(
26          fontSize: 32,
27          color: Color.fromRGBO(10, 10, 10, 1),
28        ),
29      ),
30      anchor: Anchor.center,
31      position: Vector2(game.size.x - 60, 20),
32    );
33    add(_scoreTextComponent);
34
35    final starSprite = await game.loadSprite('star.png');
36    add(
37      SpriteComponent(
38        sprite: starSprite,
39        position: Vector2(game.size.x - 100, 20),
40        size: Vector2.all(32),
41        anchor: Anchor.center,
42      ),
43    );
44
45    for (var i = 1; i <= game.health; i++) {
46      final positionX = 40 * i;
47      await add(
48        HeartHealthComponent(
49          heartNumber: i,
50          position: Vector2(positionX.toDouble(), 20),
51          size: Vector2.all(32),
52        ),
53      );
54    }
55
56    return super.onLoad();
57  }
58
59  @override
60  void update(double dt) {
61    _scoreTextComponent.text = '${game.starsCollected}';
62    super.update(dt);
63  }
64}
overlays/main_menu.dart
 1import 'package:flutter/material.dart';
 2
 3import '../ember_quest.dart';
 4
 5class MainMenu extends StatelessWidget {
 6  // Reference to parent game.
 7  final EmberQuestGame game;
 8
 9  const MainMenu({required this.game, super.key});
10
11  @override
12  Widget build(BuildContext context) {
13    const blackTextColor = Color.fromRGBO(0, 0, 0, 1.0);
14    const whiteTextColor = Color.fromRGBO(255, 255, 255, 1.0);
15
16    return Material(
17      color: Colors.transparent,
18      child: Center(
19        child: Container(
20          padding: const EdgeInsets.all(10.0),
21          height: 300,
22          width: 300,
23          decoration: const BoxDecoration(
24            color: blackTextColor,
25            borderRadius: BorderRadius.all(
26              Radius.circular(20),
27            ),
28          ),
29          child: Column(
30            mainAxisAlignment: MainAxisAlignment.center,
31            children: [
32              const Text(
33                'Ember Quest',
34                style: TextStyle(
35                  color: whiteTextColor,
36                  fontSize: 24,
37                ),
38              ),
39              const SizedBox(height: 40),
40              SizedBox(
41                width: 200,
42                height: 75,
43                child: ElevatedButton(
44                  onPressed: () {
45                    game.overlays.remove('MainMenu');
46                  },
47                  style: ElevatedButton.styleFrom(
48                    backgroundColor: whiteTextColor,
49                  ),
50                  child: const Text(
51                    'Play',
52                    style: TextStyle(
53                      fontSize: 40.0,
54                      color: blackTextColor,
55                    ),
56                  ),
57                ),
58              ),
59              const SizedBox(height: 20),
60              const Text(
61                '''Use WASD or Arrow Keys for movement.
62Space bar to jump.
63Collect as many stars as you can and avoid enemies!''',
64                textAlign: TextAlign.center,
65                style: TextStyle(
66                  color: whiteTextColor,
67                  fontSize: 14,
68                ),
69              ),
70            ],
71          ),
72        ),
73      ),
74    );
75  }
76}