The Flutter community has been experimenting with game development for a while, resulting in many incredible packages, experiments, and games. We saw interest in game development reach a new level when Google announced the Casual Games Toolkit at Google I/O 2022. At the same time, Google released I/O Pinball, a showcase game that pushed the limits of Flutter and demonstrated that the framework can in fact be used to build a robust 2D game. I/O Pinball was also our first experience building a game with Flutter as a team at VGV.
With interest in game development growing in the Flutter community, we wanted to push our skills further and figure out how we at VGV might standardize and approach game development in a way that mirrors our “boring” approach to app development. Today we release our demo: Very Good Ranch, a game of our own, built with Flutter and Flame.
About Very Good Ranch
How to play it
Make friends by keeping your unicorns happy — watch them grow with the right food and enough love. How many unicorns can you befriend?
Gameplay goes like this:
- From time to time, a baby unicorn will arrive at your ranch.
- Each unicorn has two bars: the top indicates their happiness and the bottom bar indicates their fullness level.
- If you keep your unicorns’ levels high, they will evolve. If you don’t, they will fly away.
- At times, different types of food will spawn at your ranch.
- As time passes, unicorns get hungry and bored.
- Clicking a unicorn pets them and increases their enjoyment (the top bar increases).
- Dragging and dropping food on the unicorn increases their fullness (the bottom bar increases).
- If the food fed to the unicorn is its favorite, their enjoyment will also increase (the top bar increases).
- If you keep a unicorn happy, it will evolve into the next unicorn stage.
Concepts and ideas
Game ideation is a big challenge on its own. When creating a game, you have to account for game design, playability, game mechanics, and rules, and on top of all that, making the game fun. We had quite the challenge ahead of us!
While we are not a game studio, we knew that we could create something cool and learn a lot in the process. So we agreed on only two requirements: the game should be simple and it should feature unicorns 🦄
We brainstormed for some time and landed on a Tamagotchi-like game, where you have to take care of multiple unicorns that arrive at your ranch. Like all games, we started focusing on its core mechanics, as outlined in this file (note that a few principles have been updated since the original gameplay design, for example, there are only four types of food, not five). Here are some early explorations into designs and game mechanics:
Once the basics of the gameplay were in place, we started polishing the game, adding sprite sheets, animations, and lastly, making the game look good, thanks to the magical assets provided by HOPR!
Then, came time to build the game — a huge shoutout to our teammate Renan Araújo, who led the development of this project. Let’s take a closer look at the code!
Bringing our scalable standards to game development
When we started planning the development of the game, we wanted to take what we learned on I/O Pinball and expand it. It was important for us to be able to apply our standards for app development to make this a scalable project that we could iterate upon, while maintaining a high-quality game. Read on for a closer look at some of the elements that embody our approach to scalable app development.
Separation of concerns
At Very Good Ventures, we stick to a layered architecture when building apps. This allows us to decouple our business logic from the presentation of the application so that we can easily test, iterate, and reuse components. We applied a similar approach to this game, with the help of components and entities.
Components: How an object looks in a game
In the context of Very Good Ranch, we used flame_behaviors to help us decouple the components from the entities and their behaviors. For example, we have a unicorn component which is focused on rendering the unicorn assets. Each unicorn component uses a sprite sheet because the assets are always animated, whether they’re walking around the ranch or eating food.
The above component is just a representation of how the unicorn looks. It does not have any logic or behaviors for interacting with anything else in the ranch. Components live in the ranch_components subpackage and come with their own sandbox so that you can play around and tweak them in isolation before adding them to the game.
Entities: A composition of one or more components and their behaviors
An entity consists of one or more components and one or more behaviors — think of it as how an object looks and behaves in a game. For example, a car entity in a game could consist of multiple components (such as wheels, axle, and a body, which may all be separate components) and multiple behaviors (such as moving and turning). Entities are the basic building blocks of your game.
In Very Good Ranch, we have a unicorn entity that contains a unicorn component (the visual representation), along with state properties such as the current evolution phase. Various behaviors are applied to the unicorn entity, such as the evolving behavior, moving behavior, petting behavior, and more, to determine how the unicorn interacts with its surroundings and other entities (food) within the game.
Behaviors: How an entity acts
As we mentioned above, our unicorn entity contains many behaviors such as the EvolvingBehavior to decouple the logic of how a unicorn acts from the state of a unicorn. Behaviors have access to their respective entity and can manipulate state, position, and other properties on the entity. In the case of the EvolvingBehavior, we inspect the evolution stage along with fullness, happiness, and number of times fed in order to determine whether a unicorn should evolve. If it’s time for the unicorn to evolve, we update the evolution stage on the entity accordingly. This triggers the evolution animation and the unicorn’s appearance changes.
Managing game state with Flame Bloc
In addition to flame_behaviors, Very Good Ranch also uses flame_bloc in order to manage state that is shared by Flame components as well as vanilla Flutter widgets (learn more about flame_bloc in this article).
Building on the evolution example, when a unicorn evolves, the EvolvingBehavior notifies a BlessingBloc (did you know a herd of unicorns is called a blessing?) which keeps track of the number of unicorns at your ranch in each stage. The EvolvingBehavior does this using the FlameBlocReader mixin which allows the behavior to have access to the BlessingBloc instance:
The BlessingBloc state looks like:
When the BlessingBloc receives a UnicornEvolved event, it emits a new state with the updated unicorn count. In addition, the BlessingBloc reacts to other events such as UnicornSpawned, and UnicornDespawned.
The _UnicornCounter widget reacts to changes in the BlessingBloc state using a BlocBuilder and renders the updated count accordingly.
This approach allows us to share state across Flame and Flutter in a reactive way while keeping things easy to test and using familiar mechanisms such as BlocProvider, FlameBlocProvider, and BlocBuilder to interface with a bloc.
One of the side effects of the layered, decoupled architecture is we can easily test things in isolation. For example, when testing a component we can leverage golden tests to ensure it looks as it should at any point in its animation cycle. The following snippet illustrates what a golden test for the baby unicorn’s idle animation looks like.
When testing an entity, we can ensure the entity contains the correct behaviors in response to changes in its state. For example, when a unicorn has the correct level of fullness, enjoyment, and number of times fed, we can verify it evolves to the next stage.
When it comes to testing behaviors, we can verify that when the behavior is applied to an entity with a predefined state, it should apply the correct changes to the entity. For example, we can verify that when the EvolvingBehavior is added to a baby unicorn that is ready to evolve, it evolves to the next stage and notifies the BlessingBloc.
When building scalable applications, it’s really important to be able to enforce best practices via automation. We use Very Good Workflows to run various checks in our CI, such as ensuring all tests are passing and that the affected packages are fully tested (in terms of line coverage).
For more about testing, check out our Flutter testing resource blog.
Games can include a variety of different assets including images, sprite sheets, custom fonts, audio files, and more. Because games are constantly updating, any asset loading in the middle of the game can impact performance. In Very Good Ranch, we wanted to be able to preload the assets so that they are available immediately when the user starts a game.
To do this, we created a LoadingPage which handles rendering a loading progress bar while assets are being loaded. Once all assets are loaded, the user is redirected to the TitlePage. The LoadingPage integrates with a PreloadCubit that manages the current preload progress and schedules the preloading of each asset group (FoodComponent, UnicornComponent, etc.). The PreloadCubit is provided at the root of the application via BlocProvider and initiates the preloading phase immediately:
Internally, the LoadingPage listens for state changes in the PreloadCubit via BlocBuilder and updates the progress bar using an AnimatedProgressBar.
The LoadingPage also uses a BlocListener to redirect to the TitlePage when the assets have all been loaded successfully:
The end result is a loading screen that reports the progress to the user and ensures all assets are available by the time the user enters the game. This process of loading the assets to the time you can start playing the game should only take a few seconds (depending on the speed of your internet connection, of course).
There is a saying in software development: "The first 90 percent of the code accounts for the first 90 percent of the development time. The remaining 10 percent of the code accounts for the other 90 percent of the development time." We’ve found that this can be true of game development when it came to Very Good Ranch. The last ten percent of development certainly seemed to account for 90 percent of the time spent due to final tweaks, polish, bug fixes, playtesting, and generally making the game juicy — or, as we like to think of it, taking our game from good to very good.
One of the most difficult challenges that we faced was synchronizing the unicorn animations. Animations can become stacked easily. For example, when a unicorn is in the process of evolving, the user can feed or even pet the unicorn, which can cause interruptions in the evolution animation.
While there is no super secret tip here on how to solve that, plenty of playtests were used to identify any issues and unit tests were added to ensure that any bugs we fixed would not resurface. Separation of concerns played a big role, as it made writing these tests easier and less fragile.
Very Good Ranch was a fun opportunity to continue to explore how Flutter (and Flame) can be used for game development. While VGV is primarily a Flutter app development consultancy, it’s pretty cool that we can easily switch gears and use the same tech stack we use to build apps to make robust games. Building this Very Good Ranch demo allowed us to stretch our development capabilities, test our existing tools, and provide some additional resources and tooling for the Flutter Community.
While we don’t plan to actively iterate on Very Good Ranch, we will continue to maintain it and hope that it serves as a good resource for Flutter developers looking to learn more about building complex games in Flutter with a focus on architecture, testing, and automation. We also hope you enjoy taking care of your unicorns and watching them grow — how many unicorns can you keep in your ranch?
Renan Araújo contributed to this article.