mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-11-01 01:18:38 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			459 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			459 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # 3. Building the World
 | |
| 
 | |
| 
 | |
| ## Creating Segments
 | |
| 
 | |
| For this world to be infinite, the best way to approach this is to create segments that can be
 | |
| reloaded over and over.  To do this, we need a rough sketch of what our level segments will look
 | |
| like. I have created the following sketch to show what the segments would look like and how they can
 | |
| be repeated:
 | |
| 
 | |
| 
 | |
| 
 | |
| Each segment is a 10x10 grid and each block is 64 pixels x 64 pixels.  This means Ember Quest has a
 | |
| height of 640 with an infinite width.  In my design, there must always be a ground
 | |
| block at the beginning and the end.  Additionally, there must be at least 3 ground blocks that come
 | |
| before an enemy, including if the segment wraps to another segment.  This is because the plan is to
 | |
| have the enemies traverse back and forth for 3 blocks. Now that we have a plan for the segments,
 | |
| let's create a segment manager class.
 | |
| 
 | |
| 
 | |
| ### Segment Manager
 | |
| 
 | |
| To get started, we have to understand that we will be referencing our blocks in the segment manager,
 | |
| so first create a new folder called `lib/objects`.  In that folder, create 3 files called
 | |
| `ground_block.dart`, `platform_block.dart`, and `star.dart`.  Those files just need basic
 | |
| boilerplate code for the class, so create the following in their respective files:
 | |
| 
 | |
| ```dart
 | |
| class GroundBlock{}
 | |
| 
 | |
| class PlatformBlock{}
 | |
| 
 | |
| class Star{}
 | |
| ```
 | |
| 
 | |
| Also, create `water_enemy.dart` in the `lib/actors` folder using this boilerplate code:
 | |
| 
 | |
| ```dart
 | |
| class WaterEnemy{}
 | |
| ```
 | |
| 
 | |
| Now we can create a file called `segment_manager.dart` which will be placed in a new folder called
 | |
| `lib/managers`.  The segment manager is the heart and soul, if you will, of Ember Quest.  This is
 | |
| where you can get as creative as you want.  You do not have to follow my design, just remember that
 | |
| whatever you design, the segment must follow the rules outlined above. Add the following code to
 | |
| `segment_manager.dart`:
 | |
| 
 | |
| ```dart
 | |
| class Block {
 | |
|   // gridPosition position is always segment based X,Y.
 | |
|   // 0,0 is the bottom left corner.
 | |
|   // 10,10 is the upper right corner.
 | |
|   final Vector2 gridPosition;
 | |
|   final Type blockType;
 | |
|   Block(this.gridPosition, this.blockType);
 | |
| }
 | |
| 
 | |
| final segments = [
 | |
|   segment0,
 | |
| ];
 | |
| 
 | |
| final segment0 = [
 | |
| 
 | |
| ];
 | |
| ```
 | |
| 
 | |
| So what this does, is allows us to create segments (segment0, segment1, etc) in a list format that
 | |
| gets added to the `segments` list.  The individual segments will be made up of multiple entries of the
 | |
| `Block` class.  This information will allow us to translate the block position from a 10x10 grid to
 | |
| the actual pixel position in the game world.  To create a segment, you need to create
 | |
| entries for each block that you wish to be rendered from the sketch.
 | |
| 
 | |
| To understand each segment, if we start in the bottom left corner of the grid in the sketch, we see
 | |
| that we should place a `Block()` in the `segment0` list with a first parameter `gridPosition` of a
 | |
| `Vector2(0,0)` and a `blockType` of the `GroundBlock` class that we created earlier.  Remember, the
 | |
| very bottom left cell is x=0 and y=0 thus the `Vector2(x,y)` is `Vector2(0,0)`.
 | |
| 
 | |
| 
 | |
| 
 | |
| The full segment would look like this:
 | |
| 
 | |
