Bevy Defer
A simple asynchronous runtime for executing async coroutines.
Motivation
Async rust is incredible for modelling wait centric tasks like coroutines. Not utilizing async in game development is a huge waste of potential.
Imagine we want to model a rapid sword attack animation, in async rust this is straightforward:
swing_animation.await;
show_damage_number.await;
damage_vfx.await;
swing_animation.await;
show_damage_number.await;
damage_vfx.await;
...
At each await point we wait for something to complete, without wasting resources
spin looping a thread or defining a complex state machine in a system.
What if we want damage number and damage vfx to run concurrently and wait for both
before out next attack? It's simple with async semantics!
join! ;
swing_animation.await;
Bridging Sync and Async
Communicating between sync and async in notoriously difficult. See this amazing tokio article: https://tokio.rs/tokio/topics/bridging.
Fortunately we are running in lock step with bevy, so a lot of those headache can be mitigated by using proper communication methods.
Communicating from sync to async is simple, async code can hand out channels
and await on them, pausing the task.
Once sync code sends data through the channel, it will
wake and resume the corresponding task.
bevy_defer heavily utilizes one-shot channels to perform its operations.
Communicating from async to sync usually requires mutating the world in an async function, then a system can listen for that particular change in sync code. This is pretty seamless with regular bevy workflow.
Spawning
Spawning is a straightforward way to run some logic immediately.
You can spawn a coroutine to schedule some tasks. The main benefit is this function can take as long as it needs to complete, instead of a single frame like a normal system.
commands.spawn_task;
In fact a single function can drive the entire game!
World Accessors
We provide types mimicking bevy's types:
| Query Type | Corresponding Bevy/Sync Type |
|---|---|
AsyncWorldMut |
World / Commands |
AsyncEntityMut |
EntityMut / EntityCommands |
AsyncQuery |
WorldQuery |
AsyncEntityQuery |
WorldQuery on Entity |
AsyncSystemParam |
SystemParam |
AsyncComponent |
Component |
AsyncResource |
Resource |
AsyncNonSend |
NonSend |
AsyncEventReader |
EventReader |
AsyncAsset |
Handle |
world can be accessed by the world() method and
for example a Component can be accessed by
world.entity.
See the access module for more detail.
You can add extension methods to these accessors via Deref if you own the
underlying types. See the extension module for more detail.
Signals
Signals are the cornerstone of reactive programming that bridges the sync and async world.
The Signals component can be added to an entity, and the NamedSignals resource can be used to
provide matching signals when needed.
Here are the guarantees of signals:
- A
Signalcan hold only one value. - A
Signalis read at most once per write for every reader. - Values are not guaranteed to be read if updated in rapid succession.
- Value prior to reader creation will not be read by a new reader.
Signals erases the underlying types and utilizes the SignalId trait to disambiguate signals,
this ensures no archetype fragmentation.
In systems, you can use SignalSender and SignalReceiver just like you would in async,
do keep in mind these parameters do not filter archetypes.
AsyncSystems
AsyncSystem is a system-like async function on a specific entity.
The component AsyncSystems is a collection of AsyncSystems that runs independently.
Example
To create an AsyncSystem, use a macro:
// Scale up for a second when clicked.
let system = async_system!
The parameters implement AsyncEntityParam.
How an AsyncSystem executes
Think of an AsyncSystem like a loop:
- if this
Futureis not running at the start of this frame, run it. - If the function finishes, rerun on the next frame.
- If the entity is dropped, the
Futurewill be cancelled.
So this is similar to
spawn
If you want some state to persist, for example keeping a handle alive or using a
AsyncEventReader, you might want to implement the async system as a loop:
let system = async_system!
Thread Locals
We can push resources, !Send resources and even &World (readonly) onto
thread local storage during execution by adding them to the plugin:
empty.with.with;
This allows some access to be immediate without deferring.
If &world is available, all get access is immediate.
This would block parallelization, however.
Implementation Details
bevy_defer uses a single threaded runtime that always runs on bevy's main thread inside the main schedule,
this is ideal for wait heavy or IO heavy tasks, but CPU heavy tasks should not be run in bevy_defer.
The AsyncComputeTaskPool in bevy_tasks is ideal for this use case.
We can use AsyncComputeTaskPool::get().spawn() to spawn a future on task pool and call await.
At each execution point, we will poll our futures until no progress can be made.
Imagine AsyncPlugin::default_settings() is used, which means we have 3 execution points per frame, this code:
let a = query1.await;
let b = query2.await;
let c = query3.await;
let d = query4.await;
let e = query5.await;
let f = query6.await;
takes at least 2 frames to complete, since queries are deferred and cannot resolve immediately.
To complete the task faster, try use futures::join! or futures_lite::future::zip to
run these queries concurrently.
let = join! .await;
Versions
| bevy | bevy_defer |
|---|---|
| 0.12 | 0.1 |
| 0.13 | 0.2-latest |
License
License under either of
Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option.
Contribution
Contributions are welcome!
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.