August 19, 2020

Camera Follow System

A system for moving the player’s sprite based on user input was implemented in the Running Animation post. Here we will implement a system that has the game camera follow the moving sprite around.

The Amethyst project actually has a sprite_camera_follow example. The main part of this implementation is attaching a Parent component to an entity that also has a Camera component attached. This entity will be referred to as the “camera entity” throughout this writing.

The camera entity has three main components: Transform, Camera, and Parent. The Transform component describes the entity’s local position, rotation, and scale. As we’ve seen with the “Running Animation” post, Transform is useful for performing transformations on the entity it’s attached to. The Camera component is responsible for what part of the game is rendered to the screen. For our game, we use Camera::standard_2d to create a default 2D camera that renders the game in 2D space. More information on how to use the Camera component can be found in this section of Amethyst Pong tutorial.

Finally, the Parent component describes an entity with this component as having a parent entity.

world
    .create_entity()
    .with(transform)
    .with(Camera::standard_2d(200.0, 200.0))
    .with(Parent { entity: parent })
    .build()

According to the Amethyst API documentation for Parent, attaching a parent entity with transforms will also apply the same transformations to the child entity. If we have a Parent component where the parent entity is the player entity and attach it to the camera entity then all transformations applied to the player will be applied to the camera entity. That’s really useful!

The Problem

Then why is this post talking about implementing a camera follow system for the player? This is because in “Running Animation”, the MovePlayerSystem that was implemented is rotating the player’s sprite 180 degrees about the y-axis if a negative x-value input is registered (or the player is moving west).

This is problematic for the camera entity. Remember that the camera entity has a Parent component that is the player. If the player entity has a rotation transform, then the camera entity will also have the same rotation transform applied to it! The player’s sprite component will not be rendered to the screen anytime the user moves west because the camera is not pointing at the right area.

The camera entity transformations need to be independent of the player entity’s, while also being able to read them. One solution I found straightforward to implement was attaching component “tags” to the player and camera entities.

By using a component as a tag, the camera follow system can use these to filter through the Transform components belonging to either the player or camera entites. This will be explored when implementing the CameraFollowSystem.

PlayerTag and CameraTag components

Amethyst documentations mentions that components can be used to tag entities. Component tags are what we’ll be using to distinguish between Transform components attached to either the player or camera entities in the CameraFollowSystem.

We’ll create a PlayerTag and CameraTag component to identify the player and camera entities, respectively.

struct PlayerTag;

impl Component for PlayerTag {
  type Storage = NullStorage<Self>;
}

struct CameraTag;

impl Component for CameraTag {
  type Storage = NullStorage<Self>;
}

Then make sure to attach the components to their entities:

// add tag components
let player_tag = PlayerTag::default();
let camera_tag = CameraTag::default();

// create the player entity
world
  .create_entity()
  .with(player_tag)
  .with(transform)
  // ...
  .build()

// create the camera entity
  world
  .create_entity()
  .with(camera_tag)
  .with(transform)
  // ...
  .build()

Now that we have the player and camera entities with their own component tags and Transform components, let’s move onto implementing the CameraFollowSystem!

Implementing CameraFollowSystem

For the camera follow system we’re about to implement, we’ll tell the system that it needs to be able to do two things:

  1. Write to the Transform component storage so that the game can update the positions of the camera and player entities.
  2. Read from the PlayerTag and CameraTag storages to restrict the joining of components to specific entities.
struct CameraFollowSystem;

impl<'s> System<'s> for CameraFollowSystem {
  type SystemData = (
    WriteStorage<'s, Transform>,
    ReadStorage<'s, CameraTag>,
    ReadStorage<'s, PlayerTag>,
  );

  fn run(&mut self, (mut transforms, camera_tags, player_tags): Self::SystemData) {
    let mut player_x = 0.0;
    let mut player_y = 0.0;

    // Get the local position of the player.
    for (_player, transform) in (&player_tags, &transforms).join() {
      player_x = transform.translation().x as f32;
      player_y = transform.translation().y as f32;
    }

    // Update the camera position
    for (_camera, transform) in (&camera_tags, &mut transforms).join() {
      transform.set_translation_x(player_x);
      transform.set_translation_y(player_y);
    }
  }
}

The CameraFollowSystem will first get the transforms for the player entity by joining components that are PlayerTag and Transform. Then the x and y positions of the player entity are obtained from Transform::translation and stored in local variables player_x and player_y.

The player_x and player_y variables are used to update the Transform component for the camera entity. Like the player entity, we join components that are CameraTag and Transform so that we can update the camera position using Transform::set_translation_x and Transform::set_translation_y.

Awesome! This system will allow the camera to follow the player’s sprite around now!

Short Demo

To see this in action, I’ve made a demo on GitHub for anyone who would like to see this implementation in full-context: https://github.com/tigleym/sprite_animations_demo.

camera-follow

© Micah Tigley 2020

Powered by Hugo & Kiss.