| ```dart
 | |
| final segment0 = [
 | |
|   Block(Vector2(0, 0), GroundBlock),
 | |
|   Block(Vector2(1, 0), GroundBlock),
 | |
|   Block(Vector2(2, 0), GroundBlock),
 | |
|   Block(Vector2(3, 0), GroundBlock),
 | |
|   Block(Vector2(4, 0), GroundBlock),
 | |
|   Block(Vector2(5, 0), GroundBlock),
 | |
|   Block(Vector2(5, 1), WaterEnemy),
 | |
|   Block(Vector2(5, 3), PlatformBlock),
 | |
|   Block(Vector2(6, 0), GroundBlock),
 | |
|   Block(Vector2(6, 3), PlatformBlock),
 | |
|   Block(Vector2(7, 0), GroundBlock),
 | |
|   Block(Vector2(7, 3), PlatformBlock),
 | |
|   Block(Vector2(8, 0), GroundBlock),
 | |
|   Block(Vector2(8, 3), PlatformBlock),
 | |
|   Block(Vector2(9, 0), GroundBlock),
 | |
| ];
 | |
| ```
 | |
| 
 | |
| Proceed to build the remaining segments.  The full segment manager should look like this:
 | |
| 
 | |
| ```dart
 | |
| import 'package:flame/components.dart';
 | |
| 
 | |
| import '../actors/water_enemy.dart';
 | |
| import '../objects/ground_block.dart';
 | |
| import '../objects/platform_block.dart';
 | |
| import '../objects/star.dart';
 | |
| 
 | |
| class Block {
 | |
|   // gridPosition position is always segment based X,Y.
 | |
|   // 0,0 is the bottom left corner.
 | |
|   // 10,10 is the upper right corner.
 | |
|   final Vector2 gridPosition;
 | |
|   final Type blockType;
 | |
|   Block(this.gridPosition, this.blockType);
 | |
| }
 | |
| 
 | |
| final segments = [
 | |
|   segment0,
 | |
|   segment1,
 | |
|   segment2,
 | |
|   segment3,
 | |
|   segment4,
 | |
| ];
 | |
| 
 | |
| final segment0 = [
 | |
|   Block(Vector2(0, 0), GroundBlock),
 | |
|   Block(Vector2(1, 0), GroundBlock),
 | |
|   Block(Vector2(2, 0), GroundBlock),
 | |
|   Block(Vector2(3, 0), GroundBlock),
 | |
|   Block(Vector2(4, 0), GroundBlock),
 | |
|   Block(Vector2(5, 0), GroundBlock),
 | |
|   Block(Vector2(5, 1), WaterEnemy),
 | |
|   Block(Vector2(5, 3), PlatformBlock),
 | |
|   Block(Vector2(6, 0), GroundBlock),
 | |
|   Block(Vector2(6, 3), PlatformBlock),
 | |
|   Block(Vector2(7, 0), GroundBlock),
 | |
|   Block(Vector2(7, 3), PlatformBlock),
 | |
|   Block(Vector2(8, 0), GroundBlock),
 | |
|   Block(Vector2(8, 3), PlatformBlock),
 | |
|   Block(Vector2(9, 0), GroundBlock),
 | |
| ];
 | |
| 
 | |
| final segment1 = [
 | |
|   Block(Vector2(0, 0), GroundBlock),
 | |
|   Block(Vector2(1, 0), GroundBlock),
 | |
|   Block(Vector2(1, 1), PlatformBlock),
 | |
|   Block(Vector2(1, 2), PlatformBlock),
 | |
|   Block(Vector2(1, 3), PlatformBlock),
 | |
|   Block(Vector2(2, 6), PlatformBlock),
 | |
|   Block(Vector2(3, 6), PlatformBlock),
 | |
|   Block(Vector2(6, 5), PlatformBlock),
 | |
|   Block(Vector2(7, 5), PlatformBlock),
 | |
|   Block(Vector2(7, 7), Star),
 | |
|   Block(Vector2(8, 0), GroundBlock),
 | |
|   Block(Vector2(8, 1), PlatformBlock),
 | |
|   Block(Vector2(8, 5), PlatformBlock),
 | |
|   Block(Vector2(8, 6), WaterEnemy),
 | |
|   Block(Vector2(9, 0), GroundBlock),
 | |
| ];
 | |
| 
 | |
| final segment2 = [
 | |
|   Block(Vector2(0, 0), GroundBlock),
 | |
|   Block(Vector2(1, 0), GroundBlock),
 | |
|   Block(Vector2(2, 0), GroundBlock),
 | |
|   Block(Vector2(3, 0), GroundBlock),
 | |
|   Block(Vector2(3, 3), PlatformBlock),
 | |
|   Block(Vector2(4, 0), GroundBlock),
 | |
|   Block(Vector2(4, 3), PlatformBlock),
 | |
|   Block(Vector2(5, 0), GroundBlock),
 | |
|   Block(Vector2(5, 3), PlatformBlock),
 | |
|   Block(Vector2(5, 4), WaterEnemy),
 | |
|   Block(Vector2(6, 0), GroundBlock),
 | |
|   Block(Vector2(6, 3), PlatformBlock),
 | |
|   Block(Vector2(6, 4), PlatformBlock),
 | |
|   Block(Vector2(6, 5), PlatformBlock),
 | |
|   Block(Vector2(6, 7), Star),
 | |
|   Block(Vector2(7, 0), GroundBlock),
 | |
|   Block(Vector2(8, 0), GroundBlock),
 | |
|   Block(Vector2(9, 0), GroundBlock),
 | |
| ];
 | |
| 
 | |
| final segment3 = [
 | |
|   Block(Vector2(0, 0), GroundBlock),
 | |
|   Block(Vector2(1, 0), GroundBlock),
 | |
|   Block(Vector2(1, 1), WaterEnemy),
 | |
|   Block(Vector2(2, 0), GroundBlock),
 | |
|   Block(Vector2(2, 1), PlatformBlock),
 | |
|   Block(Vector2(2, 2), PlatformBlock),
 | |
|   Block(Vector2(4, 4), PlatformBlock),
 | |
|   Block(Vector2(6, 6), PlatformBlock),
 | |
|   Block(Vector2(7, 0), GroundBlock),
 | |
|   Block(Vector2(7, 1), PlatformBlock),
 | |
|   Block(Vector2(8, 0), GroundBlock),
 | |
|   Block(Vector2(8, 8), Star),
 | |
|   Block(Vector2(9, 0), GroundBlock),
 | |
| ];
 | |
| 
 | |
| final segment4 = [
 | |
|   Block(Vector2(0, 0), GroundBlock),
 | |
|   Block(Vector2(1, 0), GroundBlock),
 | |
|   Block(Vector2(2, 0), GroundBlock),
 | |
|   Block(Vector2(2, 3), PlatformBlock),
 | |
|   Block(Vector2(3, 0), GroundBlock),
 | |
|   Block(Vector2(3, 1), WaterEnemy),
 | |
|   Block(Vector2(3, 3), PlatformBlock),
 | |
|   Block(Vector2(4, 0), GroundBlock),
 | |
|   Block(Vector2(5, 0), GroundBlock),
 | |
|   Block(Vector2(5, 5), PlatformBlock),
 | |
|   Block(Vector2(6, 0), GroundBlock),
 | |
|   Block(Vector2(6, 5), PlatformBlock),
 | |
|   Block(Vector2(6, 7), Star),
 | |
|   Block(Vector2(7, 0), GroundBlock),
 | |
|   Block(Vector2(8, 0), GroundBlock),
 | |
|   Block(Vector2(8, 3), PlatformBlock),
 | |
|   Block(Vector2(9, 0), GroundBlock),
 | |
|   Block(Vector2(9, 1), WaterEnemy),
 | |
|   Block(Vector2(9, 3), PlatformBlock),
 | |
| ];
 | |
| ```
 | |
