Game Dev Diary

Day 0: Reborn like an orange Phoenix #

So it has been a year! I went on an adventure of creating the software that will drive this diary until the day it dies, I then went on an adventure to reverse engineer Unity’s formats mostly for fun. And then I was on a real life adventure to move across the world. And then I rented a house for the first time in my life. It’s really been one heck of a year.

Exactly one year ago I was inspired by my friend L and Daniel Shiffman to really focus on learning what it means to make games. With the good Bevy by my side I had fun and learnt a lot and wanted to share what I was learning with future Mathy and the world. I wanted to document the natural learning process which I deeply enjoy and thus the idea of making a “diary” was born. Also I love diaries.

As life got busy the diary never left my mind. I always wanted to go back and continue the journey. I tried to wait for the “right time” when life would get a bit less hectic but that might not be happening for quite some time. Also I clearly still did it anyway what with all the being “the day of the diary” and all.

Exactly one month ago I actually got to meet Daniel in real life. And a bunch of other really cool people. That was a really fun day.

So with all those dates aligning. Also with another friend of mine starting to learn game development. LET’S DO THIS.

I did a bunch of changes to the previous posts, originally was called “Day 0”. The point of “Day 0” and “Day 1” is to mark the day when I decided to start this journey. The dates are meant to be real time; if I miss a week because I am busy with something else the entries would jump by 7 days. This comes back to the point of the diary being a representation of the natural learning process. Ever watched a video of someone making something fantastic and thinking, “How many human days with all their imperfections, being sleepy and tired after a full time job did it take this random person on the internet to come to this entry that I find really appealing?” Well with my diary that’s the first thing in the title!

But because I didn’t really learn all that much during the original run, I decided I get this reset. But everything is in place now. My diary generator is working great. I won’t need to move across continents for a while… probably. Life is still busy and exhausting but let’s make the most of it! So I am promising myself this is my one reset. will be “Day 1” forever.

There were some WIP entries for few more days after Day -364: Enter Bevy & Shaders are hard but I don’t think I remember exactly what happened one year ago to finish them properly at this point. And I think that’s okay, organic natural diary and all.

Bevy got a bunch of updates during that year, then even started a release train model which I think is going fairly nicely. Generic Associate Types were just released in Rust and Bevy is already using them. Isn’t that crazy? We will be using the brand new Bevy 0.9, which is not released yet but that’s how we always roll!

There will also be more articles and more infrastructure around game dev diary because you know I can’t stop myself from going on whacky tangents. But the goal is for the tangents to not stop the main trip this time.

ONWARDS MY FRIENDS! 🧡

Day 1: Isn’t Mesh a 3D only shape, though? #

Weird start but today I took my friend A (mysterious names all around, also I hope nobody reading this ever watched Pretty Little Liars) through the core Bevy concepts in an interactive tour/lesson hybrid. Also I realized midway this might not be the best way to start someone who is very new to programming but she really gets bored of anything that isn’t immediately practical so I thought Bevy might be a good place to start. And I actually still think that but I think that Bevy needs some training wheels. And we will explore what those wheels look like today.

So where did we start from? Well my friend got to this example herself independently and written it in her editor and ran it successfully. So the first thing I wanted to do was make sure she fully got what was going on in it.

She started by pointing out that change_clear_color was where the Window was created, but that’s not true so I decided to take that angle in the explanation. So I told her she can comment out that system and see what happens. Once commented out the window still appeared with its color but spacebar was no longer working. We went over the other parts and then I told her the secret, everything happens in the magical DefaultPlugins where the Window is created and the “game loop” is started. Commenting that out results in an app that just immediately close.

I also briefly re-explained Commands as “instructions for Bevy to do at the end of the frame.” I am not even sure of the accuracy of this definition, hope it’s accurate enough… Look we are learning together, like good friends should be…

