Define an ECS Resource for Keyboard Input State

Mục tiêu: Create a new Rust module and define an InputState struct to hold the state of keyboard keys. This introduces the concept of an ECS Resource in bevy\_ecs for managing global, unique data like player input, distinguishing it from per-entity components.


Absolutely phenomenal work! You have successfully built a complete, end-to-end rendering pipeline. Your game engine now has a dynamic, internal state that is visually represented on the screen—a white square moving with purpose. This is the core of any game engine, and you’ve built it from the ground up. You’ve transitioned from an invisible simulation to a tangible, moving world.

However, right now, our world is a deterministic one. The square moves according to its pre-programmed Velocity, and we, the players, are mere spectators. The next great leap for our engine is to break this barrier and introduce interactivity. We need to give the player a voice, a way to influence the game world. This begins with listening to their primary input device: the keyboard.

Event vs. State: The Challenge of Input

When you press a key, the browser fires a keydown event. When you release it, it fires a keyup event. These are instantaneous moments in time. A naive approach might be to try and change the player’s velocity directly inside the keydown event listener. This works for single-press actions like jumping or shooting, but it fails for continuous actions like movement. We want our character to move as long as the ‘right arrow’ key is held down, not just for a single frame when the key was first pressed.

To solve this, we need to model the state of the keyboard, not just react to its events. We need a central place in our engine that, at any given moment during the game loop, can answer the question, “Is the ‘up’ key currently being pressed?”

This is why our first task in building an input handler is to define a data structure that will hold this state. We will create a simple InputState struct.

Introducing ECS Resources

Up until now, all the data we’ve created (Position, Velocity, Renderable) has been in the form of Components. A component is data that belongs to a specific entity. One entity has its own Position, and another entity has its own, separate Position.

But our input state is different. It’s global. There is only one keyboard and one set of pressed keys for the entire game. In bevy_ecs, this kind of unique, global data is called a Resource. A Resource is a data type that you can store directly in the World and access from any system. There is, at most, one instance of any given Resource type in the World at a time. The InputState is a perfect candidate for our first Resource.

To keep our code organized, let’s create a new module for our resources, just as we did for our components.

  1. In your src directory, create a new file named resources.rs.
  2. Open src/lib.rs and declare this new module near the top, alongside your components module.
// src/lib.rs

// ... (other use statements)
mod components;
use components::*;

// --- NEW CODE STARTS HERE ---
mod resources;
use resources::*;
// --- NEW CODE ENDS HERE ---

use bevy_ecs::prelude::*;
// ...

Now, let’s define our InputState struct inside the new src/resources.rs file.

// src/resources.rs

// We will need to register this struct as a Resource with bevy_ecs.
// The `Resource` trait is a "marker trait" that tells Bevy's ECS
// that this struct can be stored as a global resource in the World.
use bevy_ecs::prelude::Resource;

// We use `#[derive(Resource, Default)]`.
// `Resource`: Registers this struct as a valid ECS resource.
// `Default`: Allows us to easily create a new instance with default values
// (in this case, `false` for all booleans) using `InputState::default()`.
// This is good practice for state-holding structs.
#[derive(Resource, Default)]
pub struct InputState {
    // Each field represents the state of a key we care about.
    // `true` means the key is currently held down, `false` means it is not.
    // We'll start with the four cardinal directions.
    pub up_pressed: bool,
    pub down_pressed: bool,
    pub left_pressed: bool,
    pub right_pressed: bool,
}

Deconstructing the Code

  • mod resources; / use resources::*;: Just like with components, these lines in lib.rs tell Rust about our new file and bring its public contents into scope, making InputState available.
  • use bevy_ecs::prelude::Resource;: We import the Resource trait, which is required by the derive macro.
  • #[derive(Resource, Default)]: This is another powerful procedural macro from bevy_ecs.
    • Resource: This automatically implements the necessary traits on our struct to allow it to be stored in the World’s resource collection. Without this, the World wouldn’t know how to manage it.
    • Default: This is a standard Rust derive macro. For a struct where all fields have a default value (booleans default to false, numbers to 0, etc.), this macro automatically implements the Default trait. This gives us a convenient InputState::default() constructor that creates a new InputState with all keys set to false, which is the perfect initial state for our game.
  • pub struct InputState: We define our struct with public boolean fields. These flags will be our single source of truth for the keyboard state on every frame.

You have now successfully defined the data structure that will form the backbone of your entire input system. It’s a clean, simple, and extensible blueprint for tracking player input. It doesn’t do anything yet, but its existence is the essential first step.

Next Steps

Our InputState blueprint exists, but an instance of it has not yet been added to our game’s World. In the next task, we will take this struct and insert it into the World as a global resource, making it accessible to any future systems that need to read the player’s input.


Further Reading

