mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 19:12:31 +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.
|