Okay next up we wanted to draw a circle! This is where things get a bit trippy. I wanted to show her a bit of the beauty of docs.rs so I guided her to Bevy’s page there and we searched for Circle and found it! And then we scrolled down, I wanted to show her how Circle is a component (or more likely a bundle), like any other and that we can create a circle and spawn it! I was surprised Circle wasn’t a component, in fact Circle had almost no useful functionality besides… turning into a Mesh. “Ah well, I guess Mesh is what implements Component then,” I thought to myself. Oh how wrong I was.

We went to Mesh’s page next and to my dismay it also didn’t implement Component. which after researching more a bit now I realize might be some kind of intentional limitation? Let’s talk about this at the bottom of this diary entry.

I was honestly quite perplexed. So I decided, it’s example time! We went to 2d and found a fitting example that was called 2d_shapes.rs. Neat. And they even draw the Circle, and oh heck what is this:

// Circle
commands.spawn_bundle(MaterialMesh2dBundle {
    mesh: meshes.add(shape::Circle::new(50.).into()).into(),
    material: materials.add(ColorMaterial::from(Color::PURPLE)),
    transform: Transform::from_translation(Vec3::new(-100., 0., 0.)),
    ..default()
});

Well now I get it… but this is not super beginner friendly… but I guess we have to roll with it? (or do we?) I can’t just tell my friend this mess is a circle without explaining why it’s like that. So now this diary entry will turn into an initial writing for “ecs2” the article that is going to one day follow up the other one I wrote a year ago at this point.

BUT BEFORE THAT. My friend looked at this in disgust, and not for the reason I was ever going to guess. No, not because it looked overly complex or confusing. Not at all. She said in disgust, “okay I don’t like this” and I asked “why?”

“Because Meshes are only for 3D.”

oh.

Then I started saying “Well but see” and she was “Heck no” and we started laughing and opening Wikipedia and reading the first few lines and laughing some more… Not where I expected that to go at all. After the laughing subsided I explained to the best of my ability that yes well meshes are 3D but that every 2D shape is also a valid 3D shape. She knows meshes are a bunch of triangles and so I pointed out that one triangle alone is still a valid mesh and it’s clearly 2D and that one could make a mesh of a bunch of triangles layered in a flat surface and it would be 2D.

She thought “meshes are 3D and sprites are 2D.” And I don’t really understand sprites well enough to explain why that’s not exactly the case… Real question for future Mathy though, are sprites just a texture rendered on a 2D mesh lol?

We opened Wikipedia page for sprite and read some more and I explained that if you wanna draw a circle you really have two main ways of going about it. You can either draw a lot of triangle slices and make a mesh that looks like a circle or you can draw a circle image and render it as a sprite. We were both happy enough to move on for now.

Okay so I explain to my friend briefly how this code is meant to be read, which is not an ideal reading experience let me tell you:

// you first read this
shape::Circle::new(50.)

// then this
.into()

// then this
meshes.add(..)

// and finally another one of these...?
.into()

Luckily I know my way around Rust and docs.rs so I will be able to figure out what the heck all of this is for. Let’s go!

// we create a circle and give it some radius
shape::Circle::new(50.)

// then we call .into which I described as a magical Rust incantation 
// for triggering `From` implementations which I also explain briefly
.into()

// and then I point out that meshes is defined earlier as a ResMut<Assets<Mesh>>
// and we are adding this mesh to them... but what does that mean?
meshes.add(..)

So here is that ecs2 tangent. I really like explaining components and entities as a table and systems as things that operate on this table. And yes I know that resources are factually part of this table in most ECS implementations but I personally think it’s easy to think of them as something separate. So I explain that resources are like a bucket next to the table that we put stuff in… and that there are actually many buckets each for a different “type of thing”… and assets are stuff we will put in this bucket. And that to find something from the bucket we will wrap it in yarn and pull on the yarn when we need it again…

See, I actually really like the yarn and bucket metaphor but this is the second time this happens to me; I remember this actually happened to me personally a year ago when I was learning Bevy I tripped on Res<Asset<T>> hard. My brain merges the resource concept with the asset concept. Still I made sure to explain that Asset is mainly a performance concern to not load things multiple times and have multiple copies around of them.

