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:
And then let's add our player into a FlameGame:
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:
Next we need to provide our new bloc to our game, we can do that by simply using flutter_bloc's BlocProvider widget:
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:
Finally, we add some logic on the Player class to collect items when pressing the space bar.
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:
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.