| 
 | |
| 
 | |
| ### Loading the Segments into the World
 | |
| 
 | |
| Now that our segments are defined, we need to create a way to load these blocks into our world.  To
 | |
| do that, we are going to start work in the `ember_quest.dart` file.  We will create a `loadSegments`
 | |
| method that when given an index for the segments list, will then loop through that segment from
 | |
| our `segment_manager` and we will add the appropriate blocks later.  It should look like this:
 | |
| 
 | |
| ```dart
 | |
| void loadGameSegments(int segmentIndex, double xPositionOffset) {
 | |
|     for (final block in segments[segmentIndex]) {
 | |
|       switch (block.blockType) {
 | |
|         case GroundBlock:
 | |
|           break;
 | |
|         case PlatformBlock:
 | |
|           break;
 | |
|         case Star:
 | |
|           break;
 | |
|         case WaterEnemy:
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| ```
 | |
| 
 | |
| You will need to add the following imports if they were not auto-imported:
 | |
| 
 | |
| ```dart
 | |
| import 'actors/water_enemy.dart';
 | |
| import 'managers/segment_manager.dart';
 | |
| import 'objects/ground_block.dart';
 | |
| import 'objects/platform_block.dart';
 | |
| import 'objects/star.dart';
 | |
| ```
 | |
