Building Games in Flutter with Flame: Getting Started - Exotic Digital Access
  • Kangundo Road, Nairobi, Kenya
  • support@exoticdigitalaccess.co.ke
  • Opening Time : 07 AM - 10 PM
Building Games in Flutter with Flame: Getting Started

Building Games in Flutter with Flame: Getting Started

Learn how to build a beautiful game in Flutter with Flame. In this tutorial, you’ll build a virtual world with a movable and animated character.

Update note: Brian Moakley updated this tutorial for Flutter 3.3 and Dart 2.18. Vincenzo Guzzi wrote the original.

Flutter is changing the world by bringing fast, natively compiled software to the masses. This allows indie developers to release applications for every platform in the same time it would usually have taken a software company. It’s only natural that game developers want to take advantage of that, too.

Traditionally, a mobile game developer would need to choose between native performance but slow development time or building with a multi-platform tool like Unity but risk slow loading times and large app sizes.

If only there were a way to develop beautiful native games without all the bloat. That’s where Flame comes in.

Today, you’ll build a virtual world using Flutter and the Flame engine. You’ll learn how to:

  • Use Flame version 1.5 to make a game for the web, Android and iOS.
  • Use a game loop.
  • Create a movable player character.
  • Animate your character with sprite sheets.
  • Add box collision by reading from a tile map.
Note: This tutorial assumes you have basic knowledge of Flutter. If you’re new to Flutter, check out Flutter Apprentice. At the very least, you should know how to open a project in your favorite IDE, navigate the source code, initialize your packages with pub get and run your app in a simulator.

Getting Started

You’ll develop a game called RayWorld, a 2-D orthographic game in the style of old-school Pokemon.

Building Games in Flutter with Flame: Getting Started

Using an older game engine written in something like C++, a tutorial like this would span over three or four series. But with the power of Flutter and the Flame engine combined, you’ll create all this in just one.

You’ll need the starter project to complete this tutorial. Download it by clicking the Download Materials button at the top or bottom of the tutorial.

Build and run your project in your preferred IDE. This tutorial will use Visual Studio Code.

You’ll see a blank screen with a joypad in the bottom right corner:

Building Games in Flutter with Flame: Getting Started

What you see here is rendered purely with Flutter; you’ll need Flame to build the rest of your components.

The Flame Game Engine

Flame — a lightweight game engine built on top of Flutter — gives game developers a set of tools such as a game loop, collision detection and sprite animations to create 2-D games.

This tutorial will use Flame 1.5.

The Flame engine is modular, allowing users to pick and choose which API’s they would like to use, such as:

  • Flame – The core package, which offers the game loop, basic collision detection, Sprites and components.
  • Forge2D – A physics engine with advanced collision detection, ported from Box2D to work with Flame.
  • Tiled – A module for easily working with tile maps in Flame.
  • Audio – A module that adds audio capabilities into your Flame game.

Flame harnesses the power of Flutter and provides a lightweight approach to developing 2-D games for all platforms.

Setting up Your Flame Game Loop

The first component you’ll set up in RayWorld is your Flame game loop. This will be the heart of your game. You’ll create and manage all your other components from here.

Open your lib folder and create a new file called ray_world_game.dart, then add a new class called RayWorldGame, which extends from the Flame widget FlameGame:


import 'package:flame/game.dart';
 
class RayWorldGame extends FlameGame {
 @override
 Future<void> onLoad() async {
   // empty
 }
}

Now to use your widget. Open main_game_page.dart. Add these two imports to the top of main_game_page.dart:


import 'package:flame/game.dart';
import 'ray_world_game.dart';

Next, create an instance of your new class at the top of MainGameState:


RayWorldGame game = RayWorldGame();

Now, add a GameWidget to MainGameState as the first widget in the Stack, replacing // TODO 1 with:


GameWidget(game: game),

Right now, your game will do nothing. It needs some components to render. Time to add a playable character!

Creating Your Player

Add a folder in lib called components. This folder will store all your Flame components, starting with your player.

Create a file in components called player.dart. In this class, set up your Player class:


import 'package:flame/components.dart';
 
class Player extends SpriteComponent with HasGameRef {
 Player()
     : super(
         size: Vector2.all(50.0),
       );
 
 @override
 Future<void> onLoad() async {
   super.onLoad();
   // TODO 1
 }
}

Your Player extends a Flame component called SpriteComponent. You’ll use this to render a static image in your game. You’re setting the size of the player to be 50.

