Create the RenderingSystem Function Skeleton
Mục tiêu: Define the function skeleton for the RenderingSystem in Rust. This system is responsible for reading entity data from the ECS World and drawing it to the screen using the Canvas API. The task involves creating the function signature that accepts an immutable reference to the World and a reference to the CanvasRenderingContext2d.
With the successful integration of the MovementSystem, you’ve brought your game world to life in a profound way. The entity you created is no longer a static piece of data; its Position component is being updated on every single frame, creating a dynamic, living simulation inside the computer’s memory. However, from the user’s perspective, nothing has changed. The screen remains a solid dark grey. Our world is alive, but it is invisible.
This brings us to the next grand challenge and the very purpose of this step: to build the bridge between the abstract data in our ECS World and the concrete pixels on the screen. We need a system whose sole responsibility is to look at the state of the world and paint a picture of it. This is the RenderingSystem.
The Artist’s Logic: A System for Drawing
Just as the MovementSystem was a specialized piece of logic for handling motion, the RenderingSystem will be a specialized piece of logic for handling visuals. Its job description is simple and focused:
- Ask the
Worldfor every entity that is meant to be visible. - For each of those entities, get its position and its visual properties.
- Use the Canvas 2D API to draw it on the screen.
This system will be the artist of our engine, translating the raw data of Position and Renderable components into shapes and colors.
A key difference from our MovementSystem is the information this new system requires. The MovementSystem only needed access to the World to do its job. The RenderingSystem, however, needs two things:
- The
World, to get the component data for entities. - The
CanvasRenderingContext2d, to execute the actual drawing commands.
This means our function signature will need to accept both of these as arguments.
Creating the RenderingSystem Function Skeleton
Let’s create the function that will house our rendering logic. Just like we did for movement_system, we will define it as a standalone function in src/lib.rs. It’s good practice to group systems together, so we’ll place it right below the movement_system.
Open src/lib.rs and add the following new function:
// src/lib.rs
// ... (other use statements, log function, and movement_system are unchanged) ...
pub fn movement_system(world: &mut World) {
// ... (this function's body is unchanged)
}
// --- NEW CODE STARTS HERE ---
// The RenderingSystem needs access to both the World (to get entity data)
// and the Canvas 2D context (to perform the drawing).
//
// Note that we take an immutable reference to the world (`&World`). This is because
// our rendering system doesn't need to *change* any data; it only needs to *read*
// the Position and Renderable components. This is a great example of Rust's
// principle of least privilege, enforced by the borrow checker.
pub fn rendering_system(world: &World, context: &web_sys::CanvasRenderingContext2d) {
// The logic for querying and drawing entities will go here in the next tasks.
}
// --- NEW CODE ENDS HERE ---
pub struct Game {
// ... (Game struct is unchanged)
}
// ... (rest of the file is unchanged)
Deconstructing the Code
This is a simple addition, but the function signature is very deliberate and demonstrates important concepts:
pub fn rendering_system(...): We define a new public function, making it accessible from ourGame::tickmethod when we’re ready to call it.world: &World: The first parameter is an immutable reference (or a “shared borrow”) to theWorld. This is a crucial detail. By specifying&Worldinstead of&mut World, we are telling the Rust compiler that this function promises not to change any of the data inside theWorld. It will only read from it. This is a powerful safety feature. If we later tried to write code inside this system that modified a component, the compiler would give us an error, preventing a potential bug. Since our renderer’s job is just to read data and draw, an immutable reference is all it needs.context: &web_sys::CanvasRenderingContext2d: The second parameter is a reference to our 2D rendering context. We need this object to call its drawing methods likefill_rect()andfill(). This object is our “paintbrush,” and we must pass it to the system that will do the painting.
You have now successfully created the skeleton for our rendering logic. It’s an empty vessel, but it’s correctly designed to accept all the information it will need to do its job.
Next Steps
The RenderingSystem function exists, but it does nothing. Our immediate next task is to breathe life into it by writing an ECS query. We will ask the World to give us access to every entity that has both a Position and a Renderable component—the exact two pieces of data we need to draw something on the screen.
Further Reading
- The Bevy Book: Systems (Revisited): It’s always beneficial to reinforce the core concepts. Rereading this chapter will help solidify the pattern we are building.
- MDN Web Docs: Canvas API: This is the ultimate reference for all the drawing commands we will soon be using inside our
RenderingSystem. It’s a great page to bookmark. - Game Loop - The Rendering Phase: A high-level overview of where the rendering step fits into the overall game loop architecture.
Implement a Rendering Query in Bevy ECS
Mục tiêu: Create a query within the RenderingSystem to find all entities that have both Position and Renderable components, using immutable access to prepare for drawing.
Excellent work creating the function signature for our RenderingSystem. You’ve correctly identified that this system needs access to both the game’s World and the canvas’s drawing context. This empty function is now a perfectly prepared stage, waiting for the actors to arrive. Our current task is to write the code that finds those actors—the entities that are meant to be drawn.
Finding What to Draw: The Rendering Query
Just as our MovementSystem needed to ask the World, “Show me everything that can move,” our RenderingSystem must ask, “Show me everything that is visible.” In the language of ECS, this means we need to find every entity that has both a Position component (so we know where to draw it) and a Renderable component (so we know what to draw).
This is a job for a query. A query is our way of telling bevy_ecs the exact “shape” of the data we’re interested in. The World then uses its powerful internal machinery to efficiently find all entities that match this shape.
A crucial aspect of crafting a query is specifying the type of access we need. For the MovementSystem, we needed to change the Position, so we asked for mutable access (&mut Position). For the RenderingSystem, our job is simply to read the position and visual properties. We don’t need to change anything. Therefore, we should request immutable access (&Position, &Renderable). This is a key principle of writing safe and efficient Rust code: always request the minimum level of permission necessary. By asking for read-only access, we allow bevy_ecs to perform more optimizations and we prevent ourselves from accidentally introducing bugs by changing data in a system that shouldn’t.
Let’s now add this query to our rendering_system function in src/lib.rs.
// src/lib.rs
// ... (other use statements, log function, and movement_system are unchanged) ...
pub fn movement_system(world: &mut World) {
// ...
}
pub fn rendering_system(world: &World, context: &web_sys::CanvasRenderingContext2d) {
// --- NEW CODE STARTS HERE ---
// This is the query. It asks the `World` for read-only access to the
// `Position` and `Renderable` components for every entity that has them.
// 1. `&Position`: We need to read the entity's location.
// 2. `&Renderable`: We need to read the entity's shape and color.
// Since we are only reading data, we use immutable references (`&`), which is
// safer and allows for more parallelism in more complex scenarios.
let mut query = world.query::<(&Position, &Renderable)>();
// --- NEW CODE ENDS HERE ---
}
// ... (rest of the file is unchanged) ...
Deconstructing the Code
Let’s break down this single, powerful line of code:
let mut query = ...: We declare a new variable namedquery. We mark it asmutbecause, in the next step, the process of iterating through the query results will modify the query’s internal state.world.query::<...>(): This is the method we call on ourWorldinstance to create a new query. The::<>part is Rust’s “turbofish” syntax, used to provide generic type parameters to a function. In this case, it’s how we tell thequerymethod what components we are looking for.(&Position, &Renderable): This is the type parameter—the heart of our query. It’s a tuple that defines the component “shape” we want to retrieve for each matching entity.&Position: “For each entity, give me a read-only reference to itsPositioncomponent.”&Renderable: “And also give me a read-only reference to itsRenderablecomponent.”
The query variable now holds a prepared query object. It hasn’t actually fetched any data from the World yet. It’s a highly optimized plan for how to fetch that data when we’re ready. This lazy, two-step process (prepare, then execute) is a key feature of bevy_ecs’s performance.
You have now successfully defined the exact request our RenderingSystem will make to the game world.
Next Steps
The query is prepared and ready. The next logical step is to execute it. In the upcoming task, we will iterate over this query object. This will give us a loop that runs once for every visible entity, and inside that loop, we will have direct access to the position and renderable data for that specific entity, setting the stage for the actual drawing commands.
Further Reading
Understanding queries is fundamental to mastering ECS. These resources will provide a deeper context for the code you’ve just written.
- The Bevy Book: Queries: The official, in-depth guide to the
bevy_ecsquery system. This is the most important resource for this topic. - The Rust Book: References and Borrowing: A foundational chapter of the Rust language. Understanding the difference between
&(immutable borrow) and&mut(mutable borrow) is critical for writing correct and safe ECS systems. - API Documentation for
World::query: For the truly curious, this is the technical API documentation for the very method you just called.
Execute a Read-Only ECS Query in Bevy
Mục tiêu: Add a for loop to the rendering\_system function to iterate over the results of a Bevy ECS query. This involves using the query.iter(world) method to get read-only access to the Position and Renderable components of each matching entity.
You’ve successfully prepared the RenderingSystem by creating a query that precisely describes the data it needs: every entity that has a Position and a Renderable component. This query object is like a meticulously crafted plan of action. It knows what to look for, but it hasn’t actually gone out into the World to find the data yet. Our current task is to execute this plan, to iterate over the results of the query, and to finally get our hands on the component data for each visible entity.
Executing the Plan: Iterating Over the Query
In bevy_ecs, you execute a query by calling an iterator method on it. This method tells the World to go and find all the matching entities and then provides you with a standard Rust iterator that you can loop over. This is the moment where the abstract query becomes a concrete stream of data.
You may recall that in our MovementSystem, we used the query.iter_mut(world) method. The mut in iter_mut was critical because our query asked for mutable access (&mut Position) to change the entity’s location.
Our RenderingSystem, however, is different. It’s an observer. It only needs to read data, not write it. Because our query asks for immutable references (&Position, &Renderable), the correct method to use is query.iter(world). This method provides an iterator over read-only references to the components. This distinction is a cornerstone of Rust’s safety guarantees, preventing you from accidentally modifying data in a system that is only supposed to be reading it.
Let’s now add the for loop to our rendering_system to process the results of our query.
Update your rendering_system function in src/lib.rs with the following loop:
// src/lib.rs
// ... (other code is unchanged) ...
pub fn rendering_system(world: &World, context: &web_sys::CanvasRenderingContext2d) {
// This query was created in the previous task. It defines what data we need.
let mut query = world.query::<(&Position, &Renderable)>();
// --- NEW CODE STARTS HERE ---
// We execute the query using `.iter(world)`. This gives us an iterator that
// will visit every entity that matches the query's criteria. Since our query
// asks for read-only references, we use `iter` instead of `iter_mut`.
for (position, renderable) in query.iter(world) {
// Inside this loop, we have access to the component data for one entity
// that is ready to be rendered.
// `position` is a `&Position` - a read-only reference to the entity's Position.
// `renderable` is a `&Renderable` - a read-only reference to its Renderable.
// In the next tasks, this is where we will use the `context` to draw
// the `renderable.shape` at the coordinates from `position`.
}
// --- NEW CODE ENDS HERE ---
}
// ... (rest of the file is unchanged) ...
Deconstructing the Code
Let’s break down the new for loop. This is a common and powerful pattern in bevy_ecs.
for (position, renderable) in ...: This is a standard Rustforloop that consumes an iterator. A key feature of Rust is its ability to destructure tuples directly in the loop declaration. On each iteration, the iterator yields a tuple(&Position, &Renderable)for a single entity, and we immediately break it apart into two separate variables,positionandrenderable.query.iter(world): This is the method call that executes the query. It returns an iterator. Each item produced by this iterator corresponds to one entity that has both aPositionand aRenderablecomponent.positionandrenderablevariables: Inside the loop’s body, these variables hold the data for the current entity being processed.positionhas the type&Position. It’s an immutable reference to that entity’sPositioncomponent. We can access its fields likeposition.xandposition.y.renderablehas the type&Renderable. It’s an immutable reference to that entity’sRenderablecomponent. We can access its fields likerenderable.shapeandrenderable.color.
You have now successfully created a loop that will run once for every visible object in your game world. For now, it doesn’t do anything with the data it receives, but the connection between the system’s logic and the ECS data is now firmly established.
Next Steps
The stage is perfectly set. Inside our new for loop, we hold the two most important pieces of information for drawing an object: where it is (position) and what it looks like (renderable). In the very next task, we will finally use this data. You will write the code to access the fields of these components and use them to set the drawing location and style on the canvas context, preparing to make our entity visible at last.
Further Reading
To deepen your understanding of the concepts at play here, these resources are highly recommended.
- The Bevy Book: Queries (with Iteration): The official guide provides excellent examples of how to iterate over queries, showing both
iter()anditer_mut()in action. - The Rust Book:
forLoops: A foundational explanation of howforloops work with iterators in Rust. - Rust by Example: Destructuring Tuples: A practical guide on how destructuring works, which is the feature that allows
(position, renderable)in ourforloop.
Set Canvas Fill Style from Rust Component Data
Mục tiêu: Within the rendering system loop, retrieve the color string from each entity’s Renderable component and use it to set the fillStyle property on the CanvasRenderingContext2d. This requires converting the Rust String to a JsValue to interface with the web-sys API.
You’ve made excellent progress! In the previous task, you successfully set up a loop that iterates over every visible entity in your game world. Inside this loop, you now have direct, read-only access to the position and renderable component data for each entity. You’re holding the two essential pieces of information needed to draw something: where it is and what it looks like.
Our current task is to begin translating this abstract data into concrete instructions for the browser’s Canvas API. Before we can draw a shape, we need to tell the canvas what “paint” to use. This means taking the color information from our Renderable component and applying it to the canvas’s rendering context.
Setting the Stage for Drawing
The CanvasRenderingContext2d object works like a state machine. You configure its properties—such as color, line width, or font—and then all subsequent drawing operations will use those settings. The most fundamental of these properties is the fillStyle, which determines the color or pattern used to fill shapes.
Our Renderable component conveniently stores the color as a String (e.g., "#FFFFFF"), which is exactly the format the Canvas API understands. Our job is to bridge the gap between our Rust String and the fillStyle property, which the web-sys API expects as a JsValue.
Let’s modify the body of our for loop in the rendering_system to set this crucial property.
// src/lib.rs
// ... (previous code including movement_system is unchanged) ...
pub fn rendering_system(world: &World, context: &web_sys::CanvasRenderingContext2d) {
// This query was created in the previous task.
let mut query = world.query::<(&Position, &Renderable)>();
// We iterate over each entity that matches our query.
for (position, renderable) in query.iter(world) {
// --- NEW CODE STARTS HERE ---
// Set the fill style for the upcoming shape based on the Renderable component.
// The `renderable.color` is a Rust `String`. The `set_fill_style` method,
// however, expects a `&JsValue`. The `JsValue::from_str` function provides
// the perfect bridge, converting our Rust string slice into a value that
// JavaScript understands. The `&` on `&renderable.color` allows `from_str`
// to borrow the string data without taking ownership.
context.set_fill_style(&JsValue::from_str(&renderable.color));
// --- NEW CODE ENDS HERE ---
}
}
// ... (rest of the file is unchanged) ...
Deconstructing the Code
This single new line of code is doing a lot of important work in the communication between Rust and JavaScript. Let’s break it down:
context.set_fill_style(...): This is theweb-sysbinding for the JavaScript methodCanvasRenderingContext2D.fillStyle = .... We are calling this method on thecontextobject that was passed into our system. After this line executes, any subsequent fill operations (likefillRect) will use the color we’ve just specified.&renderable.color: Here, we are accessing thecolorfield from therenderablecomponent of the current entity. We take an immutable reference (&) to it. Because thefrom_strfunction expects a string slice (&str) andrenderable.coloris aString, Rust’s Deref Coercion automatically converts our&Stringinto a&strfor us. This is a powerful feature that makes Rust code more ergonomic.JsValue::from_str(...): This is the key translation step provided bywasm-bindgen. It takes a Rust string slice (&str) and creates an instance ofJsValuethat represents a JavaScript string. TheJsValuetype is a universal container that can represent any JavaScript value (string, number, object, boolean, etc.), making it essential for interoperability.- The Outer
&: The full expression is&JsValue::from_str(...). Theset_fill_stylemethod takes a reference to aJsValue(&JsValue), so we create theJsValueon the fly and immediately pass a reference to it into the function.
With this code, for every entity in the loop, we are now correctly configuring the canvas context with that entity’s specific color. If we have a red entity and a blue entity, this code ensures the “paintbrush” is dipped in red paint before drawing the first, and blue paint before drawing the second.
Next Steps
The canvas context is now primed and ready. On each iteration of the loop, the color is set. The final piece of the puzzle is to actually perform the drawing. In the next task, you will use the position data (where to draw) and the renderable.shape data (what to draw) to call the appropriate canvas drawing methods, such as fill_rect or arc, finally making your entity visible on the screen.
Further Reading
- MDN Web Docs:
CanvasRenderingContext2D.fillStyle: The definitive documentation for the canvas property you are now controlling. It shows the different kinds of string values it accepts. - The
wasm-bindgenBook:JsValue: A guide to the universal type for interacting with JavaScript values. Understanding this is key to working with browser APIs. - The Rust Book: Deref Coercion: An explanation of the language feature that allows
&Stringto be used where&stris expected. It’s a “magic” feature that’s worth understanding.
Implement Shape Drawing Logic in Rendering System
Mục tiêu: Use a Rust match statement within the RenderingSystem to handle different shape enums (Rectangle, Circle) and call the appropriate web-sys Canvas API functions to draw them on the screen.
The final preparations are complete. In our RenderingSystem’s loop, you’ve successfully instructed the “artist” (our canvas context) which color of paint to use for each entity by setting its fillStyle. The brush is dipped, the color is chosen. All that remains is the final, glorious act of creation: touching the brush to the canvas and drawing the shape.
This is the moment where the abstract data within our ECS World—the Position and the Shape from the Renderable component—is finally translated into visible pixels.
Handling Different Shapes with match
Our Renderable component is designed to be flexible. Its shape field is a Shape enum, which can be either a Rectangle or a Circle. How do we write code that handles these different possibilities? This is a perfect scenario to use one of Rust’s most powerful and beloved features: the match statement.
A match statement is like a switch statement on steroids. It allows you to compare a value against a series of patterns and execute code based on which pattern matches. Crucially, match is exhaustive. This means the Rust compiler guarantees that you have written code to handle every single possible variant of your enum. If you add a new shape to your Shape enum later (like Triangle), the compiler will give you an error until you add a new arm to your match statement to handle drawing it. This is an incredible safety feature that prevents bugs as your engine grows.
We will use match on the renderable.shape to decide which specific canvas drawing command to use.
Implementing the Drawing Logic
Let’s expand our rendering_system’s loop to include the final drawing commands. We’ll add a match block that inspects the shape and calls either fill_rect for a rectangle or a combination of begin_path, arc, and fill for a circle.
// src/lib.rs
// ... (previous code including movement_system is unchanged) ...
use std::f64; // Import the f64 module to get access to PI
pub fn rendering_system(world: &World, context: &web_sys::CanvasRenderingContext2d) {
let mut query = world.query::<(&Position, &Renderable)>();
for (position, renderable) in query.iter(world) {
// This line was from the previous task, setting the color.
context.set_fill_style(&JsValue::from_str(&renderable.color));
// --- NEW CODE STARTS HERE ---
// We use a `match` statement to run different drawing code depending on the
// shape variant contained in the `Renderable` component.
match &renderable.shape {
// This is a "match arm". If the shape is a Rectangle, this code runs.
// The pattern `Shape::Rectangle { width, height }` not only checks the
// variant, but it also "destructures" it, giving us direct access to
// the `width` and `height` values inside it.
Shape::Rectangle { width, height } => {
// IMPORTANT: The `position` component represents the CENTER of the shape.
// However, the `fill_rect` method draws from the TOP-LEFT corner.
// We must calculate the top-left coordinate to draw correctly.
let top_left_x = position.x - width / 2.0;
let top_left_y = position.y - height / 2.0;
// Call the `fill_rect` method on the context.
// The Canvas API expects all coordinate and dimension values to be `f64`,
// so we must cast our `f32` values from the components using `as f64`.
context.fill_rect(
top_left_x as f64,
top_left_y as f64,
*width as f64,
*height as f64,
);
}
// This is the second match arm for the Circle variant.
Shape::Circle { radius } => {
// Drawing a circle (or any custom path) is a multi-step process.
// 1. Begin a new path. This clears any previous path constructs.
context.begin_path();
// 2. Define the arc (a full circle).
// `arc` takes: center x, center y, radius, start angle, end angle.
// The angles are in radians. 0 is the 3 o'clock position.
// `2.0 * PI` creates a full 360-degree circle.
context
.arc(
position.x as f64,
position.y as f64,
*radius as f64,
0.0,
2.0 * f64::consts::PI, // Using PI from Rust's standard library
)
.unwrap(); // `arc` can fail, so web-sys returns a Result
// 3. Fill the current path with the active `fillStyle`.
context.fill();
}
}
// --- NEW CODE ENDS HERE ---
}
}
// ... (rest of the file is unchanged) ...
Deconstructing the Code
This new match block is the visual heart of our engine.
match &renderable.shape: We match on a reference to the shape. This is more efficient as it avoids moving or copying theShapedata.Shape::Rectangle { width, height } => { ... }: This is a destructuring pattern. It simultaneously checks if the shape is aRectangleand extracts thewidthandheightvalues into local variables for us to use.- Coordinate Calculation: The logic
position.x - width / 2.0is a small but critical detail in many 2D engines. Storing an object’s position by its center point makes physics and rotation calculations much simpler later on. We must remember to convert this center point to the top-left corner thatfill_rectexpects. - The Circle Drawing Process: The three calls for drawing a circle—
begin_path,arc, andfill—demonstrate the stateful nature of the Canvas API. You first define a path, then you perform an operation on that path (like filling or stroking it). 2.0 * f64::consts::PI: To draw a full circle, we need to specify an arc that goes through 2π radians (360 degrees). We use the high-precision constantPIfrom Rust’s standard library for this. Note that we needf64::consts::PIbecause thearcmethod’s parameters are allf64.- Type Casting with
as f64: Theweb-sysAPI is strictly typed to match the JavaScript APIs. Since most Canvas methods expect a JavaScriptNumber, which is represented as a 64-bit float in Rust (f64), we must explicitly cast all ourf32component data tof64before passing it to these methods.
If you were to build and run the project now, you would still see only the dark grey background. Why? Because while our RenderingSystem is now a fully capable artist, it’s still waiting backstage. It has never been called!
Next Steps
You have created a complete, working RenderingSystem. It knows how to query the world for visible entities and draw each one according to its specific shape, color, and position. This is a monumental achievement.
The final task to complete this step is to bring this system onto the stage. You will now integrate the execution of the RenderingSystem into your main game loop by calling it from the Game::tick method, right after the screen is cleared. This is the moment where all the pieces will finally come together, and you will see your entity moving on the screen for the first time.
Further Reading
- The Rust Book:
matchControl Flow Construct: The definitive guide to Rust’s powerfulmatchstatement. - MDN Web Docs for Canvas API: These pages are your ultimate reference for the drawing methods you’ve just used.
- Rust Standard Library:
f64::consts::PI: The official documentation for the mathematical constants available in Rust.
Integrate the Rendering System into the Game Loop
Mục tiêu: Call the rendering system from the main Game::tick method to draw the game world’s state to the canvas, completing the Clear-Update-Render game loop.
You have reached a truly pivotal moment in the creation of your game engine. In the preceding tasks, you have meticulously constructed a complete RenderingSystem. It’s a masterpiece of logic, capable of querying the game world for any visible entity and translating its abstract component data—its Position, Shape, and Color—into concrete drawing commands. This system is a fully equipped artist, standing ready with its palette and brushes. The only thing missing is the invitation to paint.
Our final task in this step is to extend that invitation. We will bring the RenderingSystem onto the main stage of our engine—the Game::tick method—and command it to draw the scene. This is the moment where all the disparate pieces you’ve built—the components, the entity, the movement logic, and now the rendering logic—will finally converge to create a visible, moving picture.
The Order of Operations: A Painter’s Logic
In game development, the sequence of actions within a single frame is critical. Think of it like an artist painting on a canvas. You wouldn’t paint the subject first and then try to paint the background behind it. The process is logical and sequential:
- Prepare the Canvas: You start with a clean slate. In our engine, this is the
clear_rectcall that wipes the screen with a solid color. - Decide on the New Composition: You figure out where everything should go in the new painting. This is our
movement_system, which calculates the new positions for all moving objects. - Paint the Scene: Once the new positions are known, you apply paint to the canvas. This is the job of our
rendering_system, which draws every object at its newly calculated position.
Executing these steps in this specific order—Clear -> Update State -> Render—is fundamental to creating a smooth, flicker-free animation. Our task is to add the “Render” step to our tick method, placing it correctly after the state has been updated.
Integrating the Rendering System
Let’s modify the Game::tick method in src/lib.rs. The change is wonderfully simple: a single line of code that calls our new system.
// src/lib.rs
// ... (all code before the `impl Game` block is unchanged) ...
impl Game {
pub fn new() -> Self {
// ... (this function remains unchanged)
let window = web_sys::window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
let canvas = document
.get_element_by_id("game-canvas")
.expect("document should have a canvas element");
let canvas: web_sys::HtmlCanvasElement = canvas
.dyn_into::<web_sys::HtmlCanvasElement>()
.map_err(|_| (()))
.unwrap();
let context = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.unwrap();
let mut world = World::new();
world.spawn((
Position {
x: (canvas.width() / 2) as f32,
y: (canvas.height() / 2) as f32,
},
Velocity { dx: 150.0, dy: 150.0 }, // Note: To make movement visible, let's slow it down a bit
// Velocity { dx: 2.0, dy: 2.0 }, // A slower velocity for better viewing
Renderable {
shape: Shape::Rectangle {
width: 50.0,
height: 50.0,
},
color: String::from("#FFFFFF"),
},
));
log(&format!(
"Spawned our first entity! Total entities in world: {}",
world.iter_entities().count()
));
Self {
context,
width: canvas.width(),
height: canvas.height(),
world,
}
}
pub fn tick(&mut self) {
// 1. Prepare the Canvas (Clear the screen)
self.context.set_fill_style(&JsValue::from_str("#222222"));
self.context.fill_rect(
0.0,
0.0,
self.width as f64,
self.height as f64,
);
// 2. Update Game State (Run the movement system)
movement_system(&mut self.world);
// --- NEW CODE STARTS HERE ---
// 3. Paint the Scene (Run the rendering system)
// We pass an immutable reference to the world and a reference to the context.
// This provides the rendering system with everything it needs to draw the
// current state of the game world to the screen.
rendering_system(&self.world, &self.context);
// --- NEW CODE ENDS HERE ---
}
}
// ... (the run function is unchanged) ...
(As a quick note, I noticed the initial velocity of 150.0 is per-frame, which will be extremely fast! You might want to temporarily change it in Game::new to something like Velocity { dx: 2.0, dy: 2.0 } to better observe the motion when you run it.)
Deconstructing the Code
Let’s look closely at the single line you added, as it elegantly ties together multiple core concepts of our engine:
rendering_system(&self.world, &self.context);: This is the function call that executes all the drawing logic we’ve built.&self.world: We pass an immutable reference to our ECSWorld. This perfectly matches the function signature ofrendering_system, which expects a&World. This is Rust’s borrow checker in action, enforcing our design decision that the rendering logic should only read from the game state, not change it.&self.context: We pass a reference to theCanvasRenderingContext2d. This provides therendering_systemwith the “paintbrush” it needs to perform its drawing commands on the canvas.
Build and Verify: The Grand Unveiling
This is the moment of truth. All your architectural work is about to pay off in a very visual way.
- In your terminal, rebuild the project:
bash wasm-pack build --target web - Ensure your local web server is running (
python3 -m http.server). - Refresh your browser at
http://localhost:8000.
Behold! You should now see a white square in the middle of your dark grey canvas, moving smoothly and diagonally towards the bottom-right corner of the screen. You are witnessing the complete data flow of your engine: the tick method calls the movement_system, which updates the Position data in the World, which is then read by the rendering_system to draw the square at its new location, 60 times every second.
Next Steps
Congratulations on completing this monumental step! You have successfully built a rendering pipeline on top of a working ECS architecture. Your engine is no longer just an invisible simulation; it is a visual, dynamic world.
Our entity currently moves according to its pre-programmed Velocity. The next evolution for our engine is to introduce interactivity. We need to allow the player to influence the game world. This leads us directly to the next major phase of our project, Step 5: Implementing a Robust Input Handler. In the upcoming tasks, you will learn how to listen for keyboard events from the browser and use that input to change the components of your entities, giving the player control.
Further Reading
To solidify your understanding of the game loop structure and Rust’s method syntax, these resources are excellent.
- Game Loop Structure: A great high-level overview of the different phases within a typical game loop.
- The Painter’s Algorithm: A simple concept that describes rendering objects from back to front, which is related to why we clear the screen first.
- The Rust Book: Method Syntax: A refresher on how methods are defined and called in Rust using
.and theselfkeyword, which is central to ourGame::tickfunction.