chore: Remove 1.8.0 deprecations (#2538)

Removes all the deprecated methods before 1.8.0 release.
This commit is contained in:
Lukas Klingsbo
2023-05-22 19:01:55 +02:00
committed by GitHub
parent 6e1d5466aa
commit 2d45d2be39
139 changed files with 1224 additions and 1084 deletions

View File

@ -166,12 +166,6 @@ KlondikeGame
└─ CameraComponent
```
```{note}
The **Camera** system described in this tutorial is different from the
"official" camera available as a property of the `FlameGame` class. The latter
may become deprecated in the future.
```
For this game I've been drawing my image assets having in mind the dimension of
a single card at 1000×1400 pixels. So, this will serve as the reference size for
determining the overall layout. Another important measurement that affects the

View File

@ -1,3 +1,4 @@
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
@ -23,9 +24,12 @@ class EmberQuestGame extends FlameGame
double cloudSpeed = 0.0;
double objectSpeed = 0.0;
final world = World();
late final CameraComponent cameraComponent;
@override
Future<void> onLoad() async {
//debugMode = true; //Uncomment to see the bounding boxes
//debugMode = true; // Uncomment to see the bounding boxes
await images.loadAll([
'block.png',
'ember.png',
@ -35,6 +39,10 @@ class EmberQuestGame extends FlameGame
'star.png',
'water_enemy.png',
]);
cameraComponent = CameraComponent(world: world);
cameraComponent.viewfinder.anchor = Anchor.topLeft;
addAll([cameraComponent, world]);
initializeGame(loadHud: true);
}
@ -55,7 +63,7 @@ class EmberQuestGame extends FlameGame
for (final block in segments[segmentIndex]) {
switch (block.blockType) {
case GroundBlock:
add(
world.add(
GroundBlock(
gridPosition: block.gridPosition,
xOffset: xPositionOffset,
@ -63,7 +71,7 @@ class EmberQuestGame extends FlameGame
);
break;
case PlatformBlock:
add(
world.add(
PlatformBlock(
gridPosition: block.gridPosition,
xOffset: xPositionOffset,
@ -71,7 +79,7 @@ class EmberQuestGame extends FlameGame
);
break;
case Star:
add(
world.add(
Star(
gridPosition: block.gridPosition,
xOffset: xPositionOffset,
@ -79,7 +87,7 @@ class EmberQuestGame extends FlameGame
);
break;
case WaterEnemy:
add(
world.add(
WaterEnemy(
gridPosition: block.gridPosition,
xOffset: xPositionOffset,
@ -102,9 +110,9 @@ class EmberQuestGame extends FlameGame
_ember = EmberPlayer(
position: Vector2(128, canvasSize.y - 128),
);
add(_ember);
world.add(_ember);
if (loadHud) {
add(Hud());
cameraComponent.viewport.add(Hud());
}
}

View File

@ -13,9 +13,7 @@ class Hud extends PositionComponent with HasGameRef<EmberQuestGame> {
super.anchor,
super.children,
super.priority = 5,
}) {
positionType = PositionType.viewport;
}
});
late TextComponent _scoreTextComponent;

View File

@ -58,8 +58,9 @@ class MainMenu extends StatelessWidget {
),
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!''',
'''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,

View File

@ -4,7 +4,7 @@
## The Plan
Now that we have the assets loaded and a very rough idea of what classes we will need, we need to
start thinking about how we will implement this game and our goals. To do this, let's break down
start thinking about how we will implement this game and our goals. To do this, let's break down
what the game should do:
- Ember should be able to be controlled to move left, right, and jump.
@ -17,7 +17,7 @@ what the game should do:
- There should be a main menu and a game-over screen that lets the player start over.
Now that this is planned out, I know you are probably as excited as I am to begin and I just want to
see Ember on the screen. So let's do that first.
see Ember on the screen. So let's do that first.
```{note}
Why did I choose to make this game an infinite side scrolling platformer?
@ -25,16 +25,16 @@ Why did I choose to make this game an infinite side scrolling platformer?
Well, I wanted to be able to showcase random level loading. No two game plays
will be the same. This exact setup can easily be adapted to be a traditional
level game. As you make your way through this tutorial, you will see how we
could modify the level code to have an end. I will add a note in that section
could modify the level code to have an end. I will add a note in that section
to explain the appropriate mechanics.
```
## Loading Assets
For Ember to be displayed, we will need to load the assets. This can be done in `main.dart`, but by
so doing, we will quickly clutter the file. To keep our game organized, we should create files that
have a single focus. So let's create a file in the `lib` folder called `ember_quest.dart`. In that
For Ember to be displayed, we will need to load the assets. This can be done in `main.dart`, but by
so doing, we will quickly clutter the file. To keep our game organized, we should create files that
have a single focus. So let's create a file in the `lib` folder called `ember_quest.dart`. In that
file, we will add:
```dart
@ -62,14 +62,14 @@ class EmberQuestGame extends FlameGame {
As I mentioned in the [assets](step_1.md#assets) section, we are using multiple individual image
files and for performance reasons, we should leverage Flame's built-in caching system which will
only load the files once, but allow us to access them as many times as needed without an impact to
the game. `await images.loadAll()` takes a list of the file names that are found in `assets\images`
the game. `await images.loadAll()` takes a list of the file names that are found in `assets\images`
and loads them to cache.
## Scaffolding
So now that we have our game file, let's prepare the `main.dart` file to receive our newly created
`FlameGame`. Change your entire `main.dart` file to the following:
`FlameGame`. Change your entire `main.dart` file to the following:
```dart
import 'package:flame/game.dart';
@ -86,14 +86,53 @@ void main() {
}
```
You can run this file and you should just have a blank screen now. Let's get Ember loaded!
You can run this file and you should just have a blank screen now. Let's get Ember loaded!
## CameraComponent and World
Since `FlameGame.camera` is deprecated, we want to add a `CameraComponent` that we can move around,
and a world that we can add all our components to and move around our player in.
(The `CameraComponent` will be built-in in Flame v2).
```dart
import 'package:flame/components.dart';
import 'package:flame/game.dart';
class EmberQuestGame extends FlameGame {
EmberQuestGame();
final world = World();
late final CameraComponent cameraComponent;
@override
Future<void> onLoad() async {
await images.loadAll([
'block.png',
'ember.png',
'ground.png',
'heart_half.png',
'heart.png',
'star.png',
'water_enemy.png',
]);
cameraComponent = CameraComponent(world: world);
// Everything in this tutorial assumes that the position
// of the `CameraComponent`s viewfinder (where the camera is looking)
// is in the top left corner, that's why we set the anchor here.
cameraComponent.viewfinder.anchor = Anchor.topLeft;
addAll([cameraComponent, world]);
}
}
```
## Ember Time
Keeping your game files organized can always be a challenge. I like to keep things logically
organized by how they will be involved in my game. So for Ember, let's create the following folder,
`lib/actors` and in that folder, create `ember.dart`. In that file, add the following code:
Keeping your game files organized can always be a challenge. I like to keep things logically
organized by how they will be involved in my game. So for Ember, let's create the following folder,
`lib/actors` and in that folder, create `ember.dart`. In that file, add the following code:
```dart
import 'package:flame/components.dart';
@ -121,19 +160,19 @@ class EmberPlayer extends SpriteAnimationComponent
```
This file uses the `HasGameRef` mixin which allows us to reach back to `ember_quest.dart` and
leverage any of the variables or methods that are defined in the game class. You can see this in
use with the line `game.images.fromCache('ember.png')`. Earlier, we loaded all the files into
leverage any of the variables or methods that are defined in the game class. You can see this in
use with the line `game.images.fromCache('ember.png')`. Earlier, we loaded all the files into
cache, so to use that file now, we call `fromCache` so it can be leveraged by the `SpriteAnimation`.
The `EmberPlayer` class is extending a `SpriteAnimationComponent` which allows us to define
animation as well as position it accordingly in our game world. When we construct this class, the
animation as well as position it accordingly in our game world. When we construct this class, the
default size of `Vector2.all(64)` is defined as the size of Ember in our game world should be 64x64.
You may notice that in the animation `SpriteAnimationData`, the `textureSize` is defined as
`Vector2.all(16)` or 16x16. This is because the individual frame in our `ember.png` is 16x16 and
there are 4 frames in total. To define the speed of the animation, `stepTime` is used and set at
`0.12` seconds per frame. You can change the `stepTime` to any length that makes the animation seem
`Vector2.all(16)` or 16x16. This is because the individual frame in our `ember.png` is 16x16 and
there are 4 frames in total. To define the speed of the animation, `stepTime` is used and set at
`0.12` seconds per frame. You can change the `stepTime` to any length that makes the animation seem
correct for your game vision.
Now before you rush to run the game again, we have to add Ember to the game world. To do this, go
Now before you rush to run the game again, we have to add Ember to the game world. To do this, go
back to `ember_quest.dart` and add the following:
```dart
@ -145,6 +184,9 @@ class EmberQuestGame extends FlameGame {
EmberQuestGame();
late EmberPlayer _ember;
final world = World();
late final CameraComponent cameraComponent;
@override
Future<void> onLoad() async {
@ -157,10 +199,15 @@ class EmberQuestGame extends FlameGame {
'star.png',
'water_enemy.png',
]);
cameraComponent = CameraComponent(world: world);
cameraComponent.viewfinder.anchor = Anchor.topLeft;
addAll([cameraComponent, world]);
_ember = EmberPlayer(
position: Vector2(128, canvasSize.y - 70),
);
add(_ember);
world.add(_ember);
}
}
```
@ -171,4 +218,4 @@ Run your game now and you should now see Ember flickering in the lower left-hand
## Building Blocks
Now that we have Ember showing on screen and we know our basic environment is all working correctly,
it's time to create a world for Embers Quest! Proceed on to [](step_3.md)!
it's time to create a world for Embers Quest! Proceed on to [](step_3.md)!

View File

@ -4,16 +4,16 @@
## 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
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](../../images/tutorials/platformer/LevelSegmentSketch.jpg)
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
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.
@ -21,8 +21,8 @@ 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
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
@ -40,8 +40,8 @@ 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
`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`:
@ -65,14 +65,14 @@ 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
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
`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](../../images/tutorials/platformer/Segment0Sketch.jpg)
@ -99,7 +99,7 @@ final segment0 = [
];
```
Proceed to build the remaining segments. The full segment manager should look like this:
Proceed to build the remaining segments. The full segment manager should look like this:
```dart
import 'package:flame/components.dart';
@ -225,10 +225,10 @@ final segment4 = [
### 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`
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:
our `segment_manager` and we will add the appropriate blocks later. It should look like this:
```dart
void loadGameSegments(int segmentIndex, double xPositionOffset) {
@ -273,18 +273,18 @@ Now we can refactor our game a bit and create an `initializeGame()` method which
_ember = EmberPlayer(
position: Vector2(128, canvasSize.y - 70),
);
add(_ember);
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
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
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
`initializeGame` method. This means I can now make the call in `onLoad` to `initializeGame` such
as:
```dart
@ -299,6 +299,11 @@ as:
'star.png',
'water_enemy.png',
]);
cameraComponent = CameraComponent(world: world);
cameraComponent.viewfinder.anchor = Anchor.topLeft;
addAll([cameraComponent, world]);
initializeGame();
}
```
@ -309,10 +314,10 @@ 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
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
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:
@ -345,22 +350,22 @@ class PlatformBlock extends SpriteComponent
```
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`
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
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.
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
`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;
double objectSpeed = 0.0;
```
So to implement that movement, declare a variable at the top of the `PlatformBlock` class and make
@ -381,7 +386,7 @@ final Vector2 velocity = Vector2.zero();
```
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
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.
@ -391,7 +396,7 @@ By multiplying the `velocity` vector by the `dt` we can move our component to th
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:
Now we just need to finish the `onLoad` method. So make your `onLoad` method look like this:
```dart
@override
@ -406,25 +411,25 @@ Now we just need to finish the `onLoad` method. So make your `onLoad` method lo
```
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.
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
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
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.
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
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:
the block in a `Vector2`. So add the following to your `loadGameSegments` method:
```dart
case PlatformBlock:
@ -439,8 +444,8 @@ If you run your code, you should now see:
![Platforms Displayed](../../images/tutorials/platformer/Step3Platforms.jpg)
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
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
@ -452,7 +457,7 @@ Color backgroundColor() {
}
```
Excellent! Ember is now in front of a blue sky.
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.

