Implement a Collider Component for Collision Detection

Mục tiêu: Define a new Collider component in Rust using an Axis-Aligned Bounding Box (AABB) to represent the physical hitbox for game entities. This task is the foundational step for implementing a physics and collision detection system in the game engine.


An outstanding achievement! Your game engine now engages two of the most important senses: sight and sound. The player ship is a distinct visual entity, and its actions are met with satisfying audio feedback. You’ve built the core of an immersive experience. However, the world your ship inhabits is still a ghost world; entities can pass through each other without consequence, existing only as pixels and soundwaves.

It’s time to give your game world a sense of touch, of physical presence. This begins the journey into one of the most fundamental aspects of game development: physics and collision detection. We need to define the physical boundaries of our entities, to give them a shape that the engine can use for calculations. Our current task is to lay the foundation for this entire system by defining a new data structure: the Collider component.

From Sprites to Hitboxes: Defining Physical Shape

In game development, the shape used for physics calculations is often separate from the visual shape of a sprite. This physics shape is commonly called a collider or a hitbox. While a sprite can be complex and detailed, its collider is usually a much simpler, invisible shape (like a rectangle or a circle) that approximates its boundaries. This separation is key to performance; calculating intersections between simple rectangles is vastly faster than doing pixel-perfect checks on complex images.

For our engine, we will start with the most common and efficient type of 2D collider: the Axis-Aligned Bounding Box (AABB).

An AABB is simply a rectangle whose sides are always parallel to the game world’s X and Y axes. This means it is never rotated. The “Axis-Aligned” constraint is what makes the math for detecting overlaps incredibly fast and simple, making it the perfect starting point for any physics system.

Our Collider component will be a simple struct that holds the data needed to define this box: its width and height.

Implementing the Collider Component

Let’s head over to our components module and define this new building block. Open src/components.rs and add the Collider struct.

// src/components.rs

// We need this to derive the Component trait.
use bevy_ecs::prelude::Component;

// --- Your existing components are unchanged ---
#[derive(Component)]
pub struct Position {
    pub x: f32,
    pub y: f32,
}

#[derive(Component)]
pub struct Velocity {
    pub dx: f32,
    pub dy: f32,
}

#[derive(Component)]
pub struct Player;

#[derive(Component)]
pub struct Sprite {
    pub asset_key: String,
    pub width: f32,
    pub height: f32,
}

// --- NEW CODE STARTS HERE ---

// The Collider component defines the physical bounding box for an entity,
// used for collision detection. We are starting with an Axis-Aligned Bounding Box (AABB).
#[derive(Component)]
pub struct Collider {
    // The width of the rectangular hitbox.
    pub width: f32,
    // The height of the rectangular hitbox.
    pub height: f32,
}

// --- NEW CODE ENDS HERE ---

Deconstructing the Code

This addition is small but conceptually massive for our engine.

  • #[derive(Component)]: As with all our other components, this macro from bevy_ecs registers our Collider struct with the ECS World, allowing us to attach it to entities.
  • pub struct Collider: We define a new data type that encapsulates all the necessary information for a physical boundary.
  • pub width: f32, pub height: f32: These two fields are all that’s needed to describe the dimensions of our AABB. The entity’s Position component will provide the center point of this box, and these fields will define its size.

You might wonder, “Why not just use the width and height from the Sprite component?” This is a great question that gets to the heart of the Entity-Component-System design philosophy: separation of concerns.

  • A Sprite is concerned with rendering. Its dimensions describe how large an image should be drawn.
  • A Collider is concerned with physics. Its dimensions describe the entity’s physical footprint.

By keeping them separate, we gain incredible flexibility. We could have a tiny sprite with a huge collider (like a power-up with a generous collection radius) or a large, decorative sprite with a small, precise collider at its core. We can even have entities with colliders but no sprites at all, like invisible walls or trigger zones. This clean separation is the hallmark of a well-designed engine.

You have now successfully defined the data structure that represents physical presence in your game world. The concept of a “solid object” now formally exists within your engine’s vocabulary.

Next Steps

The Collider component has been defined, but no entity is using it yet. Our player ship is still a ghost. The next logical task is to add the Collider component to our player entity, officially giving it a physical hitbox that can be used in future systems.


Further Reading

  • Game Collision Detection Basics: A fantastic series of articles on the MDN Web Docs that covers the theory and practice of collision detection in games.
  • Axis-Aligned Bounding Box (AABB): A more technical look at AABBs, why they are used, and the mathematics behind them.
  • The Rust Book: Defining and Instantiating Structs (Revisited): It’s always a good idea to refresh your memory on the fundamentals of creating custom data types in Rust.

Add the Collider component to relevant entities (paddles, ball, etc.).

Mục tiêu:


Excellent work on defining the Collider component. You have successfully created the blueprint for physical presence in your game world. A blueprint, however, is just a plan on paper. An entity in our game remains a ghost, able to pass through anything, until we take that blueprint and apply it. Our current task is to do exactly that: to bestow a physical body upon our player ship by adding the Collider component to it.

Giving Form to the Ethereal

Right now, our player entity is a collection of components that define its position, velocity, visual appearance (Sprite), and its identity as the player (Player). By adding a Collider to this collection, we are formally declaring its physical boundaries. We are telling the engine, “This entity is no longer just a picture; it occupies this specific rectangle of space.”

This action takes place where the entity is born: inside the world.spawn() call in your Game::new() function. We will simply add our new Collider to the tuple of components that constitutes the player’s initial state.

