Set Up the Initial Scene for a Pong Game
Mục tiêu: Create the initial game entities (paddles, ball) for a Pong clone. This task involves writing a dedicated setup function and refactoring asset loading to be concurrent using the Rust futures crate.
An extraordinary milestone! You have successfully constructed all the fundamental pillars of a modern game engine from the ground up. You have systems for rendering visuals, handling player input, playing audio, and—most recently—a complete, event-driven physics system for detecting collisions. The engine is a powerful, silent machine waiting for instructions. It’s time to give it those instructions and build your first full game: the timeless classic, Pong.
This entire step is about using the components, resources, and systems you have already built to assemble a playable experience. Our first task is to set the stage by creating a dedicated function to spawn the initial game entities: two paddles and a ball.
Housekeeping: Creating the Game Assets
Before we can spawn entities with sprites, we need the sprite images themselves.
- Navigate to your
assetsdirectory. - Create two small, simple, white, square images. A 16x16 pixel PNG with a white square on a transparent background is perfect.
- Name them
paddle.pngandball.png.
Our Sprite component will scale these simple images to the correct dimensions in-game.
Step 1: Concurrent Asset Loading with futures::join!
In previous steps, we loaded assets one by one. As our game grows, we’ll need to load more assets at startup. Loading them sequentially can increase loading times. We can improve this by loading all our assets concurrently using the futures crate.
First, add futures as a dependency in your Cargo.toml:
# Cargo.toml
[dependencies]
# ... (your other dependencies)
futures = "0.3"
# ...
Next, let’s refactor the spawn_local block in your run function to load all game assets—the old player ship, the new paddle and ball images, and the laser sound—all at the same time.
// src/lib.rs
use futures::join; // Add this to your use statements at the top
// ...
#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
let game = Rc::new(RefCell::new(Game::new()));
spawn_local(async move {
// --- UPDATED ASSET LOADING ---
// The `futures::join!` macro takes multiple futures and runs them concurrently.
// It waits until all of them have completed and then returns a tuple
// containing all of their results. This is far more efficient than
// awaiting each asset one by one.
let (paddle_img, ball_img, laser_sound) = join!(
load_image("assets/paddle.png"),
load_image("assets/ball.png"),
// We wrap the sound loading in an async block to make it a future
// that can be passed to the join! macro.
async {
let game_state = game.borrow();
let audio_manager = game_state
.world
.get_resource::<AudioManager>()
.expect("AudioManager not found");
load_sound(&audio_manager.context, "assets/laser_shoot.wav").await
}
);
log("All assets for Pong loaded and decoded successfully.");
// --- ASSET INSERTION ---
let mut game_state = game.borrow_mut();
// Insert images into the AssetManager
if let Some(mut asset_manager) = game_state.world.get_resource_mut::<AssetManager>() {
asset_manager
.assets
.insert(String::from("assets/paddle.png"), paddle_img.unwrap());
asset_manager
.assets
.insert(String::from("assets/ball.png"), ball_img.unwrap());
}
// Insert sounds into the AudioManager
if let Some(mut audio_manager) = game_state.world.get_resource_mut::<AudioManager>() {
audio_manager
.sounds
.insert(String::from("assets/laser_shoot.wav"), laser_sound.unwrap());
}
});
// ... (the rest of the run function is unchanged)
Ok(())
}
Step 2: Defining the setup_pong Function
It’s excellent practice to encapsulate scene-building logic into its own function. This keeps your Game::new constructor clean and makes it easy to switch between different game setups in the future. Let’s create this function in src/lib.rs.
// src/lib.rs
// ... (after your system functions)
// This function is responsible for spawning all the initial entities for a game of Pong.
pub fn setup_pong(world: &mut World, canvas_width: f32, canvas_height: f32) {
// Define some constants for our game entities to keep things tidy.
const PADDLE_WIDTH: f32 = 12.0;
const PADDLE_HEIGHT: f32 = 80.0;
const BALL_SIZE: f32 = 10.0;
// Spawn the left paddle (Player 1)
world.spawn((
// Position it on the left side of the screen, centered vertically.
Position { x: 50.0, y: canvas_height / 2.0 },
// It starts stationary.
Velocity { dx: 0.0, dy: 0.0 },
// The visual representation.
Sprite {
asset_key: String::from("assets/paddle.png"),
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
},
// The physical hitbox.
Collider {
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
},
));
// Spawn the right paddle (Player 2 or AI)
world.spawn((
// Position it on the right side of the screen, centered vertically.
Position { x: canvas_width - 50.0, y: canvas_height / 2.0 },
Velocity { dx: 0.0, dy: 0.0 },
Sprite {
asset_key: String::from("assets/paddle.png"),
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
},
Collider {
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
},
));
// Spawn the ball
world.spawn((
// Position it in the exact center of the screen.
Position { x: canvas_width / 2.0, y: canvas_height / 2.0 },
// Give it an initial velocity to start the game.
// These values are in pixels per second.
Velocity { dx: 250.0, dy: -250.0 },
Sprite {
asset_key: String::from("assets/ball.png"),
width: BALL_SIZE,
height: BALL_SIZE,
},
Collider {
width: BALL_SIZE,
height: BALL_SIZE,
},
));
}
Step 3: Integrating the Setup into Game::new
Now, we must call our new setup_pong function when the game is first created. This involves removing the old code that spawned a single player ship and replacing it with a call to our new function.
// src/lib.rs
impl Game {
pub fn new() -> Self {
// ... (getting the window, document, canvas, context is unchanged) ...
// ... (creating the world and inserting resources is unchanged) ...
world.insert_resource(InputState::default());
world.insert_resource(AssetManager::default());
world.insert_resource(audio_manager); // assuming audio_manager creation
world.insert_resource(CollisionEvents::default());
// --- HIGHLIGHTED CHANGES ---
// REMOVE the old player entity spawn:
/*
world.spawn((
Position { ... },
Velocity { ... },
Sprite { ... },
Collider { ... },
Player,
));
*/
// ADD the call to our new Pong setup function.
// We pass it the world and the canvas dimensions so it can position
// the entities correctly, regardless of screen size.
setup_pong(&mut world, canvas.width() as f32, canvas.height() as f32);
// --- END HIGHLIGHTED CHANGES ---
Self {
world,
canvas,
context,
}
}
// ... (the rest of the Game impl is unchanged) ...
}
What to Expect
Build and run your project. You should now see the classic Pong starting screen: a paddle on the left, a paddle on the right, and a ball in the middle. The ball will immediately start moving according to the initial velocity you gave it.
More importantly, something profound is happening behind the scenes. For the first time, your CollisionSystem has more than one collidable object in the world. Its iter_combinations loop is now running, and it is actively checking for overlaps between the ball and the paddles. Nothing will happen when they touch yet, because no system is listening for the CollisionEvents it is now generating. You have perfectly set the stage for the next steps.
Next Steps
The scene is set, but it’s not yet a game. The paddles are static and the player has no control. The next logical step is to give the player control over their paddle. You will do this by creating a new, simple Controllable component and adding it to the player’s paddle entity. This will allow your PlayerControlSystem to identify which entity it should move in response to keyboard input.
Further Reading
- The Bevy Book:
Commandsand Spawning Entities: This chapter from the Bevy Book covers the engine’s higher-level API for spawning entities, which is a more advanced version of theworld.spawn()you are using directly. - Game Programming Patterns: Game State: An overview of different ways to manage the state of a game, including the concept of “scenes” or “levels” which your
setup_pongfunction is a simple implementation of. - The
futures::join!macro: Official documentation for the powerful concurrency macro you just used to speed up your asset loading.
Dissecting Pong Entities: The Role of Components in ECS
Mục tiêu: Analyze the ‘setup_pong’ function to understand how entities like the paddles and ball are defined by their components (Position, Velocity, Sprite, Collider) in an Entity-Component-System (ECS) architecture.
Excellent work on creating the setup_pong function. You have successfully laid out the entire scene for your first playable game. This is a monumental step where all your hard work on the engine’s individual systems begins to pay off. You’ve instructed the engine to spawn the paddles and the ball, but the real power lies in how you defined them.
Our current task is to take a closer look at the components you assigned to each entity. An entity’s behavior is not defined by some complex class or object, but by the simple, modular pieces of data—the components—that we attach to it. Let’s dissect the “bundle” of components you’ve given to each Pong entity to understand precisely how they work together to create the game.
The Anatomy of a Pong Entity
The code you wrote in the setup_pong function is the blueprint for your game. It doesn’t just place objects; it endows them with the potential for position, movement, appearance, and physical presence. Let’s review that function, but this time with a focus on what each component contributes.
// src/lib.rs
// This function is the complete recipe for starting a game of Pong.
pub fn setup_pong(world: &mut World, canvas_width: f32, canvas_height: f32) {
// Using constants makes the code readable and easy to tweak.
const PADDLE_WIDTH: f32 = 12.0;
const PADDLE_HEIGHT: f32 = 80.0;
const BALL_SIZE: f32 = 10.0;
// --- Spawning the Left Paddle ---
world.spawn((
// 1. Position: Where is it?
// Places the paddle 50 pixels from the left edge and exactly in the
// vertical center of the screen, making the setup resolution-independent.
Position { x: 50.0, y: canvas_height / 2.0 },
// 2. Velocity: How is it moving?
// The paddle starts perfectly still. The MovementSystem will read this,
// and the paddle won't move until its velocity is changed by another
// system (like the PlayerControlSystem).
Velocity { dx: 0.0, dy: 0.0 },
// 3. Sprite: What does it look like?
// Tells the RenderingSystem to find "assets/paddle.png" in the AssetManager
// and draw it at the entity's position, scaled to these dimensions.
Sprite {
asset_key: String::from("assets/paddle.png"),
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
},
// 4. Collider: What is its physical shape?
// This gives the paddle a physical body. The CollisionSystem will now
// include this entity in its checks. For a simple rectangle like a paddle,
// the collider's size is identical to the sprite's size.
Collider {
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
},
));
// --- Spawning the Right Paddle ---
// This entity has the exact same set of components, only its Position.x
// is different, placing it on the opposite side of the screen.
world.spawn((
Position { x: canvas_width - 50.0, y: canvas_height / 2.0 },
Velocity { dx: 0.0, dy: 0.0 },
Sprite {
asset_key: String::from("assets/paddle.png"),
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
},
Collider {
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
},
));
// --- Spawning the Ball ---
world.spawn((
// 1. Position: Starts in the dead center of the screen.
Position { x: canvas_width / 2.0, y: canvas_height / 2.0 },
// 2. Velocity: This is key!
// Unlike the paddles, the ball starts with an initial velocity. The
// MovementSystem will immediately pick this up and start moving the ball
// 250 pixels per second to the right and 250 pixels per second upwards.
// This is what kicks off the gameplay.
Velocity { dx: 250.0, dy: -250.0 },
// 3. Sprite: Tells the RenderingSystem to draw the "ball.png" image.
Sprite {
asset_key: String::from("assets/ball.png"),
width: BALL_SIZE,
height: BALL_SIZE,
},
// 4. Collider: Gives the ball its physical presence, allowing it to
// trigger CollisionEvents when it overlaps with a paddle.
Collider {
width: BALL_SIZE,
height: BALL_SIZE,
},
));
}
Composition as Behavior: The Power of ECS
Take a moment to appreciate what you’ve just done. You did not create a Paddle class or a Ball object. Instead, you composed entities from modular data blocks. This is the heart of the Entity-Component-System philosophy.
- What makes an entity movable? The presence of
PositionandVelocitycomponents. TheMovementSystemdoesn’t care if it’s a ball or a paddle; if it has those components, it will move it. - What makes an entity visible? The presence of
PositionandSpritecomponents. TheRenderingSystemwill draw any entity that satisfies its query for these components. - What makes an entity “solid”? The presence of
PositionandCollidercomponents. TheCollisionSystemwill check for overlaps between any entities that have this combination.
By adding or removing components, you can fundamentally change an entity’s behavior without rewriting any of your systems. This is the flexibility and power you have been building this entire time.
Next Steps
The scene is set, and all the actors are on stage with their properties correctly defined. However, the game is not yet interactive. The paddles are static objects that the player cannot influence.
To solve this, we need a way to distinguish the player’s paddle from the other one. In the next task, you will create a new, simple “marker” component called Controllable. By adding this component to just one of the paddles, you will give the PlayerControlSystem a specific target to apply its logic to, officially connecting your input handling to the game of Pong.
Further Reading
- 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.
- The Bevy Book: Bundles: While you are creating the tuple of components manually,
bevy_ecshas a concept calledBundlesto make spawning entities with a common set of components cleaner. It’s a great next step for organizing your entity creation code. - Archetypes in ECS: A more advanced topic on how ECS frameworks like
bevy_ecsstore entities with the same set of components together in memory for incredibly fast iteration. Your Pong entities (paddles and the ball) now form distinct archetypes.
Add a Controllable component to the player’s paddle.
Mục tiêu:
You have done a fantastic job setting up the initial scene for Pong. The paddles and the ball are all present on the screen, each defined by a carefully chosen set of components that grant them visibility (Sprite), physicality (Collider), and the potential for movement (Position, Velocity). You have composed your game’s actors from simple, reusable data blocks, which is a testament to the power of the ECS architecture you’ve built.
However, if you run the game now, you’ll notice it’s more of a movie than a game. The ball moves on its own, but the paddles remain static. The player is merely a spectator. Our PlayerControlSystem, which we built in a previous step, is still wired to look for an entity with a Player component, which no longer exists. More importantly, even if it did, how would it know which of the two identical paddles to move? We need a way to flag one of the paddles as being under the player’s direct control.
This is the perfect scenario to introduce a simple yet incredibly powerful ECS pattern: the marker component.
The Power of a Label: Marker Components
A marker component (also known as a tag component) is a component that contains no data. Its sole purpose is to exist. By attaching this empty component to an entity, we are essentially putting a label on it. A system can then query for all entities that have this specific label.
This is a clean, scalable, and highly declarative way to manage entity behavior. Instead of having a boolean field like is_player_controlled inside another component, we create a distinct type, Controllable, that cleanly separates this concern.
Step 1: Defining the Controllable Component
First, we need to create this new component. It will be the simplest component you’ve defined so far.
Open src/components.rs and add the new Controllable 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,
}
// ... (Velocity, Sprite, Collider are unchanged) ...
// --- NEW CODE STARTS HERE ---
// `Controllable` is a "marker component". It has no data.
// Its only purpose is to "tag" an entity, allowing systems to query for
// entities that have this specific property. In this case, it marks the entity
// that should be controlled by the player's input.
#[derive(Component)]
pub struct Controllable;
// --- NEW CODE ENDS HERE ---
And that’s it! The component is defined. It has no fields like x or width. Its existence on an entity is all the information we need.
Step 2: Tagging the Player’s Paddle
Now that we have our Controllable label, we need to attach it to the entity we want the player to control. We’ll choose the left paddle for this role.
Navigate to your setup_pong function in src/lib.rs and add the Controllable component to the component bundle for the left paddle.
// src/lib.rs
// Make sure your new component is in scope at the top of the file
use crate::components::Controllable;
// ...
// ... (inside the setup_pong function)
pub fn setup_pong(world: &mut World, canvas_width: f32, canvas_height: f32) {
// ... (constants are unchanged)
// Spawn the left paddle (Player 1)
world.spawn((
Position { x: 50.0, y: canvas_height / 2.0 },
Velocity { dx: 0.0, dy: 0.0 },
Sprite {
asset_key: String::from("assets/paddle.png"),
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
},
Collider {
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
},
// --- HIGHLIGHTED CHANGE ---
// We add our new marker component. This is the only change needed to
// designate this specific paddle as the one the player will control.
Controllable,
// --- END HIGHLIGHTED CHANGE ---
));
// The right paddle is unchanged. It does NOT get the `Controllable` component.
world.spawn((
Position { x: canvas_width - 50.0, y: canvas_height / 2.0 },
// ... (other components are the same)
));
// The ball is also unchanged.
world.spawn((
Position { x: canvas_width / 2.0, y: canvas_height / 2.0 },
// ... (other components are the same)
));
}
What You’ve Achieved
By adding this single, data-less component, you have cleanly and clearly separated the data defining which entity is player-controlled from the logic that will act on it. Your setup_pong function now explicitly states: “Create a paddle here, and by the way, this one is controllable.”
If you build and run the project now, you will see no change in behavior. This is correct and expected. Just like when you first added the Collider component, you have added a piece of data that is currently not being read by any system. You have placed the signpost, but no one is reading the directions yet.
This powerful, decoupled approach means that in the future, if you want to create a two-player mode, you could simply add a Controllable(PlayerId) component and have a more advanced control system. If you want to build Asteroids, you’ll add Controllable to the spaceship entity. The PlayerControlSystem doesn’t need to know about “paddles” or “spaceships”; it only needs to know about things that are “controllable.”
Next Steps
The signpost is in place. The next logical and exciting task is to modify the PlayerControlSystem to read it. You will change its query from looking for the old Player component to looking for entities that have both a Velocity and your new Controllable component, finally connecting the player’s key presses to the paddle on the screen.
Further Reading
- Entity Component System Patterns: Tag Components: A great blog post that discusses the marker/tag component pattern in detail.
- The Bevy Book: Marker Components: Bevy, the ECS framework your
bevy_ecscrate comes from, has this pattern as a core concept. - The Rust Book: Unit-like Structs: A refresher on the specific type of struct you just created—one with no fields.
Implement Player-Controlled Paddle Movement
Mục tiêu: Modify the PlayerControlSystem to query for entities with the Controllable marker component and apply vertical movement based on keyboard input, making the Pong paddle interactive.
Excellent! In the previous task, you brilliantly introduced the Controllable marker component, a clean and declarative way to label the specific entity that the player should be able to move. You’ve placed this “You are here” sign on the left paddle, but our PlayerControlSystem is still reading an old map, looking for a component that no longer exists in our Pong setup.
It’s time to update the system’s instructions. Our current task is to modify the PlayerControlSystem to read this new Controllable signpost. We will change its query to find the entity with this specific marker and then apply vertical movement to it, officially connecting your keyboard input to the paddle and making the game truly interactive.
From General Movement to Paddle Control
Your original PlayerControlSystem was likely designed for a free-moving entity, like the ship in Asteroids, responding to all four arrow keys. For Pong, this isn’t quite right. Paddles only move up and down. We will take this opportunity not only to update the system’s query but also to refine its logic to be perfectly suited for the game we are building.
Let’s modify the player_control_system function in src/lib.rs.
// src/lib.rs
// ... (your use statements at the top of the file)
use crate::components::{Controllable, Velocity}; // Ensure Controllable is in scope
use crate::resources::InputState;
use bevy_ecs::prelude::{Query, Res, With, World}; // Add With to your bevy_ecs imports if it's not there
// ... (other systems)
// We're updating the PlayerControlSystem to be specific to Pong's needs.
pub fn player_control_system(world: &mut World) {
// --- HIGHLIGHTED CHANGES START ---
// Define a constant for the paddle's speed. This makes the code cleaner and easier to tweak.
const PADDLE_SPEED: f32 = 400.0; // pixels per second
// Use `world.query` to build our query. We are looking for any entity that
// has a `Velocity` component AND the `Controllable` marker component.
// The `With<Controllable>` is a filter that ensures we only get entities that
// are tagged as controllable, but we don't need to read any data from the
// Controllable component itself. We request mutable access to Velocity
// because we intend to change it.
let mut paddle_query = world.query_filtered::<&mut Velocity, With<Controllable>>();
// We also need to get the `InputState` resource to know which keys are pressed.
// We can't query for resources at the same time we query for components in this way,
// so we fetch it from the world separately.
if let Some(input_state) = world.get_resource::<InputState>() {
// Since we expect there to be exactly one controllable paddle, we can use
// `get_single_mut` on our query. This is a safe way to get the one result.
// If there were zero or more than one controllable entities, this would return an Err,
// preventing bugs.
if let Ok(mut velocity) = paddle_query.get_single_mut(world) {
// Reset the vertical velocity to 0.0 at the start of each frame.
// This ensures the paddle stops moving instantly when the key is released.
velocity.dy = 0.0;
// Check the input state and set the velocity accordingly.
if input_state.up_pressed {
// In our coordinate system, a negative Y velocity moves the entity upwards.
velocity.dy = -PADDLE_SPEED;
}
if input_state.down_pressed {
// A positive Y velocity moves the entity downwards.
velocity.dy = PADDLE_SPEED;
}
}
}
// --- HIGHLIGHTED CHANGES END ---
}
Deconstructing the New Logic
This updated system is now a finely tuned piece of logic specifically for our Pong game. Let’s break down the key improvements.
- The New Query:
world.query_filtered::<&mut Velocity, With<Controllable>>()is the new heart of the system.&mut Velocity: We ask for mutable access to theVelocitycomponent, as this is the data we intend to change.With<Controllable>: This is a powerful filter frombevy_ecs. It tells the query, “Only give me entities that also have aControllablecomponent.” We useWithbecause we don’t need to read any data fromControllable(it’s empty), we just need to confirm its presence. This is the most efficient way to use marker components in a query.
get_single_mut: In Pong, we know there should only be one player-controlled paddle. Instead of aforloop, we useget_single_mut(world). This method attempts to get the single, unique result from the query. It returns aResult:Ok(velocity): If it finds exactly one matching entity.Err(...): If it finds zero or more than one. This is a fantastic safety feature. If you were to accidentally add theControllablecomponent to two entities, your game’s logic wouldn’t silently break; this.is_ok()check would fail, protecting you from unexpected behavior.
- Pong-Specific Controls: We have removed the logic for left and right movement, as it’s irrelevant for a Pong paddle. The system now only checks for
up_pressedanddown_pressed. - Instant Stop: By setting
velocity.dy = 0.0;at the beginning of every check, we ensure the paddle’s movement is snappy and responsive. The moment you release a key,up_pressedordown_pressedbecomesfalse, the conditions are no longer met, and the velocity remains at zero. Without this line, the paddle would continue to drift in the last direction it was moved.
See It in Action!
This is the moment of truth. Build and run your project.
wasm-pack build --target web
When you refresh the browser, you will see the Pong scene as before. But now, when you press the Up and Down Arrow Keys, the left paddle will glide smoothly up and down the screen, under your complete command. The right paddle and the ball will remain unaffected. You have successfully made your game interactive!
Next Steps
The player can now move, and the ball is in motion. The next logical step is to make the world react when they interact. The CollisionSystem is already diligently posting CollisionEvents whenever the ball touches a paddle, but no system is listening. The next task is to create a new BallSystem whose entire job is to read these CollisionEvents and change the ball’s velocity, making it bounce off the paddles and the walls.
Further Reading
- The Bevy Book: Queries: Revisit the official documentation on queries. Understanding filters like
WithandWithoutis key to writing powerful and efficient systems. - The Bevy Book: Query
get_single_mut: The specific documentation for the safe and idiomatic method you just used to fetch a unique entity. - Game Programming Patterns: Input: A fantastic chapter on the theory and various patterns for handling player input in a game engine.
Create a BallSystem that reads CollisionEvents.
Mục tiêu:
Of course! Let’s get this task done. Here is the detailed solution structured as a blog article.
You have constructed a truly impressive foundation. Your CollisionSystem is working diligently, detecting overlaps and posting CollisionEvents onto a global “bulletin board” for all to see. This is a critical achievement in creating a decoupled and scalable engine. The collision system is the reporter, broadcasting the news. Now, we need to create the subscriber—a system that reads this news and takes action.
This brings us to the heart of gameplay logic: reacting to the world. For our Pong game, the most important reaction is the ball bouncing off the paddles. Our current task is to create a new system, the BallSystem, whose primary job is to listen for these CollisionEvents and identify when the ball has been involved in a collision.
Labeling the Star Player: The Ball Marker Component
Before we can write the system, we face a crucial question: when our system reads a CollisionEvent containing two entity IDs, how does it know which one is the ball? Both paddles and the ball have Position and Collider components, making them indistinguishable to a generic query.
The solution is to use the same elegant pattern you employed for the player’s paddle: a marker component. We will create a simple, data-less Ball component to uniquely tag the ball entity.
Step 1: Define the Ball Component
First, let’s define our new marker component. Open src/components.rs and add the Ball struct.
// src/components.rs
// ... (other use statements and components are unchanged) ...
#[derive(Component)]
pub struct Controllable;
// --- NEW CODE STARTS HERE ---
// The `Ball` component is another marker component. Its sole purpose is to
// tag the entity that should be treated as the game's ball, allowing
// systems like our new `BallSystem` to easily query for it.
#[derive(Component)]
pub struct Ball;
// --- NEW CODE ENDS HERE ---
Step 2: Tag the Ball Entity
Now that we have our Ball label, we need to attach it to the ball entity when it’s created. Navigate to your setup_pong function in src/lib.rs and add the component to the ball’s spawn bundle.
// src/lib.rs
// Make sure your new component is in scope at the top of the file
use crate::components::{Ball, Controllable}; // Add Ball here
// ...
// ... (inside the setup_pong function)
pub fn setup_pong(world: &mut World, canvas_width: f32, canvas_height: f32) {
// ... (constants and paddle spawns are unchanged) ...
// Spawn the ball
world.spawn((
Position { x: canvas_width / 2.0, y: canvas_height / 2.0 },
Velocity { dx: 250.0, dy: -250.0 },
Sprite {
asset_key: String::from("assets/ball.png"),
width: BALL_SIZE,
height: BALL_SIZE,
},
Collider {
width: BALL_SIZE,
height: BALL_SIZE,
},
// --- HIGHLIGHTED CHANGE ---
// We add our new `Ball` marker component to this entity.
Ball,
// --- END HIGHLIGHTED CHANGE ---
));
}
With this simple change, our game state now clearly distinguishes the ball from all other entities.
Creating the BallSystem to Read Events
Now we can create the BallSystem. This system will query for the CollisionEvents resource, iterate through the events, and check if any of them involve our newly tagged ball entity. For this task, we will simply log a message to the console to confirm that our event-reading logic is working correctly.
Step 1: Define the ball_system Function
Create the new system in src/lib.rs, alongside your other systems. We’ll need &mut World because we will soon be modifying the ball’s velocity.
// src/lib.rs
// ... (other systems like player_control_system, movement_system, etc.)
// --- NEW CODE STARTS HERE ---
use crate::components::Ball; // Ensure Ball is in scope
use crate::resources::CollisionEvents; // Ensure CollisionEvents is in scope
// The BallSystem is responsible for all game logic related to the ball.
// For now, its only job is to read collision events and identify when the ball
// is involved.
pub fn ball_system(world: &mut World) {
// First, we need the Entity ID of the ball. We create a query to find the
// entity that has the `Ball` marker component.
let mut ball_query = world.query_filtered::<Entity, With<Ball>>();
// We expect there to be exactly one ball, so we use `get_single` to get it.
// If there isn't one, we can't do anything, so we just return early.
let Ok(ball_entity) = ball_query.get_single(world) else {
return;
};
// To avoid issues with Rust's borrow checker, we can't easily iterate through
// resources while also preparing to mutate components. A common and safe pattern
// is to read the events into a temporary, local list first.
let events_this_frame = if let Some(collision_events) = world.get_resource::<CollisionEvents>() {
// `clone()` creates a new Vec with a copy of all the events.
collision_events.events.clone()
} else {
// If the resource doesn't exist, create an empty Vec.
Vec::new()
};
// Now, we can safely iterate over our local copy of the events.
for event in events_this_frame {
// Check if the collision event involves the ball entity.
// The collision could be (ball, paddle) or (paddle, ball), so we must check both.
if event.entity_a == ball_entity || event.entity_b == ball_entity {
// A collision involving the ball has occurred!
// For now, we will just log a message to the console to confirm
// that our event-reading logic is working perfectly.
log("Ball collision detected!");
}
}
}
// --- NEW CODE ENDS HERE ---
Step 2: Integrate the System into the Game Loop
A system that is never called does nothing. We must add our new ball_system to the Game::tick method. The correct order is crucial: we should check for and react to collisions after they have been detected but before we render the final scene.
// src/lib.rs
impl Game {
// ... (new function is unchanged)
pub fn tick(&mut self) {
// ... (clearing the screen and collision events is unchanged) ...
// --- GAME LOGIC (RUN SYSTEMS) ---
player_control_system(&mut self.world);
movement_system(&mut self.world);
collision_system(&mut self.world);
// --- HIGHLIGHTED CHANGE ---
// We call our new BallSystem right after the CollisionSystem.
// This is the perfect place for gameplay logic that needs to react to physics events.
ball_system(&mut self.world);
// --- END HIGHLIGHTED CHANGE ---
rendering_system(&self.world, &self.context);
}
}
The Result: A Voice in the Console
Build and run your project. The game will look the same, and the ball will still pass straight through the paddles. However, open your browser’s developer console. Now, every time the ball’s sprite overlaps with a paddle’s sprite, you will see the message “Ball collision detected!” printed.
This is a massive success! You have officially verified that your event system works end-to-end. The CollisionSystem is correctly detecting overlaps, the CollisionEvents resource is correctly storing them, and your new BallSystem is correctly reading them. The communication channel is live.
Next Steps
The BallSystem can now hear when a collision happens. The next exciting step is to make it react. In the next task, you will replace the simple log! message with the actual game logic. You will query for the ball’s Velocity component and invert its direction, making it bounce realistically off the paddles and the top and bottom walls of the game area.
Further Reading
- Game Programming Patterns: Event Queue (Revisited): You have now implemented both the sender and receiver parts of an event queue. This chapter will provide even deeper insight into this powerful pattern.
- The Bevy Book: Events: It’s worth re-reading Bevy’s official documentation on their event system to see how a mature framework builds upon the manual pattern you’ve just created, providing helpers like
EventReaderto manage state automatically. - Rust’s Borrow Checker Explained: The pattern of cloning the event list is a direct consequence of Rust’s ownership and borrowing rules. Understanding these rules more deeply is fundamental to becoming a proficient Rust developer.
Implement Ball Bouncing Logic for Pong
Mục tiêu: Update the BallSystem to implement physics for ball bouncing. This involves reacting to paddle collision events by inverting the ball’s horizontal velocity and checking for wall collisions to invert its vertical velocity, introducing a GameArea resource to define the boundaries.
Magnificent work! You’ve successfully created a BallSystem that acts as a subscriber to your engine’s event channel. It can now “hear” every time the CollisionSystem reports a collision involving the ball, and it dutifully logs this information to the console. The communication channel is fully operational.
Now, it’s time to transform that information into action. A log message is great for debugging, but players want to see a reaction on screen. This task is all about implementing the core gameplay mechanic of Pong: making the ball bounce. We will replace the simple log! message with physics logic that inverts the ball’s velocity, bringing your game to life with dynamic interactions.
The Physics of a Bounce
A bounce, in its simplest form, is a reversal of direction along a specific axis.
- When the ball hits a vertical surface (like a paddle), its horizontal movement should reverse. We achieve this by inverting its horizontal velocity (
dx). - When the ball hits a horizontal surface (like the top or bottom wall), its vertical movement should reverse. We achieve this by inverting its vertical velocity (
dy).
Our BallSystem will be responsible for handling both of these cases.
Step 1: Handling Paddle Collisions
We will start by upgrading the logic that already detects collisions with the paddles. Instead of just logging, we will now get mutable access to the ball’s Velocity component and invert its dx value.
The core logic remains the same: we read the CollisionEvents to know when a bounce should occur.
Step 2: Handling Wall Collisions
A collision with a wall is different. There is no second entity, so no CollisionEvent will be generated. Instead, we must manually check the ball’s Position against the boundaries of the play area.
To do this cleanly, our system needs to know the dimensions of the canvas. The best practice for making global information like this available to any system is to create a Resource. Let’s create a GameArea resource.
Create the GameArea Resource
First, define the resource in src/resources.rs.
// src/resources.rs
// ... (your existing use statements and other resource structs)
// --- NEW CODE STARTS HERE ---
// This resource will hold the dimensions of our game area.
#[derive(Resource)]
pub struct GameArea {
pub width: f32,
pub height: f32,
}
// --- NEW CODE ENDS HERE ---
Note: We don’t derive Default because this resource must be initialized with specific values from the canvas.
Insert the GameArea Resource
Next, in your Game::new function within src/lib.rs, create and insert this resource so all systems can access it.
// src/lib.rs
// ... (make sure GameArea is in your `use` statements)
use crate::resources::GameArea;
impl Game {
pub fn new() -> Self {
// ... (getting canvas, context, creating world is unchanged) ...
// Insert existing resources
world.insert_resource(InputState::default());
world.insert_resource(AssetManager::default());
world.insert_resource(audio_manager); // assuming audio_manager creation
world.insert_resource(CollisionEvents::default());
// --- NEW CODE STARTS HERE ---
// Create and insert our new GameArea resource.
world.insert_resource(GameArea {
width: canvas.width() as f32,
height: canvas.height() as f32,
});
// --- NEW CODE ENDS HERE ---
// Call setup_pong
setup_pong(&mut world, canvas.width() as f32, canvas.height() as f32);
// ... (rest of the function is unchanged)
}
// ...
}
Step 3: Implementing the Full ball_system Logic
Now we have all the pieces we need to build our complete ball_system. We will combine paddle collision logic and wall collision logic into a single, cohesive system.
Replace your existing ball_system in src/lib.rs with this updated version:
// src/lib.rs
// ... (your use statements)
use crate::components::{Ball, Position, Velocity}; // Add Position and Velocity
use crate::resources::{CollisionEvents, GameArea}; // Add GameArea
// The BallSystem is now responsible for handling all ball physics and interactions.
pub fn ball_system(world: &mut World) {
// --- HIGHLIGHTED CHANGES START ---
// Get the GameArea resource. If it doesn't exist, we can't do anything.
let Some(game_area) = world.get_resource::<GameArea>() else { return };
let game_area_width = game_area.width;
let game_area_height = game_area.height;
// To avoid borrow checker issues, get a cloned list of events first.
let events_this_frame = world.get_resource::<CollisionEvents>().map_or(Vec::new(), |events| events.events.clone());
// Query for the ball's components. We need its Entity ID to check events,
// its Position for wall checks, and a mutable reference to its Velocity to change it.
let mut ball_query = world.query_filtered::<(Entity, &Position, &mut Velocity), With<Ball>>();
// We expect exactly one ball, so we use `get_single_mut`.
let Ok((ball_entity, ball_pos, mut ball_vel)) = ball_query.get_single_mut(world) else {
return;
};
// --- Part 1: Handle Paddle Collisions from Events ---
for event in events_this_frame {
// Check if the collision event involves the ball.
if event.entity_a == ball_entity || event.entity_b == ball_entity {
// It does! Invert the horizontal velocity to make it bounce.
ball_vel.dx *= -1.0;
// We only need to process one paddle collision per frame for the ball.
// Breaking here prevents bugs where hitting a corner could double-invert the velocity.
break;
}
}
// --- Part 2: Handle Wall Collisions ---
// Check for collision with the top wall.
if ball_pos.y <= 0.0 {
// Invert the vertical velocity.
ball_vel.dy *= -1.0;
}
// Check for collision with the bottom wall.
if ball_pos.y >= game_area_height {
// Invert the vertical velocity.
ball_vel.dy *= -1.0;
}
// --- HIGHLIGHTED CHANGES END ---
}
Deconstructing the New System
Your BallSystem is now a proper gameplay system! Let’s break down the new logic: * Resource and Component Queries: At the top of the system, we gather all the data we need: the GameArea dimensions, the list of CollisionEvents, and mutable access to the ball’s Position and Velocity. * Paddle Bounce Logic: The for loop iterates through the collision events. If an event involves the ball, we perform the core bounce logic: ball_vel.dx *= -1.0;. This line takes the current horizontal speed and flips its sign, perfectly reversing its direction. The break; is a small but important detail to ensure the logic only runs once per frame, even in unusual collision scenarios. * Wall Bounce Logic: The if statements check the ball’s y coordinate against the top edge (0.0) and the bottom edge (game_area_height). If it has crossed a boundary, we invert its vertical velocity with ball_vel.dy *= -1.0;.
See a Living Game!
Build and run your project. The moment of truth has arrived.
wasm-pack build --target web
Refresh your browser. You will see the ball start its movement, but now, when it hits your paddle, it will ricochet back across the screen! When it hits the top or bottom edge of the canvas, it will bounce off perfectly. You are no longer watching a movie; you are playing a game.
Next Steps
The game feels alive, but it’s not yet complete. The ball bounces off the top, bottom, and paddles, but what happens when it passes the left or right edge? Currently, nothing. It just flies off-screen forever.
This is where scoring comes in. The next task is to implement scoring logic. When the ball goes off-screen horizontally, you will increment a score, reset the ball’s position to the center, and serve it again. This will involve creating a new Score resource and adding logic to your BallSystem to handle these “out of bounds” conditions.
Further Reading
- Basic Game Physics - Bouncing: An article explaining the simple vector math behind bouncing objects.
- The Bevy Book: Resources (Revisited): You’ve now created several resources. Solidifying your understanding of this core ECS pattern is highly beneficial.
- Managing Game State: A high-level overview of concepts like “playing,” “game over,” and “main menu,” which are the next logical steps after you have a core gameplay loop.
Implement Scoring System for Pong Game
Mục tiêu: Add a scoring mechanism to a Rust and WebAssembly-based Pong game. This involves creating an ECS resource to track scores, detecting when the ball goes out of bounds, updating the score, and resetting the ball for the next round.
Incredible! Your game is no longer just a physics simulation; it’s an interactive experience. The ball now ricochets off the paddles and walls, creating the dynamic and engaging core loop of Pong. You’ve masterfully connected your collision detection system to your gameplay logic, turning abstract events into tangible reactions.
However, a game needs stakes. It needs a goal. Right now, the ball bounces forever, and there’s no way to win or lose. When the ball flies past a paddle and off the screen, it’s an anticlimax—it simply disappears into the void. This task is about giving that moment meaning by implementing a scoring system. We will detect when the ball goes out of bounds, update a score, and reset the ball for the next round, officially turning your project into a complete game with a win condition.
The Scoreboard: A Home for Global State
First, we need a place to store the score. This is a classic example of a piece of data that is unique and global to the entire game state. In our ECS architecture, the perfect tool for this job is a Resource. We will create a Score resource to keep track of each player’s points.
Step 1: Define the Score Resource
Let’s start by defining the structure for our new resource. Open src/resources.rs and add the Score struct.
// src/resources.rs
// ... (your existing use statements and other resource structs)
// --- NEW CODE STARTS HERE ---
// The Score resource holds the current score for both players.
// We derive `Default` because a score of 0-0 is a sensible starting state.
#[derive(Resource, Default)]
pub struct Score {
pub player1: u32,
pub player2: u32,
}
// --- NEW CODE ENDS HERE ---
This simple struct perfectly represents our scoreboard.
Step 2: Insert the Resource into the World
Just like our other resources, we need to initialize the Score and place it in the World when the game starts. The Default trait we derived makes this incredibly easy.
Open src/lib.rs and add the initialization logic to your Game::new function.
// src/lib.rs
// ... (make sure Score is in your `use` statements)
use crate::resources::{GameArea, Score};
impl Game {
pub fn new() -> Self {
// ... (getting canvas, context, creating world is unchanged) ...
// Insert existing resources
world.insert_resource(InputState::default());
world.insert_resource(AssetManager::default());
world.insert_resource(audio_manager); // assuming audio_manager creation
world.insert_resource(CollisionEvents::default());
world.insert_resource(GameArea { /* ... */ });
// --- NEW CODE STARTS HERE ---
// Insert our new Score resource. `default()` will create a Score { player1: 0, player2: 0 }.
world.insert_resource(Score::default());
// --- NEW CODE ENDS HERE ---
// ... (rest of the function is unchanged)
}
// ...
}
The scoreboard is now officially part of your game’s world state, ready to be read from and written to by any system.
Step 3: Upgrade the BallSystem with Scoring Logic
Now for the main event. We will enhance the BallSystem to handle the “out of bounds” condition. This system already checks the ball’s position against the top and bottom walls; we just need to add checks for the left and right sides.
When the ball goes off one of these sides, we will:
- Get mutable access to our new
Scoreresource. - Increment the appropriate player’s score.
- Log the new score to the console for immediate feedback.
- Reset the ball’s
PositionandVelocityto serve it again.
Replace your existing ball_system in src/lib.rs with this final, complete version.
// src/lib.rs
// ... (your use statements)
use crate::components::{Ball, Position, Velocity};
use crate::resources::{CollisionEvents, GameArea, Score}; // Add Score here
// The BallSystem now handles all game logic: bouncing, scoring, and resetting.
pub fn ball_system(world: &mut World) {
// --- HIGHLIGHTED CHANGES START ---
// We can't have multiple mutable borrows of `world` at once.
// So, we use a block scope `{}` to get the resources we need, which releases the
// immutable borrow on `world` before we try to get mutable access for our query.
let (game_area_width, game_area_height, events_this_frame) = {
let Some(game_area) = world.get_resource::<GameArea>() else { return };
let events = world.get_resource::<CollisionEvents>().map_or(Vec::new(), |events| events.events.clone());
(game_area.width, game_area.height, events)
};
// We need mutable access to the Score resource for the entire system.
let Some(mut score) = world.get_resource_mut::<Score>() else { return };
// Query for the ball's components.
let mut ball_query = world.query_filtered::<(Entity, &mut Position, &mut Velocity), With<Ball>>();
let Ok((ball_entity, mut ball_pos, mut ball_vel)) = ball_query.get_single_mut(world) else {
return;
};
// --- Part 1: Handle Paddle Collisions (Unchanged) ---
for event in events_this_frame {
if event.entity_a == ball_entity || event.entity_b == ball_entity {
ball_vel.dx *= -1.0;
break;
}
}
// --- Part 2: Handle Wall Bounces (Unchanged) ---
if ball_pos.y <= 0.0 || ball_pos.y >= game_area_height {
ball_vel.dy *= -1.0;
}
// --- Part 3: Handle Scoring and Resetting ---
let mut scored = false;
// Check if the ball went off the left side (Player 2 scores).
if ball_pos.x <= 0.0 {
score.player2 += 1;
log(&format!("Player 2 scores! Score is now {}-{}", score.player1, score.player2));
scored = true;
}
// Check if the ball went off the right side (Player 1 scores).
if ball_pos.x >= game_area_width {
score.player1 += 1;
log(&format!("Player 1 scores! Score is now {}-{}", score.player1, score.player2));
scored = true;
}
// If a player scored, reset the ball.
if scored {
ball_pos.x = game_area_width / 2.0;
ball_pos.y = game_area_height / 2.0;
// Simple reset: serve in the opposite direction it was last going.
ball_vel.dx *= -1.0;
// We could also add some randomness here for variety.
}
// --- HIGHLIGHTED CHANGES END ---
}
Deconstructing the Final Logic
- Borrow Scoping: We carefully manage our borrows from the
world. We get immutable resources likeGameAreain a separate scope, then get the mutableScoreresource, and finally, the mutable query result for the ball. This satisfies the Rust borrow checker. - Scoring Logic: We add two new
ifstatements that checkball_pos.xagainst the side boundaries. Inside, we directly access and mutate ourscoreresource withscore.player1 += 1;. - Console Feedback: The
log!macro provides immediate confirmation that our scoring logic is working correctly by printing the updated score. - Resetting the Ball: When a score occurs (
scored = true), we reset the ball’sPositionto the center of the screen. We also reverse its horizontalVelocityto serve it to the player who just won the point, keeping the game flowing.
A Complete Game Loop
Build and run your project one more time.
wasm-pack build --target web
Refresh your browser and play. The ball will bounce off the paddles and walls as before. But now, when you let the ball get past your paddle, you will see a score message appear in the console, and the ball will reset to the center, ready for the next point.
You have done it. You have implemented a complete, playable game loop for Pong, powered by the very engine you built from scratch.
Next Steps
This is a monumental achievement. The game is fully functional. However, the player experience can still be improved. Forcing the player to look at the developer console to see the score is not ideal.
The final task in creating our Pong demo is to build a UiSystem. This system’s job will be to read the Score resource on every frame and use the canvas 2D context’s text-drawing methods to render the score directly onto the screen, providing a complete and professional user interface.
Further Reading
- Game State Management: An article that discusses patterns for managing different states in a game, such as “Playing,” “MainMenu,” “GameOver,” which is the next logical step beyond a simple scoring resource.
- The Rust Book: Recoverable Errors with
Result: Your systems useif let Some(...)andelse { return }. Understanding Rust’sResultandOptiontypes more deeply is crucial for robust error handling. - Canvas API: Drawing Text: The MDN documentation for the methods you will use in the next task to draw the score on the screen.
Implement a UI System for Score Display in a Rust+Wasm Pong Game
Mục tiêu: Create a dedicated UiSystem in Rust to render the current score on the HTML5 canvas. This involves fetching the Score resource from the ECS world, using the CanvasRenderingContext2d to draw the text, and integrating the new system into the main game loop to provide real-time visual feedback to the player.
Absolutely phenomenal work. You have breathed life into your game engine, creating a complete, playable version of Pong with a full gameplay loop, including player control, physics, and a robust scoring mechanism. The game is functionally complete. However, the player’s experience is still missing a crucial element: visual feedback for the score. Forcing players to open the developer console is not an ideal user interface.
This final task in building our Pong demo is about closing that gap. We will create a dedicated UiSystem responsible for reading the game’s state—specifically the Score resource—and rendering it directly onto the canvas. This will provide the player with a clean, immediate, and professional-looking display of the game’s progress, marking the true completion of your first game.
The Canvas API: Your Digital Paintbrush for Text
Drawing shapes is something you’ve already mastered in the RenderingSystem. Drawing text uses a very similar set of tools from the canvas 2D context. The main steps are:
- Set the Font: We tell the context what font family, size, and style to use (e.g.,
"48px sans-serif"). - Set the Color: We set the
fillStyle, just as we did for clearing the screen. - Set the Alignment: We can tell the context how to align the text relative to the coordinates we provide (e.g.,
"center","left","right"). - Draw the Text: We call
fillText()with the string we want to draw and the(x, y)coordinates where it should be placed.
Step 1: Creating the ui_system Skeleton
Let’s start by creating our new system. Like the RenderingSystem, it needs access to both the World (to read game state) and the CanvasRenderingContext2d (to draw).
Create this new function in src/lib.rs, alongside your other systems.
// src/lib.rs
// ... (your other systems)
// --- NEW CODE STARTS HERE ---
use crate::resources::{GameArea, Score}; // Ensure these are in scope
// The UiSystem is responsible for drawing all user interface elements, like the score.
pub fn ui_system(world: &World, context: &web_sys::CanvasRenderingContext2d) {
// We will implement the drawing logic here.
}
// --- NEW CODE ENDS HERE ---
Step 2: Implementing the Drawing Logic
Now, let’s fill in the system with the logic to fetch the score and draw it. We’ll use the GameArea resource we created previously to position the text perfectly in the top-center of the screen.
// src/lib.rs
// ... (your other systems)
// The UiSystem is responsible for drawing all user interface elements, like the score.
pub fn ui_system(world: &World, context: &web_sys::CanvasRenderingContext2d) {
// --- HIGHLIGHTED CHANGES START ---
// Get the resources we need. If they don't exist, we can't draw the UI, so we return early.
let Some(score) = world.get_resource::<Score>() else { return };
let Some(game_area) = world.get_resource::<GameArea>() else { return };
// 1. Format the score into a displayable string.
let score_text = format!("{} - {}", score.player1, score.player2);
// 2. Set the font properties for our text.
// This sets the font to a 48-pixel, sans-serif font.
context.set_font("48px sans-serif");
// This sets the color of the text to white.
context.set_fill_style(&JsValue::from_str("white"));
// This will align the text horizontally to the center of the X coordinate we provide.
context.set_text_align("center");
// 3. Draw the text on the canvas.
// We use `fill_text` which draws the text filled with the current fillStyle.
// The X coordinate is the center of the screen.
// The Y coordinate is 60 pixels down from the top, giving it some padding.
// The `unwrap()` is safe here as fill_text only returns an error in rare cases
// (like if the text string is unparsable, which ours is not).
context
.fill_text(&score_text, (game_area.width / 2.0) as f64, 60.0)
.unwrap();
// --- HIGHLIGHTED CHANGES END ---
}
Step 3: Integrating the System into the Game Loop
Our final step is to call this new system from our main Game::tick loop. The UI should always be the very last thing drawn in a frame, ensuring it appears on top of all game sprites and the background.
// src/lib.rs
impl Game {
// ... (new function is unchanged)
pub fn tick(&mut self) {
// ... (clearing the screen and other housekeeping is unchanged) ...
// --- GAME LOGIC (RUN SYSTEMS) ---
player_control_system(&mut self.world);
movement_system(&mut self.world);
collision_system(&mut self.world);
ball_system(&mut self.world);
rendering_system(&self.world, &self.context);
// --- HIGHLIGHTED CHANGE ---
// We call the ui_system LAST, so it draws on top of everything else.
ui_system(&self.world, &self.context);
// --- END HIGHLIGHTED CHANGE ---
}
}
The Final Product: A Complete Game
This is it. Build and run your project one last time for this step.
wasm-pack build --target web
Refresh your browser. You will be greeted by the familiar Pong setup, but now, proudly displayed at the top of the screen, is the score: “0 - 0”. As you play, and points are scored, this text will instantly update. Let the ball go past your paddle, and you’ll see it change to “0 - 1”. The feedback is no longer hidden in a console; it’s an integral part of the player experience.
Congratulations on Building Pong!
Take a moment to appreciate this incredible achievement. You have not just hard-coded a single game; you have built a modular, extensible, and powerful 2D game engine from scratch in Rust and WebAssembly, and then used that engine to create a complete, classic game. Every bounce, every point scored, every paddle movement is powered by the systems you architected and implemented.
Next Steps
Your engine has proven its capabilities. Now, it’s time to push it further. The world of Pong is simple—it has no rotation, no projectiles, no complex entities. The upcoming Step 10: Advanced Features and a Second Demo (‘Asteroids’) will challenge you to expand your engine’s feature set. Your first task will be to add a Rotation component and modify the RenderingSystem to draw rotated sprites, a crucial feature for creating the iconic spinning asteroids and the player’s spaceship.
Further Reading
- MDN Web Docs: Drawing Text on a Canvas: The definitive guide to the text rendering capabilities of the Canvas API.
- Game UI Design Principles: A high-level look at what makes a good user interface in a game—clarity, unobtrusiveness, and providing the right information at the right time.
- The Bevy Book: UI with Bevy: For a look ahead, see how a mature engine like Bevy handles UI. It uses a dedicated, feature-rich UI framework that builds on the same ECS principles you’ve been learning.