View File

@ -3,9 +3,9 @@
## Star
The star is pretty simple. It is just like the Platform block except we are going to add an effect
to make it pulse in size. For the effect to look correct, we need to change the object's `Anchor`
to `center`. This means we will need to adjust the position by half of the image size. For brevity,
The star is pretty simple. It is just like the Platform block except we are going to add an effect
to make it pulse in size. For the effect to look correct, we need to change the object's `Anchor`
to `center`. This means we will need to adjust the position by half of the image size. For brevity,
I am going to add the whole class and explain the additional changes after.
```dart
@ -33,8 +33,8 @@ class Star extends SpriteComponent
final starImage = game.images.fromCache('star.png');
sprite = Sprite(starImage);
position = Vector2(
(gridPosition.x * size.x) + xOffset + (size.x / 2),
game.size.y - (gridPosition.y * size.y) - (size.y / 2),
(gridPosition.x * size.x) + xOffset + (size.x / 2),
game.size.y - (gridPosition.y * size.y) - (size.y / 2),
);
add(RectangleHitbox(collisionType: CollisionType.passive));
add(
@ -64,30 +64,32 @@ So the only change between the Star and the Platform beyond the anchor is simply
```dart
add(
SizeEffect.by(
Vector2(-24, -24),
SizeEffect.by(
Vector2(-24, -24),
EffectController(
duration: .75,
reverseDuration: .5,
infinite: true,
curve: Curves.easeOut,
),
duration: .75,
reverseDuration: .5,
infinite: true,
curve: Curves.easeOut,
),
),
);
```
The `SizeEffect` is best explained by going to their [help
docs](../../flame/effects.md#sizeeffectby). In short, we simply reduce the size of the star
docs](../../flame/effects.md#sizeeffectby). In short, we simply reduce the size of the star
by -24 pixels in both directions and we make it pulse infinitely using the `EffectController`.
Don't forget to add the star to your `lib/ember_quest.dart` file by doing:
```dart
case Star:
add(Star(
add(
Star(
gridPosition: block.gridPosition,
xOffset: xPositionOffset,
));
),
);
break;
```
@ -129,8 +131,8 @@ class WaterEnemy extends SpriteAnimationComponent
),
);
position = Vector2(
(gridPosition.x * size.x) + xOffset + (size.x / 2),
game.size.y - (gridPosition.y * size.y) - (size.y / 2),
(gridPosition.x * size.x) + xOffset + (size.x / 2),
game.size.y - (gridPosition.y * size.y) - (size.y / 2),
);
add(RectangleHitbox(collisionType: CollisionType.passive));
add(
@ -158,23 +160,25 @@ class WaterEnemy extends SpriteAnimationComponent
The water drop enemy is an animation just like Ember, so this class is extending the
`SpriteAnimationComponent` class but it uses all of the previous code we have used for the Star and
the Platform. The only difference will be instead of the `SizeEffect`, we are going to use the
`MoveEffect`. The best resource for information will be their [help
docs](../../flame/effects.md#sizeeffectby).
the Platform. The only difference will be instead of the `SizeEffect`, we are going to use the
`MoveEffect`. The best resource for information will be their [help
docs](../../flame/effects.md#sizeeffectby).
In short, the `MoveEffect` will last for 3 seconds, alternate directions, and run infinitely. It
will move our enemy to the left, 128 pixels (-2 x image width). You may have noticed that in the
constructor, I set `Anchor` to `center`. This was done just for the sake of making the calculations
In short, the `MoveEffect` will last for 3 seconds, alternate directions, and run infinitely. It
will move our enemy to the left, 128 pixels (-2 x image width). You may have noticed that in the
constructor, I set `Anchor` to `center`. This was done just for the sake of making the calculations
easier but could have been left as `bottomLeft`.
Don't forget to add the water enemy to your `lib/ember_quest.dart` file by doing:
```dart
case WaterEnemy:
add(WaterEnemy(
gridPosition: block.gridPosition,
xOffset: xPositionOffset,
));
add(
WaterEnemy(
gridPosition: block.gridPosition,
xOffset: xPositionOffset,
),
);
break;
```
@ -185,7 +189,7 @@ If you run the game now, the Water Enemy should be displayed and moving!
## Ground Blocks
Finally, the last component that needs to be displayed is the Ground Block! This component is more
Finally, the last component that needs to be displayed is the Ground Block! This component is more
complex than the others as we need to identify two times during a block's life cycle.
- When the block is added, if it is the last block in the segment, we need to update a global value
@ -217,8 +221,9 @@ class GroundBlock extends SpriteComponent with HasGameRef<EmberQuestGame> {
void onLoad() {
final groundImage = game.images.fromCache('ground.png');
sprite = Sprite(groundImage);
position = Vector2((gridPosition.x * size.x) + xOffset,
game.size.y - (gridPosition.y * size.y),
position = Vector2(
gridPosition.x * size.x + xOffset,
game.size.y - gridPosition.y * size.y,
);
add(RectangleHitbox(collisionType: CollisionType.passive));
}
@ -233,7 +238,7 @@ class GroundBlock extends SpriteComponent with HasGameRef<EmberQuestGame> {
```
The first thing we will tackle is registering the block globally if it is the absolute last block to
be loaded. To do this, add two new global variables in `lib/ember_quest.dart` called:
be loaded. To do this, add two new global variables in `lib/ember_quest.dart` called:
```dart
late double lastBlockXPosition = 0.0;
@ -279,14 +284,14 @@ Now we can address updating this information, so in the `update` method, add the
```
`game.lastBlockXPosition` is being updated by the block's current x-axis position plus its width -
10 pixels. This will cause a little overlap, but due to the potential variance in `dt` this
10 pixels. This will cause a little overlap, but due to the potential variance in `dt` this
prevents gaps in the map as it loads while a player is moving.
### Loading the Next Random Segment
To load the next random segment, we will use the `Random()` function that is built-in to
`dart:math`. The following line of code gets a random integer from 0 (inclusive) to the max number
`dart:math`. The following line of code gets a random integer from 0 (inclusive) to the max number
in the passed parameter (exclusive).
```dart
@ -301,7 +306,9 @@ if (position.x < -size.x) {
removeFromParent();
if (gridPosition.x == 0) {
game.loadGameSegments(
Random().nextInt(segments.length), game.lastBlockXPosition);
Random().nextInt(segments.length),
game.lastBlockXPosition,
);
}
}
```
@ -309,7 +316,7 @@ if (position.x < -size.x) {
This simply extends the code that we have in our other objects, where once the block is off the
screen and if the block is the first block of the segment, we will call the `loadGameSegments`
method in our game class, get a random number between 0 and the number of segments and pass in the
offset. If `Random()` or `segments.length` does not auto-import, you will need:
offset. If `Random()` or `segments.length` does not auto-import, you will need:
```dart
import 'dart:math';
@ -345,8 +352,9 @@ class GroundBlock extends SpriteComponent with HasGameRef<EmberQuestGame> {
void onLoad() {
final groundImage = game.images.fromCache('ground.png');
sprite = Sprite(groundImage);
position = Vector2((gridPosition.x * size.x) + xOffset,
game.size.y - (gridPosition.y * size.y),
position = Vector2(
gridPosition.x * size.x + xOffset,
game.size.y - gridPosition.y * size.y,
);
add(RectangleHitbox(collisionType: CollisionType.passive));
if (gridPosition.x == 9 && position.x > game.lastBlockXPosition) {
@ -364,8 +372,9 @@ class GroundBlock extends SpriteComponent with HasGameRef<EmberQuestGame> {
removeFromParent();
if (gridPosition.x == 0) {
game.loadGameSegments(
Random().nextInt(segments.length),
game.lastBlockXPosition);
Random().nextInt(segments.length),
game.lastBlockXPosition,
);
}
}
if (gridPosition.x == 9) {
@ -384,10 +393,12 @@ Finally, don't forget to add your Ground Block to `lib/ember_quest.dart` by addi
```dart
case GroundBlock:
add(GroundBlock(
add(
GroundBlock(
gridPosition: block.gridPosition,
xOffset: xPositionOffset,
));
),
);
break;
```
@ -395,6 +406,6 @@ If you run your code, your game should now look like this:
![Ground Blocks](../../images/tutorials/platformer/Step4Ground.jpg)
You might say, but wait! Ember is in the middle of the ground and that is correct because Ember's
`Anchor` is set to center. This is ok and we will be addressing this in [](step_5.md) where we will
You might say, but wait! Ember is in the middle of the ground and that is correct because Ember's
`Anchor` is set to center. This is ok and we will be addressing this in [](step_5.md) where we will
be adding movement and collisions to Ember!

View File

@ -1,11 +1,11 @@
# 5. Controlling Movement
If you were waiting for some serious coding, this chapter is it. Prepare yourself as we dive in!
If you were waiting for some serious coding, this chapter is it. Prepare yourself as we dive in!
## Keyboard Controls
The first step will be to allow control of Ember via the keyboard. We need to start by adding the
The first step will be to allow control of Ember via the keyboard. We need to start by adding the
appropriate mixins to the game class and Ember. Add the following:
`lib/ember_quest.dart'
@ -39,7 +39,7 @@ import 'package:flutter/services.dart';
```
To control Ember's movement, it is easiest to set a variable where we think of the direction of
movement like a normalized vector, meaning the value will be restricted to -1, 0, or 1. So let's
movement like a normalized vector, meaning the value will be restricted to -1, 0, or 1. So let's
set a variable at the top of the class:
```dart
@ -65,8 +65,8 @@ Now in our `onKeyEvent` method, we can register the key pressed by adding:
}
```
Let's make Ember move by adding a few lines of code and creating our `update` method. First, we
need to define a velocity variable for Ember. Add the following at the top of the `EmberPlayer`
Let's make Ember move by adding a few lines of code and creating our `update` method. First, we
need to define a velocity variable for Ember. Add the following at the top of the `EmberPlayer`
class:
```dart
@ -104,10 +104,10 @@ Now Ember looks in the direction they are traveling.
## Collisions
It is time to get into the thick of it with collisions. I highly suggest reading the
[documentation](../../flame/collision_detection.md) to understand how collisions work in Flame. The
It is time to get into the thick of it with collisions. I highly suggest reading the
[documentation](../../flame/collision_detection.md) to understand how collisions work in Flame. The
first thing we need to do is make the game aware that collisions are going to occur using the
`HasCollisionDetection` mixin. Add that to `lib/ember_quest.dart` like:
`HasCollisionDetection` mixin. Add that to `lib/ember_quest.dart` like:
```dart
class EmberQuestGame extends FlameGame
@ -182,7 +182,7 @@ add(
```
Now that we have the basic collisions created, we can add gravity so Ember exists in a game world
with very basic physics. To do that, we need to create some more variables:
with very basic physics. To do that, we need to create some more variables:
```dart
final double gravity = 15;
@ -219,7 +219,7 @@ velocity.y = velocity.y.clamp(-jumpSpeed, terminalVelocity);
```
Earlier I mentioned that Ember was in the center of the grass, to solve this and show how collisions
and gravity work with Ember, I like to add a little drop-in when you start the game. So in
and gravity work with Ember, I like to add a little drop-in when you start the game. So in
`lib/ember_quest.dart` in the `initializeGame` method, change the following:
```dart
@ -293,8 +293,8 @@ collide with an enemy, Ember should blink.
## Adding the Scrolling
This is our last task with Ember. We need to restrict Ember's movement because as of now, Ember can
go off-screen and we never move the map. So to implement this feature, we simply need to add the
This is our last task with Ember. We need to restrict Ember's movement because as of now, Ember can
go off-screen and we never move the map. So to implement this feature, we simply need to add the
following to our `update` method:
```dart
@ -311,8 +311,8 @@ if (position.x + 64 >= game.size.x / 2 && horizontalDirection > 0) {
```
If you run the game now, Ember can't move off-screen to the left, and as Ember moves to the right,
once they get to the middle of the screen, the rest of the objects scroll by. This is because we
are now updating `game.objectSpeed` which we established early on in the series. Additionally,
once they get to the middle of the screen, the rest of the objects scroll by. This is because we
are now updating `game.objectSpeed` which we established early on in the series. Additionally,
you will see the next random segment be generated and added to the level based on the work we did in
Ground Block.
@ -325,5 +325,5 @@ object, we could reload the level and start all over maintaining the stars
collected and health.
```
We are almost done! In [](step_6.md), we will add the health system, keep track of
We are almost done! In [](step_6.md), we will add the health system, keep track of
the score, and provide a HUD to relay that information to the player.

View File

@ -3,8 +3,8 @@
## Setting up the HUD
Now that the game is up and running, the rest of the code should come fairly easily. To prepare for
the hud, we need to add some variables in `lib/ember_quest.dart`. Add the following to the top of
Now that the game is up and running, the rest of the code should come fairly easily. To prepare for
the hud, we need to add some variables in `lib/ember_quest.dart`. Add the following to the top of
the class:
```dart
@ -13,8 +13,8 @@ int health = 3;
```
Start by creating a folder called `lib/overlays`, and in that folder, create a component called
`heart.dart`. This is going to be the health monitoring component in the upper left-hand corner of
the game. Add the following code:
`heart.dart`. This is going to be the health monitoring component in the upper left-hand corner of
the game. Add the following code:
```dart
import 'package:ember_quest/ember_quest.dart';
@ -74,7 +74,7 @@ class HeartHealthComponent extends SpriteGroupComponent<HeartState>
```
The `HeartHealthComponent` is just a [SpriteGroupComponent](../../flame/components.md#spritegroup)
that uses the heart images that were created early on. The unique thing that is being done, is when
that uses the heart images that were created early on. The unique thing that is being done, is when
the component is created, it requires a `heartNumber`, so in the `update` method, we check to see if
the `game.health` is less than the `heartNumber` and if so, change the state of the component to
unavailable.
@ -97,14 +97,12 @@ class Hud extends PositionComponent with HasGameRef<EmberQuestGame> {
super.anchor,
super.children,
super.priority = 5,
}) {
positionType = PositionType.viewport;
}
});
late TextComponent _scoreTextComponent;
@override
Future<void>? onLoad() async {
Future<void> onLoad() async {
_scoreTextComponent = TextComponent(
text: '${game.starsCollected}',
textRenderer: TextPaint(
@ -138,26 +136,23 @@ class Hud extends PositionComponent with HasGameRef<EmberQuestGame> {
),
);
}
return super.onLoad();
}
@override
void update(double dt) {
_scoreTextComponent.text = '${game.starsCollected}';
super.update(dt);
}
}
```
In the `onLoad` method, you can see where we loop from 1 to the `game.health` amount, to create
the number of hearts necessary. The last step is to add the hud to the game.
the number of hearts necessary. The last step is to add the hud to the game.
Go to 'lib/ember_quest.dart` and add the following code in the `initializeGame` method:
```dart
add(Hud());
cameraComponent.viewport.add(Hud());
```
If the auto-import did not occur, you will need to add:
@ -173,38 +168,38 @@ If you run the game now, you should see:
## Updating the HUD Data
The last thing we need to do before closing out the HUD is to update the data. To do this, we need
The last thing we need to do before closing out the HUD is to update the data. To do this, we need
to open `lib/actors/ember.dart` and add the following code:
`onCollision`
```dart
if (other is Star) {
other.removeFromParent();
game.starsCollected++;
other.removeFromParent();
game.starsCollected++;
}
```
```dart
void hit() {
if (!hitByEnemy) {
if (!hitByEnemy) {
game.health--;
hitByEnemy = true;
}
add(
}
add(
OpacityEffect.fadeOut(
EffectController(
alternate: true,
duration: 0.1,
repeatCount: 5,
),
EffectController(
alternate: true,
duration: 0.1,
repeatCount: 5,
),
)..onComplete = () {
hitByEnemy = false;
},
);
);
}
```
If you run the game now, you will see that your health is updated and the stars are incremented as
appropriate. Finally, in [](step_7), we will finish the game by adding the main menu and the
appropriate. Finally, in [](step_7), we will finish the game by adding the main menu and the
game-over menu.