Open your src/lib.rs file and navigate to the Game::new function. Add the Collider component to the player’s spawn bundle as shown below.

// src/lib.rs

// ... (in the impl Game block)

impl Game {
    pub fn new() -> Self {
        // ... (getting canvas, context, and inserting resources is unchanged) ...

        world.spawn((
            Position {
                x: (canvas.width() / 2) as f32,
                y: (canvas.height() / 2) as f32,
            },
            Velocity { dx: 0.0, dy: 0.0 },
            Sprite {
                asset_key: String::from("assets/player_ship.png"),
                width: 64.0,
                height: 64.0,
            },

            // --- NEW CODE STARTS HERE ---

            // We add the Collider component to give the player a physical hitbox.
            // This hitbox is centered on the entity's Position, just like the Sprite.
            Collider {
                // It's a common and good practice to make the hitbox slightly smaller
                // than the sprite. This feels more "fair" to the player, as it
                // prevents collisions with the transparent edges of the sprite image.
                width: 58.0, 
                height: 58.0,
            },

            // --- NEW CODE ENDS HERE ---

            Player,
        ));

        // ... (the rest of the function is unchanged) ...
    }

    // ... (the rest of the Game impl is unchanged) ...
}

// ...

Deconstructing the Code

This single addition is a profound step in making your game world tangible.

  • Component Composition: You are witnessing the core power of ECS in action. An entity is defined purely by the components attached to it. By adding Collider, you’ve seamlessly extended the player’s definition without changing any existing systems or components. It now has the data required for physics calculations.
  • Physics vs. Visuals: Note that we’ve made the collider’s dimensions (58.0 x 58.0) slightly smaller than the sprite’s dimensions (64.0 x 64.0). This is a deliberate and common game design choice. It creates a more forgiving experience for the player, as they won’t feel cheated by a collision that seemed to only graze the transparent pixels at the edge of their ship’s sprite. This separation of concerns between the Sprite and Collider components is what gives you this powerful flexibility.
  • A Shared Center: It’s important to understand that both the Sprite and the Collider are centered on the entity’s one and only Position component. The RenderingSystem uses this position to draw the sprite, and soon, our CollisionSystem will use this same position to place the hitbox for its calculations.

The Tangible Ghost

After adding this code, if you were to build and run the project, you would see… absolutely no change. The ship moves, the laser sound plays, but there is no new visual or behavioral effect.

This is the correct and expected outcome.

Why? Because in an ECS architecture, components are just data. The Collider you just added is a silent declaration of a physical shape. It has no behavior on its own. It’s a piece of information waiting for a system to read and act upon it. Until we create a system that queries for Collider components and performs calculations with them, our player ship remains, for all practical purposes, a ghost with a physical blueprint.

You have now successfully given your player entity a physical form, at least in data. The stage is perfectly set for the next step.

Next Steps

With at least one entity in our World now possessing a Collider, we can finally build the logic that makes use of this data. The next task is to create the CollisionSystem. This new system will be responsible for querying all entities with colliders and implementing the mathematical checks to determine if any of them are overlapping, laying the groundwork for all future physical interactions in your game.


Further Reading

  • The Bevy Book: Bundles: While you are creating the tuple of components manually, bevy_ecs has a concept called Bundles to make spawning entities with a common set of components cleaner. It’s a great next step for organizing your entity creation code.
  • Game Programming Patterns: Component: A deep dive into the Component pattern, which is the heart of ECS. This will reinforce your understanding of why you are building your engine this way.
  • Hitboxes and Hurtboxes Explained: A short article and video explaining the common game development concepts of hitboxes (for dealing damage) and hurtboxes (for receiving it), which are specific applications of the Collider concept you’re building.

Set Up the Collision System

Mục tiêu: Create the skeleton for the collision\_system function and integrate it into the game’s main loop, ensuring it runs after the movement system to establish the foundation for collision detection logic.


You’ve done the essential groundwork. By defining a Collider component and attaching it to your player, you’ve successfully created the data that represents a physical presence. This is the “what” and “where” of your entity’s physical form. However, in the world of Entity-Component-System architecture, data is inert. It does nothing on its own. It patiently waits for a piece of logic—a System—to read it and act upon it.

This brings us to our current task: creating the actor for our physics stage, the CollisionSystem. This system will be the engine’s “physicist.” On every single frame of the game, its job will be to look at all the physical objects in the world and begin the process of determining if any of them are overlapping.

The Anatomy of a System

In bevy_ecs, and ECS in general, a system is simply a Rust function. It’s a self-contained piece of logic that we execute once per frame. Its most common characteristic is that it takes the ECS World as an argument, giving it access to all the entities, components, and resources that make up your game’s current state.

For this task, we are only creating the function’s skeleton and plugging it into our engine’s main loop. We won’t add the actual collision-checking logic just yet; the goal here is to establish the architectural slot where that logic will live.

Step 1: Creating the collision_system Function

Let’s create our new system in src/lib.rs, alongside your other systems like movement_system and rendering_system.

// src/lib.rs

// ... (your use statements and other systems like movement_system are unchanged) ...

// --- NEW CODE STARTS HERE ---

// This is the skeleton for our CollisionSystem.
// For now, it doesn't do anything, but it establishes the place where our
// collision detection logic will live. It's a regular function that takes a
// reference to the World, allowing it to query for components.
pub fn collision_system(world: &World) {
    // In the next tasks, we will fill this function with a query to find all
    // entities with Position and Collider components, and then implement the
    // logic to check for overlaps.
}

