Game Dev Diary

Day 7: Let’s make obstacles and learn about collision #

So we start the day discussing what we wanna do next, we decided that we could try to aim for understanding how we could make a pong game and so for that one of the things that are missing is how to handle collision!

But before we get to that we just talked a little bit about super random things:

So the first thing we need to do to handle collision is probably to refactor the move_systems into one large move_system so we can handle the collisions in that one system instead of many systems, and we do just that!

fn move_system(
    input: Res<Input<KeyCode>>,
    mut query: Query<&mut Transform, With<Balloon>>,
) {
    if let Ok(transform_player) = query.get_single_mut() {
        if input.pressed(KeyCode::Up) {
            transform_player.translation.y += BALLOON_SPEED
        }
        if input.pressed(KeyCode::Down) {
            transform_player.translation.y -= BALLOON_SPEED
        }
        if input.pressed(KeyCode::Right) {
            transform_player.translation.x += BALLOON_SPEED
        }
        if input.pressed(KeyCode::Left) {
            transform_player.translation.x -= BALLOON_SPEED
        }
    }
}

NICE! Simple enough!

Now let’s add something to collide with! We will create a big rectangle to collide with, we look up how to create rectangles, we find Box in the documentation, she creates a Component for it and spawn it in setup.

#[derive(Component)]
struct Object;

// snip...

fn setup(
    mut commands: Commands,
    // snip...
) {
    // snip...

    commands
        .spawn(MaterialMesh2dBundle {
            mesh: meshes.add(shape::Box::new(70., 7000., 5.9).into()).into(),
            material: materials.add(ColorMaterial::from(Color::ORANGE_RED)),
            transform: Transform::from_translation(Vec3::new(-200., 80., 0.)),
            ..default()
        })
        .insert(Object);
}

And it works! We have a pretty long rectangle that extends as far as our window

A green background. With two circles on top of it, one red and the other pink, to the left of them there’s a long orange strip that extends from the top of the window to the bottom of the window.

Okay cool! Next we gotta figure out how can we calculate collision! I remember that Bevy had a collision function, and so I guided A to try searching for it. And she does! A searches for colli results in a collide appearing as the first result because Rust docs are awesome!

Now time to understand what does it do or how does it work! This is a bit of a new lesson as A is not used to function signatures yet. I briefly explain function syntax and that she should read the documentation below the function to figure out what it does.

Here’s the function signature:

pub fn collide(
    a_pos: Vec3,
    a_size: Vec2,
    b_pos: Vec3,
    b_size: Vec2
) -> Option<Collision>

We talk through what each of the parameters mean and I ask A where does she think she could get the parameters to give the function from. She says she can get a_pos and b_pos from the Transforms and yep, that’s correct! But she is not sure where to get a_size and b_size from. I guide her to the idea that the sizes right now are determined by our meshes and I take her to the Mesh page and leave her there to explore for a bit. She scrolls down for some time until eventually I nudged her towards the method at the bottom of the page compute_aabb! Because it shares the aabb part with the collide_aabb::collide function!

I explain what compute_aabb is by asking her to copy the “Axis-Aligned Bounding Box” and googling it and I use some of the images to explain that this is kinda like those collision boxes 3D games use around their character models which don’t match the characters perfect since she is familiar with them.

Next up we need to click the return value of the compute_aabb since I have never used it before I actually have no clue what it looks like.

pub struct Aabb {
    pub center: Vec3A,
    pub half_extents: Vec3A,
}

Oh interesting, Vec3As instead of Vec3s. And also half_extents. I have a suspicion what that is but let’s google it just to be certain. And yep it means “radius but for rectangle.” It’s half the size of the dimensions, which for a circle would be the radius. Neat, I explain that to her on one of the images and we go make some magic happen!

We know everything our system will need and for simplicity we can add this functionality to the move_system so we update its inputs:

fn move_system(
    input: Res<Input<KeyCode>>,
    mut query: Query<(&mut Transform, &Mesh2dHandle), With<Balloon>>,
    query2: Query<(&Transform, &Mesh2dHandle), With<Object>>,
) {
    if let Ok((mut transform_player, mesh_player)) = query.get_single_mut() {
        if let Ok((transform_object, mesh_object)) = query2.get_single() {

She then imports the collide function and start filling the requirements by using transform.translation for the a_pos and b_pos parameters of the collide function.

Then I start guiding her towards how to use what we know about the meshes, firstly I have a prediction that Handle<Mesh> (which is basically what Mesh2dHandle is) implements Deref and so we can easily access .compute_aabb from it. But as you’d expect I have a terrible habit of confusing Assets and Resources, again. And so no, Asset's handles don’t magically implement Deref, in fact if you look at the methods on Handle<T> while ignoring its documentation you will realize that it has no methods that return T or even &T at all! Instead you have to follow the yarn manually to the bucket and there you will find your asset from its handle!

This is the point where I realize teaching via guidance might not be ideal for this and teaching by example is probably the way to go, so I ask for the keyboard and write the rest while explaining it.

So let’s go! We need to add one extra thing to our system:

fn move_system(
    input: Res<Input<KeyCode>>,
    mut query: Query<(&mut Transform, &Mesh2dHandle), With<Balloon>>,
    query2: Query<(&Transform, &Mesh2dHandle), With<Object>>,
    meshes: Res<Assets<Mesh>>,
) {

Now we have access to the meshes themselves because they are Assets!

Next up let’s get the actual meshes:

let size_player = meshes
    .get(&mesh_player.0)
    .unwrap();

I explain that this is how we ask the bucket (Assets) for our particular mesh, and that unwrap() is because the yarn might have been severed and the thing it was supposed to lead us to is no longer there, in which case we will return a Option::None and because we unwrap() this means if we ever remove our meshes this system will cause the game to crash which is generally considered a bad idea but we are fairly confident we will not be removing these meshes because otherwise we won’t really have a balloon or objects at all and that’d be an even more confusing problem than just crashing!

And then I finish writing this monstrosity while explaining it:

let size_player = meshes
    .get(&mesh_player.0)
    .unwrap()
    .compute_aabb()
    .unwrap()
    .half_extents
    .truncate()
    * 2.0;

After we get our Mesh we want to get the Aabb from that mesh, but that too might not be possible! (Not actually sure why at the time of writing, maybe I looked it up back then, quite interesting) and after we have our Aabb we want to get the half_extents from it because it’s the actual property that talks about the size of the mesh. But out collide wants a Vec2 not a Vec3 (or in this case it’s a Vec3A) so we want to truncate it. Which drops the Z value of the Vec3 and leaves us with a Vec2. Also interestingly enough Vec3A returns a Vec2 when you truncate it because Vec2A is not an actual thing, how curious! I should look into that sometime in the future.

Last but not least, we need to multiple the half extents by 2.0 to get the actual full size! And that’s it! I duplicate (yes, no reason for premature abstraction no good, especially when you’re teaching someone new) this whole thing to measure the size_object from the mesh_object too and we are off to the races!

And finally my tired brain filled up the rest of collide and got back the Option<Collision> from it:

let collision = collide(
    transform_object.translation,
    size_object,
    transform_player.translation,
    size_player,
);

And somehow decided to wrap all the movement conditionals within one big

if let Some(collision) = collision {
}

Which meant that we can only move if we are colliding with the object, which resulted in a minor debugging session after which I realized my mistake and we ended up with this instead:

if input.pressed(KeyCode::Up) && collision != Some(Collision::Top) {
    transform_player.translation.y += BALLOON_SPEED
}
if input.pressed(KeyCode::Down) && collision != Some(Collision::Bottom) {
    transform_player.translation.y -= BALLOON_SPEED
}
if input.pressed(KeyCode::Right) && collision != Some(Collision::Right) {
    transform_player.translation.x += BALLOON_SPEED
}
if input.pressed(KeyCode::Left) && collision != Some(Collision::Left) {
    transform_player.translation.x -= BALLOON_SPEED
}

Which I think is actually fine for now! What this does is instead of moving the balloon to a direction whenever that direction’s key is pressed, it also checks whether there is a collision in that direction before doing the move. Which I think is fairly elegant honestly if not a little bit repetitive! Maybe one day we will use Axis based movement!

Finally before we put a close to today’s learning I ask one last quiz question:

What is one other way you think of doing this that involves less Assets and Meshes calculation?

She thinks of using Sprites instead of Meshes which is probably a smart idea! I still have yet to use Sprites though so I have no idea how much would that have truly simplified lol.

The answer I had in mind though was thinking of creating meshes with resolutions of 1 (0.5-radius circle, 1x1 boxes) and using them as our meshes and relying on their Transform.scale property to both scale the object to fit the size we want but also to be our size! This would save us infinite trouble as we wouldn’t need access to assets or the meshes and we can just rely on the Transforms completely.

A pink circle on a green background being chased by a red circle while attempting to pass through a long orange column that expands the screen vertically. Whenever the pink circle as controlled by the player attempts to pass through the orange column, it is stopped and cannot pass through it but can continue moving vertically or going back.

But that’s a journey for another day, see you later ❤️