mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-11-01 01:18:38 +08:00 
			
		
		
		
	 6938c860a0
			
		
	
	6938c860a0
	
	
	
		
			
			This adds a platformer tutorial called Ember Quest. I hope I have done a service, because I am tired, lol. I am sure there will be comments. I just want to say, I did my best. I approached this as someone new to Flame, just like I was about 10 months ago. Are there concepts that can be improved, sure. We can always optimize code, but I didn't want any concepts to be super abstract. I had never coded a game before when I began my journey with Flame this year, so things might be a bit simple for experienced game developers, but for myself, I had never even thought about a game loop or animations, etc.
		
			
				
	
	
		
			323 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			323 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # 7. Adding Menus
 | |
| 
 | |
| To add menus to the game, we will leverage Flame's built-in
 | |
| [overlay](../../flame/game.md#flutter-widgets-and-game-instances) system.  
 | |
| 
 | |
| 
 | |
| ## Main Menu
 | |
| 
 | |
| In the `lib/overlays` folder, create `main_menu.dart` and add the following code:
 | |
| 
 | |
| ```dart
 | |
| 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:
 | |
| 
 | |
| ```dart
 | |
| 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:
 | |
| 
 | |
| ```dart
 | |
| @override
 | |
| Future<void> onLoad() async {
 | |
|     await images.loadAll([
 | |
|         'block.png',
 | |
|         'ember.png',
 | |
|         'ground.png',
 | |
|         'heart_half.png',
 | |
|         'heart.png',
 | |
|         'star.png',
 | |
|         'water_enemy.png',
 | |
|     ]);
 | |
|     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`:
 | |
| 
 | |
| ```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:
 | |
| 
 | |
| ```dart
 | |
| 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:
 | |
| 
 | |
| ```dart
 | |
| // 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:
 | |
| 
 | |
| ```dart
 | |
| if (position.x < -size.x || game.health <= 0) {
 | |
|     removeFromParent();
 | |
| }
 | |
| ```
 | |
| 
 | |
| In `lib/objects/ground_block.dart`, in the `update` method update the following code:
 | |
| 
 | |
| ```dart
 | |
| if (game.health <= 0) {
 | |
|     removeFromParent();
 | |
| }
 | |
| ```
 | |
| 
 | |
| In `lib/objects/platform_block.dart`, in the `update` method update the following code:
 | |
| 
 | |
| ```dart
 | |
| if (position.x < -size.x || game.health <= 0) {
 | |
|     removeFromParent();
 | |
| }
 | |
| ```
 | |
| 
 | |
| In `lib/objects/star.dart`, in the `update` method update the following code:
 | |
| 
 | |
| ```dart
 | |
| if (position.x < -size.x || game.health <= 0) {
 | |
|     removeFromParent();
 | |
| }
 | |
| ```
 | |
| 
 | |
| Finally, in `lib/ember_quest.dart`, add the following `update` method:
 | |
| 
 | |
| ```dart
 | |
| @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.
 | |
| 
 | |
| ```{flutter-app}
 | |
| :sources: ../tutorials/platformer/app
 | |
| :show: popup code
 | |
| ```
 |