To deepen your understanding of the important concepts you’ve just implemented, please explore these resources.

  • The Bevy Book: Resources: The official documentation explaining the concept of ECS Resources. This is a crucial concept to grasp.
  • The Rust Book: Defining and Instantiating Structs: A foundational chapter on creating your own data types with struct.
  • The Default Trait in Rust: Official documentation on the Default trait, explaining how it provides a useful default value for a type.
  • Game Input: Event-Driven vs. Polling: A great article or discussion explaining the difference between reacting to input events and polling input state, which is the pattern we are building.

Initialize and Insert the InputState Resource

Mục tiêu: Modify the Game::new() function to initialize the InputState struct using default() and insert it as a global singleton resource into the bevy\_ecs World using the insert\_resource method.


Excellent! You have successfully created the data blueprint for our input system by defining the InputState struct. This struct, with its #[derive(Resource, Default)] attributes, is a perfectly designed container for tracking which keys are held down. It’s the “what” of our input state. Now, we must take this blueprint and forge a real, living instance of it within our game’s universe, the ECS World.

From Blueprint to Singleton: Inserting the Resource

Think of the InputState struct you just created as a schematic. It describes the fields and properties of our input tracker, but it doesn’t exist as a concrete piece of data in our game yet. We need to perform a one-time setup action when our engine starts: create a single InputState object and place it into the World’s special storage area for global resources.

Once inserted, this InputState object becomes a singleton within our ECS. Any system, anywhere in our engine, can then ask the World for access to this one-and-only input state. This is a powerful and clean way to manage global data without resorting to messy global variables.

The ideal place for this one-time setup is, of course, our Game::new() function, right after we create the World itself. bevy_ecs provides a straightforward method for this: world.insert_resource().

Updating Game::new()

Let’s modify src/lib.rs to initialize and insert our new resource. The change is a single, expressive line of code.

// src/lib.rs

// ... (your use statements are unchanged) ...
mod components;
use components::*;
mod resources;
use resources::*;
use bevy_ecs::prelude::*;
// ... (the rest of your use statements and the log function) ...

// ... (your system functions are unchanged) ...

impl Game {
    pub fn new() -> Self {
        // ... (getting canvas and context is unchanged) ...
        let window = web_sys::window().expect("no global `window` exists");
        // ... (getting canvas and context continues) ...
        let context = canvas
            .get_context("2d")
            .unwrap()
            .unwrap()
            .dyn_into::<web_sys::CanvasRenderingContext2d>()
            .unwrap();

        let mut world = World::new();

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

        // Initialize and insert our InputState resource into the World.
        // We use `InputState::default()`, which is available because we derived the
        // `Default` trait on the struct. This creates a new `InputState` with
        // all boolean fields set to `false`, which is the perfect starting state.
        // The `insert_resource` method takes ownership of this new instance and
        // stores it in the World's dedicated resource storage.
        world.insert_resource(InputState::default());

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

        // Spawning the entity remains the same.
        world.spawn((
            Position {
                x: (canvas.width() / 2) as f32,
                y: (canvas.height() / 2) as f32,
            },
            Velocity { dx: 2.0, dy: 2.0 }, // Using a slower speed for visibility
            Renderable {
                shape: Shape::Rectangle {
                    width: 50.0,
                    height: 50.0,
                },
                color: String::from("#FFFFFF"),
            },
        ));

        // ... (the rest of the function is unchanged) ...
        Self {
            context,
            width: canvas.width(),
            height: canvas.height(),
            world,
        }
    }

    // ... (the tick method is unchanged) ...
    pub fn tick(&mut self) {
        // ...
    }
}

// ... (the run function is unchanged) ...

Deconstructing the Code

This one line is doing more than it appears, so let’s break it down fully:

  • world.insert_resource(...): This is a method on the World object. Its job is to take an instance of any struct that has been marked as a Resource and store it. The World organizes its memory to hold exactly one instance of each resource type. If you were to call insert_resource with a new InputState later, it would overwrite the old one.
  • InputState::default(): This is where the #[derive(Default)] from the previous task pays off. This is a static function, provided for us by the compiler, that constructs a new InputState where every field is initialized to its default value. For bool, the default is false. This is not only convenient but also expressive; it clearly communicates that we are creating a fresh, “zeroed-out” state.

With this change, your World is now fully equipped. It contains component data for your entities, and now it also contains global resource data for your engine’s systems. The InputState resource is now “live” in memory, waiting for something to interact with it.

Next Steps

Our InputState resource exists and is safely stored in the World, but its boolean flags will remain false forever because nothing is changing them. The next logical and exciting step is to bridge the gap between the browser and our Rust code. You will use web-sys and the Closure mechanism to create event listeners that listen for real keydown and keyup events from the browser. When those events fire, your code will reach into the World, get mutable access to the InputState resource, and update its flags accordingly.


Further Reading