By using the HasGameRef mixin, the Player now has access to the core functionality of the Flame engine. Now to use that functionality by loading the sprite.

Replace // TODO 1 in Player with logic to load your player image and set the player’s initial position.


sprite = await gameRef.loadSprite('player.png');
position = gameRef.size / 2;

Here, you use that game reference from the HasGameRef mixin to load a sprite into your game with the image of player.png. This image is located in your Flutter assets folder. You also set the players position to be in the middle of the game.

Go back to your ray_world_game.dart file and add your new Player component as an import at the top of the file:


import 'components/player.dart';

In the top of RayWorldGame, create your Player:


final Player _player = Player();

In the game onLoad method, replace // empty with code to add your player into the game:


add(_player);

add is a super important method when building games with the Flame engine. It allows you to register any component with the core game loop and ultimately render them on screen. You can use it to add players, enemies, and lots of other things as well.

Build and run, and you’ll see a little dude standing in the center of your game.

Building Games in Flutter with Flame: Getting Started

Pretty exciting!

Now, it’s time to get your player moving.

Adding Movement to Your Player

To move your player, you first need to know what direction the joypad is dragged.

The joypad direction is retrieved from the Joypad Flutter widget that lives outside the game loop. The direction then gets passed to the GameWidget in main_game_page.dart. In turn, this can pass it to Player, which can react to the direction change with movement.

Start with the Player.

Open your player.dart file and add the import for direction:


import '../helpers/direction.dart';

Then, declare a Direction variable in the top of Player and instantiate it to Direction.none:


Direction direction = Direction.none;

The joypad will change to either up, down, left, right, or none. With each new position, you want to update the direction variable.

Open ray_world_game.dart. Import the direction.dart:


import '../helpers/direction.dart';

Now add a function to update the direction of your player in RayWorldGame:


void onJoypadDirectionChanged(Direction direction) {
   _player.direction = direction;
}

Now, head back to main_game_page.dart and replace // TODO 2 with a call to your game direction function:


game.onJoypadDirectionChanged(direction);

And voilà, you’ve passed a user input from a Flutter widget into your game and player components.

Now that your player component knows what direction it should be moving in, it’s time to execute on that information and actually move your player!

Executing on Player Movement

To start acting on the information passed through to the player component, head back to player.dart and add these two functions:


@override
 void update(double delta) {
   super.update(delta);
   movePlayer(delta);
 }
 
 void movePlayer(double delta) {
   // TODO
 }

update is a function unique to Flame components. It will be called each time a frame must be rendered, and Flame will ensure all your game components update at the same time. The delta represents how much time has passed since the last update cycle and can be used to move the player predictably.

Replace // TODO in the movePlayer function with logic to read the direction. You haven’t written the move methods yet. You’ll take care of that soon enough. For now, you’ll have to endure some compile errors:


switch (direction) {
  case Direction.up:
    moveUp(delta);
    break;
  case Direction.down:
    moveDown(delta);
    break;
  case Direction.left:
    moveLeft(delta);
    break;
  case Direction.right:
    moveRight(delta);
    break;
  case Direction.none:
    break;
}

movePlayer will now delegate out to other more specific methods to move the player. Next, add the logic for moving the player in each direction.

Start by adding a speed variable to the top of your Player class:


final double _playerSpeed = 300.0;

Now, add a moveDown function to the bottom of your Player class:


void moveDown(double delta) {
   position.add(Vector2(0, delta * _playerSpeed));
}

Here, you update the Player position value — represented as an X and a Y inside Vector2 — by your player speed multiplied by the delta.

You can picture your game view drawn on a 2-D plane like so:

Building Games in Flutter with Flame: Getting Started

If the game view is 2500×2500 pixels in diameter, your player starts in the middle at the coordinates of x:1250, y:1250. Calling moveDown adds about 300 pixels to the player’s Y position each second the user holds the joypad in the down direction, causing the sprite to move down the game viewport.

You must add a similar calculation for the other three missing methods: moveUp, moveLeft and moveRight.

Now for the other move methods:


void moveUp(double delta) {
  position.add(Vector2(0, delta * -_playerSpeed));
}
 
void moveLeft(double delta) {
  position.add(Vector2(delta * -_playerSpeed, 0));
}
 
void moveRight(double delta) {
  position.add(Vector2(delta * _playerSpeed, 0));
}

Run your application once more, and your little dude will move around the screen in all directions based on your joypad input.

Building Games in Flutter with Flame: Getting Started

Animating Your Player

Your player is moving around the screen like a boss – but it looks a bit off because the player is always facing in the same direction! You’ll fix that next using sprite sheets.

