Making a gamepad for my Flutter game
Did you know that building games in Flutter is just as easy as building apps? There is even a game engine built on top of it called Flame. Today we are first going to explore building a simple game with Flame and then enhance the game for dual-screen and foldable devices. This space themed game will use an on-screen gamepad controller as input. Surface Duo in dual-landscape mode feels like a bespoke device for gaming and we want to place the gamepad controller on the bottom screen.
This is also a talk
The contents of this article are the same as the talk that we presented recently at the Flutter Global Summit. I recommend watching the video while you skim through the article. The code presented here is also published on GitHub so you can also follow along by browsing the code.
First, let’s build a game
You control a ship in space. Your mission is to destroy space rocks while also staying alive. The rocks can hurt your ship if they collide. The code and talk are structured in eleven steps. Steps one through eight focus on building the actual game. I think looking at the actual code provides developers a lot more value than explaining code in blog form, but here is a summary of the components I wrote and how collision, positioning, and time works in Flame.
Everything in Flutter seems to be a Widget, the same way that everything in Flame seems to be a Component. So, while the game itself is a widget, everything inside it is not. Here is every component of our game.
Joystick and Button
This is where player input comes from. These are components included with Flame. The subtle difference between them is in the way they are used. The button emits events that we react to, using an
onPressed function. The joystick is more analog. We read values from it on every frame, and we move the ship accordingly. Behavior is not pushed by the joystick. Other components pull data from it on every frame. The joystick and button are the focus of the second part of this article. We want to display them on the bottom screen in dual-landscape.
This is the component that the player controls. It takes both the joystick and button as dependencies, making this component the one that interprets input. Flame uses mixins a lot. The ship is a sprite component with a collision mixin and a game reference mixin. So, the ship detects collision and does things when it collides with other components. It also has access to the top-most game component, so it can add bullets to it when needed.
Rocks also detect collisions. If they collide with bullets, they break into smaller pieces. When they become very small, they disappear. This is a good example of how hit boxes work in Flame. Rocks are circular and therefore have circle hit boxes.
Bullets are almost the same as rocks. They are just smaller and have a slightly different logic. All moving components in this game make use of the
dt parameter in the
update method. This is a good place to discuss how most games deal with variable framerate. The
update method gets called on every frame. If you want a component to move between frames, you might be tempted to simply change their x and y coordinates by a set amount. But framerate is not constant, which means that in one second you might get 60 frames, and in the next second you might get 50. When this happens, the component moves slower, making the movement jumpy and uneven. The
dt parameter tells us the “delta time” from the last frame and we use it to make movement depend on time, rather than number of frames.
Enhancing the game for foldables
When running the game on a foldable device, more specifically Surface Duo, we want to have a controller on the bottom screen when in dual-landscape. This already happens with the game as it is at step eight, but there are a few things that don’t seem right. Here is a preview of it:
For this experience to be better, we want the bottom screen to display only the joystick and button. This way, the player can focus on the top screen, without fingers blocking the view. Also, this setup would prevent rocks from lingering behind the hinge, where we can’t see them.
Up until this point, the game components are simply thrown together in the top-most game component. They coexist without any structure. Here is a representation of that:
Since we want the input to be on a separate screen, it makes sense to group the game components into two groups: game elements and game controller. In addition to that, the ship, rock, and bullet components now look at the top-most game component to decide if they are on the screen or out of bounds. We want to change that behavior. They now need to look at their immediate parent for that information. By doing this, the game can span across both screens and the game elements can be limited to the top screen. Here is a representation of that:
Position each group on a screen
This change now allows us to position the two larger components so that they each occupy one screen. The smaller components no longer use the top-most game component to make changes but use their parent instead. Positioning the larger components is done using MediaQuery displayFeatures. This property exposes data about foldable displays and tells us if a hinge exists and where it is positioned. We did a deep dive on this when foldable support was promoted to stable. You can find the specific code from this game that looks at display features at this link.
Everything we have done so far is using game components and not widgets. There are cases where you might want your gamepad controller to be a separate widget. This would encourage reuse of the controller you create. It would also allow you to use the controller with games not made with Flame. It made some sense to me, and therefore the video and GitHub repo also include an eleventh step: Making the controller a widget and separating it from the game using TwoPane.
If you are learning about TwoPane now, there is a dedicated article for it that we invite you to read. The short version is that TwoPane is a widget that helps with dual-screen, foldable, tablet, and desktop enhancements. It displays two children side-by-side in certain scenarios, making it easy to accomplish certain layouts. The reason we use it today is that it automatically positions the controller and the game as we need, each on its own screen.
Final version on Surface Duo
The result is a game that has a controller on the bottom screen and the game on the top screen. If you unspan the app or rotate the device, it reverts to overlaying the controller on top of the game.
Game running on Surface Duo. The bottom screen shows a controller on a black background. The top screen shows the rest of the game on a gray background.
Call to action
- Clone the game and run it.
- View the talk that inspired this article.
- If you have any questions or would like to tell us about your apps, use the feedback forum or message us on Twitter @surfaceduodev.