To solidify your understanding of how resources are managed, these references will be invaluable.

  • The Bevy Book: Resources (Revisited): Now that you’ve inserted a resource, re-reading the official guide will provide even deeper context.
  • bevy_ecs API Docs: World::insert_resource: Dive into the technical documentation for the exact method you just used. See how it’s defined and what it guarantees.
  • The Default Trait in Rust (Revisited): Reinforce your understanding of this incredibly useful standard trait. It’s a cornerstone of writing clean, initializable data structures in Rust.

Use web-sys and Closure to create event listeners for keydown and keyup events on the window.

Mục tiêu:


You’ve done an excellent job setting up the data foundation for our input system. The InputState resource now exists as a singleton within your ECS World, a central source of truth waiting to be updated. However, at this moment, it’s a silent observer. Its flags are all false, and nothing in our world can change them. The bridge between the physical press of a key and the digital state of our game has yet to be built.

This task is all about constructing that bridge. We will reach out from our Rust Wasm module into the browser’s domain and listen for keyboard events directly. This is a quintessential part of WebAssembly development, where the sandboxed, high-performance world of Rust needs to communicate with the event-driven, API-rich environment of JavaScript.

The Challenge: Rust Functions vs. JavaScript Callbacks

When you want to listen for an event in JavaScript, you call a method like window.addEventListener("keydown", myJavaScriptFunction). The second argument must be a JavaScript function. The problem is that we cannot directly pass a Rust function to a JavaScript API. The calling conventions and memory models are completely different.

This is where wasm-bindgen provides a critical tool: wasm_bindgen::closure::Closure. A Closure is a special wrapper that takes a Rust closure (an anonymous, stateful function) and transforms it into a JavaScript-compatible function that can be passed to browser APIs. It’s the magic translator that makes this communication possible.

We will create two of these closures: one to handle the keydown event (when a key is pressed down) and another for the keyup event (when a key is released). These closures will be responsible for reaching into our World and flipping the boolean flags in our InputState resource.

Implementing the Event Listeners

The best place to set up these “listen forever” event listeners is in our run function, right after we’ve created our Game instance but before we start the main loop. This is a one-time setup that needs to happen when the application starts.

Let’s modify the run function in src/lib.rs. This will be a significant addition, so we’ll break it down carefully.

First, ensure you have the necessary use statements at the top of your lib.rs file. We’ll need a few new ones for this task.

// src/lib.rs

// ... (existing use statements)
// Add these new 'use' statements for event handling
use wasm_bindgen::JsCast;
use web_sys::KeyboardEvent;
// ...

Now, let’s update the run function. The previous implementation is being replaced by this more detailed setup.

// src/lib.rs

// ... (all other code before run function is unchanged) ...

#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
    // The existing setup code from Step 2, creating the shared Game state.
    // `Rc` stands for Reference Counted. It allows multiple owners for the same data.
    // `RefCell` provides "interior mutability", allowing us to borrow the data
    // mutably even when the `Rc` is immutable. This is essential for callbacks.
    let game = Rc::new(RefCell::new(Game::new()));

    let f = Rc::new(RefCell::new(None));
    let g = f.clone();

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

    // Get a reference to the browser's window object.
    let window = web_sys::window().expect("no global `window` exists");

    // --- KEYDOWN EVENT LISTENER ---
    {
        // We clone `game` to move it into the closure. `Rc::clone` is cheap;
        // it only increments a reference counter, it doesn't copy the game data.
        let game = game.clone();

        // Create a Rust closure that will be our event handler.
        // `Closure::wrap` takes a closure and turns it into something JS can call.
        // `Box::new` allocates the closure on the heap.
        let keydown_closure = Closure::<dyn FnMut(_)>::new(move |event: KeyboardEvent| {
            // Get mutable access to our game state.
            let mut game = game.borrow_mut();

            // Get mutable access to the InputState resource within the world.
            // `get_resource_mut` returns an Option, so we use `if let` to safely
            // unwrap it. This is robust and prevents crashes if the resource
            // somehow doesn't exist.
            if let Some(mut input_state) = game.world.get_resource_mut::<InputState>() {
                // Use a `match` statement on the event's `key` property to update
                // the correct flag. The `key()` method returns a string like
                // "ArrowUp", "ArrowLeft", etc.
                match event.key().as_str() {
                    "ArrowUp" => input_state.up_pressed = true,
                    "ArrowDown" => input_state.down_pressed = true,
                    "ArrowLeft" => input_state.left_pressed = true,
                    "ArrowRight" => input_state.right_pressed = true,
                    // The `_` is a wildcard pattern that matches any other key.
                    // We do nothing for keys we don't care about.
                    _ => {}
                }
            }
        });

        // Add the event listener to the window object.
        // We cast the closure to a `JsValue` and then to a `Function`.
        window
            .add_event_listener_with_callback("keydown", keydown_closure.as_ref().unchecked_ref())
            .unwrap();

        // IMPORTANT: We must "forget" the closure. If we don't, Rust will drop it
        // at the end of this scope, and our event listener will point to freed memory,
        // causing a runtime crash when a key is pressed. `forget` leaks the memory,
        // allowing the closure to live for the entire session.
        keydown_closure.forget();
    }

    // --- KEYUP EVENT LISTENER ---
    {
        // We create a new clone for the keyup closure.
        let game = game.clone();

        let keyup_closure = Closure::<dyn FnMut(_)>::new(move |event: KeyboardEvent| {
            let mut game = game.borrow_mut();
            if let Some(mut input_state) = game.world.get_resource_mut::<InputState>() {
                // The logic is identical to keydown, but we set the flags to `false`.
                match event.key().as_str() {
                    "ArrowUp" => input_state.up_pressed = false,
                    "ArrowDown" => input_state.down_pressed = false,
                    "ArrowLeft" => input_state.left_pressed = false,
                    "ArrowRight" => input_state.right_pressed = false,
                    _ => {}
                }
            }
        });

        window
            .add_event_listener_with_callback("keyup", keyup_closure.as_ref().unchecked_ref())
            .unwrap();

        keyup_closure.forget();
    }

    // --- The Game Loop (from Step 2) ---
    // This part remains the same, it starts the `requestAnimationFrame` loop.
    *g.borrow_mut() = Some(Closure::new(move || {
        // On every frame, call our game's tick method.
        game.borrow_mut().tick();

        f.borrow().as_ref().map(|f| {
            web_sys::window()
                .unwrap()
                .request_animation_frame(f.as_ref().unchecked_ref())
                .unwrap();
        });
    }));

    web_sys::window()
        .unwrap()
        .request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref())?;

    Ok(())
}