// --- NEW CODE ENDS HERE ---

pub fn rendering_system(world: &World, context: &web_sys::CanvasRenderingContext2d) {
    // ... (rendering_system is unchanged)
}

// ... (the rest of your file) ...

This is a simple but vital step. We’ve officially defined a new phase in our game’s frame-by-frame execution: the collision detection phase.

Step 2: Integrating the System into the Game Loop

A system that is defined but never called has no effect. We must now add our new collision_system to the engine’s main “heartbeat”—the Game::tick method. The order in which we call our systems is critically important and dictates the flow of data and logic each frame.

A standard and robust order for a game loop is:

  1. Process Input: Update velocities and states based on player commands.
  2. Update Positions (Movement): Apply velocities to positions.
  3. Check for Collisions: Now that everything has moved, check for overlaps in their new positions.
  4. Render: Draw everything in its final, resolved position for the frame.

Let’s add our new system to Game::tick in the correct spot.

// src/lib.rs

// ... (inside the `impl Game` block) ...

impl Game {
    pub fn new() -> Self {
        // ... (this function is unchanged)
    }

    pub fn tick(&mut self) {
        // Clear the screen first.
        self.context.set_fill_style(&JsValue::from_str("#333333"));
        self.context.fill_rect(
            0.0,
            0.0,
            self.canvas.width() as f64,
            self.canvas.height() as f64,
        );

        // --- HIGHLIGHTED CHANGES START ---

        // Run the systems in a logical order.
        player_control_system(&mut self.world);
        movement_system(&mut self.world);

        // NEW: We call our collision system after movement.
        // This ensures we are checking for collisions based on the entities'
        // positions for the *current* frame.
        collision_system(&self.world);

        // Rendering should happen last, after all game logic has been processed.
        rendering_system(&self.world, &self.context);

        // --- HIGHLIGHTED CHANGES END ---
    }
}

// ... (the rest of your file is unchanged) ...

Deconstructing the Changes

  • collision_system(&self.world): We are calling our newly created function and passing it a reference to the game’s World, giving it the access it will need to perform its job.
  • System Ordering: Placing collision_system after movement_system is a deliberate and crucial architectural choice. It ensures that our collision checks are always based on the most up-to-date positions for the current frame. If we checked for collisions before moving everything, we would be detecting collisions based on where things were in the previous frame, leading to logical errors and visual artifacts (like objects appearing to overlap for a frame before a collision is detected).

As with the previous task, if you build and run your project now, you will see no change in behavior. This is correct. You have successfully integrated an empty system into your engine. You have built the factory floor, but you haven’t turned on the machines yet.

Next Steps

The CollisionSystem is now a recognized part of your engine’s per-frame lifecycle. The next logical step is to bring it to life. In the upcoming task, you will populate this empty function with its first piece of logic: writing a query to fetch all entities from the World that have both a Position and a Collider component, gathering all the “solid” objects that need to be checked against each other.


Further Reading

  • The Bevy Book: Systems: The official documentation from bevy_ecs on what systems are and how they work. This is a must-read to solidify your understanding.
  • Game Loop - Game Programming Patterns: A fantastic and in-depth chapter by Robert Nystrom on the theory and practice of the game loop, the heart of every game engine.
  • Anatomy of a Game Loop: A high-level article that provides a great overview of the different phases within a typical game loop, reinforcing why system order matters.

Collision System: Query for Collidable Entities

Mục tiêu: Populate the collision\_system function by creating a bevy\_ecs query to gather all entities that have both a Position and a Collider component, which are the raw materials for collision detection.


Excellent! You’ve successfully established the CollisionSystem as a formal stage in your engine’s per-frame lifecycle. By creating the empty function and calling it from your main game loop, you’ve built the factory floor. It’s now time to install the machinery. The very first machine any physics system needs is a way to gather all the raw materials it needs to work with.

For our CollisionSystem, the raw materials are every single “solid” object in our game world. In the language of ECS, this translates to: “Find every entity that has a Position component to tell me where it is, and a Collider component to tell me its physical shape.” This process of gathering the necessary data is accomplished with a query.

Gathering the Physical Objects

You’ve already become proficient at writing queries for your other systems. The movement_system queries for entities that can move (Position and Velocity), and the rendering_system queries for entities that can be drawn (Position and Sprite/Renderable). We will now apply this exact same powerful pattern to our collision_system.

The query we write will ask the World to give us an iterator over all entities that possess both a Position and a Collider. This will effectively give our system a list of all the hitboxes in the game, ready for us to begin checking for overlaps.

Let’s populate our empty collision_system function in src/lib.rs with this essential first step.

// src/lib.rs

// ... (your use statements and other systems like movement_system are unchanged) ...

// We are now adding logic to our previously empty collision_system.
pub fn collision_system(world: &World) {
    // --- NEW CODE STARTS HERE ---

    // Create a query to find all entities that have both a Position and a Collider.
    // This gives us an iterator over all the "physical" objects in our game.
    // Note that we are requesting read-only, immutable references (`&`) to these
    // components. This is a crucial design choice. The collision system's job is
    // to DETECT collisions, not to RESOLVE them. It only needs to read the state
    // of the world, not change it.
    let mut collidables_query = world.query::<(&Position, &Collider)>();

    // For now, we are just setting up the query. In the next task, we will iterate
    // over the results of this query to perform the actual intersection checks.
    // The `collidables_query` variable now holds an iterable collection of all
    // entities that match our criteria.

    // --- NEW CODE ENDS HERE ---
}

