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:
- Write to the
Transform
component storage so that the game can update the positions of the camera and player entities. - Read from the
PlayerTag
andCameraTag
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.