Deconstructing the Code

This is a dense but incredibly important piece of code. Let’s break it down concept by concept:

  • let game = game.clone();: We need to give our closure a way to access the game state. The move keyword on the closure forces it to take ownership of its environment. By cloning the Rc, we give the closure its own reference-counted pointer to the game state. This is the standard, safe way to share data with callbacks.
  • Closure::<dyn FnMut(_)>::new(...): This creates the magic wrapper. We’re telling it that we’re wrapping a Rust closure that can be called multiple times (FnMut) and takes one argument (_), which we later specify as event: KeyboardEvent.
  • game.world.get_resource_mut::<InputState>(): This is how we access our global resource. We ask the World for a mutable reference to the InputState resource. This returns an Option<T>, which is why we use if let Some(...) to safely access the data inside.
  • event.key().as_str(): The event object is of type web_sys::KeyboardEvent, a direct mapping to the JavaScript KeyboardEvent. The .key() method returns a Rust String representing the key pressed (e.g., "ArrowUp", "a"). We match on this string to determine which flag to update.
  • .add_event_listener_with_callback(...): This is the web-sys function that attaches our listener. It takes the event name ("keydown") and a reference to our wrapped closure. The .as_ref().unchecked_ref() part is a standard incantation to get the necessary &Function type that the API expects.
  • keydown_closure.forget();: This is arguably the most critical and non-obvious line. Rust’s memory safety is based on ownership and lifetimes. Normally, keydown_closure would be destroyed at the end of its {} block. If this happened, the browser would be holding a callback that points to invalid memory. Closure::forget() tells Rust’s memory manager to intentionally leak this memory, ensuring the closure stays alive as long as the web page is open, ready to be called at any time.

You have now successfully wired up the keyboard to your ECS World. If you were to run this code and press the arrow keys, the boolean flags inside the InputState resource in your Wasm memory would be flipping between true and false in real-time. The connection is made.

Next Steps

The player’s intentions are now being correctly recorded in our InputState resource. The final task of this step is to act on those intentions. You will create a new system, the PlayerControlSystem, which will run on every frame. This system will read the InputState resource and use its values to modify the Velocity component of the player’s entity, finally giving the player control over the white square.


Further Reading

  • The wasm-bindgen Book: Closures: The official guide on how to pass Rust closures to JavaScript. This is essential reading.
  • MDN Web Docs: keydown event: The definitive web documentation for the keyboard event you are now handling.
  • MDN Web Docs: KeyboardEvent.key: Documentation for the property you are using to identify which key was pressed.
  • The Rust Book: Rc<T>, the Reference Counted Smart Pointer: A deep dive into the smart pointer that makes sharing data with closures possible.
  • The Rust Book: RefCell<T> and the Interior Mutability Pattern: An explanation of the pattern that allows us to get a mutable reference to data that is otherwise immutable.

Implement Keyboard Listeners to Update ECS State

Mục tiêu: Implement the logic within the keydown and keyup browser event closures. Access the InputState resource from the bevy\_ecs world and update its boolean flags to reflect the current state of the arrow keys.