More explanation and tripping on my own misunderstanding later, she asked something like “why not just use like different drawers for all the different stuff instead of wrap them in yarn?” Which is quite a fantastic question. “How would you find the drawer that contains the thing you’re searching for?” I responded. She said, “well you could sort them alphabetically.” I replied, “but they are just unnamed thingies,” excited for the direction where this is going. “You could name the drawers.” perfect. “Well but then you would need to look through all the drawers for the drawer with the name you have on a piece of paper to remember which drawer has the thing right?”

Yeah okay I have an unconventional way of teaching, sue me.

Now I am going to interrupt this story briefly to mention how much drawer thing was exactly what I needed and I am so happy she got me there. Resources are named cabinet of drawers. Assets are buckets you usually put inside the drawers and wrap stuff in them in yarn. Yep. I got there finally. Perfect metaphor, no take backsies now.

Eventually we got through the rabbit hole I took us down through abut resources and assets and we came out with a Handle. And then I looked at that last .into() and asked myself “Wonder why that one is there.”

We got to the docs for it and… wat. Mesh2dHandle is a new type around Handle<Mesh>? And Handle<Mesh> is public? I actually have absolutely no idea why this is like that. I understand it’s trying to do some kind of distinction or specialization between 2D and 3D but why 3D doesn’t get its own also if it’s pub and uses .into() instead of .try_into() then it’s clearly possible for you to just stick a 3D Mesh in there and be like 🙃. Docs kinda lacking for it too. I wonder if this is some kind of temporary limitation or something permanent? I’d have to look into it more. Anyway I try to briefly explain this but give up rather quickly because I didn’t really get it myself too.

Next we go briefly over the material which is basically exactly the same as the mesh except with less confusing .into()s and last but not least we go over the transform which she is fairly familiar with already. Finally the default is easy to explain. Okay great! We have a circle and we get how it works!

We revise things and I let her poke at different parts of the circle and we both get to see what happens. Impressively the error for not passing a Material2d to MaterialMesh2dBundle is a work of art… at least if you understand Rust. (I am not sarcastic, it’s genuinely amazing that the lack of inference basically forces you to provide a Material to the MaterialMesh2dBundle or otherwise you really should be looking for another bundle. Was genuinely impressed.)

She cracks few jokes here and there about Bevy misusing 3D meshes and we go on a little tangent explaining why rotating a circle could be relevant in 2D if we wanted to, where I try to rotate the circle around the Z for a few seconds before facepalming about my absurdity and remembering that’s the only axis I wanted to avoid not use for demoing what a rotated 2D circle would look like lol.

And that’s all for today! We watch an episode of The Owl House which you should be watching right now instead of reading this rambling madness.


Okay this is the part where I talk to myself a bit more because I am not already insane enough so.

Mesh is not a component #

Right. So I guess Mesh is not a component because… Mesh2Handle is? And for 3D… Handle<Mesh> is a component because Handle<T> where T: Asset and Mesh is Asset. Well okay then. Whacky and a bit overly complex and I am not sure if it’s some kind of temporary limitation or not but I will take it. Honestly my gut tells me it’s specialization problem and specialization is probably never happening so it might not be very temporary even if it is. Either ways always exciting digging through Bevy’s traits and their implementations. It genuinely feels like a treasure hunt and I love it.

Bevy needs training wheels #

I have this dream of teaching programming via game dev powered by the helpful Ferris and the adoring Bevy. I got to have a bit of a taste of what that would be like today. It’s a bit too hard to swallow. BUT I THINK IT’S FIXABLE:

// Circle
commands.spawn_bundle(training_wheels::Circle1 {
    color: Color::PURPLE,
    translation: Vec3::new(-100., 0., 0.),
});

What if we could start here? What if we could then take it here:

// Circle
commands.spawn_bundle(training_wheels::Circle2 {
    color: Color::PURPLE,
    transform: Transform::from_translation(Vec3::new(-100., 0., 0.)),
    ..default()
});

