Warning: you are currently viewing the docs for an older version of Flame.
Please click here to go see the documentation for the latest released version.
7. Adding Menus¶
To add menus to the game, we will leverage Flame’s built-in overlay system.
Main Menu¶
In the lib/overlays
folder, create main_menu.dart
and add the following code:
import 'package:flutter/material.dart';
import '../ember_quest.dart';
class MainMenu extends StatelessWidget {
// Reference to parent game.
final EmberQuestGame game;
const MainMenu({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: 250,
width: 300,
decoration: const BoxDecoration(
color: blackTextColor,
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Ember Quest',
style: TextStyle(
color: whiteTextColor,
fontSize: 24,
),
),
const SizedBox(height: 40),
SizedBox(
width: 200,
height: 75,
child: ElevatedButton(
onPressed: () {
game.overlays.remove('MainMenu');
},
style: ElevatedButton.styleFrom(
backgroundColor: whiteTextColor,
),
child: const Text(
'Play',
style: TextStyle(
fontSize: 40.0,
color: blackTextColor,
),
),
),
),
const SizedBox(height: 20),
const Text(
'''Use WASD or Arrow Keys for movement.
Space bar to jump.
Collect as many stars as you can and avoid enemies!''',
textAlign: TextAlign.center,
style: TextStyle(
color: whiteTextColor,
fontSize: 14,
),
),
],
),
),
),
);
}
}
This is a pretty self-explanatory file that just uses standard Flutter widgets to display
information and provide a Play
button. The only Flame-related line is
game.overlays.remove('MainMenu');
which simply removes the overlay so the user can play the
game. It should be noted that the user can technically move Ember while this is displayed, but
trapping the input is outside the scope of this tutorial as there are multiple ways this can be
accomplished.
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.
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}
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}
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}
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}
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];
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}
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}
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}
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}
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}
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}
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}