Add wasm-bindgen-futures for Asynchronous Web APIs

Mục tiêu: Add the wasm-bindgen-futures crate to the project’s Cargo.toml file. This dependency is essential for bridging the gap between JavaScript’s asynchronous Promises and Rust’s Futures, enabling the handling of async operations like loading images.


Fantastic work completing the input handling system! You’ve successfully bridged the gap between the player and the game world. Your white square is no longer a passive object on a pre-determined path; it’s an entity that responds to your commands. This is a massive milestone. You now have a truly interactive application.

The next evolutionary step for any game engine is to move beyond simple, programmatically drawn shapes and into the rich, visual world of sprites. We want to render characters, spaceships, and environments from image files. This introduces a new and fundamental challenge of web development: asynchronicity.

The Asynchronous Nature of the Web

When our Rust code runs, it wants to execute instructions one after another, synchronously. However, when we ask the browser to fetch an image from a server (even a local one), it can’t just stop everything and wait. Halting the browser’s main thread would freeze the entire web page, leading to a terrible user experience.

Instead, web APIs are asynchronous. When you request an image, the browser starts the download in the background and immediately gives you back a placeholder—a promise that the data will arrive eventually. In JavaScript, this placeholder is an object called a Promise.

Our Rust code, running in WebAssembly, needs a way to interact with these JavaScript Promises. We want to write code that says, “start loading this image, and once it’s finished, let me know so I can use it,” without freezing our game loop. To do this, we need a bridge between Rust’s own asynchronous concept, called a Future, and JavaScript’s Promise.

This bridge is a crate called wasm-bindgen-futures. Its entire job is to provide functions that convert a Promise into a Future, allowing us to use Rust’s powerful async/await syntax to write clean, non-blocking code that works seamlessly with the web platform.

Adding the Dependency

Our first task is to add this essential tool to our project’s toolbox. We do this by declaring it as a dependency in our Cargo.toml file. Cargo, Rust’s build system and package manager, will see this entry, download the crate from the central repository (crates.io), and link it into our project during compilation.

Open your Cargo.toml file at the root of your project and find the [dependencies] section. Add the line for wasm-bindgen-futures.

# Cargo.toml

[package]
name = "your-project-name" # This will be your project's name
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
# Existing dependencies
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = [
    'CanvasRenderingContext2d',
    'Document',
    'Element',
    'HtmlCanvasElement',
    'Window',
    'KeyboardEvent', # You added this in the last step
]}
js-sys = "0.3"
bevy_ecs = "0.12" # Or your current version
console_error_panic_hook = { version = "0.1.7", optional = true }

# --- NEW CODE STARTS HERE ---

# This crate provides a bridge between Rust's `Future`s and JavaScript's `Promise`s.
# It is essential for handling any asynchronous web APIs from within Rust, such
# as fetching files, making network requests, or waiting for timers.
wasm-bindgen-futures = "0.4"

# --- NEW CODE ENDS HERE ---

A Quick Note on Versions

I’ve specified version 0.4 for wasm-bindgen-futures. While using "0.4" is common, for production projects, it’s best practice to use a more specific version like "0.4.40" to ensure your builds are reproducible and don’t break unexpectedly when a new, potentially incompatible minor version is released. For this learning project, "0.4" is perfectly fine.

After adding this line and saving the file, the next time you run wasm-pack build, Cargo will automatically handle the download and compilation of wasm-bindgen-futures for you.

You have now equipped your project with the foundational tool needed to handle any asynchronous operation the browser can throw at it. We haven’t written any code that uses it yet, but the capability is now there, ready for us to leverage.

Next Steps

With the tool for asynchronicity in place, our next task is to create the structure that will manage our assets once they are loaded. We will create a new ECS Resource called AssetManager. This will act as a central repository—a cache—where we can store our loaded images so that our RenderingSystem can easily access them by name.


Further Reading

  • The wasm-bindgen-futures Crate: The official page for the crate on crates.io. It’s always good to see the source of your dependencies.
  • The Rust and WebAssembly Book: Futures: The official documentation on how wasm-bindgen-futures works to bridge the gap between Rust Futures and JS Promises.
  • MDN Web Docs: Using Promises: To understand what’s happening on the JavaScript side, there is no better resource than the Mozilla Developer Network. Understanding Promises is key to modern web development.
  • The Rust Book: Asynchronous Rust: A chapter from the official Rust book explaining the async/.await syntax and the concept of Futures within Rust itself.

Create an AssetManager resource to store loaded assets (e.g., in a HashMap).

Mục tiêu:


Excellent work! By adding the wasm-bindgen-futures crate to your project, you’ve equipped our engine with the fundamental tool for handling the asynchronous nature of the web. This was a crucial preparatory step. Before we can start fetching an image, which is an asynchronous operation, we need a well-defined place to store it once it has arrived. We don’t want to load the same image from the server every single time we need to draw it; that would be incredibly inefficient.

This brings us to the concept of an asset cache. We need a central, global repository within our engine that will hold all loaded assets. We can load an image once when the game starts, store it in this repository, and then every system that needs to draw that image can simply ask the repository for it. This is precisely the job for our new AssetManager resource.

The Role of the AssetManager

The AssetManager will act as our engine’s digital warehouse. Its responsibilities are simple but vital:

  1. Store Assets: It will hold onto loaded assets, like images, so they are always available in memory.
  2. Provide Access: It will allow other parts of our engine, primarily the RenderingSystem, to retrieve these assets by a unique name.
  3. Prevent Redundancy: By caching assets, it ensures we only perform the expensive operation of loading and decoding an image from the network once.