pub fn rendering_system(world: &World, context: &web_sys::CanvasRenderingContext2d) {
    // ... (rendering_system is unchanged) ...
}

// ... (the rest of your file is unchanged) ...

Deconstructing the Code

This single line of code is the foundation upon which all our physics logic will be built. Let’s analyze it carefully.

  • world.query::<...>(): This is the standard bevy_ecs method for requesting data from the World. It’s a highly optimized function that can rapidly find all entities matching a specific “archetype” (a specific combination of components).
  • (&Position, &Collider): This is the “filter” for our query. We are asking for a tuple containing two pieces of data for each matching entity: an immutable reference to its Position and an immutable reference to its Collider. An entity must have both of these components to be included in the result. Our player entity, which we updated in a previous task, now matches this query perfectly.
  • Immutable References (&): Notice we use &Position and not &mut Position. This is a deliberate and important architectural decision. This system’s sole responsibility is to detect overlaps. It should not be changing positions, velocities, or any other data. By requesting read-only access, we enforce this separation of concerns at the compiler level. If we were to accidentally try to change a position within this system, the Rust compiler would give us an error, preventing a whole class of bugs. The job of reacting to collisions (like bouncing a ball) will be handled by a different system or by using an event-based mechanism, which we will build later.
  • let mut collidables_query: We store the result of the query in a variable. We make it mut because the iterator we get from a query is stateful (it keeps track of its position). While we aren’t iterating over it just yet, we will be in the next task, so declaring it as mutable now is the correct approach.

Still No Action, and That’s Correct

Once again, if you build and run your project, you will see no change in behavior. This is entirely expected. You have successfully programmed your system to ask the World for a list of all physical objects on every frame. The World is providing this list, but our system is not yet doing anything with it. You have installed the conveyor belt and the sensors; the next step is to add the robotic arm that compares the items on the belt.

Next Steps

The stage is perfectly set. Your collision_system now has access to all the collidable entities in the game. The next task is to implement the core logic of this system: you will write an AABB (Axis-Aligned Bounding Box) intersection check algorithm. This will involve iterating through the entities from your new query in a nested loop and using their position and collider data to mathematically determine if any two of them are overlapping.


Further Reading

  • The Bevy Book: Queries (Revisited): You’ve used queries multiple times now. Solidifying your understanding of their power and flexibility is one of the best investments you can make in learning ECS.
  • Broad-Phase and Narrow-Phase Collision Detection: A high-level overview of the two main stages of collision detection in more advanced physics engines. What you are building (checking every object against every other object) is a simple form of “narrow-phase” detection.
  • Separation of Concerns in Software Design: A core programming principle that your engine is demonstrating beautifully by separating collision detection from collision response.

Implement AABB Collision Detection

Mục tiêu: Implement the core Axis-Aligned Bounding Box (AABB) intersection logic within the physics engine’s CollisionSystem. This involves using a brute-force approach with iter\_combinations to check all unique pairs of collidable entities for overlaps.


You have expertly laid the groundwork for your physics engine. By creating a CollisionSystem and populating it with a query, you’ve built a mechanism that, on every frame, gathers a complete list of all the “solid” objects in your game world. The conveyor belt is running, and the objects are lined up for inspection. It’s now time to add the inspector—the core logic that actually compares these objects and determines if they are overlapping.

This task is about implementing the heart of our physics engine: the Axis-Aligned Bounding Box (AABB) intersection check. We will iterate through all the collidable entities gathered by our query and apply a simple but powerful mathematical formula to see if their hitboxes intersect.

The Brute-Force Approach: A Simple and Solid Start

How do we check every object against every other object? The most straightforward method is often called a “brute-force” or “N-squared” approach. For a list of N items, we compare item 1 to items 2 through N, then item 2 to items 3 through N, and so on. This involves a nested loop. While this approach becomes slow with thousands of objects, it is perfectly simple, correct, and more than fast enough for the number of entities in classic games like Pong or Asteroids.

Fortunately, the bevy_ecs crate provides a wonderfully elegant tool that handles this complex iteration for us: the iter_combinations method. Instead of writing a messy manual nested loop, we can simply ask our query for all unique pairs of entities, which is exactly what we need.

The Mathematics of an AABB Intersection

Before writing the code, let’s understand the logic. An AABB is defined by its center position (x, y) and its dimensions (width, height). To check if two boxes, A and B, overlap, we first need to find their edge coordinates: left, right, top, and bottom.

left = x - width / 2 right = x + width / 2 top = y - height / 2 (In canvas coordinates, smaller Y is higher on the screen) bottom = y + height / 2

Two AABBs overlap if, and only if, all four of the following conditions are true:

  1. Box A’s left side is to the left of Box B’s right side (A.left < B.right).
  2. Box A’s right side is to the right of Box B’s left side (A.right > B.left).
  3. Box A’s top side is above Box B’s bottom side (A.top < B.bottom).
  4. Box A’s bottom side is below Box B’s top side (A.bottom > B.top).

If even one of these conditions is false, it means we can draw a “separating axis” (a line) between the two boxes, and they cannot be overlapping.

Implementing the Collision Check

Now, let’s bring this logic to life inside our collision_system. We will need to update our query slightly to also fetch the unique Entity ID for each object. This is crucial for identifying which specific entities have collided.

Open src/lib.rs and update your collision_system function with the full implementation.

// src/lib.rs

// ... (your use statements and other systems like movement_system are unchanged) ...