| 
 | |
| Now we can refactor our game a bit and create an `initializeGame()` method which will call our
 | |
| `loadGameSegments` method.
 | |
| 
 | |
| ```dart
 | |
|   void initializeGame() {
 | |
|     // 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 - 70),
 | |
|     );
 | |
|     add(_ember);
 | |
|   }
 | |
| ```
 | |
| 
 | |
| We simply are taking the width of the game screen, divide that by 640 (10 blocks in a segment times
 | |
| 64 pixels wide for each block), and round that up.  As we only defined 5 segments total, we need to
 | |
| restrict that integer from 0 to the length of the segments list in case the user has a really wide
 | |
| screen.  Then we simply loop through the number of `segmentsToLoad` and call `loadGameSegments` with
 | |
| the integer to load and then calculate the offset.
 | |
| 
 | |
| Additionally, I have moved the Ember-related code from the `onLoad` method to our new
 | |
| `initializeGame` method.  This means I can now make the call in `onLoad` to `initializeGame` such
 | |
| as:
 | |
| 
 | |
| ```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();
 | |
|   }
 | |
| ```
 | |
| 
 | |
| At this point, you probably have errors for all the object classes and the enemy class, but don't
 | |
| worry, we will solve those right now.
 | |
| 
 | |
| 
 | |
| ### The Platform Block
 | |
| 
 | |
| One of the easiest blocks to start with is the Platform Block.  There are two things that we need to
 | |
| develop beyond getting the sprite to be displayed; that is, we need to place it in the correct
 | |
| position and as Ember moves across the screen, we need to remove the blocks once they are off the
 | |
| screen.  In Ember Quest, the player can only move forward, so this will keep the game lightweight as
 | |
| it's an infinite level.
 | |
| 
 | |
| Open the `lib/objects/platform_block.dart` file and add the following code:
 | |
| 
 | |
| ```dart
 | |
| import 'package:flame/collisions.dart';
 | |
| import 'package:flame/components.dart';
 | |
| 
 | |
| import '../ember_quest.dart';
 | |
| 
 | |
| class PlatformBlock extends SpriteComponent
 | |
|     with HasGameRef<EmberQuestGame> {
 | |
|   final Vector2 gridPosition;
 | |
|   double xOffset;
 | |
| 
 | |
|   PlatformBlock({
 | |
|     required this.gridPosition,
 | |
|     required this.xOffset,
 | |
|   }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);
 | |
| 
 | |
|   @override
 | |
|   Future<void> onLoad() async {
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void update(double dt) {
 | |
|     super.update(dt);
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| We are going to extend the Flame `SpriteComponent` and we will need the `HasGameRef` mixin to access
 | |
| our game class just like we did before.  We are starting with the empty `onLoad` and `update`
 | |
| methods and we will begin adding code to create the functionality that is necessary for the game.
 | |
| 
 | |
| The secret to any gaming engine is the game loop.  This is an infinite loop that calls all the
 | |
| objects in your game so you can provide updates.  The `update` method is the hook into this and it
 | |
| uses a `double dt` to pass to your method the amount of time in seconds since it was last
 | |
| called.  This `dt` variable then allows you to calculate how far your component needs to move
 | |
| on-screen.  
 | |
| 
 | |
| All components in our game will need to move at the same speed, so to do this, open
 | |
| `lib/ember_quest.dart`, and let's define a global variable called `objectSpeed`.  At the top of the
 | |
| `EmberQuestGame` class, add:
 | |
| 
 | |
| ```dart
 | |
|   late EmberPlayer _ember;
 | |
| double objectSpeed = 0.0;
 | |
| ```
 | |
| 
 | |
| So to implement that movement, declare a variable at the top of the `EmberQuestGame` class and make
 | |
| your `update` method look like this:
 | |
| 
 | |
| ```dart
 | |
| final Vector2 velocity = Vector2.zero();
 | |
