Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ze rework #7

Merged
merged 16 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ edition = "2021"
bevy = { version = "0.11.0", features = ["dynamic_linking"] }
pin-project = "1"
tinyset = "0.4.15"
oneshot = { version = "0.1.6", default-features = false }

[profile.dev]
opt-level = 1
Expand Down
38 changes: 23 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,47 @@ An experimental reactive coroutine plugin for [`Bevy`](https://github.com/bevyen
</div>

```rust
async fn count(mut fib: Fib) {
let mut i = 0;
async fn on_hit(mut scope: Scope, hp: Rd<Hp>, on_hp_change: OnChange<Hp>) {
loop {
fib.duration(Duration::from_secs(1)).await;
i += 1;
println!("This coroutine has started since {} seconds", i);
let prev = hp.get(&scope);
on_hp_change.observe(&mut scope).await;

let curr = hp.get(&scope);
if curr < prev {
println!("Lost {} hp(s)", prev - curr);
}
}
}
```

# Objectives
This crate aims to provide a nice and lightweight implementation of reactive coroutines
for Bevy. Those would be useful for scripting like logic, chain of actions over time, and so on.
This crate aims to provide a nice and lightweight implementation of reactive
coroutines for Bevy. Those are useful for scripting like logic, chain of
actions over time, complex scheduling and so on.

# Warning
Most features are not implemented yet and those that are available are pretty slow (and probably buggy as well).
This crate is under heavy development, none of the APIs are stable yet.

# Overview
* Define coroutines as async functions, that can be added to any entity.
* Define coroutines as async functions.
* Coroutines can yield and be resumed after a certain condition:
This includes, waiting for some time, waiting until a component is mutated and so on.
* Coroutines are structured: they can spawn sub-coroutines, and wait until all of them
terminates (`par_and`) or until one of them terminates (`par_or`). This also includes
terminates (`all`) or until one of them terminates (`first`). This also includes
automatic cancellation.

# Example
## Example
TODO

# Features to implement soon
* Reading, writing and reacting to regular bevy events.
## Features to implement soon
* Using `Commands` to queue structural mutations and await them.
* Other form of structured operations.
* Exclusive coroutines, that can access the whole World
* More forms of inter-coroutine communication, using `Signals`, `Producers` and `Receivers`.
* The ability to run systems from coroutines, useful to define complex schedules.
* More coroutine parameters, such as resources, queries and bevy events.

## Multithreading
For now the executor runs on a single thread, but ultimatly it should be
possible to run coroutines in parallel when needed.

# Contributions
It's a bit early to accept contributions right now, but if you're interested, don't hesitate to play around with this crate and share your ideas.
Expand Down
23 changes: 20 additions & 3 deletions examples/basic.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

use bevy::{prelude::*, sprite::MaterialMesh2dBundle};
use corentin::prelude::*;

Expand All @@ -24,10 +26,25 @@ fn setup_scene(
..default()
})
.add(coroutine(
|mut fib: Fib, mut transform: Wr<Transform>| async move {
|mut s: Scope, mut transform: Wr<Transform>| async move {
loop {
let dt = s.next_tick().await;
transform.get_mut(&mut s).translation.x += 100.0 * dt.as_secs_f32();
}
},
))
.add(coroutine(
|mut s: Scope, transform: Rd<Transform>| async move {
let mut i = 0;
let original_x = transform.get(&s).translation.x;
loop {
let dt = fib.next_tick().await;
transform.get_mut(&fib).translation.x += 100.0 * dt.as_secs_f32();
s.duration(Duration::from_secs(1)).await;
i += 1;
println!(
"After {} seconds, we moved {} to the right",
i,
transform.get(&s).translation.x - original_x
);
}
},
));
Expand Down
64 changes: 43 additions & 21 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,61 @@
use bevy::ecs::system::EntityCommand;
use bevy::utils::synccell::SyncCell;
use bevy::ecs::system::{Command, EntityCommand};
use std::marker::PhantomData;

use bevy::prelude::{Entity, World};

use crate::coroutine::UninitCoroutine;
use crate::executor::Executor;
use super::executor::Executor;
use super::function_coroutine::CoroutineParamFunction;

pub struct AddCoroutine<Marker, C> {
coro: C,
_phantom: PhantomData<Marker>,
pub struct AddRootCoroutine<Marker, T, C> {
coroutine: C,
_phantom1: PhantomData<Marker>,
_phantom2: PhantomData<T>,
}

impl<Marker: Send + 'static, C: Send + 'static> EntityCommand for AddCoroutine<Marker, C>
pub struct AddCoroutineTo<Marker, T, C> {
coroutine: C,
_phantom1: PhantomData<Marker>,
_phantom2: PhantomData<T>,
}

impl<Marker, C, T> EntityCommand for AddCoroutineTo<Marker, T, C>
where
C: UninitCoroutine<Marker>,
C: CoroutineParamFunction<Marker, T>,
T: Sync + Send + 'static,
Marker: 'static + Send,
{
fn apply(self, id: Entity, world: &mut World) {
fn apply(self, owner: Entity, world: &mut World) {
world.resource_scope::<Executor, ()>(|world, mut executor| {
executor.add_function_coroutine(Some(owner), world, self.coroutine);
});
}
}

impl<Marker, C, T> Command for AddRootCoroutine<Marker, T, C>
where
C: CoroutineParamFunction<Marker, T>,
T: Sync + Send + 'static,
Marker: 'static + Send,
{
fn apply(self, world: &mut World) {
world.resource_scope::<Executor, ()>(|w, mut executor| {
if let Some(coroutine) = self.coro.init(id, w) {
executor.add(SyncCell::new(Box::pin(coroutine)));
}
executor.add_function_coroutine(None, w, self.coroutine);
});
}
}

pub fn coroutine<M, C>(coro: C) -> AddCoroutine<M, C> {
AddCoroutine::new(coro)
pub fn root_coroutine<M, C, T>(coroutine: C) -> AddRootCoroutine<M, T, C> {
AddRootCoroutine {
coroutine,
_phantom1: PhantomData,
_phantom2: PhantomData,
}
}

impl<M, C> AddCoroutine<M, C> {
pub fn new(coro: C) -> Self {
Self {
coro,
_phantom: PhantomData,
}
pub fn coroutine<M, C, T>(coroutine: C) -> AddCoroutineTo<M, T, C> {
AddCoroutineTo {
coroutine,
_phantom1: PhantomData,
_phantom2: PhantomData,
}
}
140 changes: 140 additions & 0 deletions src/coro_param/component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use std::marker::PhantomData;

use bevy::{
ecs::{component::ComponentId, world::unsafe_world_cell::UnsafeWorldCell},
prelude::{Component, Entity, Mut},
};

use crate::{executor::msg::SignalId, function_coroutine::scope::Scope, CoroMeta, SourceId};

use super::{on_change::ChangeTracker, CoroParam};

/// A readonly reference to a [`Component`] from the owning [`Entity`].
///
/// Note that a Coroutine with such parameter will be canceled if the entity does not have the
/// relevent component (or does not exist).
pub struct Rd<T: Component> {
owner: Entity,
_phantom: PhantomData<T>,
}

impl<T: Component> CoroParam for Rd<T> {
fn init(world: UnsafeWorldCell<'_>, coro_meta: &mut CoroMeta) -> Option<Self> {
let id = world.components().component_id::<T>()?;
let owner = coro_meta.owner?;

if !coro_meta.access.add_read(SourceId::Entity(owner), id) {
return None;
}

Some(Self {
owner,
_phantom: PhantomData,
})
}

fn is_valid(world: UnsafeWorldCell<'_>, coro_meta: &CoroMeta) -> bool {
if let Some(owner) = coro_meta.owner {
if let Some(entity) = world.get_entity(owner) {
return entity.contains::<T>();
}
}

false
}
}

impl<T: Component> Rd<T> {
/// Return the current value of the [`Component`]. The result ([`InGuard`]) cannot be held
/// accros any await.
pub fn get<'a>(&'a self, scope: &'a Scope) -> &'a T {
unsafe {
scope
.resume_param()
.world_cell()
.get_entity(self.owner)
.unwrap()
.get::<T>()
.unwrap()
}
}
}

/// A read-write exclusive reference to a [`Component`] from the owning [`Entity`].
///
/// Note that a Coroutine with such parameter will be canceled if the entity does not have the
/// relevent component.
pub struct Wr<T: Component> {
_phantom: PhantomData<T>,
owner: Entity,
id: ComponentId,
}

impl<T: Component> CoroParam for Wr<T> {
fn init(world: UnsafeWorldCell<'_>, coro_meta: &mut CoroMeta) -> Option<Self> {
let id = world.components().component_id::<T>()?;
let owner = coro_meta.owner?;

if !coro_meta.access.add_write(SourceId::Entity(owner), id) {
return None;
}

Some(Self {
_phantom: PhantomData,
id,
owner,
})
}

fn is_valid(world: UnsafeWorldCell<'_>, coro_meta: &CoroMeta) -> bool {
if let Some(owner) = coro_meta.owner {
if let Some(entity) = world.get_entity(owner) {
return entity.contains::<T>();
}
}

false
}
}

impl<T: Component> Wr<T> {
pub fn get<'a>(&'a self, scope: &'a Scope) -> &'a T {
let value = unsafe {
scope
.resume_param()
.world_cell()
.get_entity(self.owner)
.unwrap()
.get::<T>()
.unwrap()
};

value
}

// TODO Should ideally only borrow scope immutably
pub fn get_mut<'a>(&'a mut self, scope: &'a mut Scope) -> Mut<'a, T> {
unsafe {
let entity = scope
.resume_param()
.world_cell()
.get_entity(self.owner)
.unwrap();

if entity.contains::<ChangeTracker<T>>() {
scope.resume_param_mut().emit_signal(SignalId {
signal_type: self.id,
owner: Some(self.owner),
});
}

scope
.resume_param()
.world_cell()
.get_entity(self.owner)
.unwrap()
.get_mut::<T>()
.unwrap()
}
}
}
48 changes: 48 additions & 0 deletions src/coro_param/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use bevy::{ecs::world::unsafe_world_cell::UnsafeWorldCell, utils::all_tuples};

use super::CoroMeta;

pub mod component;
pub mod on_change;
pub mod signals;

pub mod prelude {
#[doc(hidden)]
pub use super::component::{Rd, Wr};

#[doc(hidden)]
pub use super::on_change::{ChangeTracker, OnChange};
}

/// A function taking a scope and 0 or many [`CoroParam`]
/// can be trurned into a [`Coroutine`](super::Coroutine).
pub trait CoroParam: Sized {
/// Initialize this parameter, and update the metadata.
/// The world can only be used to read metadata.
fn init(world: UnsafeWorldCell<'_>, coro_meta: &mut CoroMeta) -> Option<Self>;

/// Return true iff this parameter is still valid.
/// The world can only be used to read metadata.
fn is_valid(world: UnsafeWorldCell<'_>, coro_meta: &CoroMeta) -> bool;
}

macro_rules! impl_coro_param {
($($param: ident),*) => {
#[allow(non_snake_case, unused_parens, unused_variables)]
impl<$($param: CoroParam),*> CoroParam for ($($param,)*) {
fn init(world: UnsafeWorldCell<'_>, meta: &mut CoroMeta) -> Option<Self> {
$(let $param = $param::init(world, meta)?;)*

Some(($($param,)*))

}

fn is_valid(world: UnsafeWorldCell<'_>, coro_meta: &CoroMeta) -> bool {
true $(&& $param::is_valid(world, coro_meta))*
}
}

};
}

all_tuples!(impl_coro_param, 0, 16, P);
Loading