In the previous task, you masterfully set up the complex machinery required to listen for browser events from within Rust. You now have two Closures registered with the browser, one for keydown and one for keyup, ready and waiting for input. However, their internal logic is still empty. They are listening, but they don’t yet know what to do with the information they receive.

This task is about filling that final gap. We will write the code inside these closures to reach into our ECS World, find the InputState resource, and update its boolean flags based on which key was pressed or released. This is the critical step where a physical keyboard press is translated into a persistent state change within our game engine.

Accessing Resources from a Callback

Our closures need to modify the InputState resource. In bevy_ecs, the way to get mutable access to a global resource is via the world.get_resource_mut::<T>() method. This method is designed for safety:

  1. It checks if a resource of type T (e.g., InputState) actually exists in the World.
  2. Because the resource might not exist, the method doesn’t return the resource directly. Instead, it returns an Option<ResMut<T>>. Option is Rust’s way of handling potentially null values safely. ResMut is a special “smart pointer” that provides mutable access to the resource.
  3. The idiomatic and safe way to handle this Option is with an if let Some(...) block. This code block will only execute if the resource exists, and it gives us direct access to the resource data inside the block.

Let’s implement this logic. We will modify the empty closures you created in the run function.

Implementing the State Update Logic

We’ll start with the keydown_closure. When a key is pressed, we want to set its corresponding flag to true. We’ll use a match statement on the event’s key() property to handle the different arrow keys.

Update the keydown_closure in your src/lib.rs file’s run function as follows. The surrounding code from the previous task remains the same; we are only filling in the body of the closure.

// Inside the `run` function, within the `keydown_closure` definition:

let keydown_closure = Closure::<dyn FnMut(_)>::new(move |event: KeyboardEvent| {
    // --- NEW LOGIC STARTS HERE ---

    // First, get mutable access to the entire game state.
    let mut game = game.borrow_mut();

    // Then, ask the world for mutable access to our InputState resource.
    // This returns an Option, so we use `if let` to safely unwrap it.
    if let Some(mut input_state) = game.world.get_resource_mut::<InputState>() {
        // Use a `match` statement on the string representation of the key that was pressed.
        match event.key().as_str() {
            // If the key is "ArrowUp", set the `up_pressed` flag to true.
            "ArrowUp" => input_state.up_pressed = true,
            "ArrowDown" => input_state.down_pressed = true,
            "ArrowLeft" => input_state.left_pressed = true,
            "ArrowRight" => input_state.right_pressed = true,
            // The `_` is a wildcard pattern that matches any other key.
            // We do nothing for keys we don't care about, like 'a' or 'Shift'.
            _ => {}
        }
    }

    // --- NEW LOGIC ENDS HERE ---
});

Next, we’ll do the same for the keyup_closure. The logic is identical, but this time, we set the flags to false to indicate the key has been released.

// Inside the `run` function, within the `keyup_closure` definition:

let keyup_closure = Closure::<dyn FnMut(_)>::new(move |event: KeyboardEvent| {
    // --- NEW LOGIC STARTS HERE ---

    let mut game = game.borrow_mut();
    if let Some(mut input_state) = game.world.get_resource_mut::<InputState>() {
        // The logic is the same, but we set the flags to `false` on key release.
        match event.key().as_str() {
            "ArrowUp" => input_state.up_pressed = false,
            "ArrowDown" => input_state.down_pressed = false,
            "ArrowLeft" => input_state.left_pressed = false,
            "ArrowRight" => input_state.right_pressed = false,
            _ => {}
        }
    }

    // --- NEW LOGIC ENDS HERE ---
});

Deconstructing the Code

  • game.borrow_mut(): As established in the previous task, this line from RefCell gives us mutable access to our Game struct from within the closure.
  • game.world.get_resource_mut::<InputState>(): This is the core ECS interaction. We are asking the World for write access to the one and only InputState resource.
  • if let Some(mut input_state) = ...: This pattern is fundamental to safe Rust. It means, “If the resource exists, give me a mutable reference to it called input_state and execute the following block.” This prevents our code from panicking if we ever forget to insert the resource.
  • event.key().as_str(): The event object, of type web_sys::KeyboardEvent, gives us access to all the event’s data. The .key() method returns a String that uniquely identifies the key pressed (e.g., "ArrowUp", "a", "Enter"). We use .as_str() to get a string slice, which is efficient for matching.
  • match ...: The match statement is a clean and powerful way to handle the different possible key strings. It’s more readable and often safer than a series of if/else if statements.
  • input_state.up_pressed = true: This is the final step. We are directly modifying the field of the resource, and this change will persist in the World until the keyup event flips it back to false.

The bridge is now complete. When you press an arrow key, the browser fires an event, your Closure is executed, and a boolean flag in your Rust Wasm module’s memory is flipped to true. The player’s intent is now being successfully captured and stored.

Verification (Optional)

