Integrate Bevy ECS for Game Architecture
Mục tiêu: Introduce the Entity-Component-System (ECS) architectural pattern and add the bevy_ecs crate as a dependency to establish a flexible and performant foundation for the game engine.
Congratulations on completing the foundational second step! You now have a live, beating heart for your engine—a game loop that faithfully clears the canvas every frame, ready for you to draw upon it. This is a massive accomplishment and the bedrock upon which everything else will be built.
However, before we start filling that canvas with spaceships and paddles, we need to think about a critical, non-visual aspect of our engine: its architecture. How will we represent and manage all the different objects in our game world?
The Challenge of Growing Complexity
Right now, all of our engine’s state is contained within the Game struct. This is fine for holding a rendering context and canvas dimensions, but imagine building a full game like Asteroids. We would need to add fields for the player’s ship, a list of asteroids, a list of bullets, the current score, the number of lives, and so on. The Game struct would quickly become a monolithic, unwieldy “god object” that knows about and manages everything. This approach leads to tightly coupled code that is difficult to understand, modify, and extend.
A common approach in software development is Object-Oriented Programming (OOP), where you might create a GameObject base class and have Player, Asteroid, and Bullet classes inherit from it. This can also become problematic, leading to deep, rigid inheritance hierarchies (the “diamond problem”) and forcing objects to inherit data and behavior they don’t need. For example, does a static explosion effect need velocity and health fields? Traditional OOP can make these kinds of flexible compositions awkward.
A Better Way: The Entity-Component-System (ECS) Pattern
Modern game development, particularly in performance-focused languages like Rust, has embraced a powerful architectural pattern that solves these problems: Entity-Component-System (ECS). ECS favors composition over inheritance and separates data from logic, leading to a flexible, performant, and highly modular design.
Let’s break down its three core concepts:
- Entity: An entity is not an object; it’s just a unique identifier, essentially just a number. It represents a single “thing” in your game world (the player, an asteroid, a bullet, a sound effect) but contains no data or logic itself. It’s a simple ID that we can attach components to.
-
Component: A component is a piece of raw data. It is a simple
structthat contains no logic or methods. Components represent a single aspect or property of an entity. For example:Position { x: f32, y: f32 }Velocity { dx: f32, dy: f32 }Renderable { color: String, shape: Shape }PlayerControlled(a “tag” component that contains no data) An entity is defined by the collection of components attached to it. A moving, visible object is an entity that hasPosition,Velocity, andRenderablecomponents. A static, invisible boundary might only have aPositionand aCollidercomponent.
-
System: A system is pure logic. It is a function that runs on every entity that has a specific set of components. For example:
- A
MovementSystemwould query the ECS world for all entities that have both aPositionand aVelocitycomponent. It would then iterate over them and update eachPositionbased on its correspondingVelocity. - A
RenderingSystemwould query for all entities withPositionandRenderableand draw them to the screen.
- A
This separation is incredibly powerful. Logic and data are completely decoupled. To add a new feature, you simply define a new component and a new system to operate on it, without having to change any existing code. This design also has significant performance benefits because it encourages a data-oriented approach where all components of the same type are stored together in memory, which is extremely friendly to modern CPU caches.
Choosing and Adding an ECS Crate
Implementing an efficient ECS framework from scratch is a complex project in itself. Fortunately, the Rust ecosystem has several excellent, battle-tested ECS libraries (crates). While popular options like specs and legion exist, we will use bevy_ecs.
bevy_ecs is the core ECS implementation from the wildly popular Bevy game engine. It is widely considered to be one of the fastest, most ergonomic, and most powerful ECS libraries available today. Its modern design will serve as a fantastic foundation for our engine.
Our first task in implementing this architecture is to add bevy_ecs as a dependency to our project. Open your Cargo.toml file and add the following line to the [dependencies] section.
[dependencies]
wasm-bindgen = "0.2.84"
js-sys = "0.3.64"
bevy_ecs = "0.13.2"
# Add web-sys with features for the APIs we need.
# ... (rest of your dependencies)
By adding this line, you are telling Cargo, Rust’s package manager, that your project now depends on version 0.13.2 of the bevy_ecs crate. The next time you build your project, Cargo will automatically download the bevy_ecs source code (and any dependencies it has), compile it, and link it into your final WebAssembly module, making all of its powerful features available to your Rust code.
You’ve now officially chosen your architectural foundation and taken the first concrete step to integrate it. This single line in your configuration file unlocks a world of flexible and performant game development patterns.
Next Steps
With the bevy_ecs crate added to our project, our next task is to start defining the “data” part of our new architecture. We will create our very first components as simple Rust structs: Position and Velocity. These will be the fundamental building blocks that describe the state of objects in our game world.
Further Reading
To gain a deeper understanding of the concepts we’ve just introduced, these resources are invaluable.
bevy_ecson crates.io: The official registry page for the crate, where you can find metadata and links to its documentation and repository.- The Bevy Book: Entity Component System: The official guide to using
bevy_ecs. This will be an essential reference as we build our engine. - A Deep Dive into Data-Oriented Design: An excellent article explaining the “why” behind patterns like ECS and how they leverage modern hardware for better performance.
- Entity Component System (Wikipedia): A high-level overview of the architectural pattern and its history.
Define Position and Velocity ECS Components
Mục tiêu: Create a components.rs module and define the Position and Velocity structs. Use the #[derive(Component)] macro to register them with the bevy\_ecs framework.
Excellent! By adding bevy_ecs to your Cargo.toml, you’ve equipped our project with a powerful, world-class Entity-Component-System framework. The library is now linked and ready to use, but right now, it’s an empty container. It provides the structure for managing game objects, but it has no knowledge of the specific kinds of data that will define our game. Our next logical step is to teach our new ECS framework about the fundamental properties of objects in our 2D world.
This is where we define our first Components.
From Abstract Objects to Concrete Data
In an ECS architecture, we stop thinking about what an object is (e.g., a “Player” object) and start thinking about what an object has. What properties does a thing need to exist and move in our game world? The two most fundamental properties are a location and a direction of movement. We will represent these as two distinct pieces of data: a Position component and a Velocity component.
A component in bevy_ecs (and most ECS libraries) is nothing more than a simple Rust struct. It’s a “Plain Old Data” (POD) type, meaning it contains data fields but no methods or logic. This strict separation of data (Components) from logic (Systems) is the cornerstone of the ECS pattern.
To keep our project organized as it grows, we will create a dedicated module for all of our components.
Creating the Components Module
First, in your src directory, create a new file named components.rs. This file will be the home for all the component structs we define.
Next, we need to tell our main library file, src/lib.rs, that this new file exists and is part of our project. Open src/lib.rs and add the following lines near the top.
// src/lib.rs
mod components;
use components::*;
use bevy_ecs::prelude::*;
//... (the rest of your use statements)
The mod components; line declares a new module named components, which corresponds to the components.rs file. The use components::*; line then brings everything we declare as pub (public) inside that module into the current scope, making our components easy to use throughout lib.rs. We also add use bevy_ecs::prelude::*; to bring in the necessary traits and macros from bevy_ecs, most importantly the Component derive macro.
Defining the Position and Velocity Components
Now, open your new, empty src/components.rs file and add the following code:
// src/components.rs
// We bring the `Component` derive macro into scope from the bevy_ecs prelude.
use bevy_ecs::prelude::Component;
// The `#[derive(Component)]` macro is the magic that tells bevy_ecs that this
// struct can be used as a component. Bevy's macro will generate the necessary
// boilerplate code to allow the World to store, query, and manage this struct
// efficiently. Without this, the ECS World would not recognize it.
#[derive(Component)]
pub struct Position {
// We use `f32` (a 32-bit floating-point number) for our coordinates.
// This allows for smooth, sub-pixel movement, which is essential for
// fluid game animations. Using integers would result in jerky movement.
pub x: f32,
pub y: f32,
}
#[derive(Component)]
pub struct Velocity {
// 'dx' and 'dy' stand for "delta x" and "delta y". They represent the
// change in position that should be applied to an entity in each unit of
// time (e.g., per frame or per second). A positive `dx` moves the entity
// to the right, and a positive `dy` moves it down (in a typical 2D canvas).
pub dx: f32,
pub dy: f32,
}
Deconstructing the Code
use bevy_ecs::prelude::Component;: Thebevy_ecsprelude conveniently exports the most common items you’ll need. Here, we’re specifically importing theComponenttrait, which the derive macro requires.#[derive(Component)]: This is a procedural derive macro. It’s a powerful Rust feature that allows a library likebevy_ecsto automatically generate code for your structs at compile time. By adding this attribute, you are instructingbevy_ecsto implement all the necessary internal traits and functions on yourPositionandVelocitystructs, enabling them to be stored efficiently in the ECSWorld. Any struct you want to use as a component must have this derive macro.pub struct ...: We declare our structs aspub(public) so that they can be accessed from other modules, namelylib.rs.pub x: f32, pub y: f32: The fields within the structs are also marked aspubso that our systems can read and write to them. We usef32(a single-precision float) because game physics and rendering calculations almost always involve fractional numbers for smooth motion.dxanddy: In theVelocitystruct, we use the common naming conventiondxanddy. Thedis short for “delta,” a mathematical symbol representing change. So,dxis the “change in x.”
You have now successfully defined the fundamental data building blocks for your game objects. We haven’t created any objects yet, but we’ve defined the types of data that will give them properties. An entity with a Position component has a location. An entity with both Position and Velocity components has a location and the ability to move.
Next Steps
We’ve defined components for an entity’s position and movement, but we can’t see anything yet. To make our entities visible, we need to give them visual properties. In the next task, you will define a Renderable component that will hold information about how an entity should be drawn on the screen, such as its shape and color.
Further Reading
- The Bevy Book: Components: The official guide to defining and using components in
bevy_ecs. This will be an essential resource. - The Rust Book: Defining and Instantiating Structs: A foundational chapter on creating your own data types with
struct. - The Rust Book: Procedural Macros: For a deeper dive into how
#[derive(Component)]works under the hood. - The Rust Book: Modules: A guide to organizing your code into separate files and modules, just as we did with
components.rs.
Define a Renderable Component for Game Visuals
Mục tiêu: Create a Renderable component in Rust to define how game entities are drawn on screen. This involves implementing a Shape enum for geometric primitives and a Renderable struct to hold the shape and color information, continuing the development of an ECS-based game.
Building on our last task, you have successfully defined the core data components that give our game objects a presence in the world: Position and Velocity. An entity with these components has a location and knows how to move. This is the foundation of our game’s physics. But there’s a missing piece: we can’t see them! An object can exist and move around in memory, but without visual properties, it’s just a ghost.
Our current task is to bridge this gap by defining what our entities look like. We will create a Renderable component, a dedicated piece of data that describes how an entity should be drawn on the screen. This continues our ECS philosophy of composing entities from granular pieces of data. By keeping rendering information separate, we gain incredible flexibility. We can have entities that are purely logical (like an invisible trigger zone that only has a Position and a Collider) and entities that are purely visual (like a background effect that has a Position and a Renderable but no Velocity).
Designing the Visuals: Shape and Color
A Renderable component needs to answer two basic questions: “What shape is it?” and “What color is it?”.
For the shape, we can represent the different geometric primitives we might want to draw using a Rust enum. An enum (short for enumeration) is a type that can be one of several possible variants. This is a perfect fit for representing a choice between, for example, a rectangle or a circle.
For the color, the simplest and most direct approach for now is to use a String. This allows us to store CSS color values (e.g., "#FF5733", "rgb(255, 87, 51)", or "orange") directly, which we can pass straight to the Canvas 2D API when it’s time to draw.
Let’s add this new component and its supporting Shape enum to our src/components.rs file.
Implementing the Renderable Component
Open your src/components.rs file and add the new code. The file should now contain the Shape enum and the Renderable component struct, in addition to your existing Position and Velocity structs.
// src/components.rs
// We bring the `Component` derive macro into scope from the bevy_ecs prelude.
use bevy_ecs::prelude::Component;
#[derive(Component)]
pub struct Position {
pub x: f32,
pub y: f32,
}
#[derive(Component)]
pub struct Velocity {
pub dx: f32,
pub dy: f32,
}
// --- NEW CODE STARTS HERE ---
// An enum to represent the different shapes we can render.
// We `derive(Clone)` to make it easy to copy shape data. This is often useful
// for components that might be duplicated or used in multiple places.
#[derive(Clone)]
pub enum Shape {
// A rectangle, defined by its width and height.
Rectangle { width: f32, height: f32 },
// A circle, defined by its radius.
Circle { radius: f32 },
}
// The `Renderable` component contains all the information needed for our
// rendering system to draw an entity.
#[derive(Component)]
pub struct Renderable {
// The geometric shape of the entity.
pub shape: Shape,
// The color of the entity, stored as a string that can be directly
// used by the HTML Canvas API (e.g., "#FFFFFF" for white).
pub color: String,
}
// --- NEW CODE ENDS HERE ---
Deconstructing the Code
Let’s break down the new additions:
pub enum Shape: We’ve defined a publicenumnamedShape. An instance ofShapecan only be one of its defined variants at any given time.#[derive(Clone)]: We’ve added a derive macro to theShapeenum.Cloneallows us to create a deep copy of aShapevalue. While not strictly required bybevy_ecs, it’s good practice for data-holding types like this, as it can simplify certain operations later on.- Enum Variants:
Rectangle { width: f32, height: f32 }: This variant represents a rectangle. Crucially, it also carries data associated with it: awidthand aheight.Circle { radius: f32 }: This variant represents a circle and carries itsradius. This ability to associate data with each variant makes Rust enums incredibly powerful for data modeling.
#[derive(Component)] pub struct Renderable: Just like withPositionandVelocity, we defineRenderableas a public struct and use the#[derive(Component)]macro to register it withbevy_ecs.- Fields of
Renderable:pub shape: Shape: This field holds an instance of ourShapeenum. So, aRenderablecomponent will contain either aRectangle(with its dimensions) or aCircle(with its radius).pub color: String: This field will store the color value. We use Rust’s ownedStringtype, which can hold text data of any length.
With these three components—Position, Velocity, and Renderable—we now have a complete set of data building blocks to define a basic, visible, moving object in our game world.
Next Steps
We’ve now defined all the fundamental component types for our initial entities. However, these are just blueprints. They don’t exist in our game yet. The next logical step is to bring our ECS framework to life within the engine itself. We will modify our Game struct to include the main ECS container, the World, which will be responsible for storing and managing all of our future entities and their components.
Further Reading
- The Rust Book: Enums and Pattern Matching: The definitive guide to Rust’s powerful
enumtype. Understanding enums is fundamental to writing idiomatic Rust. - Rust by Example:
derive: A practical look at thederiveattribute and some of the common traits it can automatically implement for you, likeClone. - The Bevy Book: Components (Revisited): It’s always good to reinforce core concepts. Re-read this page to solidify your understanding of how components are the data-centric foundation of an ECS architecture.
Initialize the ECS World
Mục tiêu: Integrate the bevy\_ecs::World into the Game struct. This task involves adding the World as a new field and initializing it, establishing the central container for all game entities and components.
You have done an outstanding job defining the fundamental data building blocks of our game world. With Position, Velocity, and Renderable components, you have created the blueprints for what our game objects have. These components are like individual LEGO bricks, each with a specific purpose. However, right now, they are just scattered in a box. We need a place to build with them—a central container to store, manage, and organize all the entities and their associated components.
This is the role of the World.
The World: Your Game’s Universe
In bevy_ecs, and indeed in most ECS frameworks, the World is the most important object. It is the heart of the simulation. Think of it as a highly optimized, in-memory database specifically designed for game data. The World is responsible for:
- Storing all components: It manages contiguous blocks of memory for each component type (e.g., all
Positions together, allVelocitys together), which is key to the high performance of ECS. - Tracking all entities: It creates unique entity IDs and maintains the mapping between an entity and the components attached to it.
- Managing Resources: Besides components, the
Worldcan also store global, unique data types called “resources”. We’ll use these later for things like user input state, asset managers, or the game score. - Providing a Query Interface: It allows systems to efficiently ask for and iterate over entities that have a specific combination of components (e.g., “give me every entity that has a
Positionand aRenderable”).
Our task now is to create this World and place it at the center of our engine’s state, inside our Game struct. This will officially transition our engine’s architecture to be driven by the ECS pattern.
Integrating the World into the Game Struct
Let’s modify our src/lib.rs file. We will add the World as a new field to our Game struct and initialize it in the Game::new() function.
First, ensure you have the bevy_ecs::prelude::* use statement at the top of src/lib.rs. This will bring the World type and other essentials into scope.
// src/lib.rs
// ... other use statements ...
use bevy_ecs::prelude::*; // This is important!
// ...
Now, let’s update the Game struct and its new function.
// src/lib.rs
// ... (log function and other use statements) ...
pub struct Game {
context: web_sys::CanvasRenderingContext2d,
width: u32,
height: u32,
// --- NEW FIELD ---
// The `world` is the container for all of our game's entities, components,
// and other data. It is the heart of the ECS.
pub world: World,
}
impl Game {
pub fn new() -> Self {
// ... (existing code to get canvas and context is 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();
// --- NEW INITIALIZATION ---
// We create a new, empty World. This will be our blank slate, ready
// to be populated with game entities.
let mut world = World::new();
Self {
context,
width: canvas.width(),
height: canvas.height(),
// Move the newly created world into our Game struct.
world,
}
}
// ... (the tick method is unchanged for now) ...
pub fn tick(&mut self) {
// ...
}
}
// ... (the run function is unchanged) ...
Deconstructing the Code
This was a small but conceptually massive change. Let’s review what we did:
pub world: World,: We added a new public field to ourGamestruct. The type isWorld, which we get frombevy_ecs. We make itpubso that we can access it from our systems and other logic withinlib.rs. TheGamestruct now officially owns the entire game simulation state.let mut world = World::new();: In ourGame::newconstructor, we create a new instance of theWorld. TheWorld::new()function gives us a fresh, empty world, ready to be filled with life.world,: We use struct field init shorthand to move theworldwe just created into theworldfield of theGameinstance we are returning. This happens once at the start of the application, setting up the universe in which our game will take place.
Our engine’s architecture has now fundamentally evolved. The Game struct is no longer just a simple container for rendering info; it is the high-level owner of the entire ECS World. All future game objects, physics, and logic will be managed through this world field.
Next Steps
We have an empty universe. The World exists, but it’s a void—there are no entities within it. The next logical and exciting step is to create our very first entity. We will spawn a new entity into the World and attach our Position, Velocity, and Renderable components to it, bringing our first game object into existence.
Further Reading
To deepen your understanding of the central role of the World in bevy_ecs, these resources are highly recommended.
- The Bevy Book: The World: The official guide explaining what the
Worldis and what it contains. This is a must-read. - The
bevy_ecsWorldAPI Documentation: The detailed, technical documentation for theWorldstruct itself. You can explore all the methods available for interacting with it.
Spawn Your First Entity in Bevy ECS
Mục tiêu: Populate the empty ECS World by spawning the first entity. This involves using the world.spawn() method with a bundle of components (Position, Velocity, Renderable) to create a new game object in Rust.
You’ve brilliantly set up the foundational structure for our ECS architecture. In the previous task, you created the World, the vast, empty universe that will contain every object in our game. It’s a perfectly engineered container, ready and waiting. Now, it’s time to perform the act of creation: we will populate this empty universe by spawning our very first entity.
From Blueprint to Being: Spawning an Entity
In bevy_ecs, creating a new game object is called “spawning”. When we spawn an entity, the World generates a new, unique ID. This ID is just a number—it has no data or behavior on its own. It’s a blank canvas. To give it properties and bring it to life, we must attach the component “blueprints” we’ve so carefully designed.
We will do this by creating instances of our Position, Velocity, and Renderable structs and inserting them into the World, associating them with our newly spawned entity ID. This process perfectly illustrates the core ECS philosophy: an entity is nothing more than the sum of its components.
The most convenient place to create our initial game objects is right after we initialize the World inside our Game::new() constructor. Let’s modify src/lib.rs to spawn a single entity: a square, positioned in the center of the screen, with an initial velocity.
Populating the World
First, ensure you have the log function and the necessary use statements at the top of your src/lib.rs file. Your mod components; and use components::*; lines are crucial here, as they make your component structs available.
Now, let’s add the entity creation logic to the Game::new function.
// src/lib.rs
// ... (log function and use statements are unchanged) ...
mod components;
use components::*;
use bevy_ecs::prelude::*;
// ...
pub struct Game {
context: web_sys::CanvasRenderingContext2d,
width: u32,
height: u32,
pub world: World,
}
impl Game {
pub fn new() -> Self {
// ... (getting canvas and context is 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();
// --- NEW CODE STARTS HERE ---
// Spawn our first entity into the world.
// The `spawn` method takes a "bundle" of components as a tuple.
// This is the most ergonomic and efficient way to create an entity with
// a set of starting components in bevy_ecs.
world.spawn((
// Add the Position component, starting the square in the middle of the screen.
// We use the canvas dimensions to calculate the center.
Position {
x: (canvas.width() / 2) as f32,
y: (canvas.height() / 2) as f32,
},
// Add the Velocity component, giving it some initial movement.
// A positive `dx` moves right, a positive `dy` moves down.
Velocity { dx: 150.0, dy: 150.0 },
// Add the Renderable component, defining its visual appearance.
Renderable {
// We use the `Rectangle` variant of our `Shape` enum.
shape: Shape::Rectangle {
width: 50.0,
height: 50.0,
},
// We'll make it white. `String::from` creates an owned String.
color: String::from("#FFFFFF"),
},
));
// Log a confirmation message to the browser console.
// `world.iter_entities().count()` is a way to ask the world how many entities it contains.
log(&format!(
"Spawned our first entity! Total entities in world: {}",
world.iter_entities().count()
));
// --- NEW CODE ENDS HERE ---
Self {
context,
width: canvas.width(),
height: canvas.height(),
world,
}
}
// ... (the tick method is unchanged for now) ...
pub fn tick(&mut self) {
// ...
}
}
// ... (the run function is unchanged) ...
Deconstructing the Code
This is a pivotal moment for our engine. Let’s break down exactly what this new block of code does.
world.spawn((...)): This is the central command. We are telling ourWorldto create a new entity. Thespawnmethod inbevy_ecsis designed to be highly flexible. Here, we’re using its most powerful form, which accepts a bundle.- A Bundle of Components: A bundle is simply a tuple of components, like
(Position, Velocity, Renderable). Passing a bundle tospawnis a single, highly optimized operation that creates the entity and attaches all specified components at once. This is the preferred “best practice” for creating entities inbevy_ecs. - Instantiating Components: Inside the tuple, we are creating concrete instances of the component structs we defined earlier.
Position { x: ..., y: ... }: We’re not just defining a type anymore; we are creating actual data. We calculate the center of the canvas (canvas.width() / 2) and useas f32to cast the result to the floating-point number our component expects.Renderable { shape: Shape::Rectangle { ... }, ... }: Here you can see the composition in action. We create aRenderableinstance, and for itsshapefield, we create an instance of theShape::Rectangleenum variant, giving it a width and height of 50 pixels.
After this code runs, our World is no longer empty. It now contains one entity. This entity has a position in the center of the screen, a velocity that will eventually make it move, and the visual properties of a 50x50 white square. It exists purely as data inside the World’s memory, waiting for a system to act upon it.
The log! message serves as our proof. When you build and run this, you will see Spawned our first entity! Total entities in world: 1 in your browser’s console, confirming that your command was successful.
Next Steps
Our entity has been born! It has a position and a velocity, but it is currently frozen in time. The data exists, but there is no logic to interpret it. The Velocity component is just sitting there, unused.
This sets the stage for the next crucial part of the ECS pattern: the System. In the next task, you will create your first system, a MovementSystem, whose sole job will be to query for all entities with both Position and Velocity and apply the velocity to the position, bringing our square to life with motion.
Further Reading
- The Bevy Book: Spawning Entities: The official guide to creating entities, explaining bundles and other methods in detail.
- The Bevy Book: Bundles: A deeper dive into the concept of component bundles and why they are a powerful and ergonomic feature.
- The Rust Book: Tuples: A refresher on the tuple type in Rust, which is the foundation for Bevy’s bundle system.
Implement the Movement System
Mục tiêu: Create a movement\_system function in Rust that queries the bevy\_ecs World for entities with Position and Velocity components, and updates their position to enable movement.
You have successfully laid the data-centric foundation of your ECS architecture. The World is alive and contains its first entity, a digital being defined by its Position, Velocity, and Renderable components. Yet, this entity is frozen in a single moment. Its Velocity component is a promise of motion, a piece of data filled with potential, but as of now, it’s just a number in memory with no effect. The crucial missing link is the logic that reads this potential and turns it into action.
Welcome to the “S” in ECS: the System.
The Logic Engine: What is a System?
If components are the “nouns” of your game world (the data, the properties), then systems are the “verbs” (the logic, the action). A system is a self-contained unit of pure logic that operates on entities with specific sets of components. It is completely decoupled from the data it operates on.
The MovementSystem we are about to create has one simple, focused responsibility: find every single entity in the World that is capable of moving (i.e., has both a Position and a Velocity) and make it move. It doesn’t know or care if the entity is a player, a bullet, or a particle effect. If it can move, the MovementSystem will move it. This separation of concerns is what makes ECS so powerful and scalable.
Querying the World
To perform its job, a system needs to ask the World for the data it’s interested in. This is done through a query. A query is a request to the bevy_ecs World for a specific combination of components. The World will then use its highly optimized internal data structures to find all entities that match the query and provide the system with access to their component data.
This is where Rust’s ownership and borrowing system shines. When we define our query, we must be explicit about the kind of access we need:
- Immutable Access (
&T): We need to read the component’s data but not change it. - Mutable Access (
&mut T): We need to read and write to the component’s data.
For our MovementSystem, we need to read the Velocity to see how fast the entity should move, and we need to write to the Position to update it with the new coordinates. Therefore, our query will ask for a mutable Position and an immutable Velocity.
Implementing the MovementSystem
Let’s create this system as a standalone function in our main src/lib.rs file. Good practice is to place system functions together, so we’ll add it before the Game struct definition.
// src/lib.rs
mod components;
use components::*;
use bevy_ecs::prelude::*;
// ... (other use statements) ...
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
// --- NEW CODE STARTS HERE ---
// A system is just a plain Rust function. It's a common convention to pass in the
// `World` as a mutable reference, as systems often need to modify the data within.
pub fn movement_system(world: &mut World) {
// This is a query. It asks the `World` for access to specific components.
// `world.query::<...>()` creates a query object that we can iterate over.
// The type inside the angle brackets specifies what we want:
// 1. `&mut Position`: Mutable access to the `Position` component. We need this
// because we are going to change the entity's location.
// 2. `&Velocity`: Immutable access to the `Velocity` component. We only need
// to read its values, not modify them.
// `bevy_ecs` will find every entity that has BOTH of these components.
let mut query = world.query::<(&mut Position, &Velocity)>();
// We now iterate over all the entities that matched our query.
// `.iter_mut(world)` provides an iterator that yields tuples of components.
// The `mut` in `iter_mut` is necessary because our query asks for mutable access.
for (mut position, velocity) in query.iter_mut(world) {
// This is the core logic of our system.
// We update the x and y fields of the `Position` component by adding the
// `dx` and `dy` values from the `Velocity` component.
// A note on frame rate: Right now, our velocity values are in "pixels per frame".
// This means the speed of our entity is tied to how fast the game is running.
// A more robust engine would use "delta time" (the time elapsed since the
// last frame) to ensure movement is smooth and consistent regardless of the
// frame rate. For now, this approach is perfectly fine for getting started.
position.x += velocity.dx;
position.y += velocity.dy;
}
}
// --- NEW CODE ENDS HERE ---
pub struct Game {
// ... (Game struct is unchanged)
}
impl Game {
// ... (Game::new is unchanged)
}
// ... (rest of the file is unchanged)
Deconstructing the Code
This new function is a perfect, self-contained example of a system.
- Function Signature:
pub fn movement_system(world: &mut World)defines a public function that takes a mutable reference to theWorld. This is a common pattern for systems, as they are the primary actors that change the state of the game world. - Creating the Query:
let mut query = world.query::<(&mut Position, &Velocity)>();is the command to prepare our request. We are not yet fetching the data, just defining what we’re looking for. The tuple(&mut Position, &Velocity)is the “shape” of the data we want for each entity. - Executing the Query:
for (mut position, velocity) in query.iter_mut(world)is where the action happens. The.iter_mut(world)method executes the query and gives us an iterator. Theforloop then visits each entity that matched, and for each one, it provides us with a tuple containing the requested components. Notice howpositionis declared asmutbecause we receive a mutable reference, which Rust requires us to handle explicitly. - The Logic:
position.x += velocity.dx;is the simple, elegant heart of the system. It connects the data from two separate components to produce a meaningful result: motion.
Our movement_system is now complete. It’s a beautifully isolated piece of logic, ready to be deployed. It doesn’t know when it will be run or how often, but it knows exactly what to do when it is called.
Next Steps
We have created the actor, but they are still waiting backstage. The movement_system function exists, but it is never being called. In our final task for this step, we will integrate this system into our main game loop. We will call movement_system from within our Game::tick method, ensuring that on every single frame, our entity’s position is updated, finally bringing it to life with visible motion.
Further Reading
To solidify your understanding of this pivotal ECS concept, I highly recommend these resources.
- The Bevy Book: Systems: The official guide explaining how systems work in
bevy_ecs. - The Bevy Book: Queries: A deep dive into the query system, which is the primary way systems interact with the
World. - Rust’s Borrowing and References: A foundational chapter from “The Rust Programming Language” book. Understanding this is key to understanding why
&and&mutare so important inbevy_ecsqueries.
Integrate the Movement System into the Game Loop
Mục tiêu: Call the movement\_system from the Game::tick method to update the game state on each frame, effectively running the entity movement logic within the main game loop.
The pieces are all in place. You have masterfully constructed the data for our game world by defining components, populated our ECS World with a living entity, and in the last task, you forged the logic for motion by creating the movement_system. That system is a beautiful, self-contained engine of logic, poised and ready. Yet, it currently sits dormant, a powerful machine waiting for the command to start. This task is that command. We will now integrate this system into the engine’s beating heart—the main game loop.
Orchestrating Logic: The Role of the tick Method
Our Game::tick method is the conductor of our per-frame orchestra. Each time requestAnimationFrame fires, tick is called, signifying the start of a new frame. Its responsibility is to ensure all the necessary operations for that frame happen, and happen in the correct order. The general flow of a game frame is:
- Process Inputs (we’ll do this later)
- Update Game State (move objects, check collisions, run AI)
- Render the Scene
Our movement_system is a core part of the “Update Game State” phase. Therefore, the tick method is the perfect place to call it. By calling our system from within tick, we guarantee that every single entity’s position is updated precisely once per frame, creating the illusion of smooth, continuous motion.
Plugging in the System
Let’s modify our src/lib.rs file. The change is surprisingly small, but its impact is enormous. We will simply call the movement_system function from inside Game::tick, passing it a mutable reference to our World.
Here is the updated impl Game block. Notice the single new line inside the tick method.
// src/lib.rs
// ... (movement_system function and other code is unchanged) ...
impl Game {
pub fn new() -> Self {
// ... (this function is 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 },
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) {
// Clear the screen on every frame, as before.
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,
);
// --- NEW CODE STARTS HERE ---
// Run our movement system.
// We pass a mutable reference to the world, giving the system full
// permission to read and write the component data within.
movement_system(&mut self.world);
// --- NEW CODE ENDS HERE ---
}
}
// ... (the run function is unchanged) ...
Deconstructing the Change
movement_system(&mut self.world);: This single line is the culmination of all our work in this step.self: Inside thetickmethod,selfis a mutable reference to theGameinstance (&mut Game).self.world: We access theworldfield of ourGameinstance.&mut self.world: We take a mutable reference to theWorld. This satisfies the function signature ofmovement_system, which expects a&mut World. This is Rust’s borrowing system in action, ensuring that while themovement_systemis running, it has exclusive write access to theWorld, preventing any other part of the code from causing data races.
With this change, the flow of control is now complete:
- The browser calls our
Closure. - The closure calls
game.borrow_mut().tick(). - The
tickmethod clears the screen and then immediately callsmovement_system. - The
movement_systemqueries theWorld, finds our entity, and updates itsPositioncomponent based on itsVelocity.
If you were to build and run the project now, you would see… exactly the same thing as before: a dark grey screen. This is expected! While our entity’s Position data is now changing rapidly in memory, we haven’t written the code to actually draw it yet. We have created a dynamic, invisible world.
Next Steps
You have successfully designed and implemented a foundational ECS architecture. You’ve defined components for data, populated a World with an entity, and created a system to enact logic. This completes Step 3 of our project roadmap. The simulation is running; now we just need to see it.
This sets the stage perfectly for our next major milestone, Step 4: Building the Rendering System. In the upcoming tasks, you will create a new system, the RenderingSystem, whose job will be to query the World for all visible entities and use the Canvas API to draw them on the screen at their current positions. This is when the magic will truly become visible.
Further Reading
To learn more about the concepts of system execution and game loop structure, explore these resources:
- The Bevy Book: Schedules and System Ordering: While we are calling our system manually, a full-featured engine often uses a “scheduler” to run systems. This chapter from the Bevy book gives you a glimpse into how more complex engines orchestrate their logic.
- Game Programming Patterns: Update Method: A fantastic article on the “Update Method” pattern, which is exactly what our
tickmethod represents. It discusses the role of this central function in orchestrating game state changes. - The Rust Book: Passing References to Functions: A refresher on how Rust’s borrowing system allows functions to access data without taking ownership, which is precisely what we did when we called
movement_system(&mut self.world).