johan helsing.studio

Extreme Bevy 2: Camera and background

We’ll continue where we left off and start making the cube moving application look and feel more like a game by making the camera follow the local player and adding a map background and some borders to the map.

Keeping things organized

So before we go on, I’d like to take a short moment to reorganize our project before it gets messy.

Our main.rs file is starting to get long, and has a mix of different concerns in it.

For one, there is a lot of input stuff that in my mind is kind of its own topic. So let’s go ahead and create a new module, input.rs and move all the input related code there:

use bevy::prelude::*;
use bevy_ggrs::ggrs::PlayerHandle;

const INPUT_UP: u8 = 1 << 0;
const INPUT_DOWN: u8 = 1 << 1;
const INPUT_LEFT: u8 = 1 << 2;
const INPUT_RIGHT: u8 = 1 << 3;
const INPUT_FIRE: u8 = 1 << 4;

pub fn input(_: In<ggrs::PlayerHandle>, keys: Res<Input<KeyCode>>) -> u8 {
    let mut input = 0u8;

    if keys.any_pressed([KeyCode::Up, KeyCode::W]) {
        input |= INPUT_UP;
    }
    if keys.any_pressed([KeyCode::Down, KeyCode::S]) {
        input |= INPUT_DOWN;
    }
    if keys.any_pressed([KeyCode::Left, KeyCode::A]) {
        input |= INPUT_LEFT
    }
    if keys.any_pressed([KeyCode::Right, KeyCode::D]) {
        input |= INPUT_RIGHT;
    }
    if keys.any_pressed([KeyCode::Space, KeyCode::Return]) {
        input |= INPUT_FIRE;
    }

    input
}

The input system now needs to be public, so we can add it to the schedule in main.rs.

There is also some input related code inside move_players. What it does is basically convert the low-level input format to a direction, so let’s wrap that code in its own function:

pub fn direction(input: u8) -> Vec2 {
    let mut direction = Vec2::ZERO;
    if input & INPUT_UP != 0 {
        direction.y += 1.;
    }
    if input & INPUT_DOWN != 0 {
        direction.y -= 1.;
    }
    if input & INPUT_RIGHT != 0 {
        direction.x += 1.;
    }
    if input & INPUT_LEFT != 0 {
        direction.x -= 1.;
    }
    direction
}

Now lets add the module in main.rs and use it.

use input::*;

mod input;

fn move_players(
    inputs: Res<PlayerInputs<GgrsConfig>>,
    mut players: Query<(&mut Transform, &Player)>,
) {
    for (mut transform, player) in &mut players {
        let direction = direction(&inputs[player.handle]);

        if direction == Vec2::ZERO {
            continue;
        }

        let move_speed = 0.13;
        let move_delta = (direction * move_speed).extend(0.);

        transform.translation += move_delta;
    }
}

That unclutters main.rs quite a bit. I’d like us to do one more thing before we move on: let’s create a new module where we’ll keep all our custom components, aptly named components.rs.

use components::*;

// ...

mod components;

Player is our only component so far, lets move it into components.rs and make it public:

use bevy::prelude::*;

#[derive(Component)]
pub struct Player {
    pub handle: usize,
}

That’s enough for now, let’s get on with the actual content of this tutorial.

We’ll start by making the camera follow the local player.

The first thing we’re going to need, is some way to tell which player is the local player so we know what player the camera should follow.

We know this already inside the wait_for_players system, and we’re probably going to need this handle in several different system, so lets create a resource for it.

#[derive(Resource)]
struct LocalPlayerHandle(usize);

And then we simply insert it in wait_for_players:

    for (i, player) in players.into_iter().enumerate() {
        if player == PlayerType::Local {
            commands.insert_resource(LocalPlayerHandle(i));
        }
        // ...
    }

Now, we’re ready to write the camera system. It will take the local player resource as an optional dependency, find the right player, and snap the camera position to the player.

So let’s declare our inputs:

fn camera_follow(
    player_handle: Option<Res<LocalPlayerHandle>>,
    players: Query<(&Player, &Transform)>,
    mut cameras: Query<&mut Transform, With<Camera>>,
) {

If there is no player handle yet, we simply return early:

    let player_handle = match player_handle {
        Some(handle) => handle.0,
        None => return, // Session hasn't started yet
    };

Then we loop through and find the player position:

    for (player, player_transform) in &players {
        if player.handle != player_handle {
            continue;
        }

        let pos = player_transform.translation;

        for mut transform in &mut cameras {
            transform.translation.x = pos.x;
            transform.translation.y = pos.y;
        }
    }

When we update the camera position, we keep its original z position.

We’ll add this system to the regular systems and not the rollback schedule, since the camera shouldn’t affect shared game state, it’s only concerned with the latest player position.

        .add_systems(Startup, (setup, spawn_players, start_matchbox_socket))
        .add_systems(Update, (wait_for_players, camera_follow)) // <-- changed
        .add_systems(GgrsSchedule, move_players)

If your run the code now, you’ll see a panic in the browser console:

panicked at 'error[B0001]: Query<&mut bevy_transform::components::transform::Transform, bevy_ecs::query::filter::With<bevy_render::camera::camera::Camera>> in system extreme_bevy::camera_follow accesses component(s) bevy_transform::components::transform::Transform in a way that conflicts with a previous system parameter. Consider using Without<T> to create disjoint Queries or merging conflicting Queries into a QuerySet.'

It happens because both our queries access the Transform component, and one of them is a mutable query, and it could theoretically be problematic if a component had both a Player and a Camera component on it. We’ll do what the error message suggests and make sure our queries are disjoint by adding Without<Player> to our camera query:

    mut cameras: Query<&mut Transform, (With<Camera>, Without<Player>)>,

If you run the game now, it should work as expected.

Adding a background

It does feel a bit weird however, it kind of looks like the local player is stationary, while it’s the other player that moves in the opposite direction.

We need some kind of reference to tell what is moving in relation to what. The original Extreme Violence uses a simple grid for this. The simplest way I could think of to add a grid is to use colored rectangular sprites as lines. Let’s start by adding a constant that defines our map width and height. This will be useful later.

const MAP_SIZE: u32 = 41;
const GRID_WIDTH: f32 = 0.05;

Then in setup, we simply spawn the sprites:

    // Horizontal lines
    for i in 0..=MAP_SIZE {
        commands.spawn(SpriteBundle {
            transform: Transform::from_translation(Vec3::new(
                0.,
                i as f32 - MAP_SIZE as f32 / 2.,
                0.,
            )),
            sprite: Sprite {
                color: Color::rgb(0.27, 0.27, 0.27),
                custom_size: Some(Vec2::new(MAP_SIZE as f32, GRID_WIDTH)),
                ..default()
            },
            ..default()
        });
    }

    // Vertical lines
    for i in 0..=MAP_SIZE {
        commands.spawn(SpriteBundle {
            transform: Transform::from_translation(Vec3::new(
                i as f32 - MAP_SIZE as f32 / 2.,
                0.,
                0.,
            )),
            sprite: Sprite {
                color: Color::rgb(0.27, 0.27, 0.27),
                custom_size: Some(Vec2::new(GRID_WIDTH, MAP_SIZE as f32)),
                ..default()
            },
            ..default()
        });
    }

Now if you run the game, you’ll see the grid, but it is probably on top of the players. Instead of moving the background forward, we’ll move the players closer to the camera:

fn spawn_players(mut commands: Commands, mut rip: ResMut<RollbackIdProvider>) {
    // Player 1
    commands
        .spawn((
            Player { handle: 0 },
            SpriteBundle {
                transform: Transform::from_translation(Vec3::new(-2., 0., 100.)), // new
                sprite: Sprite {
                    color: Color::rgb(0., 0.47, 1.),
                    custom_size: Some(Vec2::new(1., 1.)),
                    ..default()
                },
                ..default()
            },
        ))
        .add_rollback();

    // Player 2
    commands
        .spawn((
            Player { handle: 1 },
            SpriteBundle {
                transform: Transform::from_translation(Vec3::new(2., 0., 100.)), // new
                sprite: Sprite {
                    color: Color::rgb(0., 0.4, 0.),
                    custom_size: Some(Vec2::new(1., 1.)),
                    ..default()
                },
                ..default()
            },
        ))
        .add_rollback();

Now we have a simple background that gives us some impression that we are actually moving.

One more thing, our players shouldn’t be able to move off the map. For now we’ll simply enforce this in the move_player system:

        let move_speed = 0.13;
        let move_delta = direction * move_speed;

        let old_pos = transform.translation.xy();
        let limit = Vec2::splat(MAP_SIZE as f32 / 2. - 0.5);
        let new_pos = (old_pos + move_delta).clamp(-limit, limit);

        transform.translation.x = new_pos.x;
        transform.translation.y = new_pos.y;

Player 1:

Player 2:

Ok, now the players have a very minimal space to move around in, however there isn’t much interaction between the players. In the next part we’ll be finally be adding some violence to this game.

Reference implementation

Diff for this part

Comments

Loading comments...