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:

Level Segment Sketch

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).

Segment 0 Sketch

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:
          break;
        case PlatformBlock:
          break;
        case Star:
          break;
        case WaterEnemy:
          break;
      }
    }
  }

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),
    );
    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',
    ]);
    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 HasGameRef<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,
    ));
    break;

If you run your code, you should now see:

Platforms Displayed

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.