What Is a Sprite Sheet?

A sprite sheet is a collection of sprites in a single image. Game developers have used them for a long time to save memory and ensure quick loading times. It’s much quicker to load one image instead of multiple images. Game engines like Flame can then load the sprite sheet and render only a section of the image.

You can also use sprite sheets for animations by lining sprites up next to each other in animation frames so they can easily be iterated over in the game loop.

This is the sprite sheet you’ll use for your playable character in RayWorld:

Building Games in Flutter with Flame: Getting Started

Each row is a different animation set and simulates moving left, right, up and down.

Adding Sprite Sheet Animations to Your Player

In player.dart, change your Player class extension from SpriteComponent to SpriteAnimationComponent as follows:


class Player extends SpriteAnimationComponent with HasGameRef {

With this new type of component, you’ll be able to set an active animation, which will run on your player Sprite.

Import the package sprite.dart. You’ll need this for setting up a SpriteSheet:


import 'package:flame/sprite.dart';

Add these six new variables to your Player class:


final double _animationSpeed = 0.15;
late final SpriteAnimation _runDownAnimation;
late final SpriteAnimation _runLeftAnimation;
late final SpriteAnimation _runUpAnimation;
late final SpriteAnimation _runRightAnimation;
late final SpriteAnimation _standingAnimation;

Replace the onLoad method with new logic to load your animations. We’ll define the _loadAnimations future in just a moment:


@override
 Future<void> onLoad() async {
   await _loadAnimations().then((_) => {animation = _standingAnimation});
}

_loadAnimations will be an async call. This method waits for the animations to load and then sets the sprite’s first active animation to _standingAnimation.

Create the _loadAnimations method and instantiate your player SpriteSheet:


Future<void> _loadAnimations() async {
   final spriteSheet = SpriteSheet(
     image: await gameRef.images.load('player_spritesheet.png'),
     srcSize: Vector2(29.0, 32.0),
   );
 
   // TODO down animation
 
   // TODO left animation
 
   // TODO up animation
 
   // TODO right animation
 
   // TODO standing animation
 }

This code loads a sprite sheet image from your Flutter assets folder that you saw previously.

The image is 116×128 pixels, and each frame 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 different frames on your sprite sheet image.

Replace // TODO down animation with logic to initialize _runDownAnimation:


_runDownAnimation =
       spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, to: 4);

This code sets up an animation that loops across the first row of the player sprite sheet from the first frame until the fourth. It’s effectively a “while” loop that repeats from 0 until less than 4, where the sprite viewport moves in 32 pixel increments across 4 rows.

Building Games in Flutter with Flame: Getting Started

Using this logic, initialize the rest 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);

Update your movePlayer function to assign the correct animations based on the player’s direction:


void movePlayer(double delta) {
   switch (direction) {
     case Direction.up:
       animation = _runUpAnimation;
       moveUp(delta);
       break;
     case Direction.down:
       animation = _runDownAnimation;
       moveDown(delta);
       break;
     case Direction.left:
       animation = _runLeftAnimation;
       moveLeft(delta);
       break;
     case Direction.right:
       animation = _runRightAnimation;
       moveRight(delta);
       break;
     case Direction.none:
       animation = _standingAnimation;
       break;
   }
 }

Build and run, and you’ll see your playable character has come to life as they run in each direction.

Building Games in Flutter with Flame: Getting Started

At this point, you have the fundamentals of a game in place: a playable character with user input and movement. The next step is to add a world for your player to move around in.

Adding a World

Create a file called world.dart in your components folder. In world.dart, create a SpriteComponent called World and load rayworld_background.png as the world sprite:


import 'package:flame/components.dart';
 
class World extends SpriteComponent with HasGameRef {
 @override
 Future<void>? onLoad() async {
   sprite = await gameRef.loadSprite('rayworld_background.png');
   size = sprite!.originalSize;
   return super.onLoad();
 }
}

Head back to RayWorldGame. Make sure to add the World import.


import 'components/world.dart';

Then add a World as a variable under Player:


final World _world = World();

Now, add _world to your game at the beginning of onLoad:


await add(_world);

You must load the world completely before loading your player. If you add the world afterward, it will render on top of your Player sprite, obscuring it.

Build and run, and you’ll see a beautiful pixel landscape for your player to run around in:

Building Games in Flutter with Flame: Getting Started

For your player to traverse the world properly, you’ll want the game viewport to follow the main character whenever they move. Traditionally, when programming video games, this requires a plethora of complicated algorithms to accomplish. But with Flame, it’s easy!