YOU SEE WHERE I AM GOING WITH THIS? And it’s really not even that hard to make at all. And it’s such a natural expansion. Maybe tomorrow I can make these training wheels and my friend can take them for a ride! Maybe that’s what tomorrow’s entry is going to be about?

Day 2: Let’s Transform everything at once what could go wrong? #

So we continue the learning journey from yesterday. We wanna move it with arrow keys now!

If you remember our example is based on the clear_color example and that example already demonstrates how to do something when a key is just pressed. So I explain briefly the idea of how systems query components, and that in this case the component we are interested in querying is the Transform component. We also need the same resource as the change_clear_color from the example.

My friend A stops me there and asked me, “but how do we pick the circle?” I reply with naive foolishness, “we will learn that later but for now our example only has the circle anyway!”

So we write this out:

fn transform(input: Res<Input<KeyCode>>, mut query: Query<&mut Transform>) {
    for mut transform in query.iter_mut() {
        if input.pressed(KeyCode::Up) {
            transform.translation.y += 1.0
        }
    }
}

And after my friend goes through the cycle of talking to Ferris and convincing them the code is correct, the application finally compiles without warnings and launches. She clicks the Up arrow and… nothing? ”Huh?” I think to myself, I was just telling her how much Rust makes coding simpler by moving all errors to the point where you are compiling your code and so once it compiles it just works. Not a great look let me tell you.

I suggest maybe the 1.0 is too small so we push it up a notch. Nothing changes, although both of us feel like the circle might be phantom moving? Or it might be our imagination…

I start having some doubts, I know we added the system to the app, so it can’t be that. Maybe there is some weird shadowing magic going on with the system name transform? My friend picked this name because she is still learning. Okay I change the name to move_system and run it again. It’s still broken.

But how?

Okay nothing a bunch of dbg! can’t solve. I add a bunch of dbg! after struggling a bit with its positioning, you can’t just wrap the transform declaration in a dbg! apparently!

Here’s where we end up:

fn move_system(input: Res<Input<KeyCode>>, mut query: Query<&mut Transform>) {
    for mut transform in query.iter_mut() {
        dbg!(&transform);
        if input.pressed(KeyCode::Up) {
            transform.translation.y += 1.0
        }
    }
}

And I look at the debug output in the terminal and within 2 seconds, I am facepalming and sighing and freaking.

In case you’re curious the debug out shows that things are indeed increasing in their Y transform. But somehow it’s not one thing that’s increasing, there are multiple things all increasing… but we only spawn the camera and the circle… wait a minute. Of course I had to forget the camera has a transform like anything else and that this is working. It really is. It’s just everything moves up by the same amount of pixels every time the up arrow is clicked. Everything including the camera that points at everything in the world.

I explain what went wrong and my friend bursts into hysterical laughter for a minute and I worry she might die from laughing but she stops after a bit and she is gladly fine.

“Welp. Remember how you asked me earlier how do we pick the circle, time to learn that!”

I explain the idea of marker components and we create one and attach it to the circle:

#[derive(Component)]
struct Balloon;

// snip...

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.spawn(Camera2dBundle::default());
    commands
        .spawn(MaterialMesh2dBundle {
            mesh: meshes.add(shape::Circle::new(50.).into()).into(),
            material: materials.add(ColorMaterial::from(Color::PINK)),
            transform: Transform {
                translation: Vec3::new(-100., 50., 0.),
                ..default()
            },
            ..default()
        })
        .insert(Balloon);
}

And then I explain how to tell Bevy to pick components that have this marker component:

fn move_system(input: Res<Input<KeyCode>>, mut query: Query<&mut Transform, With<Balloon>>) {

And we run it and it works!

Goodness. I am such a terrible teacher. I really gotta sit down and prepare these materials ahead of time, don’t I?


Phantom movements #

I need to remember to reproduce that example at some point with the same version of Bevy, I am curious if the phantom movement was just us imagining or hoping it’s moving or if the camera and circle moved at a precision loss and caused that perceived slight jittery movement

Day 3: Two circles #

I check on A’s progress and independently she created 3 new systems and hooked them up and they work nicely!

fn move_system_up(input: Res<Input<KeyCode>>, mut query: Query<&mut Transform, With<Balloon>>) {
    for mut transform in query.iter_mut() {
        if input.pressed(KeyCode::Up) {
            transform.translation.y += 1.0
        }
    }
}
fn move_system_down(input: Res<Input<KeyCode>>, mut query: Query<&mut Transform, With<Balloon>>) {
    if input.pressed(KeyCode::Down) {
        for mut transform in query.iter_mut() {
            transform.translation.y -= 1.0
        }
    }
}

fn move_system_right(input: Res<Input<KeyCode>>, mut query: Query<&mut Transform, With<Balloon>>) {
    if input.pressed(KeyCode::Right) {
        for mut transform in query.iter_mut() {
            transform.translation.x += 1.0
        }
    }
}

fn move_system_left(input: Res<Input<KeyCode>>, mut query: Query<&mut Transform, With<Balloon>>) {
    if input.pressed(KeyCode::Left) {
        for mut transform in query.iter_mut() {
            transform.translation.x -= 1.0
        }
    }
}

Not bad at all! A bit repetitive but I decide no need to refactor this right now there will be future opportunities to refactor this. Very proud of her for getting these done all by herself!

Next up she suggests implementing an “enemy” type that chases the player, cool. She really likes bullet heavens and who can blame her. This is indeed the kinda building block you want to get to a bullet heavens so let’s go!

I leave her in the driving seat with minimal directions as she creates a new Enemy marker component and setup a new circle similar to the Balloon one but with Enemy instead of Balloon marker.

Then I start this rough drawing

An extremely rough drawing of a 2-dimensional table with 3 columns: transform, balloon and enemy. And 3 rows: 1, 2, 3. All rows have a transform. Only the first row has balloon check marked. Only the second row has enemy check marked. The rest of enemies and balloon cells are completely empty.

I use this to demo some of the ideas around ECS queries, explaining that we want to get access to the second row and the first row but we don’t care about the third row. We only care about the first row because it is marked by balloon. And we only care about the second row because it is marked by enemy, if there were other enemies we’d also want access to them!

And then I give her a chance to formulate that into a query, in words. I nudge a little bit here and there and we eventually reach it! “We want all the Transforms for all the things that implement Balloon immutably. And we want all the Transforms for the all the things that implement Enemy mutably.”

And then I show her how to write that query, we copy the same query as the movement system and remove the input as well as mutable access to the Balloon then we duplicate the query and I let her give each query a distinct name and then we change With<Balloon> to With<Enemy> and we make sure it’s a mutable Transform access:

fn enemy_movement(
    mut query1: Query<&mut Transform, With<Enemy>>,
    query2: Query<&Transform, With<Balloon>>,
)

I then tell her this is a great opportunity for us to try get_single() which we saw in the docs when we were looking at Query a while back, and I show her the if let Rust syntax:

if let Ok(balloon) = query2.get_single() {

And inside of that we can get access to all the transforms exactly how we did before we had access to get_single()


for (mut enemy, mut movement) in query1.iter_mut() {

And inside we can for now just hardcode some direction and we finally get:

fn enemy_movement(
    mut query1: Query<&mut Transform, With<Enemy>>,
    query2: Query<&Transform, With<Balloon>>,
) {
    if let Ok(balloon) = query2.get_single() {
        for mut transform in query1.iter_mut() {
            transform.translation.x -= 1.0
        }
    }
}

Neat! And let’s try to run it! And a big surprising error, one which I was half expecting to me be there. I knew Bevy conflict detection changed a bunch over the releases and I was not sure what was its current form but yep, this is it:

thread 'main' panicked at 'error[B0001]: Query<&bevy_transform::components::transform::Transform, bevy_ecs::query::filter::With<diary::Balloon>> in system learning::enemy_movement 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 `ParamSet`.', /Users/mathspy/.cargo/registry/src/github.com-1ecc6299db9ec823/bevy_ecs-0.8.1/src/system/system_param.rs:205:5

It’s a bit of a long winded error but I explain to her that if we look at our table, there’s nothing stopping the someone from eventually adding Balloon component to the one of the Enemys and that would not work and cause confusing behavior because we would need to be able to access the same Transform twice, once mutably and once immutable. The error also guides us in the right direction and tells us which system is causing us the trouble: enemy_movement. It also tells us how we can fix it: `Without<T>`. And I mention briefly that this is one of the few errors Bevy detects at the startup of the application instead of rejecting to compile for it.

We add Without<Balloon> to query1:

mut query1: Query<&mut Transform, (With<Enemy>, Without<Balloon>)>,

And finally I explain that the next part to make the Enemy move towards the Balloon involves a little bit of Math. ”There be dragons; but they are tamed dragons.” I explain, “for you see, game development does have some Math. But it’s not like the Math in school. It’s not mental Math. And it’s definitely not memorization “Math”. Rarely is it here’s how to do Math. The computer does the Math for us, the only things we need to know is what Math we need to perform to get a specific result and why does that Math work that way. Sometimes it’s nice to have a better grasp of how the Math is working behind the scenes, but that’s the extend of Math in game development.”

Okay I admit it wasn’t this elegant when I said it the first time because I wasn’t fully prepared also because I kept getting distracted or interrupted and starting over and I might have said “There be dragons” a few too many times…

I also point out that I am not the bestest at explaining these topics and that I’d rather she watches Coding Train’s Nature of Code playlist and I can be there to ask some questions and makes sure she understands.

Finally we swap some of the hard coded movement speeds for both the Enemy and the Balloon with nicely named constants.

Onwards to another day!

Day 4: Vectors with Daniel Shiffman! #

Today I watched the first four videos of Nature of Code 2 about vectors with A. I already know vectors fairly well but it’s always nice to watch Shiffman and also a refresher never hurts! But the main reason to watch them with A is to allow her to ask me any questions about parts she might not have understood well enough from the videos!

I did go on a bit of tangent to explain to A how Rust methods and functions differ from JavaScript’s everything-goes nature though!

The last video we watched is fully dedicated to static functions in p5.js. I wanted to take that opportunity to explain that what p5 does is nothing but a convention. There’s no reason why instance.add() couldn’t return a new instance instead of modifying the instance the method is being called on. I also explained how each JS library has to pick a convention and go with it and that the only way to find which convention is being used by a specific library is either try the library out or read the documentations for possible mentions of the behavior of its instance methods.

However, in Rust things are much nicer! I take us to the Bevy docs where I point to four different functions that each have a slightly different method signature.

And I explain the difference between each of those, firstly by explaining that the same behavior of JS static methods exists in JS but it’s easier to know a method is static because it doesn’t mention self in its parameters.

Next I jumped directly to borrowing and I explained that if a function is borrowing it can read the instance’s data but not modify it, so if we had a vec2.add(vec2) like in p5 and its signature used &self then we know for sure that function will not be able to touch our vec2 instance! (Yes internal mutability makes this MUCH more complicated but simplifying the explanation for now is for the better of learning.)

By contract mutating methods says they will mutate the instance! And usually when a function asks for mutable access that means it will actually use it because otherwise!

Finally consuming methods are a bit unique to Rust (at least compared to JS), and they are functions that consume the instance, the instance cannot be used after they are called. I still never explained Copy types so if you’re reading this and you wonder how the heck vec2's + is consuming but you can still use the vector afterwards, that’s because Copy types can be copied before they are consumed!

And that’s all for the day! ❤️

Day 5: An enemy approaches #

Today we continue watching Shiffman to figure out how to get the enemy circle to chase us!

We continue with 1.5: Unit Vectors and 1.6 Acceleration Vectors and then we sit down to finish up our enemy_movement system

Let’s look back at where we stopped off:

fn enemy_movement(
    mut query1: Query<&mut Transform, With<Enemy>>,
    query2: Query<&Transform, With<Balloon>>,
) {
    if let Ok(balloon) = query2.get_single() {
        for mut transform in query1.iter_mut() {
            transform.translation.x -= 1.0
        }
    }
}

First thing I explain that this is where Components really get interesting! We want to create a Component for our data. We mainly have two choices, we can either create a Velocity component that is independent of another Acceleration Component or we can put them both in one place. She chooses the easier route and picks the name Movement which is actually quite fitting! We create a simple Movement component with a velocity vector and an accel.

#[derive(Component)]
struct Movement {
    velocity: Vec3,
    accel: Vec3,
}

After this we move back to the system and I show A the ability to query multiple components that relate together at the same time, we update query1:

mut query1: Query<(&mut Transform, &mut Movement), (With<Enemy>, Without<Balloon>)>,

And update the loop over query1:

for (mut enemy, mut movement) in query1.iter_mut() {

And here I leave A to pilot for a bit on her own, to try to figure out how to achieve what Shiffman achieved in his last video.

She remembers how to access data from a struct as it’s what she does to access translation from from a Transform. However, she struggles a little to remember the formulas Shiffman explained. Should have written them down instead of being like me and rewatching the whole video again when I wanna remember any part of it… after a little bit of scrubbing through the last video and rewatching parts of it she got here:

movement.accel = balloon.translation - enemy.translation;
movement.velocity = movement.velocity + movement.accel;
enemy.translation = movement.velocity + enemy.translation;

But for whatever reason the enemy seems to move blink around as if it merged with sonic and grew a teleportation organ! It’s so blinky that we both look away because it makes us feel nauseous lol. What could be the cause?

Of course that’s exactly what Shiffman added a limit on the velocity for in his video, and we can do that too! Though one of the main reasons ours is much more blinky than his was is because Bevy runs our systems A LOT, but that’s a story for another day.

We open the Bevy Vec3 docs and go through them a bit and eventually we get to clamps, and we don’t really want to clamp the minimum so we can just use clamp_length_max:

movement.accel = balloon.translation - enemy.translation;
movement.velocity = movement.velocity + movement.accel;
movement.velocity = movement.velocity.clamp_length_max(0.69);
enemy.translation = movement.velocity + enemy.translation;

And fixes the issue! Our enemy now moves at a sane speed towards us relentlessly!

A video of two circles, one pink and one red where the pink is moving and stopping based on user input while the red one is chasing it relentlessly at a fixed velocity

And see you tomorrow! You’re beautiful 💜

Day 6: Break and let’s catch up #

I don’t actually do anything new on this Sunday! Instead in this day I will try to get you and myself up to speed with where I left this diary off a year ago.

I was working through Coding Train’s Autonomous Agents playlist and I got this far: https://github.com/Mathspy/flocking_bevy/tree/21e0c3c1b0d198646b840038282c258318ac626e

This is a repo of my work a year ago, I left all the commits untouched with all their chaotic nature, and there’s one extra commit of some changes that were just left floating for a year.

This repo was using what would one day be called Bevy 0.6 directly from its main branch. We didn’t have shape at the time and I was actually researching how to render a circle? Yes, genuinely. There was going to be a chapter called Day ?: Everything is a lie and triangles that maybe I will still go back and write some day. Long story short I had no idea how to render a circle and the suggestions I found of “lots of triangle” didn’t make sense to me so I downloaded bevy_protoype_lyon and ran the examples that rendered circles and inspected what components were added… And they were just lots of circle. I WAS SO DISAPPOINTED AND I FELT SO BETRAYED WHAT DO YOU MEAN EVERYTHING IS JUST TRIANGLES WHERE ARE MY CIRCLES WHAT IS THIS.

Besides teaching A, I will be going back to this repo and resurrecting it, updating Bevy to 0.9 and removing things that aren’t needed anymore and continuing on my journey of implementing all the different cool Autonomous Agents behavior. Looks like it already had Seek and SeekCursor. I wonder if those two could be merged one way or another, probably!

So many exciting adventures ahead of us! ❤️

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 ❤️