To implement this, we’ll use one of Rust’s most useful data structures: the HashMap. A HashMap is a key-value store, which is perfect for our needs. We can use a String (like the image’s filename, e.g., "player_ship.png") as the key, and the loaded image object itself as the value.

Just like our InputState, the AssetManager will be a unique, global piece of data, making it a perfect candidate for an ECS Resource.

Implementing the AssetManager

Let’s create this new resource. We’ll define it in our src/resources.rs file.

First, however, we need to tell web-sys that we intend to use the HtmlImageElement type, which is the Rust representation of an <img> tag in the browser.

Open your Cargo.toml file and add 'HtmlImageElement' to the list of features for web-sys.

# Cargo.toml

# ... (other sections are unchanged) ...

[dependencies]
# ... (other dependencies are unchanged) ...
web-sys = { version = "0.3", features = [
    'CanvasRenderingContext2d',
    'Document',
    'Element',
    'HtmlCanvasElement',
    'Window',
    'KeyboardEvent',
    'HtmlImageElement', # <-- ADD THIS LINE
]}
# ... (the rest of the dependencies are unchanged) ...

Now, with that prerequisite handled, open src/resources.rs and add the AssetManager struct.

// src/resources.rs

use bevy_ecs::prelude::Resource;
use std::collections::HashMap; // Import the HashMap data structure
use web_sys::HtmlImageElement; // Import the type for our loaded images

// ... (InputState struct is unchanged) ...

// The AssetManager will be a resource that holds all our loaded image assets.
// We derive `Resource` so bevy_ecs knows how to manage it, and `Default`
// to easily create an empty instance.
#[derive(Resource, Default)]
pub struct AssetManager {
    // We use a HashMap to store our assets.
    // The key is a `String` that we'll use to identify the asset (e.g., its path).
    // The value is an `HtmlImageElement`, the object that the browser uses to
    // represent a loaded image, which we can then draw to a canvas.
    pub assets: HashMap<String, HtmlImageElement>,
}

Finally, just as we did for InputState, we need to create an instance of our AssetManager and insert it into the World when the game starts. This ensures the resource is available for all systems to use.

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

// src/lib.rs

// ... (all use statements and systems are unchanged) ...

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

        let mut world = World::new();

        // We insert the InputState resource.
        world.insert_resource(InputState::default());

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

        // We also insert our new AssetManager resource.
        // `AssetManager::default()` will create a new instance with an empty HashMap,
        // ready for us to start loading assets into it.
        world.insert_resource(AssetManager::default());

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

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

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

    // ... (tick method is unchanged) ...
}

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

Deconstructing the Code

  • use std::collections::HashMap;: This line brings Rust’s standard hash map implementation into scope, allowing us to use it in our struct.
  • use web_sys::HtmlImageElement;: This imports the Rust binding for the browser’s HTMLImageElement object. This is the type that will hold our actual image data after it’s been loaded.
  • #[derive(Resource, Default)]: This familiar macro duo does two things: it marks AssetManager as a valid ECS resource and provides the convenient AssetManager::default() constructor, which creates an AssetManager with an empty HashMap.
  • world.insert_resource(AssetManager::default());: This line, placed in our setup function, creates the one-and-only instance of our asset manager and places it into the World’s resource storage, making it globally accessible.

You have now successfully created an empty, but fully functional, asset management structure within your engine. The “warehouse” is built and ready; the loading docks are open.

Next Steps

Our AssetManager exists, but its HashMap is empty. The next logical step is to begin the process of filling it. In the upcoming task, you will use web-sys to programmatically create a new HtmlImageElement in memory. This is the equivalent of creating an <img> tag in HTML, but entirely within our Rust code, setting the stage for us to tell the browser what image file to load into it.


Further Reading

  • The Rust Book: Storing Keys with Associated Values in Hash Maps: The definitive guide to using HashMap in Rust. Understanding its API will be very helpful.
  • The Bevy Book: Resources (Revisited): It’s always a good idea to refresh your memory on how ECS resources work, as they are a cornerstone of engine architecture.
  • MDN Web Docs: HTMLImageElement: The JavaScript documentation for the object that web_sys::HtmlImageElement represents. Understanding its properties (src, onload) will be vital for the next tasks.

Use web-sys to create a new HtmlImageElement.

Mục tiêu:


You’ve set up the perfect foundation for asset management. By creating the AssetManager resource and inserting it into the World, you’ve built the “warehouse” where our engine will store all its visual assets. This warehouse is currently empty, but it’s ready to receive its first shipment.

Our current task is to begin the process of acquiring an asset. The very first step in loading an image from a file is to create an object in memory that can hold that image’s data. In the browser’s ecosystem, this object is an HTMLImageElement, the same kind of object that an <img> tag in an HTML file creates. Using web-sys, we can create these elements programmatically from our Rust code, without ever touching the DOM directly.

From Rust to the DOM: Creating an Image Element

Think of this step as the Rust equivalent of writing new Image() in JavaScript. We are instructing the browser to create a new, blank image element. This element doesn’t appear on the screen and doesn’t contain any image data yet. It is simply an empty container, a placeholder, waiting for us to tell it what image to load.

We will perform this action inside our Game::new() function. This is a good place for one-time setup logic that needs to run when the game starts. In a more advanced engine, you might have a dedicated “loading state,” but for now, initiating the load at startup is a clean and simple approach.

First, ensure you have the HtmlImageElement type in scope by adding a use statement at the top of src/lib.rs.

// src/lib.rs

// ... (other use statements)
use web_sys::{HtmlImageElement, KeyboardEvent}; // Add HtmlImageElement here if not present
// ...

Now, let’s add the code to create the element inside Game::new().

// src/lib.rs

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