View File

@ -1,7 +1,7 @@
# 7. Adding Menus
To add menus to the game, we will leverage Flame's built-in
[overlay](../../flame/overlays.md) system.
[overlay](../../flame/overlays.md) system.
## Main Menu
@ -69,8 +69,9 @@ class MainMenu extends StatelessWidget {
),
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!',
'''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,
@ -88,9 +89,9 @@ class MainMenu extends StatelessWidget {
```
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
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
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.
@ -168,52 +169,56 @@ class GameOver extends StatelessWidget {
```
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.
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);
await images.loadAll([
'block.png',
'ember.png',
'ground.png',
'heart_half.png',
'heart.png',
'star.png',
'water_enemy.png',
]);
cameraComponent = CameraComponent(world: world);
cameraComponent.viewfinder.anchor = Anchor.topLeft;
addAll([cameraComponent, world]);
initializeGame(true);
}
void initializeGame(bool loadHud) {
// Assume that size.x < 3200
final segmentsToLoad = (size.x / 640).ceil();
segmentsToLoad.clamp(0, segments.length);
// 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());
}
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);
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
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()`.
@ -244,25 +249,25 @@ 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
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!
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;
game.health = 0;
}
if (game.health <= 0) {
removeFromParent();
removeFromParent();
}
```
@ -270,7 +275,7 @@ In `lib/actors/water_enemy.dart`, in the `update` method update the following co
```dart
if (position.x < -size.x || game.health <= 0) {
removeFromParent();
removeFromParent();
}
```
@ -278,7 +283,7 @@ In `lib/objects/ground_block.dart`, in the `update` method update the following
```dart
if (game.health <= 0) {
removeFromParent();
removeFromParent();
}
```
@ -286,7 +291,7 @@ In `lib/objects/platform_block.dart`, in the `update` method update the followin
```dart
if (position.x < -size.x || game.health <= 0) {
removeFromParent();
removeFromParent();
}
```
@ -294,7 +299,7 @@ In `lib/objects/star.dart`, in the `update` method update the following code:
```dart
if (position.x < -size.x || game.health <= 0) {
removeFromParent();
removeFromParent();
}
```
@ -303,17 +308,16 @@ 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);
if (health <= 0) {
overlays.add('GameOver');
}
}
```
## Congratulations
You made it! You have a working Ember Quest. Press the button below to see what the resulting code
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}