| ```
 | |
| 
 | |
| ```dart
 | |
|   @override
 | |
|   void update(double dt) {
 | |
|     velocity.x = game.objectSpeed;
 | |
|     position += velocity * dt;
 | |
|     if (position.x < -size.x) removeFromParent();
 | |
|     super.update(dt);
 | |
|   }
 | |
| ```
 | |
| 
 | |
| All that is happening is we define a base `velocity` that is instantiated at 0 on both axes and then
 | |
| we update `velocity` using the global `objectSpeed` variable for the x-axis.  As this is our
 | |
| platform block, it will only scroll left and right, so our y-axis in the `velocity` will always be 0
 | |
| as do not want our blocks jumping.
 | |
| 
 | |
| Next, we update the `position` which is a special variable built into the Flame engine components.
 | |
| By multiplying the `velocity` vector by the `dt` we can move our component to the required amount.
 | |
| 
 | |
| Finally, if `x` value of position is `-size.x` (this means off the left side of the screen by the
 | |
| width of the image) then remove this platform block from the game entirely.
 | |
| 
 | |
| Now we just need to finish the `onLoad` method.  So make your `onLoad` method look like this:
 | |
| 
 | |
| ```dart
 | |
|   @override
 | |
|   Future<void> onLoad() async {
 | |
|     final platformImage = game.images.fromCache('block.png');
 | |
|     sprite = Sprite(platformImage);
 | |
|     position = Vector2((_gridPosition.x * size.x) + _xOffset,
 | |
|         game.size.y - (_gridPosition.y * size.y),
 | |
|     );
 | |
|     add(RectangleHitbox()..collisionType = CollisionType.passive);
 | |
|   }
 | |
| ```
 | |
| 
 | |
| First, we retrieve the image from cache as we did before, and because this is a `SpriteComponent`
 | |
| we can use the built-in `sprite` variable to assign the image to the component.  Next, we need to
 | |
| calculate its starting position.  This is where all the magic happens, so let's break this down.
 | |
| 
 | |
| Just like in the `update` method we will be setting the `position` variable to a `Vector2`.  To
 | |
| determine where it needs to be, we need to calculate the x and y positions.  Focusing on the x
 | |
| first, we can see that we are taking `gridPosition.x` times the width of the image and then we will
 | |
| add that to the `xOffset` that we pass in.  With the y-axis, we will take the height of the
 | |
| game and we will subtract the `gridPosition.y` times the height of the image.
 | |
| 
 | |
| Lastly, as we want Ember to be able to interact with the platform, we will add a `RectangleHitbox`
 | |
| with a `passive` `CollisionType`.  Collisions will be explained more in a later chapter.
 | |
| 
 | |
| 
 | |
| #### Display the Platform
 | |
| 
 | |
| In our `loadGameSegments` method from earlier, we will need to add the call to add our block.  We
 | |
| will need to define `gridPosition` and `xOffset` to be passed in.  `gridPostion` will be a
 | |
| `Vector2` and `xOffset` is a double as that will be used to calculate the x-axis offset for
 | |
| the block in a `Vector2`.  So add the following to your `loadGameSegments` method:
 | |
| 
 | |
| ```dart
 | |
| case PlatformBlock:
 | |
|     add(PlatformBlock(
 | |
|       gridPosition: block.gridPosition,
 | |
|       xOffset: xPositionOffset,
 | |
|     ));
 | |
|     break;
 | |
| ```
 | |
| 
 | |
| If you run your code, you should now see:
 | |
| 
 | |
| 
 | |
| 
 | |
| While this does run, the black just makes it look like Ember is in a dungeon.  Let's change that
 | |
| background real quick so there is a nice blue sky.  Just add the following code to
 | |
| `lib/ember_quest.dart`:
 | |
| 
 | |
| ```dart
 | |
| import 'package:flutter/material.dart';
 | |
| 
 | |
| @override
 | |
| Color backgroundColor() {
 | |
|     return const Color.fromARGB(255, 173, 223, 247);
 | |
| }
 | |
| ```
 | |
| 
 | |
| Excellent!  Ember is now in front of a blue sky.
 | |
| 
 | |
| On to [](step_4.md), where we will add the rest of the components now that we have a basic
 | |
| understanding of what we are going to accomplish.
 | 
