Flutter games with Bloc and Flame

Managing flame game state using the flame_bloc package

February 22, 2022
and 
February 22, 2022
updated on
April 17, 2024
By 
Guest Contributor

Flutter is an incredible and versatile framework, focused on the creation of beautiful multi-platform applications. It can also be used to develop a special type of application: games!

In this article, we are going to give you an introduction into how state can be managed in a game using bloc.

A brief introduction to Flame

Flame is an open source game engine in Flutter which offers many tools to help you build games, like:

  • Images, sprites, and animations rendering
  • Receiving input (gestures, keyboard, mouse)
  • Camera and viewport
  • and more!

It has recently achieved its first stable version and has a growing community with a few interesting games released with it.

How a game works and how its state can be managed

One of the biggest differences between a conventional application and a game is how the rendering occurs.

In conventional apps, the app is re-rendered in response to changes. For example, a touch input from the user can triggers some logic that changes the UI, or even a background event, like a push notification. This is called passive rendering.

In contrast, in games, many elements on the screen are updating constantly, even when there is no interaction from user. Since the rendering is constant, we call this active rendering.

Good performance plays a critical role in games. Of course, good performance is important in any application, but bad performance is much more noticeable in game and could render the game unplayable.

State can be managed a little bit differently when it comes to active rendering and performance in games. It could also make updating state easier. Since the game is always updating itself, we don't need reactivity update the screen. For example, we could just directly update a variable called position in our Player class and in the next frame of the game, our player would be rendered with the updated position.

By storing the state directly within variables in our game classes, we can quickly retrieve it, which helps with game performance.

That said, there are also many cases where state management packages can be more helpful than storing game data in class variables. We will explore this approach later in this article.

The bloc package

We're going to use the flutter_bloc package within this article, as the state management library of choice here at Very Good Ventures and Flame provides some helper functions for it. However, a similar approach could easily be applied with any other state management packages.

Bloc is a predicable state management library which helps to implement the BLoC design pattern. It helps you manage your state in a way that creates separation between your presentation and business logic. This makes your code easier to test and reuse. If you never used bloc before, it may be worth checking out this introduction to bloc.

A real life example

To demonstrate how bloc can be used to help manage game state, we have built a game prototype called Very Good Adventures. It looks like this:

In this example, Very Good Adventures is classic top-down adventure game, where the player navigates through landscapes, and collects items and equipment, which are used to solve mysteries and defeat enemies.

The scope of our prototype will include the following features:

  • Player movement on the vertical and horizontal axis using the keyboard keys (W, A, S, D)
  • Collection of nearby items when the space bar is pressed
  • Display of the collected items in an inventory panel
  • Display of the equipped items in a player panel
  • Player appearance changes depending on the equipped gear

We will go through how these features were implemented, but not all of the game code will be presented on the article. The complete source code can be found in this repository and it can be played live here.

Player position and movement

The position and movement of an in-game object is a very good example of a set of data which will be handled inside the game object itself. Since the object will change constantly over time, direct memory access plays an important role here.

To represent our player, we are going to create a component based in one of Flame's many components, the SpriteComponent, which is a component which holds a position and renders a sprite in the game canvas. That position is represented by a Vector2 class, which is nothing more than a point in space.

To handle movement, we need two variables, speed which will be a constant of how many logical pixels our player moves per second and we also need a direction vector which will tell us what direction the player is moving, where x = 1, player is moving right, x = -1, moving left, x = 0, not moving, and the same directions for the y axis.

Our player class will look like this:

// KeyboardHandler is a Flame mixin which allows components to handle key events
class Player extends SpriteComponent with KeyboardHandler {
  Player() : super(size: Vector2(20, 60));
  
  Vector2 direction = Vector2.zero();
  
  static const speed = 100.0;
  
  // Load method omitted
  
  @override
  void update(double dt) {
    super.update(dt);
    
    final newPosition = position + direction * speed * dt;
    
    position
      ..x = newPosition.x
      ..y = newPosition.y;
  }
  
  @override
  bool onKeyEvent(
    RawKeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    final isDown = event is RawKeyDownEvent;
    
    if (event.logicalKey == LogicalKeyboardKey.keyA) {
      direction.x = isDown ? -1 : 0;
      return true;
    }
    if (event.logicalKey == LogicalKeyboardKey.keyD) {
      direction.x = isDown ? 1 : 0;
      return true;
    }
    
    // Other directions omitted
  }
}

And then let's add our player into a FlameGame:

class VeryGoodAdventuresGame extends FlameGame
    with HasKeyboardHandlerComponents {
  static final Vector2 resolution = Vector2(600, 400);
  
  late final Player player;
  
  @override
  Future<void>? onLoad() async {
    await super.onLoad();
    
    camera.viewport = FixedResolutionViewport(
      Vector2(resolution.x, resolution.y),
    );
    
    await add(player = Player()..y = 40);
    
    camera.followComponent(player);
  }
}

Collecting items in the game area

Now that the player can roam around the map, we are going to implement the item collection. We will implement our inventory panel using Flutter widgets so that we can easily build our game UI.

To manage our inventory and create a point of communication between our Flame game and our game UI, we are going to create an InventoryBloc, which will manage a list of collected items.

Since a Flame Game is just a widget at the end of the day, we could just build this using flame and flutter_bloc packages. To make things a little bit easier, we can use the flame_bloc package, which integrates nicely with the bloc package.

