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:
class GroundBlock {}
class PlatformBlock {}
class Star {}
Also, create water_enemy.dart in the lib/actors folder using this boilerplate code:
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:
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:
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:
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:
void loadGameSegments(int segmentIndex, double xPositionOffset) {
for (final block in segments[segmentIndex]) {
switch (block.blockType) {
case GroundBlock:
case PlatformBlock:
case Star:
case WaterEnemy:
}
}
}
You will need to add the following imports if they were not auto-imported:
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.
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),
);
world.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:
@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();
}
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:
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import '../ember_quest.dart';
class PlatformBlock extends SpriteComponent
with HasGameReference<EmberQuestGame> {
final Vector2 gridPosition;
double xOffset;
PlatformBlock({
required this.gridPosition,
required this.xOffset,
}) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);
@override
void onLoad() {
}
@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:
late EmberPlayer _ember;
double objectSpeed = 0.0;
So to implement that movement, declare a variable at the top of the PlatformBlock class and make
your update method look like this:
final Vector2 velocity = Vector2.zero();
@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:
@override
void onLoad() {
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. gridPosition 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:
case PlatformBlock:
add(PlatformBlock(
gridPosition: block.gridPosition,
xOffset: xPositionOffset,
));
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:
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 4. Adding the Remaining Components, where we will add the rest of the components now that we have a basic understanding of what we are going to accomplish.