If you build and run the project now, you still won’t see any visual change. The state is being updated in memory, but nothing is using that state yet. If you want to confirm that your event listeners are working, you can add a log! statement inside one of the match arms:

// Example for debugging
"ArrowUp" => {
    input_state.up_pressed = true;
    log("Up arrow pressed!");
}

Rebuild and check your browser’s developer console. You should see the message appear every time you press the up arrow key.

Next Steps

The player’s intentions are now being correctly recorded in our InputState resource on every key press and release. The final task of this step is to act on those intentions. You will create a new system, the PlayerControlSystem, which will run on every frame. This system will read the InputState resource and use its values to modify the Velocity component of the player’s entity, finally giving the player control over the white square.


Further Reading

  • The Bevy Book: Accessing Resources: The official guide on how to get and use resources from within systems. The logic is identical for closures.
  • The Rust Book: match Control Flow Construct: The definitive guide to Rust’s powerful match statement, perfect for handling enums and different value states.
  • MDN Web Docs: KeyboardEvent.key: The web’s reference for the key property, showing all possible string values you can match against.

Implement the Player Control System in an ECS

Mục tiêu: Create a new system in a Rust-based ECS to handle player input. This involves creating a ‘Player’ tag component, querying for the player entity, reading the global ‘InputState’ resource, and modifying the entity’s ‘Velocity’ component to move it based on keyboard presses.


You have successfully built the bridge between the browser and your game engine. The InputState resource in your ECS World is now a live, accurate reflection of the player’s keyboard actions. Every press of an arrow key flips a boolean flag to true, and every release flips it back to false. The player is “speaking” to the engine, and the engine is listening.

However, the game world is not yet reacting. The InputState is a record of intent, but there is no logic to translate that intent into action. Our white square continues its pre-programmed journey, oblivious to our commands. This final task of the step is to create that translator: a new system that reads the player’s recorded intent and uses it to command the player’s entity.

From State to Action: The PlayerControlSystem

We will now create our third and final system for this stage of the project: the PlayerControlSystem. Its purpose is highly specialized:

  1. Read the global InputState resource.
  2. Find the specific entity that represents the player.
  3. Write new values to that entity’s Velocity component based on the input state.

This is a classic example of an ECS system. It’s a self-contained piece of logic that connects a global resource (the input) to a specific component (Velocity), creating interactive behavior.

Tagging the Player

But how does the system know which entity to control? If we add enemies or bullets later, we don’t want them to respond to the arrow keys. We need a way to mark an entity as “player-controlled”. The standard ECS pattern for this is a tag component. A tag component is simply an empty struct that serves as a label.

First, let’s define our Player tag component. Open your src/components.rs file and add this new struct.

// src/components.rs

// ... (existing Position, Velocity, Shape, and Renderable are unchanged) ...

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

// A "tag component" is an empty struct used to label an entity.
// This allows systems to query for entities that have this specific tag.
// It's a clean and efficient way to categorize entities.
#[derive(Component)]
pub struct Player;

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

Now that we have this tag, we must attach it to our entity when it’s created. Open src/lib.rs and add the Player component to the bundle in your Game::new function.

// src/lib.rs

// ... inside the impl Game block ...

