Key concepts in Forge 2D with Flame and Flutter

In this section we will learn about the basic elements in Forge 2D and their combination with Flame, which is the main plugin. This chapter represents an intermediate to learn the basic characteristics of Forge 2D with Flame, therefore, it is a purely referential chapter that you can use as support at any time in case you have any questions with a real implementation that we will see in the following chapters.

What is Forge 2D?

Let's start by talking about the key technology that is Forge2D; Forge2D is nothing more than an adaptation for Dart (and with this, for Flutter and Flame) of the Box2D physics engine; Box2D is an open source library that implements a two-dimensional physics engine. Therefore with Forge2D we can use a 2D physics engine for our applications and/or games.

As we mentioned before, you can use Forge2D independently in Dart or Flutter with the package:

$ dart pub add forge2d
$ flutter pub add forge2d

Or with Flame with the package:

$ flutter pub add flame_forge2d

In this book, we are going to use Forge2D along with Flame, therefore, we will use the previous package in projects in Flutter with Flame.

Main Class

In Flame, as the main class or Game type class, we used the Game or FlameGame classes, in Forge 2D we used Forge2DGame:

class MyGame extends Forge2DGame { }

In this way, we can interact with the world using the 2D physics engine.

For example, for the main class, we can specify the use of gravity:

MyGame() : super(gravity: Vector2(0, 15)); 

This means that gravity affects all dynamic bodies in the world at y=15, which means that objects will be pushed down like in real life; on the other hand, x=0 means that there is no horizontal gravity.

In Forge2D, our components are bodies and have physical qualities (unlike Flame components) and are the fundamental objects of the world in Forge2D and the main difference or addition that we have in Forge2D with respect to basic Flame.

The world

Like any game created in Flutter that uses Flame, in Forge2D you need a "world" to position all the components that make up the game; in other words, the objects or the so-called bodies that we can create in Forge2D; we are going to know this in a little more detail when we create a body, which is a Flame component, but, with the ability that this component can be interacted physically natively through Forge2D:

return world.createBody(<BODY>)

In short, the world class allows you to manage all physical entities.

To define the world, we cannot do it using the World provided in Flame's Game type class as FlameGame since they are not compatible; to access the Forge world, we must do it as follows:

import 'package:flame/camera.dart' as camera;
***
final cameraWorld = camera.World();

And add directly in the type class Forge2DGame:

add(cameraWorld);

The way to access the world in Forge with Flame is a bit strange and this implementation may change in the future for something more consistent.

Camera

As in Flame, we need to use a camera to "see the world", using the camera to specify which part of the world is being seen at any given time.

Just like in basic Flame, we must use the camera component and add directly to the Forge2DGame type class:

cameraComponent = CameraComponent(world: cameraWorld);
cameraComponent.viewfinder.anchor = Anchor.topLeft;
add(cameraComponent);

Camera and world

As we already know from Flame without any addition or basic Flame, there is a close relationship between the world and the camera, since, through the camera, we define what world is going to be observed and how.

The Forge2DGame class, the camera has a zoom level set to 10 by default, so the components will be much larger than in a normal Flame game. This is due to the speed limit in the world of Forge2D, which you would reach very quickly if you use it with zoom = 1.0. The problem with this is the positioning, since, if we want to position an element in:

10x10

What is actually happening in Flame with Forge2D is that it is going to position it in:

100x100

And it is precisely because of the zoom set at 10.

To work around this behavior, we can use the screenToWorld() method that converts screen coordinates to world coordinates; for example:

cameraComponent.viewport.size
>> Vector2 ([929,918])

Going to world coordinates, we will have:

screenToWorld(cameraComponent.viewport.size)
>> Vector2 ([92.9,91.8])

You can easily change the zoom level:

class MyGame extends Forge2DGame {
  MyGame() : super(zoom: 1);
}

And if we do the same calculation:

cameraComponent.viewport.size
>> Vector2 ([929,918])

We will see:

screenToWorld(cameraComponent.viewport.size)
>> Vector2 ([929,918])

Therefore, the screenToWorld() method returns the coordinates based on the zoom level; in the example above, when going from a zoom of 10 to 1, we will see that the objects in our window are 1/10 of the previous size.

Body

In Forge2D, instances of classes of type BodyComponent are known as bodies; these bodies are the fundamental objects in the world of a Forge 2D project; unlike the components in Flame, with Forge it is possible to use the 2D physics engine, therefore, bodies are the key piece in our games to interact with the 2D physics engine; as we will see later, we can interact with these bodies in various ways and there are different body types:

  • Static: Defines a body with zero mass, zero speed, can be moved manually.
  • Dynamic: Defines a body with positive mass, non-zero speed determined by forces.
  • Kinematic: Defines a body with zero mass, non-zero velocity set by the user.

In Forge, to create a body, we need to create a class and extend BodyComponent:

