CameraComponent in Flame to follow components in Flutter and use of World

- Andrés Cruz

En español
CameraComponent in Flame to follow components in Flutter and use of World

We currently have a background that is larger than what can be displayed in the window, resulting in a large part of it that cannot be visible; in order to take advantage of this non-visible background and have it show up as the player scrolls, we need to place a camera following the player.

The camera concept has been worked on in any game engine like Unity or Unreal or 3D/2D content creation software like Blender should be familiar to you; but in essence, the camera is nothing more than a mechanism through which we can observe a part of the world, where the world is nothing more than all the components that are within the game; and using coordinates, we can move this observer.

In these types of games, usually the camera follows the player and with this, we can visualize parts of the background that are not visible as the player moves through the world.

You can get more information about it at:

https://docs.flame-engine.org/latest/flame/camera_component.html

In Flame we have a component called CameraComponent with which we can indicate the display area.

World

The World component, as its name indicates, is a component that can be used to house all the other components that make up the game world. In the case of the game that we are implementing it would be:

  • The background.
  • The tiles.
  • The player.
  • The meteorites.

All these components must be added in an instance of the World component so that they are in the same layer and with this, to be able to interact with the player.

Using a World component is as simple as creating the instance:

final world = World();

And add in Flame:

add(world);

And at this point, all the components that are part of the game must be added to the world instance:

world.add(<COMPONENT>);

The World component and CameraComponent are designed to work together; an instance of the CameraComponent class "looks" at the world and this is why we introduced the world component along with the camera component.

It is usually not mandatory to use a World component for our Flame games, since we can add our game components directly into the Flame instance:

add(player);

Or in the same components, but, in this type of game that we have a bigger world than can fit on the screen, it is necessary to use a camera that follows or watches over our player.

CameraComponent 

CameraComponent, as we mentioned before, is the component used to observe the world, we have several properties to set on this component so that it "looks" exactly where we want and follows a component:

The Viewport is a window through which the world is seen. That window has a certain size, shape, and position on the screen.

The Viewfinder is responsible for knowing what location in the underlying game world we are currently looking at. The Viewfinder also controls the zoom level and rotation angle of the view; this property is key in these games since it is the one that is updated to "look" at the player.

Like any other game, the camera must observe the player who is moving through the world, although luckily this update is done automatically using the follow() function, which receives as a parameter, a component to observe and update the position of the camera as it moves across the screen:

cameraComponent.follow(player);

The Viewfinder allows you to customize aspects of the display such as the anchor, with which we can specify the center of the camera; that is, our world looks like the following:

The camera must be centered in the lower left corner, that is, in the bottomLeft:

And for this:

cameraComponent.viewfinder.anchor = Anchor.bottomLeft

Deploy the camera component

Having clarified how the world and camera component work, let's implement the logic in our application:

class MyGame extends FlameGame *** {
  ***
  late PlayerComponent player;

  final world = World();
  late final CameraComponent cameraComponent;

  @override
  void onLoad() {
    var background = Background();

    add(world);

    world.add(background);

    background.loaded.then(
      (value) {
        player = PlayerComponent();

        cameraComponent = CameraComponent(world: world);
        cameraComponent.follow(player);
        cameraComponent.setBounds(Rectangle.fromLTRB(0, 0, background.size.x, background.size.y)));

        // cameraComponent.viewfinder.anchor = Anchor.bottomLeft;
        cameraComponent.viewfinder.anchor = const Anchor(0.1, 0.9);
        add(cameraComponent);

        cameraComponent.world.add(player);
      },
    );

    add(ScreenHitbox());
  }
}

Key points

We define and add to Flame the world object:

final world = World();
***
add(world);

When using the camera component, it is necessary to use a world object:

cameraComponent = CameraComponent(world: world);

With an instance of the previous component, we can customize various aspects of the display, such as the camera following a component through the follow() function, which receives the component to follow as parameters:

cameraComponent.follow(player);

The center of the camera is defined in the lower left corner, but if we leave it this way:

cameraComponent.viewfinder.anchor = Anchor.bottomLeft; // Anchor(0, 1);

By positioning the camera that is following the player, we will see that the player is partly visible:

This is because the camera "follows" the player's anchor, which is aligned in the center:

 PlayerComponent({required this.mapSize}) : super() {
    anchor = Anchor.center;
    debugMode = true;
  }

To avoid this behavior, we can specify numeric values for the camera's anchor that are not as constrained as the previous value.

cameraComponent.viewfinder.anchor = const Anchor(0.1, 0.9);

And with this we have a complete visualization of the player:

Added to the above, we can also place restrictions on the camera display (the area visible by the camera, which in this case, is precisely the size of the image or map); It is important to mention that the display area corresponds to a rectangle with the size of the background; therefore:

cameraComponent.setBounds(Rectangle.fromLTRB(0, 0, background.size.x, background.size.y)));

And from the main, we pass the position of the camera:

lib\main.dart

@override
void update(double dt) {
  if (elapsedTime > 1.0) {
    Vector2 cp = cameraComponent.viewfinder.position;

    cp.y = cameraComponent.viewfinder.position.y -
        cameraComponent.viewport.size.y;
    world.add(MeteorComponent(cameraPosition: cp));
    elapsedTime = 0.0;
  }
  elapsedTime += dt;
  super.update(dt);
}

As you can see, we do not have the position of the camera directly, therefore, we must make a calculation; as we mentioned before, through the Viewfinder:

cameraComponent.viewfinder

We have the position of the camera, which is updated through the player as it moves across the screen, and we can obtain it with:

cameraComponent.viewfinder.position

But, this gives us the position of the camera in the lower corner, that is, the bottom, and we need to generate the meteorites in the upper part, for this reason, we subtract the height of the camera that we can obtain with:

cameraComponent.viewport.size.y

Este material forma parte de mi curso y libro completo sobre el desarrollo de juegos en 2D con Flutter y Flame.

Andrés Cruz

Desarrollo con Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz en Udemy

Acepto recibir anuncios de interes sobre este Blog.