First, add the import for using a Rect variable at the top of the file. You’ll use this to calculate some bounds:


import 'dart:ui';

Now at the bottom of your game onLoad method, set the player’s initial position the center of the world and tell the game camera to follow _player:


_player.position = _world.size / 2;
   camera.followComponent(_player,
       worldBounds: Rect.fromLTRB(0, 0, _world.size.x, _world.size.y));

Build and run, and you’ll see your world sprite pan as your player moves. As you’ve set the worldBounds variable, the camera will even stop panning as you reach the edge of the world sprite. Run to the edge of the map and see for yourself.

Building Games in Flutter with Flame: Getting Started

Congratulations!

You should be proud of yourself for getting this far. You’ve covered some of the core components needed in any game dev’s repertoire.

However, there’s one final skill you must learn to be able to make a full game: Collision detection.

Adding World Collision to Your Game

Creating Tile Maps

2-D game developers commonly employ tile maps. The technique involves creating artwork for your game as a collection of uniform tiles you can piece together however needed like a jigsaw, then creating a map you can use to tell your game engine which tiles go where.

You can make tile maps as basic or as advanced as you like. In a past project, a game called Pixel Man used a text file as a tile map that looked something like this:

xxxxxxxxxxx
xbooooooox
xoooobooox
xoooooooox
xoooooboox
xxxxxxxxxxx

The game engine would read these files and replace x’s with walls and b’s with collectable objects, using the tile map for both logic and artwork purposes.

These days, software makes the process of creating a tile map a lot more intuitive. RayWorld uses software called Tiled. Tiled is free software that lets you create your levels with a tile set and add additional collision layers in a graphical editor. It then generates a tile map written in JSON that can be easily read in your game engine.

A tile map called rayworld_collision_map.json already exists. You’ll use this JSON file to add collision objects into your game in the next section. It looks like this in the Tiled editor:

Building Games in Flutter with Flame: Getting Started

The pink boxes are the collision rectangles. You’ll use this data to create collision objects in Flame.

Creating World Collision in RayWorld

Add a file in your components folder called world_collidable.dart and create a class called WorldCollidable:


import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
 
class WorldCollidable extends PositionComponent{
  WorldCollidable() {
    add(RectangleHitbox());
  }
}

Here you define a new class to contain your world. It’s a type of PositionComponent that represents a position on the screen. It’s meant to represent each collidable area (i.e., invisible walls) on the world map.

Open ray_world_game.dart. First add the following imports:


import 'components/world_collidable.dart';
import 'helpers/map_loader.dart';
import 'package:flame/components.dart';

Now create a method in RayWorldGame called addWorldCollision:


void addWorldCollision() async =>
     (await MapLoader.readRayWorldCollisionMap()).forEach((rect) {
       add(WorldCollidable()
         ..position = Vector2(rect.left, rect.top)
         ..width = rect.width
         ..height = rect.height);
     });

Here, you use a helper function, MapLoader, to read rayworld_collision_map.json, located in your assets folder. For each rectangle, it creates a WorldCollidable and adds it to your game.

Call your new function 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 need to specify this if you want Flame to build a game that has collidable sprites:


class RayWorldGame extends FlameGame with HasCollisionDetection

You’ve now added all your collidable sprites into the game, but right now, you won’t be able to tell. You’ll need to incorporate additional logic to your player to stop them from moving when they’ve collided with one of these objects.

Open player.dart. Add the CollisionCallbacks mixin after with HasGameRef next to your player class declaration:


class Player extends SpriteAnimationComponent with HasGameRef, CollisionCallbacks

You now have access to onCollision and onCollisionEnd. Add them to your Player class:


@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
  super.onCollision(intersectionPoints, other);
  // TODO 1
}
 
@override
void onCollisionEnd(PositionComponent other) {
  super.onCollisionEnd(other);
  // TODO 2
}

Create and add a HitboxRectangle to your Player in the constructor. Like your WorldCollision components, your player needs a Hitbox to be able to register collisions:


Player()
     : super(
         size: Vector2.all(50.0),
       ) {
   add(RectangleHitbox());
 }

Add the WorldCollidable import above your class:


import 'world_collidable.dart';

Now, add two variables into your Player class to help track your collisions:


Direction _collisionDirection = Direction.none;
bool _hasCollided = false;

You can populate these variables in the two collision methods. Go to onCollision and replace // TODO 1 with logic to collect collision information:


