Learn to construct a good looking sport in Flutter with Flame. On this tutorial, you’ll construct a digital world with a movable and animated character.
Replace notice: Brian Moakley up to date this tutorial for Flutter 3.3 and Dart 2.18. Vincenzo Guzzi wrote the unique.
Flutter is altering the world by bringing quick, natively compiled software program to the plenty. This enables indie builders to launch functions for each platform in the identical time it might often have taken a software program firm. It’s solely pure that sport builders need to reap the benefits of that, too.
Historically, a cell sport developer would wish to decide on between native efficiency however gradual growth time or constructing with a multi-platform device like Unity however danger gradual loading instances and enormous app sizes.
If solely there have been a option to develop lovely native video games with out all of the bloat. That’s the place Flame is available in.
In the present day, you’ll construct a digital world utilizing Flutter and the Flame engine. You’ll discover ways to:
- Use Flame model 1.5 to make a sport for the online, Android and iOS.
- Use a sport loop.
- Create a movable participant character.
- Animate your character with sprite sheets.
- Add field collision by studying from a tile map.
Getting Began
You’ll develop a sport referred to as RayWorld, a 2-D orthographic sport within the model of old-school Pokemon.
Utilizing an older sport engine written in one thing like C++, a tutorial like this might span over three or 4 sequence. However with the facility of Flutter and the Flame engine mixed, you’ll create all this in only one.
You’ll want the starter undertaking to finish this tutorial. Obtain it by clicking the Obtain Supplies button on the high or backside of the tutorial.
Construct and run your undertaking in your most well-liked IDE. This tutorial will use Visible Studio Code.
You’ll see a clean display screen with a joypad within the backside proper nook:
What you see right here is rendered purely with Flutter; you’ll want Flame to construct the remainder of your elements.
The Flame Recreation Engine
Flame — a light-weight sport engine constructed on high of Flutter — offers sport builders a set of instruments akin to a sport loop, collision detection and sprite animations to create 2-D video games.
This tutorial will use Flame 1.5.
The Flame engine is modular, permitting customers to choose and select which API’s they want to use, akin to:
- Flame – The core bundle, which affords the sport loop, fundamental collision detection, Sprites and elements.
- Forge2D – A physics engine with superior collision detection, ported from Box2D to work with Flame.
- Tiled – A module for simply working with tile maps in Flame.
- Audio – A module that provides audio capabilities into your Flame sport.
Flame harnesses the facility of Flutter and supplies a light-weight strategy to creating 2-D video games for all platforms.
Establishing Your Flame Recreation Loop
The primary part you’ll arrange in RayWorld is your Flame sport loop. This would be the coronary heart of your sport. You’ll create and handle all of your different elements from right here.
Open your lib folder and create a brand new file referred to as ray_world_game.dart, then add a brand new class referred to as RayWorldGame
, which extends from the Flame widget FlameGame
:
import 'bundle:flame/sport.dart';
class RayWorldGame extends FlameGame {
@override
Future<void> onLoad() async {
// empty
}
}
Now to make use of your widget. Open main_game_page.dart. Add these two imports to the highest of main_game_page.dart:
import 'bundle:flame/sport.dart';
import 'ray_world_game.dart';
Subsequent, create an occasion of your new class on the high of MainGameState
:
RayWorldGame sport = RayWorldGame();
Now, add a GameWidget
to MainGameState
as the primary widget within the Stack
, changing // TODO 1
with:
GameWidget(sport: sport),
Proper now, your sport will do nothing. It wants some elements to render. Time so as to add a playable character!
Creating Your Participant
Add a folder in lib referred to as elements. This folder will retailer all of your Flame elements, beginning together with your participant.
Create a file in elements referred to as participant.dart. On this class, arrange your Participant
class:
import 'bundle:flame/elements.dart';
class Participant extends SpriteComponent with HasGameRef {
Participant()
: tremendous(
dimension: Vector2.all(50.0),
);
@override
Future<void> onLoad() async {
tremendous.onLoad();
// TODO 1
}
}
Your Participant
extends a Flame part referred to as SpriteComponent
. You’ll use this to render a static picture in your sport. You’re setting the dimensions of the participant to be 50.
Through the use of the HasGameRef
mixin, the Participant
now has entry to the core performance of the Flame engine. Now to make use of that performance by loading the sprite.
Change // TODO 1
in Participant
with logic to load your participant picture and set the participant’s preliminary place.
sprite = await gameRef.loadSprite('participant.png');
place = gameRef.dimension / 2;
Right here, you utilize that sport reference from the HasGameRef
mixin to load a sprite into your sport with the picture of participant.png. This picture is positioned in your Flutter belongings folder. You additionally set the gamers place to be in the midst of the sport.
Return to your ray_world_game.dart file and add your new Participant
part as an import on the high of the file:
import 'elements/participant.dart';
Within the high of RayWorldGame
, create your Participant
:
last Participant _player = Participant();
Within the sport onLoad
methodology, change // empty
with code so as to add your participant into the sport:
add(_player);
add
is an excellent vital methodology when constructing video games with the Flame engine. It means that you can register any part with the core sport loop and in the end render them on display screen. You should use it so as to add gamers, enemies, and many different issues as properly.
Construct and run, and also you’ll see somewhat dude standing within the heart of your sport.
Fairly thrilling!
Now, it’s time to get your participant shifting.
Including Motion to Your Participant
To maneuver your participant, you first must know what route the joypad is dragged.
The joypad route is retrieved from the Joypad
Flutter widget that lives exterior the sport loop. The route then will get handed to the GameWidget
in main_game_page.dart. In flip, this will cross it to Participant
, which may react to the route change with motion.
Begin with the Participant
.
Open your participant.dart
file and add the import for route:
import '../helpers/route.dart';
Then, declare a Path
variable within the high of Participant
and instantiate it to Path.none
:
Path route = Path.none;
The joypad will change to both up, down, left, proper, or none. With every new place, you need to replace the route
variable.
Open ray_world_game.dart
. Import the route.dart
:
import '../helpers/route.dart';
Now add a operate to replace the route of your participant in RayWorldGame
:
void onJoypadDirectionChanged(Path route) {
_player.route = route;
}
Now, head again to main_game_page.dart
and change // TODO 2
with a name to your sport route operate:
sport.onJoypadDirectionChanged(route);
And voilà, you’ve handed a consumer enter from a Flutter widget into your sport and participant elements.
Now that your participant part is aware of what route it needs to be shifting in, it’s time to execute on that data and truly transfer your participant!
Executing on Participant Motion
To start out performing on the knowledge handed via to the participant part, head again to participant.dart and add these two features:
@override
void replace(double delta) {
tremendous.replace(delta);
movePlayer(delta);
}
void movePlayer(double delta) {
// TODO
}
replace
is a operate distinctive to Flame elements. Will probably be referred to as every time a body should be rendered, and Flame will guarantee all of your sport elements replace on the identical time. The delta represents how a lot time has handed because the final replace cycle and can be utilized to maneuver the participant predictably.
Change // TODO
within the movePlayer
operate with logic to learn the route. You haven’t written the transfer strategies but. You’ll maintain that quickly sufficient. For now, you’ll need to endure some compile errors:
change (route) {
case Path.up:
moveUp(delta);
break;
case Path.down:
moveDown(delta);
break;
case Path.left:
moveLeft(delta);
break;
case Path.proper:
moveRight(delta);
break;
case Path.none:
break;
}
movePlayer
will now delegate out to different extra particular strategies to maneuver the participant. Subsequent, add the logic for shifting the participant in every route.
Begin by including a pace variable to the highest of your Participant
class:
last double _playerSpeed = 300.0;
Now, add a moveDown
operate to the underside of your Participant
class:
void moveDown(double delta) {
place.add(Vector2(0, delta * _playerSpeed));
}
Right here, you replace the Participant
place worth — represented as an X and a Y inside Vector2
— by your participant pace multiplied by the delta.
You’ll be able to image your sport view drawn on a 2-D aircraft like so:
If the sport view is 2500×2500 pixels in diameter, your participant begins within the center on the coordinates of x:1250, y:1250. Calling moveDown
provides about 300 pixels to the participant’s Y place every second the consumer holds the joypad within the down route, inflicting the sprite to maneuver down the sport viewport.
It’s essential to add an analogous calculation for the opposite three lacking strategies: moveUp
, moveLeft
and moveRight
.
Now for the opposite transfer strategies:
void moveUp(double delta) {
place.add(Vector2(0, delta * -_playerSpeed));
}
void moveLeft(double delta) {
place.add(Vector2(delta * -_playerSpeed, 0));
}
void moveRight(double delta) {
place.add(Vector2(delta * _playerSpeed, 0));
}
Run your utility as soon as extra, and your little dude will transfer across the display screen in all instructions based mostly in your joypad enter.
Animating Your Participant
Your participant is shifting across the display screen like a boss – but it surely appears to be like a bit off as a result of the participant is at all times going through in the identical route! You’ll repair that subsequent utilizing sprite sheets.
What Is a Sprite Sheet?
A sprite sheet is a group of sprites in a single picture. Recreation builders have used them for a very long time to save lots of reminiscence and guarantee fast loading instances. It’s a lot faster to load one picture as an alternative of a number of photographs. Recreation engines like Flame can then load the sprite sheet and render solely a bit of the picture.
You can even use sprite sheets for animations by lining sprites up subsequent to one another in animation frames to allow them to simply be iterated over within the sport loop.
That is the sprite sheet you’ll use to your playable character in RayWorld:
Every row is a special animation set and simulates shifting left, proper, up and down.
Including Sprite Sheet Animations to Your Participant
In participant.dart, change your Participant
class extension from SpriteComponent
to SpriteAnimationComponent
as follows:
class Participant extends SpriteAnimationComponent with HasGameRef {
With this new kind of part, you’ll be capable to set an energetic animation, which is able to run in your participant Sprite.
Import the bundle sprite.dart. You’ll want this for establishing a SpriteSheet
:
import 'bundle:flame/sprite.dart';
Add these six new variables to your Participant
class:
last double _animationSpeed = 0.15;
late last SpriteAnimation _runDownAnimation;
late last SpriteAnimation _runLeftAnimation;
late last SpriteAnimation _runUpAnimation;
late last SpriteAnimation _runRightAnimation;
late last SpriteAnimation _standingAnimation;
Change the onLoad
methodology with new logic to load your animations. We’ll outline the _loadAnimations
future in only a second:
@override
Future<void> onLoad() async {
await _loadAnimations().then((_) => {animation = _standingAnimation});
}
_loadAnimations
can be an async name. This methodology waits for the animations to load after which units the sprite’s first energetic animation to _standingAnimation
.
Create the _loadAnimations
methodology and instantiate your participant SpriteSheet
:
Future<void> _loadAnimations() async {
last spriteSheet = SpriteSheet(
picture: await gameRef.photographs.load('player_spritesheet.png'),
srcSize: Vector2(29.0, 32.0),
);
// TODO down animation
// TODO left animation
// TODO up animation
// TODO proper animation
// TODO standing animation
}
This code masses a sprite sheet picture out of your Flutter belongings folder that you simply noticed beforehand.
The picture is 116×128 pixels, and every body is 29×32 pixels. The latter is what you’re setting the srcSize
SpriteSheet
parameter to. Flame will use these variables to create sprites from the completely different frames in your sprite sheet picture.
Change // TODO down animation
with logic to initialize _runDownAnimation
:
_runDownAnimation =
spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, to: 4);
This code units up an animation that loops throughout the primary row of the participant sprite sheet from the primary body till the fourth. It’s successfully a “whereas” loop that repeats from 0 till lower than 4, the place the sprite viewport strikes in 32 pixel increments throughout 4 rows.
Utilizing this logic, initialize the remainder of your animation variables.
_runLeftAnimation =
spriteSheet.createAnimation(row: 1, stepTime: _animationSpeed, to: 4);
_runUpAnimation =
spriteSheet.createAnimation(row: 2, stepTime: _animationSpeed, to: 4);
_runRightAnimation =
spriteSheet.createAnimation(row: 3, stepTime: _animationSpeed, to: 4);
_standingAnimation =
spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, to: 1);
Replace your movePlayer
operate to assign the proper animations based mostly on the participant’s route:
void movePlayer(double delta) {
change (route) {
case Path.up:
animation = _runUpAnimation;
moveUp(delta);
break;
case Path.down:
animation = _runDownAnimation;
moveDown(delta);
break;
case Path.left:
animation = _runLeftAnimation;
moveLeft(delta);
break;
case Path.proper:
animation = _runRightAnimation;
moveRight(delta);
break;
case Path.none:
animation = _standingAnimation;
break;
}
}
Construct and run, and also you’ll see your playable character has come to life as they run in every route.
At this level, you have got the basics of a sport in place: a playable character with consumer enter and motion. The following step is so as to add a world to your participant to maneuver round in.
Including a World
Create a file referred to as world.dart
in your elements folder. In world.dart
, create a SpriteComponent referred to as World and cargo rayworld_background.png because the world sprite:
import 'bundle:flame/elements.dart';
class World extends SpriteComponent with HasGameRef {
@override
Future<void>? onLoad() async {
sprite = await gameRef.loadSprite('rayworld_background.png');
dimension = sprite!.originalSize;
return tremendous.onLoad();
}
}
Head again to RayWorldGame
. Be certain so as to add the World
import.
import 'elements/world.dart';
Then add a World
as a variable underneath Participant
:
last World _world = World();
Now, add _world
to your sport originally of onLoad
:
await add(_world);
It’s essential to load the world fully earlier than loading your participant. Should you add the world afterward, it can render on high of your Participant
sprite, obscuring it.
Construct and run, and also you’ll see a good looking pixel panorama to your participant to run round in:
To your participant to traverse the world correctly, you’ll need the sport viewport to comply with the principle character at any time when they transfer. Historically, when programming video video games, this requires a plethora of difficult algorithms to perform. However with Flame, it’s straightforward!
First, add the import for utilizing a Rect
variable on the high of the file. You’ll use this to calculate some bounds:
import 'dart:ui';
Now on the backside of your sport onLoad
methodology, set the participant’s preliminary place the middle of the world and inform the sport digicam to comply with _player
:
_player.place = _world.dimension / 2;
digicam.followComponent(_player,
worldBounds: Rect.fromLTRB(0, 0, _world.dimension.x, _world.dimension.y));
Construct and run, and also you’ll see your world sprite pan as your participant strikes. As you’ve set the worldBounds
variable, the digicam will even cease panning as you attain the sting of the world sprite. Run to the sting of the map and see for your self.
Congratulations!
You ought to be pleased with your self for getting this far. You’ve lined a few of the core elements wanted in any sport dev’s repertoire.
Nonetheless, there’s one last ability you need to be taught to have the ability to make a full sport: Collision detection.
Including World Collision to Your Recreation
Creating Tile Maps
2-D sport builders generally make use of tile maps. The method includes creating paintings to your sport as a group of uniform tiles you may piece collectively nevertheless wanted like a jigsaw, then making a map you should utilize to inform your sport engine which tiles go the place.
You can also make tile maps as fundamental or as superior as you want. In a previous undertaking, a sport referred to as Pixel Man used a textual content file as a tile map that seemed one thing like this:
xxxxxxxxxxx
xbooooooox
xoooobooox
xoooooooox
xoooooboox
xxxxxxxxxxx
The sport engine would learn these recordsdata and change x’s with partitions and b’s with collectable objects, utilizing the tile map for each logic and paintings functions.
As of late, software program makes the method of making a tile map much more intuitive. RayWorld makes use of software program referred to as Tiled. Tiled is free software program that allows you to create your ranges with a tile set and add extra collision layers in a graphical editor. It then generates a tile map written in JSON that may be simply learn in your sport engine.
A tile map referred to as rayworld_collision_map.json already exists. You’ll use this JSON file so as to add collision objects into your sport within the subsequent part. It appears to be like like this within the Tiled editor:
The pink packing containers are the collision rectangles. You’ll use this information to create collision objects in Flame.
Creating World Collision in RayWorld
Add a file in your elements folder referred to as world_collidable.dart and create a category referred to as WorldCollidable
:
import 'bundle:flame/collisions.dart';
import 'bundle:flame/elements.dart';
class WorldCollidable extends PositionComponent{
WorldCollidable() {
add(RectangleHitbox());
}
}
Right here you outline a brand new class to include your world. It’s a sort of PositionComponent
that represents a place on the display screen. It’s meant to symbolize every collidable space (i.e., invisible partitions) on the world map.
Open ray_world_game.dart
. First add the next imports:
import 'elements/world_collidable.dart';
import 'helpers/map_loader.dart';
import 'bundle:flame/elements.dart';
Now create a technique in RayWorldGame
referred to as addWorldCollision
:
void addWorldCollision() async =>
(await MapLoader.readRayWorldCollisionMap()).forEach((rect) {
add(WorldCollidable()
..place = Vector2(rect.left, rect.high)
..width = rect.width
..peak = rect.peak);
});
Right here, you utilize a helper operate, MapLoader
, to learn rayworld_collision_map.json, positioned in your belongings folder. For every rectangle, it creates a WorldCollidable
and provides it to your sport.
Name your new operate beneath add(_player)
in onLoad
:
await add(_world);
add(_player);
addWorldCollision(); // add
Now to register collision detection. Add the HasCollisionDetection
mixin to RayWorldGame
. You’ll must specify this if you would like Flame to construct a sport that has collidable sprites:
class RayWorldGame extends FlameGame with HasCollisionDetection
You’ve now added all of your collidable sprites into the sport, however proper now, you gained’t be capable to inform. You’ll want to include extra logic to your participant to cease them from shifting once they’ve collided with certainly one of these objects.
Open participant.dart
. Add the CollisionCallbacks
mixin after with HasGameRef
subsequent to your participant class declaration:
class Participant extends SpriteAnimationComponent with HasGameRef, CollisionCallbacks
You now have entry to onCollision
and onCollisionEnd
. Add them to your Participant
class:
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent different) {
tremendous.onCollision(intersectionPoints, different);
// TODO 1
}
@override
void onCollisionEnd(PositionComponent different) {
tremendous.onCollisionEnd(different);
// TODO 2
}
Create and add a HitboxRectangle
to your Participant
within the constructor. Like your WorldCollision
elements, your participant wants a Hitbox
to have the ability to register collisions:
Participant()
: tremendous(
dimension: Vector2.all(50.0),
) {
add(RectangleHitbox());
}
Add the WorldCollidable
import above your class:
import 'world_collidable.dart';
Now, add two variables into your Participant
class to assist observe your collisions:
Path _collisionDirection = Path.none;
bool _hasCollided = false;
You’ll be able to populate these variables within the two collision strategies. Go to onCollision
and change // TODO 1
with logic to gather collision data:
if (different is WorldCollidable) {
if (!_hasCollided) {
_hasCollided = true;
_collisionDirection = route;
}
}
Set _hasCollided
again to false in onCollisionEnd
, changing // TODO 2
:
_hasCollided = false;
Participant
now has all the knowledge it must know whether or not it has collided or not. You should use that data to ban motion. Add these 4 strategies to your Participant
class:
bool canPlayerMoveUp() {
if (_hasCollided && _collisionDirection == Path.up) {
return false;
}
return true;
}
bool canPlayerMoveDown() {
if (_hasCollided && _collisionDirection == Path.down) {
return false;
}
return true;
}
bool canPlayerMoveLeft() {
if (_hasCollided && _collisionDirection == Path.left) {
return false;
}
return true;
}
bool canPlayerMoveRight() {
if (_hasCollided && _collisionDirection == Path.proper) {
return false;
}
return true;
}
These strategies will verify whether or not the participant can transfer in a given route by querying the collision variables you created. Now, you should utilize these strategies in movePlayer
to see whether or not the participant ought to transfer:
void movePlayer(double delta) {
change (route) {
case Path.up:
if (canPlayerMoveUp()) {
animation = _runUpAnimation;
moveUp(delta);
}
break;
case Path.down:
if (canPlayerMoveDown()) {
animation = _runDownAnimation;
moveDown(delta);
}
break;
case Path.left:
if (canPlayerMoveLeft()) {
animation = _runLeftAnimation;
moveLeft(delta);
}
break;
case Path.proper:
if (canPlayerMoveRight()) {
animation = _runRightAnimation;
moveRight(delta);
}
break;
case Path.none:
animation = _standingAnimation;
break;
}
}
Rebuild your sport and attempt to run to the water’s edge or right into a fence. You’ll discover your participant will nonetheless animate, however you gained’t be capable to transfer previous the collision objects. Strive operating between the fences or barrels.
Bonus Part: Keyboard Enter
As a result of RayWorld is constructed with Flutter, it will possibly additionally run as an online app. Usually, for net video games, individuals need to use keyboard enter as an alternative of a joypad. Flame has an interface referred to as KeyboardEvents you may override in your sport object to obtain notification of keyboard enter occasions.
For this bonus part, you’ll hear for keyboard occasions for the up, down, left and proper arrows, and use these occasions to set the participant’s route. You’ll really use the instruments offered Flutter itself. Add the next imports:
import 'bundle:flutter/widgets.dart';
import 'bundle:flutter/companies.dart';
Now, in RayWorldGame
, override the onKeyEvent methodology:
@override
KeyEventResult onKeyEvent(
RawKeyEvent occasion,
Set<LogicalKeyboardKey> keysPressed,
) {
last isKeyDown = occasion is RawKeyDownEvent;
Path? keyDirection;
// TODO 1
// TODO 2
return tremendous.onKeyEvent(occasion, keysPressed);
}
Change // TODO 1
with logic to learn RawKeyEvent
and set the keyDirection
:
if (occasion.logicalKey == LogicalKeyboardKey.keyA) {
keyDirection = Path.left;
} else if (occasion.logicalKey == LogicalKeyboardKey.keyD) {
keyDirection = Path.proper;
} else if (occasion.logicalKey == LogicalKeyboardKey.keyW) {
keyDirection = Path.up;
} else if (occasion.logicalKey == LogicalKeyboardKey.keyS) {
keyDirection = Path.down;
}
Right here, you might be listening for key adjustments with the keys W, A, S and D and setting the corresponding motion route.
Now, change // TODO 2
with logic to vary the participant’s route:
if (isKeyDown && keyDirection != null) {
_player.route = keyDirection;
} else if (_player.route == keyDirection) {
_player.route = Path.none;
}
The participant’s route is being up to date if a key’s being pressed, and if a key’s lifted the gamers route is ready to Path.none
if it’s the energetic route.
Launch your sport on the net or an emulator, and also you’ll now be capable to run round utilizing the W, A, S and D keys in your keyboard.
The place to Go From Right here?
You’ll be able to obtain the finished undertaking recordsdata by clicking the Obtain Supplies button on the high or backside of the tutorial.
You now have all of the instruments to make an entire 2-D sport utilizing the Flame Engine. However why cease there? You can strive including:
- Extra sport UI: Incorporate UI parts akin to a participant well being bar, an assault button and a bounce button. You can construct these utilizing a Flame part or a Flutter Widget.
- Enemies: Populate RayWorld with enemies akin to goblins or aggressive animals that would assault your participant.
- Totally different ranges: Load new world sprites and tile maps into your sport because the participant leaves the world.
Try the awesome-flame GitHub repository to see what video games have already been developed utilizing the Flame Engine and to learn another nice Flame tutorials. Be certain to remain tuned to raywenderlich.com for extra nice sport growth tutorials.