impl Game {
    pub fn new() -> Self {
        // ... (getting canvas and context, creating world, inserting resources is unchanged) ...
        world.insert_resource(InputState::default());
        world.insert_resource(AssetManager::default());

        // --- HIGHLIGHTED CHANGES START ---

        // In a real engine, asset loading would be a more complex process, perhaps
        // driven by a configuration file or a dedicated loading state. For now,
        // we'll begin the process of loading a single, hardcoded asset right here.

        // Step 1: Create the HtmlImageElement in memory.
        // This is the Rust equivalent of JavaScript's `const image = new Image();`.
        // It creates an image element object that exists in the browser's memory
        // but is not attached to the visible DOM. It doesn't contain any pixel data yet.
        // The `new()` function can fail in some rare browser environments, so it
        // returns a `Result`. We use `.expect()` here for simplicity; this will get
        // the `HtmlImageElement` out of the `Ok` variant or panic if it fails.
        let image = HtmlImageElement::new().expect("Could not create HtmlImageElement");

        // --- HIGHLIGHTED CHANGES END ---

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

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

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

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

Deconstructing the Code

Let’s break down this important new line:

  • HtmlImageElement::new(): This is a static method (a function associated with a type, not an instance) provided by web-sys. It directly calls the underlying JavaScript new Image() constructor.
  • -> Result<HtmlImageElement, JsValue>: The new() function doesn’t return an HtmlImageElement directly. It returns a Result. This is a standard Rust enum that represents either success (Ok(value)) or failure (Err(error)). In this case, if the image element is created successfully, the function returns Ok(HtmlImageElement). If something goes wrong (which is rare but possible), it returns an Err(JsValue) containing a JavaScript error object. This is a core principle of Rust: functions that can fail must make that possibility explicit in their return type, forcing the programmer to handle it.
  • .expect("Could not create HtmlImageElement"): This is a convenience method on Result. It’s a simple way to handle a Result when you believe a failure is unrecoverable. If the Result is Ok, expect will unwrap it and give you the HtmlImageElement inside. If the Result is Err, the program will immediately panic (a controlled crash) and print the message you provided. For our engine, failing to create a basic image element is a critical error, so panicking is an acceptable strategy.

You have now successfully created an empty image container in the browser’s memory, controlled entirely by your Rust code. The variable image now holds a Rust-friendly handle to this browser object.

Next Steps

Our in-memory image element exists, but it’s a blank canvas. It has no idea what picture it’s supposed to represent. The next logical and crucial step is to assign a URL to its src attribute. This is the action that will trigger the browser to start downloading the actual image file from the server in the background.


Further Reading

  • MDN Web Docs: Image() constructor: The ultimate JavaScript reference for the new Image() constructor, which is exactly what web-sys is calling under the hood.
  • The Rust Book: Recoverable Errors with Result: The definitive chapter on Rust’s primary error handling mechanism. Understanding Result is absolutely essential for writing robust Rust programs.
  • web_sys Docs: HtmlImageElement: The official Rust documentation for the HtmlImageElement type, showing all the properties and methods you can call on it from your Rust code.

Trigger Asynchronous Image Loading for a Sprite

Mục tiêu: Learn how to initiate the asynchronous download of a sprite image by setting the src attribute on an HtmlImageElement from your Rust and WebAssembly code using web-sys.


You’ve done an excellent job of setting up the in-memory container for our future sprite. In the last task, you programmatically created an HtmlImageElement using web-sys. Right now, this element is like a blank picture frame hanging in the browser’s memory—it exists, it has dimensions and properties, but it contains no picture.

Our current task is to give this empty frame its purpose. We need to tell it what image to display. In the world of web browsers, this is done by setting the element’s src (source) attribute. This single action is the trigger that tells the browser, “Go to this web address, download the image you find there, and prepare it for display.”

Kicking Off the Asynchronous Download

Setting the src attribute is a fundamentally asynchronous operation. When our Rust code executes the line to set the source, it does not pause and wait for the image to download. That could take milliseconds or even seconds on a slow connection, and pausing would freeze our entire game loop.

Instead, the browser’s rendering engine receives the instruction and immediately starts the download in a background thread. Our Rust code continues executing without missing a beat. This is the heart of non-blocking I/O on the web, and it’s a critical concept to master. Our job is simply to provide the URL and let the browser handle the complex networking details.

A Place for Our Assets

Before we can set the src path, we need an actual asset to point to.

  1. At the root level of your project (in the same directory as Cargo.toml and your src folder), create a new directory named assets.
  2. Find or create a small PNG image for your player. A simple spaceship or even a uniquely colored square will do. For this guide, we’ll assume the image is named player_ship.png.
  3. Place player_ship.png inside the new assets directory.
  4. Make sure your local web server (e.g., python3 -m http.server) is running from the root of your project directory so it can find and serve files from the assets folder. If it was running before you created the folder, you might need to restart it.

Setting the src from Rust

With our asset in place, let’s add the code to Game::new() that sets the src property on the HtmlImageElement we created in the previous task. The web-sys crate provides a simple setter method, set_src(), for this purpose.

Open src/lib.rs and add the highlighted line:

// src/lib.rs

// ... (all other code remains unchanged)

impl Game {
    pub fn new() -> Self {
        // ... (getting canvas and context, creating world, inserting resources is unchanged) ...
        world.insert_resource(InputState::default());
        world.insert_resource(AssetManager::default());

        // This line is from the previous task. It creates our in-memory image element.
        let image = HtmlImageElement::new().expect("Could not create HtmlImageElement");

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

        // Step 2: Set the `src` attribute on the image element.
        // This is the critical action that triggers the browser to start fetching the
        // image data from the provided URL in the background. The path is relative
        // to the `index.html` file.
        // The browser's networking stack will handle the download asynchronously,
        // so our Rust code does not block or wait here. It continues execution immediately.
        image.set_src("assets/player_ship.png");

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

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

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

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

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

Deconstructing the Code

This single line of code is a powerful bridge between our Rust logic and the browser’s core functionality.

  • image.set_src(...): This is the web-sys method that binds to the JavaScript property assignment image.src = .... It takes a Rust string slice (&str) as input, which wasm-bindgen transparently converts into a JavaScript string for the browser API.
  • "assets/player_ship.png": This is the URL. Since our index.html is at the root of the server, a path starting with assets/ correctly points into the directory you just created. The browser will now issue an HTTP GET request for http://localhost:8000/assets/player_ship.png.

The download has now been initiated. The browser is off fetching the pixels for our player ship. If you were to run the code and check the “Network” tab in your browser’s developer tools, you would see the request for this image file. However, our Rust code currently has no idea when this download will finish. The image object is still just a placeholder; it does not contain the usable pixel data yet.

Next Steps

We’ve successfully kicked off the loading process, but this is only half the story. We need a mechanism to be notified when the download is complete and the image is fully decoded and ready to be used. In the next task, you will learn how to attach an onload callback to the image element. This callback is a function that the browser will automatically execute at the exact moment the image becomes ready, allowing us to finally store the loaded asset in our AssetManager.


Further Reading

  • MDN Web Docs: HTMLImageElement.src: The definitive documentation for the src property. Understanding this is fundamental to working with images on the web.
  • web_sys Docs: HtmlImageElement::set_src: The official Rust documentation for the exact method you just used.
  • How Browsers Work: A comprehensive article on the inner workings of a web browser, including how it parses HTML and initiates resource fetching.

Handle Asynchronous Image Loading with an onload Callback

Mục tiêu: Refactor asset loading logic to the run function. Implement an onload event listener using wasm\_bindgen::Closure to store the loaded HtmlImageElement in the AssetManager resource once it’s available.


You have skillfully initiated the asset loading pipeline. By creating an HtmlImageElement and setting its src attribute, you have commanded the browser to fetch our player’s sprite from the server. This download is happening silently in the background, but our Rust code is currently blind to its progress. We’ve sent a message out into the asynchronous world of the web, and now we need a way to receive the reply.

This is where the onload event comes in. The browser, after successfully downloading and decoding the image, will fire an onload event on the image element. It’s the browser’s way of saying, “Your package has arrived! The image at assets/player_ship.png is fully loaded and ready for use.” Our task is to attach a listener—a callback function—to this event, so our code can spring into action the moment the image is ready.

The Challenge of Asynchronous State Modification

Our goal is simple: once the image is loaded, we want to take the HtmlImageElement object and store it in our AssetManager’s HashMap. However, this presents a classic asynchronous challenge:

  1. The onload callback function will execute at some unknown time in the future.
  2. This callback needs mutable access to the AssetManager resource, which is stored inside the World, which is managed by our Game struct.
  3. The Game struct is wrapped in an Rc<RefCell<T>> in our run function to allow it to be shared safely between the main game loop and callbacks.

This means the logic for setting up the image loading and its callback cannot live inside Game::new(). The new function is synchronous and runs before the Rc<RefCell<Game>> is created. The correct place for this one-time, asynchronous setup is the run function itself, where we have access to the shared, mutable game state.

Let’s refactor our code to move the asset loading logic to the run function and attach our onload callback.

Implementing the onload Callback

First, we’ll remove the image creation lines from Game::new(). This keeps our constructor clean and synchronous.

// src/lib.rs

// ... in the `impl Game` block ...
pub fn new() -> Self {
    // ... (getting canvas and context is unchanged) ...
    let mut world = World::new();

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

    // REMOVE the image creation and set_src lines from here.

    world.spawn((
        // ... (spawning the entity is unchanged) ...
    ));

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

Now, let’s add the complete asset loading logic to our run function. This is where we’ll use wasm_bindgen::closure::Closure once again, just like we did for the keyboard events, to bridge the gap between Rust and JavaScript’s event-driven world.

// src/lib.rs

// Make sure you have this `use` statement at the top.
use web_sys::{HtmlImageElement, KeyboardEvent};

// ...

#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
    let game = Rc::new(RefCell::new(Game::new()));

    // --- NEW ASSET LOADING LOGIC STARTS HERE ---
    {
        // Create a clone of the Rc<RefCell<Game>> to move into our onload closure.
        // This allows the closure to access the game state.
        let game_clone = game.clone();

        // Step 1: Create the HtmlImageElement in memory.
        let image = HtmlImageElement::new().expect("Could not create HtmlImageElement");

        // Step 2: Create the `onload` callback using `Closure::wrap`.
        let onload = Closure::<dyn FnMut()>::new(move || {
            // This code will run AFTER the image has finished loading.

            // Get mutable access to the game state.
            let mut game = game_clone.borrow_mut();

            // Get mutable access to the AssetManager resource.
            if let Some(mut asset_manager) = game.world.get_resource_mut::<AssetManager>() {
                log("Image loaded, inserting into AssetManager.");

                // Insert the now-loaded image into the asset manager's HashMap.
                // We use the same path as the key, so we can retrieve it later.
                // The `move` on the closure gave it ownership of `image`.
                asset_manager.assets.insert(
                    String::from("assets/player_ship.png"),
                    image,
                );
            }
        });

        // Step 3: Assign the closure to the `onload` property of the image element.
        // `set_onload` expects an `Option<&Function>`. We use `Some` and cast our
        // closure to a generic function reference.
        image.set_onload(Some(onload.as_ref().unchecked_ref()));

        // Step 4: Tell Rust to "forget" about the closure.
        // If we don't do this, `onload` would be dropped at the end of this scope,
        // and our callback would point to invalid memory. This is crucial for
        // long-lived event listeners.
        onload.forget();

        // Step 5: Set the `src` attribute. THIS MUST BE DONE *AFTER* SETTING `onload`.
        // If the image is already cached by the browser, the `onload` event can
        // fire immediately. If we haven't set our listener yet, we'll miss it.
        image.set_src("assets/player_ship.png");
    }
    // --- NEW ASSET LOADING LOGIC ENDS HERE ---

    // The rest of the `run` function (keyboard listeners and game loop setup) remains the same.
    let f = Rc::new(RefCell::new(None));
    // ... (rest of the function is unchanged)

    Ok(())
}

Deconstructing the Code

This is a powerful pattern for handling asynchronous events in Wasm.

  • Refactoring to run(): We moved the logic to run() because it’s the only place where we have the Rc<RefCell<Game>>, which is essential for safely sharing mutable access to the World with a future callback.
  • let game_clone = game.clone(): This is the standard pattern for sharing Rc data. We create a new cheap reference-counted pointer to give to the closure.
  • Closure::<dyn FnMut()>::new(move || { ... }): We create a Closure that takes no arguments (FnMut()). The move keyword is critically important here. It forces the closure to take ownership of the variables it uses from its environment, namely game_clone and image. This ensures these variables are still alive when the callback eventually runs.
  • asset_manager.assets.insert(...): Inside the callback, we perform the final action. We get mutable access to our AssetManager resource and use the standard HashMap::insert method to store the loaded image object, using its path as the lookup key.
  • image.set_onload(...): This web-sys method attaches our Rust closure (wrapped to look like a JS function) to the image’s onload event handler.
  • Order of Operations: It’s a subtle but vital best practice to set the .onload handler before setting the .src. If an image is in the browser’s cache, the load can be almost instantaneous. Setting .src first could cause the onload event to fire before you’ve attached your listener, and you would miss it entirely.

You have now successfully implemented a complete, albeit simple, asynchronous asset loading pipeline. You command the browser to fetch an image, and you’ve provided a callback that safely stores the result in your ECS World once it’s ready.

Next Steps

While using closures for callbacks works perfectly, it can become cumbersome when you need to load multiple assets or chain asynchronous operations. A more modern and idiomatic way to handle this in Rust is by using async/await syntax with Futures. The next task is to take this working onload callback and convert the entire process into a Rust Future. This will make your asynchronous code cleaner, more readable, and easier to manage as your engine grows in complexity.


Further Reading

  • MDN Web Docs: GlobalEventHandlers.onload: The definitive web documentation for the onload event, which applies to images, scripts, and the window itself.
  • The Rust Book: move Closures: An essential chapter explaining how the move keyword changes a closure’s relationship with the data it captures from its environment.
  • The wasm-bindgen Book: Closures (Revisited): It’s always a good idea to refresh your memory on this fundamental tool for Rust-JS interop.
  • web_sys Docs: HtmlImageElement::set_onload: The specific API documentation for the method you used to attach the event handler.

Refactor Image Loading to use Async/Await in Rust Wasm

Mục tiêu: Refactor the existing callback-based image loading logic to use the modern async/await syntax. This involves creating an async function that wraps a JavaScript Promise in a Rust Future using wasm-bindgen-futures to create more readable and maintainable asynchronous code.


You’ve successfully established a complete, working pipeline for loading an image from the server into your game engine. By attaching an onload callback using wasm_bindgen::closure::Closure, you’ve tamed the asynchronous nature of the web and can now reliably get a loaded HtmlImageElement into your AssetManager. This is a fantastic achievement and a common pattern in Wasm development.

However, as you might imagine, if we needed to load ten, twenty, or a hundred assets, this callback-based approach could become difficult to manage. We would have nested callbacks or complex state machines to track what has finished loading. This is often nicknamed “callback hell.”

Fortunately, modern Rust, in concert with wasm-bindgen-futures, provides a much more elegant and powerful solution: the async/await syntax. This brings us to our current task: to refactor our working callback logic into an idiomatic Rust Future. The goal is to make our asynchronous code read almost like synchronous, top-to-bottom code, which is easier to write, understand, and maintain.

From Callbacks to Futures

A Rust Future is a type that represents a value that may not be ready yet. It’s the direct conceptual equivalent of a JavaScript Promise. It’s a placeholder for a result that will be computed in the background.

The async/await syntax is the magic that makes working with Futures feel effortless: * An async fn is a special kind of function that, instead of returning a value directly, returns a Future that will eventually resolve to that value. * The .await keyword can be used inside an async fn to pause the function’s execution until a Future has resolved. While paused, it yields control back to the event loop, so the browser doesn’t freeze.

We will create an async function to handle our image loading. Inside it, we’ll create a JavaScript Promise that resolves when the image’s onload event fires. Then, we’ll use wasm-bindgen-futures to convert that Promise into a Rust Future that we can .await.

Implementing the async Asset Loader

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

// src/lib.rs

// ... (existing use statements)
// Add these new 'use' statements for our async logic
use js_sys::Promise;
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{HtmlImageElement, KeyboardEvent};
// ...

Next, let’s create our new async function. We can place this right above the run function. This function will contain all the logic for loading a single image.

// src/lib.rs

// ... (after your systems and before the run function)

// This is an `async` function. It will not execute immediately when called.
// Instead, it returns a `Future`. This Future must be given to an "executor"
// (like `spawn_local`) to be run.
async fn load_image(src: &str) -> Result<HtmlImageElement, JsValue> {
    // We create a new JavaScript `Promise` which we will resolve or reject
    // inside our `onload` and `onerror` callbacks.
    let promise = Promise::new(&mut |resolve, reject| {
        // Create the HtmlImageElement.
        let image = HtmlImageElement::new().unwrap();

        // --- ONLOAD CALLBACK ---
        // When the image loads successfully, we want to resolve the promise.
        let onload = Closure::<dyn FnMut()>::new(move || {
            // The first argument to `resolve` is the value the promise will fulfill with.
            // We pass a reference to our image element.
            resolve.call1(&js_sys::global(), &image).unwrap();
        });
        image.set_onload(Some(onload.as_ref().unchecked_ref()));
        // We must `forget` the closure to keep it alive.
        onload.forget();

        // --- ONERROR CALLBACK ---
        // If the image fails to load, we want to reject the promise.
        let onerror = Closure::<dyn FnMut(_)>::new(move |err| {
            // The first argument to `reject` is the error.
            reject.call1(&js_sys::global(), &err).unwrap();
        });
        image.set_onerror(Some(onerror.as_ref().unchecked_ref()));
        onerror.forget();

        // Set the src to trigger the download.
        image.set_src(src);
    });

    // Convert the JavaScript `Promise` into a Rust `Future`.
    let future = JsFuture::from(promise);

    // `await` the future. This will pause the `async fn` until the promise
    // is resolved or rejected.
    let result = future.await?;

    // `JsFuture` resolves to a `Result<JsValue, JsValue>`.
    // We expect the success value to be our `HtmlImageElement`, so we cast it.
    Ok(result.dyn_into::<HtmlImageElement>()?)
}

Now that we have this powerful, reusable async function, we can dramatically simplify our run function. We will replace the entire manual Closure-based loading block with a call to spawn_local.

spawn_local is the executor provided by wasm-bindgen-futures. It takes a Future and tells the browser’s event loop to run it.

// src/lib.rs

#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
    let game = Rc::new(RefCell::new(Game::new()));

    // --- REFACTORED ASSET LOADING LOGIC ---

    // `spawn_local` takes an `async` block or function (a Future) and runs it.
    // This happens in the background, not blocking the rest of the setup.
    spawn_local(async move {
        // Here we call our `async` function to load the image.
        // The `.await` keyword pauses execution *within this async block*
        // until the image is loaded, but it doesn't block the main thread.
        let image_src = "assets/player_ship.png";
        let image = load_image(image_src).await.unwrap();

        log("Image loaded, inserting into AssetManager.");

        // Now that we have the loaded image, get the AssetManager and insert it.
        // This is the same logic that was previously inside the `onload` callback.
        let mut game = game.borrow_mut();
        if let Some(mut asset_manager) = game.world.get_resource_mut::<AssetManager>() {
            asset_manager
                .assets
                .insert(String::from(image_src), image);
        }
    });

    // The rest of the `run` function (keyboard listeners and game loop setup) remains the same.
    let f = Rc::new(RefCell::new(None));
    // ... (rest of the function is unchanged)

    Ok(())
}

Deconstructing the Code

This refactor is a huge leap forward in code quality. Let’s review the key new concepts:

  • async fn load_image(...): We’ve encapsulated the messy details of setting up callbacks inside a clean, reusable async function. It returns a Result wrapped in a Future, making it clear that this operation can fail and is asynchronous.
  • Promise::new(...): We manually create a JavaScript Promise. This gives us resolve and reject functions that we can call from our Rust closures, signaling the success or failure of the asynchronous operation.
  • JsFuture::from(promise): This is the core bridge from wasm-bindgen-futures. It takes a JavaScript Promise and wraps it in a Rust type that implements the Future trait.
  • .await: This is the magic keyword. When the code hits load_image(...).await, it effectively says, “Pause this async block right here, go do other things (like run the game loop), and come back to this line only when the image has finished loading or failed.”
  • spawn_local(...): This is our entry point into the async world. It’s the “starter pistol” that takes our top-level async block and hands it off to the JavaScript event loop to be executed.

Look at the new run function. The logic is now crystal clear: we spawn a task that loads an image and then inserts it into the asset manager. There are no more .forget() calls visible in our main setup logic and no more nested callbacks. If we wanted to load ten images, we could do so cleanly within this single async block.

Next Steps

Congratulations! You have refactored your asset loading to use modern, idiomatic asynchronous Rust. This is a robust and scalable pattern that will serve you well as your engine grows.

With our image successfully loaded and cached in the AssetManager, our next task is to use it. We will create a new Sprite component to replace our old Renderable for any entity that should be drawn with an image. This component will store the information our RenderingSystem needs to find the correct image in the AssetManager.


Further Reading

Create a Sprite Component for Image Rendering

Mục tiêu: Define a new Sprite component in Rust to handle image-based rendering. Update the player entity to use this new component, replacing the previous Renderable component used for primitive shapes.


Incredible work on refactoring your asset loading logic! By embracing Rust’s async/await syntax and the Future trait, you’ve created a clean, modern, and scalable foundation for handling all future asynchronous operations. Your run function now clearly expresses its intent: “in the background, load this image and put it in the asset manager.” The complex details of callbacks and promises are neatly encapsulated in the load_image helper function.

The image assets/player_ship.png is now being successfully loaded and placed into the AssetManager resource, ready and waiting in your ECS World. The final piece of the puzzle is to tell our entities how to use these loaded assets.

Our current Renderable component is excellent for drawing primitive shapes like rectangles and circles, as it stores color and shape information. However, an entity that should be drawn using an image needs a different set of instructions. It doesn’t need a color; it needs to know which image to use from our AssetManager. Trying to cram this information into the Renderable component would make it messy and confusing. The correct, clean ECS approach is to create a new component specifically for this purpose.

From Primitive to Picture: The Sprite Component

We will now define a new Sprite component. This component will serve as a “data packet” that attaches to an entity and tells the RenderingSystem everything it needs to know to draw it using a texture from the AssetManager.

A Sprite component needs to contain two key pieces of information:

  1. An Identifier: A way to look up the correct image in the AssetManager’s HashMap. The most logical identifier is the asset’s path, the same String we used as the key when we inserted it. We’ll call this asset_key.
  2. Dimensions: The desired width and height to draw the sprite on the canvas. This allows us to scale the image if needed.

Let’s create this new component in our components module.

Open your src/components.rs file and add the new Sprite struct definition.

// src/components.rs

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

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

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

pub enum Shape {
    Rectangle { width: f32, height: f32 },
    Circle { radius: f32 },
}

#[derive(Component)]
pub struct Renderable {
    pub shape: Shape,
    pub color: String,
}

#[derive(Component)]
pub struct Player;

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

// The Sprite component holds the information necessary for the RenderingSystem
// to draw an entity using a texture from the AssetManager.
#[derive(Component)]
pub struct Sprite {
    // The key that identifies which asset to use from the AssetManager's HashMap.
    // This will typically be the file path of the image.
    pub asset_key: String,

    // The desired width and height to render the sprite on the canvas.
    pub width: f32,
    pub height: f32,
}

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

Swapping Components on Our Player Entity

With our new Sprite component defined, the next step is to update our player entity. We no longer want it to be a white Renderable square; we want it to be our new player ship Sprite. This means we need to remove the Renderable component from its spawn bundle and add the Sprite component in its place.

Open src/lib.rs and navigate to the Game::new function. Modify the world.spawn call as highlighted below.

// src/lib.rs

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

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

        world.spawn((
            Position {
                x: (canvas.width() / 2) as f32,
                y: (canvas.height() / 2) as f32,
            },
            Velocity { dx: 0.0, dy: 0.0 },

            // --- HIGHLIGHTED CHANGES START ---

            // REMOVE the old Renderable component. Our player is no longer a primitive shape.
            /*
            Renderable {
                shape: Shape::Rectangle {
                    width: 50.0,
                    height: 50.0,
                },
                color: String::from("#FFFFFF"),
            },
            */

            // ADD the new Sprite component.
            // This tells the engine to render this entity using an image.
            Sprite {
                // The `asset_key` must exactly match the key we used in the `run` function
                // when inserting the loaded image into the AssetManager.
                asset_key: String::from("assets/player_ship.png"),

                // Let's define the size we want our player ship to be on screen.
                width: 64.0, 
                height: 64.0,
            },

            // --- HIGHLIGHTED CHANGES END ---

            Player,
        ));

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

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

// ...

An Important (and Expected) Disappearance

After making these changes, if you build and run your project, you will notice something interesting: your player has vanished! You can still see the dark grey background, but the white square is gone.

This is the correct and expected behavior.

Why? Because our RenderingSystem is still programmed to only look for entities that have a Position and a Renderable component. We just removed the Renderable component from our player. Therefore, the rendering query no longer finds our player entity, and nothing is drawn.

This is a perfect demonstration of the power and decoupling of an ECS architecture. The RenderingSystem doesn’t have any hardcoded knowledge about a “player”; it only knows how to draw things that are explicitly marked as Renderable. By swapping components, we have successfully changed the “identity” of our entity, and the systems correctly adapt.

You have now successfully defined the data that represents a sprite and attached it to your player. The entity is primed and ready to be drawn as an image.

Next Steps

The stage is perfectly set. Our player entity has a Sprite component, and our AssetManager holds the loaded image data. The final task in this step is to modify the RenderingSystem. You will update its query to look for entities with a Sprite component, fetch the corresponding image from the AssetManager using the asset_key, and use the context.draw_image_with_html_image_element method to finally render your player’s ship on the screen.


Further Reading

Upgrade RenderingSystem to Draw Sprites

Mục tiêu: Enhance the game engine’s rendering system to draw sprites. This involves querying for entities with Position and Sprite components, retrieving the corresponding image from the AssetManager, and using the Canvas API’s drawImage function to render it on the screen.


You have perfectly orchestrated a complex, asynchronous process. The engine is now successfully loading an image in the background and storing it in the AssetManager. Your player entity has been updated with a Sprite component, a clear declaration of its new visual identity. The only reason it remains invisible is because the RenderingSystem, our engine’s artist, hasn’t yet been taught how to paint with images.

This is the final, rewarding task of our asset management step. We will upgrade the RenderingSystem to understand the Sprite component. It will learn how to ask the AssetManager for a loaded image and how to use the powerful drawImage Canvas API to render that image to the screen. This is the moment your player ship makes its debut.

Teaching the Artist a New Skill

Our RenderingSystem is currently a specialist in primitive shapes. It knows how to query for Renderable components and draw rectangles and circles. We will now expand its repertoire. Instead of replacing the old logic, we’ll add to it. A powerful engine should be able to render both primitive shapes and sprites, so we’ll add a second query and a second rendering loop to our system.

This new logic will perform the following steps on every frame:

  1. Get access to the AssetManager resource.
  2. Query the World for every entity that has a Position and a Sprite.
  3. For each of these entities, use the asset_key from its Sprite component to look up the actual HtmlImageElement in the AssetManager.
  4. If the image is found (meaning it has finished loading), use its Position and the Sprite’s dimensions to draw it on the canvas.

Let’s modify our rendering_system in src/lib.rs to include this new capability.

// src/lib.rs

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

pub fn rendering_system(world: &World, context: &web_sys::CanvasRenderingContext2d) {
    // --- EXISTING PRIMITIVE RENDERING LOGIC ---
    // This part remains unchanged. Our engine can still render shapes.
    let mut shape_query = world.query::<(&Position, &Renderable)>();

    for (position, renderable) in shape_query.iter(world) {
        context.set_fill_style(&JsValue::from_str(&renderable.color));
        match &renderable.shape {
            Shape::Rectangle { width, height } => {
                let top_left_x = position.x - width / 2.0;
                let top_left_y = position.y - height / 2.0;
                context.fill_rect(
                    top_left_x as f64,
                    top_left_y as f64,
                    *width as f64,
                    *height as f64,
                );
            }
            Shape::Circle { radius } => {
                context.begin_path();
                context
                    .arc(
                        position.x as f64,
                        position.y as f64,
                        *radius as f64,
                        0.0,
                        2.0 * f64::consts::PI,
                    )
                    .unwrap();
                context.fill();
            }
        }
    }

    // --- NEW SPRITE RENDERING LOGIC STARTS HERE ---

    // First, we need to get read-only access to our AssetManager resource.
    // If the resource doesn't exist for some reason, we can't draw sprites,
    // so we wrap this entire block in an `if let`.
    if let Some(asset_manager) = world.get_resource::<AssetManager>() {
        // Next, we create a new query. This one looks for all entities that have
        // both a `Position` (for location) and a `Sprite` (for visual info).
        let mut sprite_query = world.query::<(&Position, &Sprite)>();

        for (position, sprite) in sprite_query.iter(world) {
            // Using the `asset_key` from the Sprite component, we attempt to retrieve
            // the corresponding `HtmlImageElement` from the asset manager's HashMap.
            // `get` returns an `Option`, so `if let` is the perfect way to handle it.
            if let Some(image) = asset_manager.assets.get(&sprite.asset_key) {
                // If we found the image, it means it has finished loading and is ready.

                // As with rectangles, our `Position` is the center of the sprite. The
                // `draw_image` function, however, draws from the top-left corner.
                // We must calculate this top-left coordinate to position the sprite correctly.
                let top_left_x = position.x - sprite.width / 2.0;
                let top_left_y = position.y - sprite.height / 2.0;

                // We call the `draw_image` variant that allows us to specify the
                // destination width and height, enabling sprite scaling.
                // This function can fail (e.g., if the image is corrupted), so web-sys
                // returns a `Result`, which we `.unwrap()` for simplicity.
                context
                    .draw_image_with_html_image_element_and_dw_and_dh(
                        image,               // The image element to draw
                        top_left_x as f64,   // The X coordinate of the top-left corner
                        top_left_y as f64,   // The Y coordinate of the top-left corner
                        sprite.width as f64, // The desired width of the drawn image
                        sprite.height as f64, // The desired height of the drawn image
                    )
                    .unwrap();
            }
            // Note: If the `if let Some(image)` check fails, it simply means the
            // asset hasn't finished loading yet. In that case, we do nothing and
            // the entity is not drawn for this frame. This is a robust, non-blocking
            // way to handle assets that are still in-flight.
        }
    }
    // --- NEW SPRITE RENDERING LOGIC ENDS HERE ---
}

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

Deconstructing the Code

This addition makes your RenderingSystem vastly more powerful.

  • world.get_resource::<AssetManager>(): Just like in our other systems, we ask the World for access to a global resource. Since we only need to read the HashMap, we request immutable access.
  • world.query::<(&Position, &Sprite)>(): This is our new, specialized query. It tells bevy_ecs to find every entity that can be rendered as a sprite.
  • asset_manager.assets.get(&sprite.asset_key): This is the crucial link. We use the asset_key string from the Sprite component to perform a lookup in the AssetManager’s HashMap. This is a highly efficient operation. The if let gracefully handles the case where the asset is not yet loaded, preventing crashes and allowing the game to continue running smoothly.
  • draw_image_with_html_image_element_and_dw_and_dh: This is a very descriptive but long function name from web-sys. Let’s break it down:
    • draw_image: The core action.
    • with_html_image_element: Specifies the source is an HtmlImageElement.
    • and_dw_and_dh: Specifies that we are providing destination width and destination height. This is the version that allows scaling and corresponds to the 6-argument version of drawImage in JavaScript.

Build and Behold!

This is the moment of truth.

  1. In your terminal, rebuild the project: bash wasm-pack build --target web
  2. Ensure your local web server is running and refresh your browser.

You should now see your player_ship.png image rendered on the canvas! The white square is gone, replaced by a proper sprite. Press the arrow keys, and you will see your ship move around the screen, fully under your control. You have successfully created a complete asset loading and sprite rendering pipeline.

Next Steps

Congratulations on completing this massive step! Your engine has evolved from dealing with abstract shapes to rendering rich visual assets. This is a cornerstone of any real game engine.

The visual experience is now taking shape. What’s the next frontier for immersion? Sound. The next step in our roadmap, Step 7: Integrating a Basic Audio Engine, will guide you through the process of loading and playing sound effects. You will create an AudioManager, learn how to use the Web Audio API from Rust, and trigger sounds in response to game events, adding another layer of life and feedback to your game world.


Further Reading

  • MDN Web Docs: CanvasRenderingContext2D.drawImage(): The definitive guide to the drawImage method. It’s an incredibly versatile API with multiple overloads for different use cases (like drawing from a sprite sheet, which you might explore later).
  • web_sys Docs: CanvasRenderingContext2d: Explore the official Rust documentation for the rendering context. You can see all the different draw_image_with... variants that web-sys provides, mapping to the different JavaScript overloads.
  • The Bevy Book: Queries (Revisited): You’ve now used queries for movement, player control, and two different kinds of rendering. Reinforcing your understanding of this core ECS concept is always time well spent.