if (other is WorldCollidable) {
  if (!_hasCollided) {
    _hasCollided = true;
    _collisionDirection = direction;
  }
}

Set _hasCollided back to false in onCollisionEnd, replacing // TODO 2:


_hasCollided = false;

Player now has all the information it needs to know whether it has collided or not. You can use that information to prohibit movement. Add these four methods to your Player class:


bool canPlayerMoveUp() {
  if (_hasCollided && _collisionDirection == Direction.up) {
    return false;
  }
  return true;
}
 
bool canPlayerMoveDown() {
  if (_hasCollided && _collisionDirection == Direction.down) {
    return false;
  }
  return true;
}
 
bool canPlayerMoveLeft() {
  if (_hasCollided && _collisionDirection == Direction.left) {
    return false;
  }
  return true;
}
 
bool canPlayerMoveRight() {
  if (_hasCollided && _collisionDirection == Direction.right) {
    return false;
  }
  return true;
}

These methods will check whether the player can move in a given direction by querying the collision variables you created. Now, you can use these methods in movePlayer to see whether the player should move:


void movePlayer(double delta) {
  switch (direction) {
    case Direction.up:
      if (canPlayerMoveUp()) {
        animation = _runUpAnimation;
        moveUp(delta);
      }
      break;
    case Direction.down:
      if (canPlayerMoveDown()) {
        animation = _runDownAnimation;
        moveDown(delta);
      }
      break;
    case Direction.left:
      if (canPlayerMoveLeft()) {
        animation = _runLeftAnimation;
        moveLeft(delta);
      }
      break;
    case Direction.right:
      if (canPlayerMoveRight()) {
        animation = _runRightAnimation;
        moveRight(delta);
      }
      break;
    case Direction.none:
      animation = _standingAnimation;
      break;
  }
}

Rebuild your game and try to run to the water’s edge or into a fence. You’ll notice your player will still animate, but you won’t be able to move past the collision objects. Try running between the fences or barrels.

Building Games in Flutter with Flame: Getting Started

Bonus Section: Keyboard Input

Because RayWorld is built with Flutter, it can also run as a web app. Generally, for web games, people want to use keyboard input instead of a joypad. Flame has an interface called KeyboardEvents you can override in your game object to receive notification of keyboard input events.

For this bonus section, you’ll listen for keyboard events for the up, down, left and right arrows, and use these events to set the player’s direction. You’ll actually use the tools provided Flutter itself. Add the following imports:


import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';

Now, in RayWorldGame, override the onKeyEvent method:


@override
KeyEventResult onKeyEvent(
    RawKeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    final isKeyDown = event is RawKeyDownEvent;
    Direction? keyDirection;

  // TODO 1

  // TODO 2

  return super.onKeyEvent(event, keysPressed);
}

Replace // TODO 1 with logic to read RawKeyEvent and set the keyDirection:


if (event.logicalKey == LogicalKeyboardKey.keyA) {
  keyDirection = Direction.left;
} else if (event.logicalKey == LogicalKeyboardKey.keyD) {
  keyDirection = Direction.right;
} else if (event.logicalKey == LogicalKeyboardKey.keyW) {
  keyDirection = Direction.up;
} else if (event.logicalKey == LogicalKeyboardKey.keyS) {
  keyDirection = Direction.down;
}

Here, you are listening for key changes with the keys W, A, S and D and setting the corresponding movement direction.

Now, replace // TODO 2 with logic to change the player’s direction:


if (isKeyDown && keyDirection != null) {
  _player.direction = keyDirection;
} else if (_player.direction == keyDirection) {
  _player.direction = Direction.none;
}

The player’s direction is being updated if a key is being pressed, and if a key is lifted the players direction is set to Direction.none if it is the active direction.

Launch your game on the web or an emulator, and you’ll now be able to run around using the W, A, S and D keys on your keyboard.

Where to Go From Here?

You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

You now have all the tools to make a complete 2-D game using the Flame Engine. But why stop there? You could try adding:

  • More game UI: Incorporate UI elements such as a player health bar, an attack button and a jump button. You could build these using a Flame component or a Flutter Widget.
  • Enemies: Populate RayWorld with enemies such as goblins or aggressive animals that could attack your player.
  • Different levels: Load new world sprites and tile maps into your game as the player leaves the area.

Check out the awesome-flame GitHub repository to see what games have already been developed using the Flame Engine and to read some other great Flame tutorials. Make sure to stay tuned to raywenderlich.com for more great game development tutorials.


Source link

Leave a Reply