class MyBody extends BodyComponent {  
  @override  
  Body createBody() {  
     
  }  
}

Inside the createBody() method, we create a BodyDef that will contain the body definition:

final bodyDef = BodyDef(  
  position: Vector2(worldSize.x / 2, 0),  
  type: BodyType.dynamic,  
);

We can see that this will be a dynamic body, positioned vertically at the top of the screen and centered horizontally.

The creadeBody() method, which is automatically invoked internally according to the life cycle of a BodyComponent, when executed, sets the body in a property called body, also internal to the aforementioned components; therefore, when we need to do some operation with the body, we can reference this property called body.

You can change the color of the component using the paint property; for example:

paint = BasicPalette.red.paint();

Characteristics of physics bodies and components

In this section we will see how we can customize bodies in Forge 2D and how we can apply forces to the body to interact with the 2D physics engine; this section is somewhat abstract and you should only take it as a reference to understand how the 2D physics engine is formed; in the following chapters we will see real examples of what is explained in this section.

Shapes

In Forge2D, bodies must have shape; It can be circular, rectangular or any type of polygon; you can see bodies like hitboxes in Flame only these hitboxes have the physics component; for example, to create a circular shape for our body:

final shape = CircleShape()..radius = .35;

To create a box, we have:

final shape = PolygonShape()..setAsBoxXY(.15, 1.25);

We have different types of Shapes that we can use:

https://pub.dev/documentation/flame_forge2d/latest/flame_forge2d/CircleShape-class.html

Fixtures

Bodies, apart from type, can have other characteristics; this is something easy to understand since, if we take bodies to the real world, a ball would be a body, just like a car would be a body, but, between them there are differences such as weight, friction or rebound, and these we can simulate characteristics with Forge2D using fixtures.

In Forge2D, a fixture has density, friction and restitution associated with it:

  • Density is the mass per square meter, that is, the weight.
  • Friction [0,1], is the amount of opposing force when the object rubs/slides along another body.
  • Restitution [0,1], is the amount of rebound of the body.

Following the previous code, we are going to create a fixture of our body:

final fixtureDef = FixtureDef(shape);
world.createBody(bodyDef)..createFixture(fixtureDef);

It is important to note that the fixtures are established in the shape and not directly in the body; all this logic must be defined in the createBody() method and once it is executed (which is done automatically by the Forge 2D life cycle) we can access the body and its components through a property called body.

Forces, impulses and speed

On the body, we can apply different magnitudes or forces to move it using the following methods.

  • Apply Force/applyForce: Apply a force to a point in the world; forces change the speed of a body gradually over time. For example, a car that begins to accelerate will gradually gain speed until it reaches the desired speed.
  • Apply linear impulse/applyLinearImpulse: Apply an impulse at a point in the world. This immediately changes the speed; the impulses produce immediate changes in the speed of the body. For example, a collision between an inert body and another moving body will immediately cause the inert body to obtain the speed of the moving body; although, the movement depends on the inert weight and the applied force.
  • Linear Velocity/linearVelocity: The linear velocity of the center of mass. Regardless of the size and mass of the body, the desired speed is established.

In the end, these methods allow us to move a body. To use the previous methods, 2-dimensional vectors are used indicating the movement towards where we want to move the body; for example, If we want to move only horizontally, we use a vector with a value in X (for example Vector2(50,0)) or in the vertical to make a jump, we use a vector with a value in Y (for example Vector2(0,-20)).

The first methods can be influenced according to the type of body, weight and gravity, unlike linearVelocity whose movement is automatically applied to the body regardless of the type of body, weight and gravity.

You can learn more about bodies, their properties and methods at:

https://pub.dev/documentation/forge2d/latest/forge2d_browser/Body-class.html

ContactCallbacks: Collisions or contact between bodies

As happens in Flame in which we have a utility that allows us to manage collisions between components, in Forge we have a practically the same scheme, but, applied to managing contacts (collisions) between bodies and to use it from a BodyComponent, we must import the ContactCallbacks mixin and we can use it through the following methods:

  • beginContact(Object other, Contact contact) void, called when two bodies start to touch each other.
  • endContact(Object other, Contact contact) void, called when two bodies stop touching each other.

Regarding the parameters:

  • Object other, corresponds to the contact BodyComponent, in Flame, they are the components.
  • Contact contact, information about the contact.

As with collisions in Flame, you can use these methods for operations like playing sounds, displaying sprites, or any other game logic. When defining the body, it is essential that you set the userData to the bodies that we are interested in listening to the contacts through this mixin:

BodyDef bodyDef = BodyDef(
    ***
    userData: this);

The userData corresponds to application data that is used internally by Forge2D to check contacts.

- Andrés Cruz

En español
Andrés Cruz

Develop with Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz In Udemy

I agree to receive announcements of interest about this Blog.