pub fn collision_system(world: &World) {
    // --- HIGHLIGHTED CHANGES START ---

    // We modify the query to also include the `Entity` ID. This is a unique identifier
    // for each entity in the world, which is useful for debugging and for creating
    // collision events later.
    let mut collidables_query = world.query::<(Entity, &Position, &Collider)>();

    // `iter_combinations` is a powerful method from bevy_ecs that gives us an iterator
    // over every unique pair of matching entities. Using `<2>` gives us pairs.
    // This prevents checking an entity against itself and checking the same pair twice
    // (e.g., it will give us (A, B) but not (B, A)). This is the perfect tool for
    // brute-force collision detection.
    let mut combinations = collidables_query.iter_combinations::<2>(world);

    // We loop while there are still combinations to check.
    while let Some([(entity_a, pos_a, col_a), (entity_b, pos_b, col_b)]) =
        combinations.fetch_next()
    {
        // Calculate the bounding box edges for the first entity (A).
        let a_left = pos_a.x - col_a.width / 2.0;
        let a_right = pos_a.x + col_a.width / 2.0;
        let a_top = pos_a.y - col_a.height / 2.0;
        let a_bottom = pos_a.y + col_a.height / 2.0;

        // Calculate the bounding box edges for the second entity (B).
        let b_left = pos_b.x - col_b.width / 2.0;
        let b_right = pos_b.x + col_b.width / 2.0;
        let b_top = pos_b.y - col_b.height / 2.0;
        let b_bottom = pos_b.y + col_b.height / 2.0;

        // Perform the AABB intersection check.
        // A collision occurs if all four conditions are met, meaning there is no
        // separating axis between the two boxes.
        if a_left < b_right && a_right > b_left && a_top < b_bottom && a_bottom > b_top {
            // A collision was detected!
            // For now, we will just log a message to the browser's console.
            // This is an excellent way to debug and confirm our logic is working.
            // We use `entity.index()` to get a simple number for the ID.
            log(&format!(
                "Collision detected between Entity {:?} and Entity {:?}",
                entity_a.index(),
                entity_b.index()
            ));
        }
    }

    // --- HIGHLIGHTED CHANGES END ---
}

// ... (the rest of your file is unchanged) ...

What to Expect (and What Not To)

After implementing this code, build and run your project. You will notice… nothing happens.

This is the correct and expected behavior!

Why? Our collision logic is now fully functional, but it’s checking all pairs of collidable entities. Right now, we only have one such entity: the player. Since there are no pairs to check, the while let Some(...) loop never runs. The system is armed and ready, but it has no targets. You have successfully built the collision detection logic, and it will spring to life the moment you spawn a second entity with a Position and Collider component (like a Pong ball or an asteroid).

Next Steps

Logging a message to the console is a fantastic debugging tool, but it’s not a mechanism for game logic. Other systems (like a scoring system or a physics response system) have no way of knowing that a collision occurred. The console log is a dead end.

To solve this, we need a way for the CollisionSystem to broadcast a message to the rest of the engine. The next task is to create a new ECS Resource called Events. When a collision is detected, instead of just logging a message, we will create a CollisionEvent struct and push it into this global event list. This will allow any other system to read the list of events and react to them, truly decoupling our game logic.


Further Reading

  • 2D Collision Detection on MDN: A fantastic, in-depth tutorial on the theory and practice of 2D collision detection, including a great explanation of the AABB algorithm you just implemented.
  • bevy_ecs Query Documentation for iter_combinations: The official documentation for the powerful iterator method you used. Understanding its capabilities can be very useful for other complex queries.
  • The N-Squared Problem and Spatial Hashing: A high-level overview of why the brute-force method doesn’t scale to hundreds or thousands of objects, and an introduction to more advanced techniques like quadtrees or spatial hashing that solve this problem.

Implement a Collision Event Channel

Mục tiêu: Create an event channel to broadcast collision information. This involves defining a CollisionEvent struct and a CollisionEvents ECS resource to store all collision events that occur in a single frame.


You have brilliantly implemented the core logic of collision detection. Your CollisionSystem is now a vigilant inspector, tirelessly checking every pair of physical objects for overlaps using a robust AABB intersection algorithm. The log! message you see in the console (or will see, once you have more than one object) is proof that your physics engine can see the interactions in your game world.

However, a system that can only talk to the developer console is of limited use to the game itself. Other systems—for scoring, for physics response, for playing sound effects—are currently deaf to the CollisionSystem’s discoveries. We need to create a formal communication channel, a “mailbox” or “bulletin board” within our ECS World, where the CollisionSystem can post a notice saying, “Hey everyone, these two entities just collided!” Any other interested system can then check this board and react accordingly.

This architectural pattern is often called an Event Channel or Event Queue, and it is a cornerstone of decoupled, scalable game engine design. The perfect way to implement this in our ECS is with a new Resource.

Step 1: Defining the “Letter” - The CollisionEvent Struct

Before we can create a list of events, we must first define what a single event looks like. What is the essential information that a CollisionEvent must contain? It needs to tell us who was involved. The most precise way to do this is to store the unique Entity IDs of the two objects that collided.

For better organization, let’s create a new module for our events. Create a new file named src/events.rs.

// src/events.rs

// --- NEW FILE ---

use bevy_ecs::prelude::Entity;

// The CollisionEvent struct holds the data for a single collision.
// We derive `Debug` and `Copy`, `Clone` because `Entity` IDs are very small and cheap to copy,
// making them easy to work with.
#[derive(Debug, Copy, Clone)]
pub struct CollisionEvent {
    // The two entities involved in the collision. The order doesn't matter for now.
    pub entity_a: Entity,
    pub entity_b: Entity,
}