So first things first, let's create our InventoryBloc, its state, and events:

class InventoryState extends Equatable {
  const InventoryState({
    required this.items,
  });
  
  const InventoryState.initial() : this(items: const []);
  
  final List<GameItem> items;
  
  @override
  List<Object> get props => [items];
  
  InventoryState copyWith({
    List<GameItem>? items,
  }) {
    return InventoryState(items: items ?? this.items);
  }
}

abstract class InventoryEvent extends Equatable {
  const InventoryEvent();
}

class GameItemPickedUp extends InventoryEvent {
  const GameItemPickedUp(this.gameItem);
  
  final GameItem gameItem;
  
  @override
  List<Object?> get props => [gameItem];
}

class InventoryBloc extends Bloc<InventoryEvent, InventoryState> {
  InventoryBloc() : super(const InventoryState.initial()) {
    on<GameItemPickedUp>(_onGameItemPickedUp);
  }
  
  void _onGameItemPickedUp(
    GameItemPickedUp event,
    Emitter<InventoryState> emit,
  ) {
    emit(
      state.copyWith(
        items: [
          ...state.items,
          event.gameItem,
        ],
      ),
    );
  }
}

Next we need to provide our new bloc to our game, we can do that by simply using flutter_bloc's BlocProvider widget:

BlocProvider<InventoryBloc>(
  create: (_) => InventoryBloc(),
  child: GameWidget(game: VeryGoodAdventuresGame()),
),

Next, we need to change our game to be a FlameBlocGame instead of just a FlameGame. FlameBlocGame is responsible for managing bloc subscriptions and also give us access read blocs from the context in order to trigger events. So lets do the change and add a few items on our game:

class VeryGoodAdventuresGame extends FlameBlocGame
    with HasKeyboardHandlerComponents {
    // Omitted
    
  @override
  Future<void>? onLoad() async {
    // Adding this to onLoad
    await add(
      Pickupable(item: GameItem.sword)
        ..x = -80
        ..y = -40,
    );
  }
}

Finally, we add some logic on the Player class to collect items when pressing the space bar.

// KeyboardHandler is a Flame mixin which allows components to handle key events
class Player extends SpriteComponent
  with KeyboardHandler,
  // Adding HasGameRef so this component can easily access its game instance
  HasGameRef<VeryGoodAdventuresGame> {
  
  // Previous code omitted
  
  @override
  bool onKeyEvent(
    RawKeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    // Add this to onKeyEvent
    if (event.logicalKey == LogicalKeyboardKey.space) {
      // Find the items that are close enough
      final closePickups =
          gameRef.children.whereType<Pickupable>().where((element) {
        final distance = position.distanceTo(element.position);
        return distance <= 50;
      }).toList();
      
      if (closePickups.isNotEmpty) {
        // Select the first one of the items that are close enough and it to be removed
        // from the game since we are collecting it
        final pickup = closePickups.first..shouldRemove = true;
        
        // Trigger an event on our InventoryBloc that we have collected the item
        // Like we mentioned before, `FlameBlocGame` gives us access to reading blocs
        // from the context, so we use the `gameRef.read` method to add the event.
        gameRef.read<InventoryBloc>().add(GameItemPickedUp(
                pickup.item,
              ),
            );
      }
      
      return true;
    }
  }
}

And that is it! Every time our player collects an item on the game, our inventory view will be automatically updated!

Handling equipped gear

To handle the equipped items, we will follow a very similar approach that we did for the InventoryBloc. Instead of using a list of all of the collected items, we will use a map, where the key is a value from a enum describing a gear slot (head, left arm, right arm), and the value will be the item, (or null if nothing is equipped). For the sake of article length, we will omit the PlayerBloc code below, but you can view the full code here.

With our PlayerBloc ready, we now need to make our Player component aware of when an item is equipped or unequipped, so that we can reflect it visually in the UI.

That is quite easy to do with flame_bloc:

class Player extends SpriteComponent
  with KeyboardHandler,
  HasGameRef<VeryGoodAdventuresGame>,
  // By adding the BlocComponent mixin to our component, it will automatically
  // receive updates of the state
  BlocComponent<PlayerBloc, PlayerState> {
  
  // Previous code omitted
  
  // With the BlocComponent mixin, we can now override
  // the onNewState method to receive live updates of the
  // bloc state
  @override
  void onNewState(PlayerState state) {
    // For the sake of simplicity, we just remove any gear component that
    // we currently have and add the currently ones on the new state
    for (final child in children) {
      child.shouldRemove = true;
    }
    
    for (final entry in state.gear.entries) {
      final item = entry.value;
      if (item != null) {
        add(PlayerGear(slot: entry.key, item: item));
      }
    }
  }
  
}

Summary

By using bloc to handle parts of our game state, we get improved separation of game logic, as well as a simple way to communicate between our game components and our UI widgets. Furthermore, we get helpful tools to make it easy for us to test the code of our game, which will allow us to continue building to our game without unintentionally breaking parts of it.

Even though we are not covering tests in this article, since that subject could be an entire article on this own, we suggest checking out two tools that can help us on that matter:

  • bloc_test: A testing library which provides helpers to test our blocs.
  • flame_test: Similar to bloc_test, but which instead focuses on helping to test Flame games and components.

The tests on the example app repository also can be used as reference.

Felix Angelov contributed to this article.

Interested in building a game with Flame? Contact us!

The demo app referenced in this article was inspired by the I/O Adventure game.

More Stories