Expand description
§About
pecs
is a plugin for Bevy that allows you to execute code asynchronously
by chaining multiple promises as part of Bevy’s ecs
environment.
pecs
stands for Promise Entity Component System
.
§Features
- Promise chaining with
then()
/then_repeat()
- State passing (
state
for promises is likeself
for items). - Complete type inference (the next promise knows the type of the previous result).
- Out-of-the-box timer, UI and HTTP promises via stateless
asyn
mod and statefulstate.asyn()
method. - Custom promise registration (add any asynchronous function you want!).
- System parameters fetching
(promise
asyn!
functions accept the same parameters as Bevy systems do). - Nested promises (with chaining, obviously).
- Combining promises with
any/all
for tuple/vec of promises via statelessany()
/all()
methods or statefulstate.any()
/state.all()
methods. - State mapping via
with(value)
/map(func)
(changes state type over chain calls). - Result mapping via
with_result(value)
/map_result(func)
(changes result type over chain calls).
§Example
use bevy::prelude::*;
use pecs::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(PecsPlugin)
.add_systems(Startup, setup)
.run();
}
fn setup(mut commands: Commands, time: Res<Time>) {
let start = time.elapsed_seconds();
commands
// create PromiseLike chainable commands
// with the current time as state
.promise(|| start)
// will be executed right after current stage
.then(asyn!(state => {
info!("Wait a second..");
state.asyn().timeout(1.0)
}))
// will be executed after in a second after previous call
.then(asyn!(state => {
info!("How large is is the Bevy main web page?");
state.asyn().http().get("https://bevyengine.org")
}))
// will be executed after request completes
.then(asyn!(state, result => {
match result {
Ok(response) => info!("It is {} bytes!", response.bytes.len()),
Err(err) => info!("Ahhh... something goes wrong: {err}")
}
state.pass()
}))
// will be executed right after the previous one
.then(asyn!(state, time: Res<Time> => {
let duration = time.elapsed_seconds() - state.value;
info!("It tooks {duration:0.2}s to do this job.");
info!("Exiting now");
asyn::app::exit()
}));
}
There is otput of the above example, pay some attention to time stamps:
18.667 INFO bevy_render::renderer: AdapterInfo { ... }
18.835 INFO simple: Wait a second..
19.842 INFO simple: How large is is the Bevy main web page?
19.924 INFO simple: It is 17759 bytes!
19.924 INFO simple: It tooks 1.09s to do this job.
19.924 INFO simple: Exiting now
§Basics
To create a new promise, you can use one of the following methods:
Promise::start()
: Creates a new promise without initial state.Promise::new(state)
: Creates a new promise with the specified initial state.Promise::register(on_invoke, on_discard)
: Registers a new promise with the specifiedon_invoke
andon_discard
callbacks.
It is also possible to create PromiseLike
promise containers
that act just like promises with:
commands.promise(|| state)
for creatingPromiseLike
from default statecommands.promise(promise)
from existing promise
The resulting promise has the signature Promise<S, R>
, where R
is the type of the result and S
is the type of the promise state. Note that R
and S
must be 'static
types, so references or lifetime types are not allowed.
Promises can be chained together using the then()
method, which
takes an Asyn
function created with the asyn!
macro. The
Asyn
function takes the promise state as its first argument, and the promise
result as its second argument. Any additional arguments are optional and correspond to the
system parameters used in Bevy’s ECS. This allows you to do
inside an Asyn
function everything you can do inside a regular system, while
still keeping track of system parameter states.
If the result of the Asyn
function is
Resolve
, the result is passed immediately to
the next promise in the chain. If the result is Await
,
the next promise in the chain is resolved when the nested promise is resolved. The type of the next
promise’s state and result arguments are inferred from the result of the previous promise:
use bevy::prelude::*;
use pecs::prelude::*;
fn inference(mut commands: Commands) {
commands.add(
Promise::start(asyn!(_ => {
Promise::resolve("Hello!")
}))
// _: PromiseState<()>, result: &str
.then(asyn!(_, result => {
info!("#1 resolved with {}", result);
Promise::resolve("Hello?")
}))
// _: PromiseState<()>, result: &str
.then(asyn!(_, result => {
info!("#2 resolved with {result}");
Promise::resolve(result.to_string())
}))
// ok_then used to take successfull results
// _: PromiseState<()>, result: String
.then(asyn!(_, result => {
info!("#3 resolved with {result}");
Promise::resolve(result)
}))
// asyn::timeout(d) returns Promise<(), (), ()>
// that resolves after after `d` seconds
.then(asyn!(_, result => {
info!("#4 resolved with {result}");
asyn::timeout(1.)
}))
// _: PromiseState<()>, result: ()
.then(asyn!(_, result => {
info!("#5 resolved with {result:?}");
Promise::resolve(())
}))
);
}
§State
When working with asynchronous operations, it is often useful to carry a state along with
the promises in a chain. The pecs
provides a convenient way to do this using the
PromiseState<S>
type.
PromiseState
is a wrapper around a 'static S
value. This value
can be accessed and modified using state.value
. PromiseState
also
implements Deref
, so in most you cases you can omit .value
.
To use PromiseState
, you don’t create it directly. Instead, it is automatically passed
as the first argument to the Asyn
function.
For example, suppose you have a stateful promise that increments a counter, waits for some time, and then logs the counter value. Here’s how you could implement it:
use bevy::prelude::*;
use pecs::prelude::*;
fn setup(mut commands: Commands) {
commands
// create a promise with int state
.promise(|| 0)
.then(asyn!(state => {
state.value += 1;
state.asyn().timeout(1.0)
}))
.then(asyn!(state => {
info!("Counter value: {}", state.value);
}));
}
In this example, we start with an initial state value of 0 and increment it by 1 in the first
promise. We then use state.asyn().timeout()
to wait for one second before logging the final
state value. The asyn method returns an AsynOps<S>
value, which can be used
to create new promises that are associated with the current state.
PromiseState
can be used with other pecs constructs like
then()
, repeat()
or
all()
to create complex promise chains that carry stateful values.
Here’s an example that uses any
method to create a promise that
resolves when any of provided promises have resolved with current state itself:
use bevy::prelude::*;
use pecs::prelude::*;
fn setup(mut commands: Commands, time: Res<Time>) {
let start = time.elapsed_seconds();
commands
// use `start: f32` as a state
.promise(|| start)
// state is f32 here
.then(asyn!(state => {
state.any((
asyn::timeout(0.4),
asyn::http::get("https://bevyengine.org").send()
))
}))
// state is f32 as well
.then(asyn!(state, (timeout, response) => {
if timeout.is_some() {
info!("Bevy site is not fast enoutgh");
} else {
let status = if let Ok(response) = response.unwrap() {
response.status.to_string()
} else {
format!("Error")
};
info!("Bevy respond pretty fast with {status}");
}
// pass the state to the next call
state.pass()
}))
// it is still f32
.then(asyn!(state, time: Res<Time> {
let time_to_process = time.elapsed_seconds() - state.value;
info!("Done in {time_to_process:0.2}s");
}));
}
See combine_vecs
and confirmation
examples to better understand the state
behaviour.
§Work in Progress
This crate is pretty young. API could and will change. App may crash. Some promises could silently drop. Documentation is incomplete.
But. But. Examples works like a charm. And this fact gives us a lot of hope.
There are a lot docs planned to put here, but I believe it is better to release something
then perfect
.