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:
- We could create a UI component that displays the speed of the balloon!
- How do games do “levels” anyway? I explained briefly the idea of scenes and how they co-exist with editor integration which simplifies creating scenes
- A floated the idea of making the camera follow the player, which we decided to implement later because right now it won’t even look like anything is moving because the whole background is the same color.
- A also floated the idea about how to spawn obstacles randomly and create a sort of procedural generated world and I chuckled “haha I am in danger”… To teach that I will have to study a lot first
- And last but not least A floated the idea of the enemy despawning once they reach the target and then new ones spawn
So the first thing we need to do to handle collision is probably to refactor the move_system
s 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
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 Transform
s 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, Vec3A
s instead of Vec3
s. 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 Asset
s and Resource
s, 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 Sprite
s instead of Mesh
es which is probably a smart idea! I still have yet to use Sprite
s 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 Transform
s completely.
But that’s a journey for another day, see you later ❤️