I really like video games. And I like to create and develop software. So I started a lot of attempts to give game development a try, even with the fact in my mind that I can’t even draw round circles on a sheet of paper…
During the development of my Game of Life clone in Flutter (linked below) I decided to start again — I welcome you to my first series of posts about how to get “real” game done, using Flutter, Flame and Box2d.
My goal is to have at least 5 posts describing the process and the involved tools. Let’s start with the basic things you will need to get started. Besides a fully installed and working Flutter environment we will make use of the following libraries:
The game we will create will render a maze on your screen and the player will control a ball rolling through it by moving his phone (gyroscope required). The goal is to get out of the maze as quick as possible. I will explain each package along the way — let’s start and create a new project:
flutter create mazeball
Add the mentioned libraries to your pubspec.yaml file. Please note that Box2D will be added automatically as soon as you add dependencies on Flame (as it is a part of it). The file should look like this:
Please remove the test folder after updating the pubspec file (the above file doesn’t contain a reference to it anymore) because for now we do not create tests for our game.
We will use Flame as our game engine. Flame does offer a nice set of features for 2D games. Sure, it’s not the next Unity or Unreal but it is easy to use and will offer all we need. We need to initialize Flame and make sure our app creates and starts our game.
Head over to the main.dart file and replace it with the one below. Don’t worry if you see any errors, we will solve them in a moment. In case you choose another name for the project change it accordingly.
I will explain each part now, feel free to skip if the code alone is fine. The file starts with our imports:
- Flame utils— helps us to start up the app as we need it
- Material — finally kicking off the app
- Services — same as point one, required in our setupFlame() method
- Game — created in the next step
- Async — asynchronous support
After the import section the well known main() function, this time with the async keyword. This allows us to wait for other functions inside, exactly what we will do while calling setupFlame(). Below this call you can find the creation of our game object and the call to the build in runApp. Thanks to Flame, we have the handy widget property that we simply hand in and all starts. The setupFlame() function does the following:
- Creates a new Util object
- Ensures our app is running in fullscreen
- Ensures our app is fixed to the portrait-up orientation, no rotation is allowed
You might have spotted the async keyword here, too. The setupFlame() function has to wait until the Util methods are finished.
Create a new file in the lib folder called game.dart and copy in the content below:
The class MazeBallGame is our main element, it extends the Game class offered by Flame. Thanks to the base class we don’t have to build our own game loop or take care about resize events. It will also be the first element called by Flame to render/draw and update all elements of our game.
You might have noticed the World object or _gravity property in the above file, below are all details about our game class.
Box2D will take care of all our physic related operations. This includes handling of collisions, accelerating/decelerating a body/mass and simulating the different shapes/materials in our game.
The two constant int properties are needed to create our World object; this happens during the construct of our MazeBall class. To set up the world you also need to define gravity (if this would be as easy as in the real world).
In our game gravity is not needed — so we define this as a zero Vector2. If you don’t know what a vector is (in our case always a Vector2) imagine it as a point inside a coordinate system: each Vector2 has an X and an Y value. Our _gravity vector is set to (0,0). A Vector3 would have X, Y and Z and this would go on like this for Vector4, Vector5, etc.
A gravity of (0,1) would make sure elements are falling down from the top of the screen to the bottom. Keep in mind our coordinate system starts in the top left corner with (0,0).
After the world has been created, we call initialize that will call resize as soon as the app is ready. Ready in this case means launched and Flame was able to check the display size.
Below the resize method you will find the two mentioned methods from the start:
- render — render is called to draw elements on our canvas
- update — update is called with a time parameter (called t); it contains the time since the last call. The goal is to run at 60fps which means the function will be called 60 times in one second. As this is not always the case, the time parameter tells you how much time has passed by since the last call.
If all is set up correctly you should be able to start the app on your phone or simulator. I know, nothing special, just a black/blank screen but this is how all great things start 🙂
I don’t want to close the first part without drawing something — let’s hack something in very quickly. Please replace the part from resize to update with the following code:
What happens in here is the following:
- Declaring a Paint object — required to paint something
- Saving the screen size and a related rectangle for later use — the values are provided by Flame inside the resize event
- Defining a scale factor to increase the elements in our app — we need this later to have at least somehow correct dimensions for our physic interactions. A scale factor of 5 was chosen because I like this number 🙂
Inside the render function we have to perform this:
- Make sure we are ready to draw — the screenSize property should be set
- Save the canvas before we draw using the build in save() method
- Apply our scale factor using the width of the screen — thanks to the build in scale() method this is very easy
- We move the (0,0) point of our canvas to the center (see phone graphic for a reminder) — the build in translate method allows you to move the canvas starting point to a given set of coordinates
- Using the drawCircle() method we draw a circle at (0,0) with a radius of 0.1 using our paint object
- The call to restore will finish our render loop for a single frame
Starting the app now should render a small circle near the center of the screen like below. Keep in mind that this was just added to have something on the screen, we will remove it in our next step.
You got into contact with Flame, Box2D. You learned a little about vectors and what you need to get a basic game started. You even had a tiny lesson about gravity. Sure, we don’t have the next AAA game on the screen, but we are on our way to build something using Flutter that will definitely be counted as a game.
In the next part we will add our player and the related movement/physic we need to make him move by using the gyroscope. In case you would like a head start checkout demos for the Sensors library or have a look at the (written in C++) Box2D examples.