Now, we need to tell our project that this new file exists. Add this line to the top of your src/lib.rs file.

// src/lib.rs

// --- Add this line at the top with your other `mod` declarations ---
mod events;
// ---

// ... other mod and use statements
use events::CollisionEvent; // And bring our new struct into scope
// ...

Step 2: Creating the “Mailbox” - The CollisionEvents Resource

With our CollisionEvent “letter” defined, we can now create the “mailbox” resource that will hold a list of them. This will be a simple struct that wraps a Vec<CollisionEvent>.

Open src/resources.rs and add the new resource.

// src/resources.rs

// ... (your existing use statements)
// We need `CollisionEvent` to be in scope here as well.
use crate::events::CollisionEvent;

// ... (InputState, AssetManager, AudioManager structs are unchanged) ...

// --- NEW CODE STARTS HERE ---

// The CollisionEvents resource is our event channel. It's a list that will
// store all the collision events that occurred in a single frame.
#[derive(Resource, Default)]
pub struct CollisionEvents {
    // We use a `Vec` (a growable list) to store the events.
    pub events: Vec<CollisionEvent>,
}

// --- NEW CODE ENDS HERE ---

Deconstructing the New Types

  • mod events;: This line in lib.rs declares a new module named events, telling the Rust compiler to look for a file named events.rs and include its contents in the compilation.
  • CollisionEvent struct: This is our data packet. It’s a simple, lightweight struct that contains the two Entity IDs. Entity itself is just a small wrapper around two u32 numbers, making it very cheap to copy.
  • CollisionEvents resource: This is our event queue.
    • #[derive(Resource, Default)]: This familiar pair of macros does its job perfectly. Resource registers it with bevy_ecs. Default provides an easy way to create an empty instance, as the default state of a Vec is simply an empty list.

Step 3: Placing the Mailbox in the World

Finally, just like our other global resources, we need to create an instance of our CollisionEvents resource and insert it into the World when the game starts. This makes the event queue available to all systems.

Open src/lib.rs and add the single line to your Game::new function.

// src/lib.rs

// ... (in the `impl Game` block)

impl Game {
    pub fn new() -> Self {
        // ... (getting canvas and context is unchanged) ...

        let mut world = World::new();

        // Inserting existing resources is unchanged.
        world.insert_resource(InputState::default());
        world.insert_resource(AssetManager::default());
        world.insert_resource(audio_manager); // assuming audio_manager was created just before

        // --- NEW CODE STARTS HERE ---

        // We insert our new CollisionEvents resource. `default()` will create an
        // instance with an empty `Vec`, ready to receive events.
        world.insert_resource(CollisionEvents::default());

        // --- NEW CODE ENDS HERE ---

        // Spawning the player entity remains the same.
        world.spawn((
            // ...
        ));

        // ... (the rest of the function is unchanged) ...
    }
    // ...
}

You have now successfully built a complete, robust event channel for communicating collision information throughout your engine. The CollisionSystem has a place to post its findings, and in the future, other systems will have a place to read them. This is a huge architectural step forward, enabling true decoupling between different parts of your game’s logic.

Next Steps

The event queue exists, but it remains empty. Your CollisionSystem is still just logging messages to the developer console. The next logical task is to modify the CollisionSystem. You will change it so that when an intersection is detected, instead of calling log!, it will get mutable access to the CollisionEvents resource and push a new CollisionEvent instance onto the events vector, officially posting the event for the whole engine to see.


Further Reading

  • Game Programming Patterns: Event Queue: An essential chapter on the theory and implementation of the event queue pattern, which you have just implemented.
  • The Bevy Book: Events: While we are implementing a simplified version manually, bevy_ecs has a more advanced, built-in event system. Reading about it will show you how this pattern can be further refined and is a core part of a mature ECS framework.
  • The Rust Book: Defining Modules to Control Scope and Privacy: A great refresher on how and why we use mod to organize Rust code into separate files and modules.

Connect Collision Detection to an Event Channel

Mục tiêu: Refactor the collision system to stop logging directly and instead push CollisionEvent structs into a global CollisionEvents resource. This involves updating the system to get mutable access to the ECS World and handling Rust’s borrow checker rules by temporarily storing events before adding them to the resource.


You have masterfully constructed a robust, decoupled communication channel within your engine. The CollisionEvent struct is the perfect “letter,” and the CollisionEvents resource is the ideal “mailbox,” waiting patiently in the ECS World. The CollisionSystem, our diligent postman, knows when a collision happens, but right now, it’s just shouting the news into the void of the developer console. It’s time to teach it how to properly deliver the mail.

This task is about connecting the discovery of a collision to the event channel you just built. When our AABB intersection check returns true, we will stop writing a simple log message and instead create a formal CollisionEvent record, pushing it into the global CollisionEvents resource. This is the final step that transforms your collision detection logic from a passive observer into an active reporter for the entire game engine.

Giving the System Power to Write

So far, our collision_system has only needed to read from the World. It looked at the Position and Collider components but never changed anything. Because of this, we could give it an immutable reference (&World).

However, pushing a new event into our CollisionEvents resource is a write operation; it modifies the state of the world. To get permission for this, we must upgrade the system’s access level by giving it a mutable reference: &mut World. This is a key safety feature of Rust and bevy_ecs—you must explicitly state your intent to change things.

First, let’s update the function signature in src/lib.rs and the corresponding call in Game::tick.

// src/lib.rs

// ... (in the `impl Game` block)