pub fn new() -> Self {
    // ... (code to get canvas and context is unchanged) ...

    let mut world = World::new();

    world.insert_resource(InputState::default());

    // Add the `Player` tag component to our entity's bundle.
    world.spawn((
        Position {
            x: (canvas.width() / 2) as f32,
            y: (canvas.height() / 2) as f32,
        },
        // We will now control velocity with input, so let's start it at zero.
        Velocity { dx: 0.0, dy: 0.0 },
        Renderable {
            shape: Shape::Rectangle {
                width: 50.0,
                height: 50.0,
            },
            color: String::from("#FFFFFF"),
        },
        // --- NEW COMPONENT ---
        Player, // Add the tag here
    ));

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

Notice we also changed the initial Velocity to { dx: 0.0, dy: 0.0 }, since the player should only move when a key is pressed.

Implementing the System

With our entity properly tagged, we can now write the PlayerControlSystem. This system needs to perform two distinct operations with the World: get a resource and query for components.

Add the following new function to src/lib.rs, alongside your other systems.

// src/lib.rs

// ... (log function, movement_system, rendering_system are unchanged) ...

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

// A constant for the player's movement speed. Using a constant makes it easy
// to tweak the game's feel in one place.
const PLAYER_SPEED: f32 = 200.0; // pixels per second

// This system reads the `InputState` resource and modifies the `Velocity`
// of the entity tagged with the `Player` component.
pub fn player_control_system(world: &mut World) {
    // First, we need to get the `InputState` resource.
    // `get_resource` returns an Option, so we use `if let` for safety.
    // We only need to read the resource, so we don't use `_mut`.
    if let Some(input_state) = world.get_resource::<InputState>() {
        // Next, we create a query to find all entities that are a `Player`
        // and have a `Velocity` component that we can modify.
        let mut query = world.query::<(&mut Velocity, &Player)>();

        // We iterate over the results of the query. In our simple game, this
        // loop will only run once, for our single player entity.
        for (mut velocity, _player) in query.iter_mut(world) {
            // We'll start by resetting the velocity to zero. This ensures that
            // the player stops moving when keys are released.
            velocity.dx = 0.0;
            velocity.dy = 0.0;

            // Now, we check the state of each key and adjust the velocity.
            // This logic allows for diagonal movement by handling horizontal
            // and vertical inputs independently.
            if input_state.left_pressed {
                velocity.dx -= PLAYER_SPEED;
            }
            if input_state.right_pressed {
                velocity.dx += PLAYER_SPEED;
            }
            if input_state.up_pressed {
                velocity.dy -= PLAYER_SPEED; // In canvas, a smaller Y is higher up.
            }
            if input_state.down_pressed {
                velocity.dy += PLAYER_SPEED;
            }

            // Note: Our movement system currently adds velocity per-frame. A better
            // approach for smooth, framerate-independent movement would be to
            // use "delta time" (the time elapsed since the last frame). For now,
            // let's adjust our velocity to be a smaller per-frame value.
            // A more advanced implementation would be:
            // position.x += velocity.dx * delta_time;
            // Let's create a temporary per-frame speed.
            let per_frame_speed = 3.0;
            if input_state.left_pressed {
                velocity.dx = -per_frame_speed;
            }
            if input_state.right_pressed {
                velocity.dx = per_frame_speed;
            }
            if input_state.up_pressed {
                velocity.dy = -per_frame_speed;
            }
            if input_state.down_pressed {
                velocity.dy = per_frame_speed;
            }
        }
    }
}

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

Correction & Refinement: My initial thought to use a PLAYER_SPEED constant was slightly ahead of schedule, as our MovementSystem doesn’t yet account for delta time. It simply adds velocity to position each frame. To make movement feel right with the current engine, we should set a small, fixed “pixels-per-frame” value. The code above has been updated to reflect this more practical approach. Let’s use 3.0 for a reasonable speed.

Deconstructing the Code

  • world.get_resource::<InputState>(): This is how a system reads a global resource. It’s the read-only counterpart to the get_resource_mut we used in our event listeners.
  • world.query::<(&mut Velocity, &Player)>(): This query is very specific. It asks the World for entities that have both a mutable Velocity component and a read-only Player tag component. This is our filter; it ensures our code will only run on the player entity.
  • for (mut velocity, _player) in query.iter_mut(world): We iterate through the query results. We need iter_mut because we are requesting mutable access to Velocity. We destructure the results into velocity and _player. We prefix player with an underscore (_) to tell the Rust compiler that we know this variable exists but we don’t plan to use it. Its presence in the query is purely for filtering.
  • Velocity Logic: The if statements check each boolean flag in the InputState resource and modify the dx and dy fields of the Velocity component accordingly. By resetting velocity to 0.0 at the start, we ensure that movement stops cleanly when a key is released.

You have now created the final piece of the input puzzle. This system is a perfect, self-contained unit of logic that translates the player’s intentions into game physics.

Next Steps

The PlayerControlSystem function exists, but just like our other systems, it sits dormant until it is called. The next and final task of this step is to integrate it into the main game loop. You will call player_control_system from within Game::tick, orchestrating it with the other systems to create a fully interactive experience.


Further Reading

  • The Bevy Book: Systems (Revisited): It’s always great to reinforce core concepts. See how systems are the “verbs” that drive your game’s logic.
  • The Bevy Book: Accessing Resources in Systems: The official guide on how systems can interact with global resources like our InputState.
  • The Bevy Book: Query Filtering: A more advanced look at how you can filter queries, similar to how we used the Player tag.
  • The Rust Book: if Expressions: A refresher on the basic control flow construct that powers our system’s logic.

Implement Player Control Logic with ECS Queries

Mục tiêu: Implement the core logic for the PlayerControlSystem in Rust. This involves querying the ECS world for the player entity using its Velocity and Player components, and then modifying its velocity based on the current InputState resource to enable keyboard-controlled movement.


In our previous task, we established the complete blueprint for our PlayerControlSystem, including tagging the player entity and setting its initial velocity to zero. We now have a function that is ready to be filled with the core logic that translates player intent into on-screen action. This task is where we bring that function to life.

We will now dive deep into the implementation within the PlayerControlSystem, focusing on two critical operations:

  1. Querying: How we ask the ECS World to find the specific player entity amongst all other potential game objects.
  2. Modifying: How we read the InputState resource and use it to precisely change the player entity’s Velocity component.

This is the very heart of player interaction in our engine.

Inside the PlayerControlSystem: Query and Logic

Let’s revisit the complete player_control_system function. While you’ve seen this code before, we will now analyze it with a specific focus on the query and the velocity modification logic, which is the core of our current task.

// src/lib.rs

// ... (log function, movement_system, rendering_system are unchanged) ...

// We define a constant for the player's movement speed in pixels per frame.
// Using a constant at the top of the file makes it very easy to find and
// adjust the game's speed to get the right "feel".
const PLAYER_SPEED_PER_FRAME: f32 = 3.0;

// This system connects player input to player movement.
pub fn player_control_system(world: &mut World) {
    // First, we get read-only access to the `InputState` resource. We use `if let`
    // to safely unwrap the Option returned by `get_resource`.
    if let Some(input_state) = world.get_resource::<InputState>() {

        // --- QUERYING FOR THE PLAYER --- //
        // This is the "seek" phase of our system. We are asking a very specific question:
        // "Give me every entity that has both a Velocity component I can change, and a Player tag."
        // - `&mut Velocity`: The `&mut` signifies we need mutable (write) access. This is
        //   essential because the entire point of this system is to *change* the velocity.
        // - `&Player`: The `&` signifies we only need immutable (read) access. We are not
        //   changing the Player tag; its presence here acts as a powerful filter. The query
        //   will completely ignore any entity that doesn't have this tag.
        let mut query = world.query::<(&mut Velocity, &Player)>();

        // We execute the query and iterate over every entity that matched.
        // In our game, this loop will only ever run once because we only have one player.
        // `(mut velocity, _player)`: We destructure the results. `velocity` is mutable.
        // We prefix `player` with an underscore (`_player`) to signal to the Rust compiler
        // that we needed it for the query filter but won't be using the variable in the loop.
        for (mut velocity, _player) in query.iter_mut(world) {

            // --- MODIFYING THE VELOCITY COMPONENT --- //
            // This is the "act" phase of our system.

            // First, we reset the velocity to zero at the start of every frame.
            // This is a crucial step! Without it, if you pressed 'right' and then released it,
            // the velocity would remain positive, and the player would slide forever.
            // This ensures movement only happens when a key is actively held down.
            velocity.dx = 0.0;
            velocity.dy = 0.0;

            // Now, we check each boolean flag in the `InputState` resource.
            // This series of `if` statements allows for independent horizontal and
            // vertical movement, which correctly results in diagonal motion when
            // two keys (e.g., up and right) are pressed simultaneously.
            if input_state.left_pressed {
                velocity.dx = -PLAYER_SPEED_PER_FRAME;
            }
            if input_state.right_pressed {
                velocity.dx = PLAYER_SPEED_PER_FRAME;
            }
            if input_state.up_pressed {
                // The canvas coordinate system has Y=0 at the TOP of the screen.
                // To move our entity "up", we must *decrease* its Y coordinate.
                velocity.dy = -PLAYER_SPEED_PER_FRAME;
            }
            if input_state.down_pressed {
                velocity.dy = PLAYER_SPEED_PER_FRAME;
            }
        }
    }
}

Deconstructing the Implementation

The Power of a Specific Query

The line world.query::<(&mut Velocity, &Player)>() is a perfect example of the expressive power of an ECS. In a single line, you have defined a precise “view” into your game world’s data. You are not just getting a list of “game objects”; you are getting direct, type-safe access to the exact pieces of data you need for a specific piece of logic, for only the entities that are relevant to that logic. Using the Player tag component as a filter is a clean, performant, and scalable pattern that you will use throughout your engine’s development.

The State-Driven Logic

The series of if statements that modify the velocity demonstrate the core benefit of the input-handling model we’ve built. The player_control_system doesn’t care about events like keydown. It only cares about the current state of the keyboard, as recorded in the InputState resource. This “state polling” approach makes logic for continuous actions like movement incredibly simple and robust. On every frame, the system looks at the current input state and sets the velocity accordingly. This is a far cleaner approach than trying to manage timers or flags within event callbacks.

You have now created the final, crucial piece of the input-handling puzzle. This system acts as the brain, translating the raw sensory data from the InputState into concrete commands for the player’s body, its Velocity component.

Next Steps

The PlayerControlSystem is fully implemented and ready for action, but like a brilliant actor, it’s still waiting backstage. It exists, but it is not yet being called. In the final task of this step, you will integrate this new system into the main game loop. You will call player_control_system from within the Game::tick method, placing it in the correct order relative to the MovementSystem and RenderingSystem to create a seamless, fully interactive experience.


Further Reading

To deepen your understanding of the concepts you’ve just put into practice, these resources are highly recommended.

  • The Bevy Book: Query Filtering: A more advanced look at how you can filter queries using components, just as we did with the Player tag.
  • Game Programming Patterns: Input: An excellent chapter on various ways to handle player input in games, reinforcing the “polling” vs. “event-driven” concepts.
  • The Rust Book: if Expressions: A refresher on the basic control flow construct that powers our system’s core logic.