Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Callbacks

When you spawn a service you get back a Service instance. The implementation of that Service will be stored inside the Bevy ECS until you choose to despawn it.

In some cases you might not want the implementation to exist inside the Bevy ECS. You might prefer an object that you can pass around and which will have RAII—freeing its memory when it is no longer needed.

Callbacks are an alternative to services whose lifecycle is outside of the Bevy ECS but still act as Bevy systems—able to interact with entities, components, and resources. They also fulfill the role of services—taking in a Request message and passing back a Response message, potentially with output streams as well.

There are three key differences between a service and a callback:

  • Callbacks do not need to be spawned with Commands.
  • Callbacks are not associated with any Entity and therefore do not have any provider that you can store components on.
  • A callback will automatically deallocate when all references to it are dropped.

Tip

The more general term that we use to refer to services and service-like things—such as callbacks—is provider.

How to use

To use a callback, simply define the callback either as a fn or a closure, and then apply .as_callback() to its name. Note the use of BlockingCallbackInput instead of BlockingServiceInput:

// We can access this resource from the callback
#[derive(Resource)]
struct Greeting {
    prefix: String,
}

// Make an fn that defines the callback implementation
fn perform_greeting(
    In(input): BlockingCallbackInput<String>,
    greeting: Res<Greeting>,
) -> String {
    let name = input.request;
    let prefix = &greeting.prefix;
    format!("{prefix}{name}")
}

// Convert the fn into a callback.
// This is necessary to initialize the fn as a bevy system.
let callback = perform_greeting.as_callback();

// Use the callback in a request
let outcome = commands.request(String::from("Bob"), callback).outcome();
Async

Async callbacks are implemented in much the same way as async services, just replacing AsyncServiceInput with AsyncCallbackInput:

async fn page_title_callback(In(srv): AsyncCallbackInput<String>) -> Option<String> {
    let response = trpl::get(&srv.request).await;
    let response_text = response.text().await;
    trpl::Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

let callback = page_title_callback.as_callback();
let outcome = commands.request(String::from("https://example.com"), callback).outcome();

Same as for blocking callbacks, you turn the fn definition into a callback by applying .as_callback() to it.

Note

There is no callback equivalent to continuous services. Continuous services must exist inside the Bevy ECS. There is currently no way around this.

Closures

You can also turn a closure into a callback. Sometimes the syntax for this is confusing, but the easiest way to make it work is to first define the closure as a variable and then convert that variable into a callback:

// Make an closure that defines the callback implementation
let perform_greeting = |
    In(input): BlockingCallbackInput<String>,
    greeting: Res<Greeting>,
| {
    let name = input.request;
    let prefix = &greeting.prefix;
    format!("{prefix}{name}")
};

// Convert the fn into a callback.
// This is necessary to initialize the fn as a bevy system.
let callback = perform_greeting.as_callback();

// Use the callback in a request
let outcome = commands.request(String::from("Bob"), callback).outcome();

This also works for async callbacks, but you need to use the async block syntax:

let page_title_callback = |In(srv): AsyncCallbackInput<String>| {
    async move {
        let response = trpl::get(&srv.request).await;
        let response_text = response.text().await;
        trpl::Html::parse(&response_text)
            .select_first("title")
            .map(|title| title.inner_html())
    }
};

let callback = page_title_callback.as_callback();
let outcome = commands.request(String::from("https://example.com"), callback).outcome();

Service/Callback Agnostic Implementation

In some cases you might want to implement a Bevy system provider but don’t want to commit to choosing a service or a callback. For that you can create a regular Bevy system that takes an input and convert it into a service or callback later.

Whether you make it a service or a callback, the Request message type will match the input of the Bevy system, and the Response message type will match either its return value (for blocking) or the output of its Future (for async).

Here’s an example of converting a blocking function into a service and a closure:

fn perform_greeting(
    In(name): In<String>,
    greeting: Res<Greeting>,
) -> String {
    let prefix = &greeting.prefix;
    format!("{prefix}{name}")
}

// Use as service
let greeting_service = commands.spawn_service(
    perform_greeting.into_blocking_service()
);
let outcome = commands.request(String::from("Bob"), greeting_service).outcome();

// Use as callback
let greeting_callback = perform_greeting.into_blocking_callback();
let outcome = commands.request(String::from("Bob"), greeting_callback).outcome();

And here’s an example for an async function:

fn get_page_element(
    In(element): In<String>,
    url: Res<Url>,
) -> impl Future<Output = Option<String>> + use<> {
    let url = (**url).clone();
    async move {
        let content = fetch_content_from_url(url).await?;
        content.get(&element).cloned()
    }
}

let element = String::from("title");

// Use as service
let title_service = commands.spawn_service(
    get_page_element.into_async_service()
);
let outcome = commands.request(element.clone(), title_service).outcome();

// Use as callback
let title_callback = get_page_element.into_async_callback();
let outcome = commands.request(element, title_callback).outcome();

Caution

.into_async_callback() does not work for systems whose only system parameter is the input. Trying to convert such a function into a callback will result in a compilation error. This problem is being tracked by #159.