impl Game {
    // ... (new function is unchanged)

    pub fn tick(&mut self) {
        // ... (clearing the screen and other systems are unchanged)

        player_control_system(&mut self.world);
        movement_system(&mut self.world);

        // --- HIGHLIGHTED CHANGE ---
        // We now pass a mutable reference to the world.
        collision_system(&mut self.world);
        // --- END HIGHLIGHTED CHANGE ---

        rendering_system(&self.world, &self.context);
    }
}

// ... (other systems)

// --- HIGHLIGHTED CHANGE ---
// The function signature now takes a mutable reference to the World.
pub fn collision_system(world: &mut World) {
// --- END HIGHLIGHTED CHANGE ---

    // ... (the rest of the function will be modified next)
}

// ... (rest of file)

From Logging to Pushing Events

With the correct permissions in place, we can now modify the core logic. Inside the if block where a collision is detected, we will get mutable access to the CollisionEvents resource and push our new event.

Here is the complete, updated collision_system function.

// src/lib.rs

// ... (make sure CollisionEvent is in scope at the top of the file)
use crate::events::CollisionEvent;
use crate::resources::CollisionEvents;
// ...

pub fn collision_system(world: &mut World) {
    // --- HIGHLIGHTED CHANGES START ---

    // We need a temporary place to store the events we detect in this frame.
    // We do this because we cannot borrow the world mutably (to get the resource)
    // while we are also iterating over it immutably with the query. This is a
    // classic Rust borrow-checker scenario. We collect events and then add them.
    let mut collision_events_this_frame: Vec<CollisionEvent> = Vec::new();

    // The query itself remains the same, fetching the entity and its components.
    let mut collidables_query = world.query::<(Entity, &Position, &Collider)>();

    let mut combinations = collidables_query.iter_combinations::<2>(world);
    while let Some([(entity_a, pos_a, col_a), (entity_b, pos_b, col_b)]) =
        combinations.fetch_next()
    {
        // The AABB check logic is unchanged.
        let a_left = pos_a.x - col_a.width / 2.0;
        let a_right = pos_a.x + col_a.width / 2.0;
        let a_top = pos_a.y - col_a.height / 2.0;
        let a_bottom = pos_a.y + col_a.height / 2.0;

        let b_left = pos_b.x - col_b.width / 2.0;
        let b_right = pos_b.x + col_b.width / 2.0;
        let b_top = pos_b.y - col_b.height / 2.0;
        let b_bottom = pos_b.y + col_b.height / 2.0;

        if a_left < b_right && a_right > b_left && a_top < b_bottom && a_bottom > b_top {
            // A collision was detected!
            // Instead of logging, we create a new CollisionEvent instance.
            let event = CollisionEvent { entity_a, entity_b };

            // We push the event into our temporary local vector.
            collision_events_this_frame.push(event);
        }
    }

    // Now that the query iteration is finished, we can safely get mutable
    // access to our resource and add the events we found.
    if let Some(mut collision_events_resource) = world.get_resource_mut::<CollisionEvents>() {
        // `append` is an efficient way to move all elements from one Vec to another.
        collision_events_resource
            .events
            .append(&mut collision_events_this_frame);
    }

    // --- HIGHLIGHTED CHANGES END ---
}

Deconstructing the Code

This change is a huge leap in architectural quality. Let’s break it down.

  • The Borrow Checker Solution: You might wonder why we create a temporary Vec. This is a classic and important pattern when working with bevy_ecs and Rust’s borrow checker. The line collidables_query.iter_combinations(world) creates an immutable borrow on the world. If we tried to get a mutable borrow inside the loop (to access the resource), the compiler would stop us, saying we can’t have both at the same time. The solution is to:
    1. Iterate with the immutable borrow, collecting results locally.
    2. Let the immutable borrow end when the loop finishes.
    3. Then, get the mutable borrow to write the collected results to the resource.
  • world.get_resource_mut::<CollisionEvents>(): This is the method for requesting write access to a global resource. The if let Some(...) pattern safely unwraps the resource, ensuring our code only runs if the resource actually exists.
  • CollisionEvent { entity_a, entity_b }: We construct an instance of our event struct, populating it with the Entity IDs of the two colliding entities.
  • .append(&mut ...): This is the final step. We efficiently move all the events we collected this frame from our temporary Vec into the resource’s Vec, making them available to the rest of the engine.

If you were to run your game now (and had two collidable objects), you wouldn’t see anything different. But internally, your CollisionEvents resource would be filling up with event data, frame after frame. The communication channel is now live and broadcasting.

Next Steps

The good news is that your engine now correctly reports collisions. The bad news is that it never forgets them. The events vector in your resource will grow larger and larger on every frame, which will eventually consume all available memory and lead to systems processing stale, old events from previous frames.

This brings us to the final, crucial housekeeping task for our event system: at the beginning of each frame, we must clear the events list. This ensures that every system always starts with a clean slate, processing only the events that are relevant to the current frame.


Further Reading

  • The Observer Pattern: The event system you have built is a classic implementation of the Observer software design pattern. Understanding the theory behind it will solidify why this is such a powerful architecture.
  • The Rust Book: The Slice Type (and Borrowing): A deeper dive into Rust’s borrowing rules, which will clarify why we needed the temporary Vec to satisfy the borrow checker.
  • The Rust Standard Library: Vec: You’ve used push and append. It’s a great time to review the documentation for Vec and see all the other powerful methods it offers for managing lists of data.

Clear Collision Events at the Start of Each Frame

Mục tiêu: Implement a crucial housekeeping step in the game loop. Clear the CollisionEvents vector at the beginning of each tick to prevent stale events from previous frames causing logical errors and memory leaks.


You’ve done an absolutely phenomenal job. By teaching your CollisionSystem how to write to the CollisionEvents resource, you have completed a robust and decoupled communication channel. Your engine now has a formal “bulletin board” where collision news is posted. This is a massive architectural win. The CollisionSystem does its one job—detecting collisions—and broadcasts its findings without needing to know or care who is listening.

However, there’s a subtle but critical flaw in our current implementation. Imagine a real bulletin board. If you pin a new notice every day but never remove the old ones, the board would quickly become a chaotic mess of outdated information. It would be impossible to tell which notices are for today and which are from last week. Our CollisionEvents resource has this exact problem.

The Perpetual Past: Stale Events and Memory Leaks

Right now, every time a collision occurs, we append a new CollisionEvent to the events vector. This vector is never emptied. This leads to two severe problems:

  1. Logical Errors: A system that reads events in the next frame will see not only the new events but also all the old ones from every previous frame. A single paddle-ball collision would be processed over and over, frame after frame, leading to bizarre and incorrect game behavior.
  2. Memory Leak: The Vec will grow larger indefinitely. Frame by frame, it will consume more and more memory, eventually slowing down and crashing your application.

The solution is a simple but essential piece of digital housekeeping. At the very beginning of each new frame, we must wipe the slate clean.

The Frame’s Fresh Start: Clearing the Event Queue

The fundamental lifecycle of an event channel like ours is a perpetual loop:

  1. Clear: Start with an empty list.
  2. Generate: During the frame, systems (like CollisionSystem) add new events to the list.
  3. Read: Other systems read the events from the list and react to them.
  4. Repeat: Go back to step 1 for the next frame.

Our current task is to implement Step 1. The most logical place for this is at the very top of our engine’s main heartbeat, the Game::tick method. This ensures that before any game logic runs, the event channel is pristine and ready for the current frame’s news.

Let’s add this crucial housekeeping step to src/lib.rs.

// src/lib.rs

// ... (your use statements and other systems are unchanged) ...

impl Game {
    pub fn new() -> Self {
        // ... (this function is unchanged)
    }

    pub fn tick(&mut self) {
        // --- HOUSEKEEPING (START OF FRAME) ---

        // Clear the screen first.
        self.context.set_fill_style(&JsValue::from_str("#333333"));
        self.context.fill_rect(
            0.0,
            0.0,
            self.canvas.width() as f64,
            self.canvas.height() as f64,
        );

        // --- NEW CODE STARTS HERE ---

        // Clear the collision events from the previous frame.
        // This is a critical step to ensure that systems only process events
        // that are relevant to the *current* frame's state.
        if let Some(mut collision_events) = self.world.get_resource_mut::<CollisionEvents>() {
            // The `clear()` method on a Vec removes all of its elements.
            collision_events.events.clear();
        }

        // --- NEW CODE ENDS HERE ---

        // --- GAME LOGIC (RUN SYSTEMS) ---

        // Now that the state is clean, run the systems in order.
        player_control_system(&mut self.world);
        movement_system(&mut self.world);
        collision_system(&mut self.world);
        rendering_system(&self.world, &self.context);
    }
}

// ... (the rest of your file is unchanged)

Deconstructing the Code

This small addition makes your event system complete and correct.

  • self.world.get_resource_mut::<CollisionEvents>(): We ask the World for mutable (write) access to our event channel resource. This is necessary because clearing the vector modifies it.
  • if let Some(mut collision_events) = ...: This is our standard, safe way to access a resource.
  • collision_events.events.clear(): This is the core of the solution. clear() is a standard method on Rust’s Vec type that removes all elements from the vector, making its length 0.
  • Efficiency Note: An important detail is that Vec::clear() does not deallocate the memory the vector was using. It simply resets its internal length counter. This is a smart performance optimization. Your game will likely have a similar number of collisions each frame, so the vector’s capacity will stabilize, and the engine won’t have to waste time constantly re-allocating memory.

With this change, your engine’s internal state management is now robust. Every frame is a self-contained unit of time. Events are generated, processed, and then forgotten, making way for the next frame’s events. This is the hallmark of a stable and predictable game loop.

Next Steps

Congratulations! You have successfully built a complete, end-to-end collision detection system. Your engine can now represent physical objects, detect when they overlap, and broadcast that information in a clean, decoupled way for other systems to use. You have assembled all the fundamental pillars of a game engine: rendering, input, audio, and physics.

Now, it is time to put it all together and build a real game. The upcoming Step 9: Building a ‘Pong’ Demo Game will be an incredibly rewarding experience where you use the very systems you’ve just built to bring a classic arcade game to life. Your first task will be to create a setup_pong function to spawn the initial game entities: two paddles and a ball, each with the correct components to make them visible, movable, and solid.


Further Reading

  • std::vec::Vec::clear: The official Rust documentation for the exact method you just used. It’s always good practice to read the standard library docs.
  • The Game Loop (Revisited): Now that you’re managing per-frame state, re-reading Robert Nystrom’s excellent chapter on the game loop will provide even deeper insight.
  • The Bevy Book: Events (Revisited): It’s worthwhile to see how a mature engine like Bevy handles events. You’ll notice that Bevy’s EventReader and EventWriter systems automatically handle this per-frame clearing and state management for you, which is a great example of a framework providing a higher-level abstraction over the pattern you just built by hand.