bevy_pipe_affect
Write systems as pure functions
Normally, Bevy systems perform some state changes as side effects.
This crate enables you to instead return Effects as system output.
Effects define an ECS state transition.
All common ECS operations have one or more Effect types provided in the library.
These "systems with effects" can then be .pipe(affect)-ed.
The affect system will perform the state transition.
This enables a more functional code-style in bevy app development.
User-written systems can all be read-only, pure functions.
All mutability can be piped out of your code.
This book
This book aims to provide a place for the following pieces of documentation:
- tutorials: lessons detailing the creation of simple games from start to finish
- explanation: clarification of concepts and strategies employed by
bevy_pipe_affect, including details about how it works and why - how-to guides: recommended solutions to common problems, as well as migration guides
This book is not an API reference.
For that, please refer to bevy_pipe_affect's documentation on docs.rs.
While this book aims to be comprehensive, it should also be easy to maintain and up-to-date.
This is why, in consort with the API reference, documentation for bevy_pipe_affect aims to satisfy The Grand Unified Theory of Documentation.
Furthermore, code snippets in this book are automatically tested by bevy_pipe_affect's CI wherever possible with the help of mdBook-Keeper.
This should help inform maintainers when changes to the library have made documentation out-of-date.
Deployment of this book to github pages is also performed by bevy_pipe_affect's CI automatically on new releases.
Splitting the documentation up this way means that docs are not necessarily meant to be read in order. Some chapters are intended to be read while working on your own project, while others are meant to be more like studying material. The following are good jumping-off points for beginners:
- Motivations explanation
- effects module api reference (a list of effects and constructors provided by the library)
Other resources
This book is not suitable documentation for Bevy. Some resources for learning Bevy include those listed on the Bevy website, as well as the unofficial Bevy Cheat Book.
bevy_pipe_affect's source code is available on github.
This repository also contains cargo examples, which can be run after cloning the repository using $ cargo run --release --all-features --example example-name.
These examples may be difficult to follow on their own, and many of their strategies are described in this book.
When viewing these examples, be careful to checkout the correct git tag for the version of the library you are using.
Some changes may have been made to the library or to the examples on the main branch that are not released yet, and trying to apply these to the version of the library you are using can lead to errors.
License
The pages of this book fall under the same license as the rest of the bevy_pipe_affect repository.
I.e., this book is dual-licensed under MIT and Apache 2.0 at your option.
Motivations
Scrawled here is basically a blog post on functional programming, Bevy, and why I made this library. It may not be the most practical piece of documentation, but I hope it shines a light on some of the design choices for library users, and maybe even reaches out to others who are future FP+ECS enthusiasts.
I'm an FP shill now
Here is a brief shill for writing FP Rust. If you already are an FP shill, you can skip this.
Rust takes a lot of inspiration from purely functional languages like Haskell.
As a person who learned Rust before functional programming, I was intrigued to learn that most of its features that I found to be revelations turned out to be derivative.
Iterator chains, algebraic data types, sum-types used in place of null and exceptions, ? operators, to name a few.
Similar things have been etched into purely functional languages for a long time.
I've written a lot of Rust professionally over the past few years, and gradually it has become obvious how beneficial it is to use these features. Or, more than anything else, it has become obvious how beneficial it is to write pure functions with the aid of these features.
For the uninitiated, pure functions are those that are deterministic and have no side effects. Like a function in mathematics, they are mere input and output, so they do not read or write anything from the state of the world at large. A purely functional language, like Haskell, is one that only allows you to write pure functions. This may seem limiting, but thanks to higher-order functions, plus the strength of FP's theoretical foundations in general, it really isn't.
Pure functions are easily unit tested, since you don't need to set up any state. They are easy to compose without unexpected consequences. Programs can become extremely complex through composition, but each component can be obvious and simple and predictable. There's relief in functions that only have input and output, for both readers and writers of the code.
Which function should perform this change?
Should the data this function uses be input or read from state?
Should the data this function calculates be output or written to state?
If you're writing pure functions, these questions aren't just foregone conclusions, they are invalid.
So in my regular programming practice now, I go to great lengths to at least push the state reading/writing to the fringes of the program. Even when designing a system of programs, I consider pushing the state to the fringes of the data flow at large. This practice isn't that common in Bevy.
Practical motivation
Now, like a true software-gamedev-hipster, I also shill Bevy. The core framework of Bevy is an ECS among many great Rust ECSs, but I especially appreciate that its systems are mere functions. Its system scheduler may be particularly attractive to FP shills as well. It is declarative, it leverages higher-order functions for scheduling your systems, it provides system composition with piping and mapping, and it does its best to abstract away the parallel execution of systems safely.
However, the main way to interact with the world in vanilla Bevy is by writing systems that have side effects.
If you want to update a resource, you must parameterize a ResMut.
If you want to edit components in-place, you must query them &mut-ably.
If you want to load an asset, you must interact with the internally mutable AssetServer.
Even if you do everything with Commands, not only do the intended effects require exclusive world access, you're still having a side effect on the command queue.
In my feable attempts to write more immutable systems, I would simply write systems that output messages, or bundles, and then have generic systems to handle these as pipe input and actually do the writing or spawning. For example:
#[derive(Component)] struct Health(u32); use bevy::prelude::*; #[derive(Debug, PartialEq, Eq, Message)] struct DeathMessage(Entity); fn detect_deaths(query: Query<(Entity, &Health)>) -> Vec<DeathMessage> { query .iter() .flat_map(|(entity, health)| { if health.0 == 0 { Some(DeathMessage(entity)) } else { None } }) .collect() } fn write_messages<M: Message>( In(messages): In<impl IntoIterator<Item = M>>, mut writer: MessageWriter<M>, ) { messages.into_iter().for_each(|message| { writer.write(message); }); } fn main() { bevy::ecs::system::assert_is_system(detect_deaths.pipe(write_messages)); }
So, in this example, I have a pure system detect_deaths that produces messages as output, and then a system that actually writes the messages write_messages.
I've gone from 0% of my systems being pure to 50%.
Since write_messages is generic, I can now write more pure systems that produce messages and reuse it.
Wouldn't it be nice if 100% of user-written systems could be pure? If somebody provided all the systems you may ever need to do the "writing" so that you only have to worry about writing ECS effects declaratively?
bevy_pipe_affect aims to provide these systems.
Or rather, a single system for all ECS mutation.
Her name is affect:
//! This test is mostly used to demonstrate testing in the book use bevy::prelude::*; use bevy_pipe_affect::prelude::*; #[derive(Component)] struct Health(u32); #[derive(Debug, PartialEq, Eq, Message)] struct DeathMessage(Entity); fn detect_deaths(query: Query<(Entity, &Health)>) -> Vec<MessageWrite<DeathMessage>> { query .iter() .flat_map(|(entity, health)| { if health.0 == 0 { Some(DeathMessage(entity)) } else { None } }) .map(message_write) .collect() } use bevy::ecs::system::RunSystemOnce; #[derive(Resource)] struct UnhealthyEntity(Entity); fn test_detect_deaths() { let mut world = World::new(); // We still need to setup the initial state of the world. let _setup = world .run_system_once( (|| { command_spawn_and(Health(100), |_| { command_spawn_and(Health(0), |entity| { command_insert_resource(UnhealthyEntity(entity)) }) }) }) .pipe(affect) .pipe(ApplyDeferred), ) .unwrap(); // Now we can just assert against system output instead of state changes let dead_entity_messages = world.run_system_once(detect_deaths).unwrap(); let UnhealthyEntity(entity) = world.get_resource::<UnhealthyEntity>().unwrap(); assert_eq!( dead_entity_messages, vec![message_write(DeathMessage(*entity))] ); } #[test] fn cargo_test_detect_deaths() { test_detect_deaths() } fn main() { bevy::ecs::system::assert_is_system(detect_deaths.pipe(affect)); }
Rather than returning a list of messages, detect_deaths now returns MessageWrites, which is an Effect.
A Vec of Effects is also an Effect.
Then, the affect system can take any Effect and do the necessary writing.
The user no longer has to write words like mut and for.
Theoretical motivation
Bevy's system scheduling APIs are higher-order functions that allow you to register system-functions to the App.
We can basically think of these higher-order functions as taking functions with two arguments, a SystemInput and a SystemParam, and then having an output.
Technically there's an extra wrinkle to this for two reasons, but both are just a bit of sugar that carmelize down to these two arguments:
- the
SystemInputcan be omitted, but the Bevy scheduling traits just use the unit type()in these cases - the
SystemParamcan occupy more than 1 arguments to the function (or even 0), but the Bevy scheduling traits just convert these cases to a tupleSystemParam
This is elegant.
Our SystemParam argument not only serves as normal function input, but it also expresses to the higher-order scheduling APIs what factor of the world needs to be input to the system.
I say factor in the sense of algebraic data types.
In the language of algebraic data types, an ECS world is sort of like a product of component storages and resources, and our SystemParam identifies a factor of this product.
Again, the reality of Bevy is more complicated (this time, much more complicated) than this theoretical framework.
The SystemParam is even composable.
The factor of the world that a system gets as input can actually be a larger product of system params.
As in, it can be a tuple of other system params, which again, is what the sugar of multi-system-param-argument functions carmelizes into.
Pipe systems also leverage this fact by composing the SystemParams of two systems into one.
So far nothing about this is functionally impure.
We have functions with two arguments and an output, the first argument is SystemInput which is parameterized by output of another system, the second argument is SystemParam which is parameterized by some data in the world.
The impurity arrives when we allow that data from the world to be mutable.
And of course, in vanilla Bevy, this is our only choice if we want to have any effect on the world other than heating up our computers.
Pure functions are just input and output.
We'd like to use the output instead of the side effects to have an effect on world data.
Hence the Effect types provided by bevy_pipe_affect, intended to be returned by user systems.
Effects, conceptually, are almost a reflection of SystemParams.
Where SystemParams allow systems to express what factor of the world should be read, Effects allow systems to express what factor of the world should be written (and how).
Where SystemParams have an identity in the form of () that requests no data from the world, Effects also treat () as an identity that has no effect on the world.
Where SystemParams offer composibility with product types and derives, Effects offer composibility with product types, derives, and sum-types.
Yes, not only is Effect implemented for tuples of effects, it can also be derived for structs of Effects and enums of Effects.
The latter is not a reflection of SystemParam behavior.
After all, it's not that common that you want a system that accepts either system param A or system param B.
It's a different story for Effects, as there are many situations where you want either effect A to happen or effect B to happen.
The composibility of Effects is as algebraic as algebraic data types.
In vanilla Bevy, systems are functions that have a side effect on system params. By returning effects instead, systems now have a more satisfying theoretical definition. They are mappings from some factor of the world to a state transition. Or, more abstractly, they are pure, deterministic declarations of world behavior.
Write systems as pure functions
Of course, none of this is required with bevy_pipe_affect.
Nothing about it forces you to write pure systems, you could write an effectful system that pipes an Effect into the affect system.
If you choose to, you will enjoy many of the benefits of pure functions. The consequences of your systems will be more obvious at a glance: they are in the system's return type. If you need more specifics, their value will always be at the very bottom of your function body. In general, these two facts make it more difficult for you to muddy your systems with effects. You will be encouraged to separate the concerns of your systems even more than you already are.
And of course, unit tests are easier to write.
Instead of observing the effects your systems have on the Bevy world, you can just observe the output of your systems.
An example, testing the detect_deaths system written above:
//! This test is mostly used to demonstrate testing in the book use bevy::prelude::*; use bevy_pipe_affect::prelude::*; #[derive(Component)] struct Health(u32); #[derive(Debug, PartialEq, Eq, Message)] struct DeathMessage(Entity); fn detect_deaths(query: Query<(Entity, &Health)>) -> Vec<MessageWrite<DeathMessage>> { query .iter() .flat_map(|(entity, health)| { if health.0 == 0 { Some(DeathMessage(entity)) } else { None } }) .map(message_write) .collect() } use bevy::ecs::system::RunSystemOnce; #[derive(Resource)] struct UnhealthyEntity(Entity); fn test_detect_deaths() { let mut world = World::new(); // We still need to setup the initial state of the world. let _setup = world .run_system_once( (|| { command_spawn_and(Health(100), |_| { command_spawn_and(Health(0), |entity| { command_insert_resource(UnhealthyEntity(entity)) }) }) }) .pipe(affect) .pipe(ApplyDeferred), ) .unwrap(); // Now we can just assert against system output instead of state changes let dead_entity_messages = world.run_system_once(detect_deaths).unwrap(); let UnhealthyEntity(entity) = world.get_resource::<UnhealthyEntity>().unwrap(); assert_eq!( dead_entity_messages, vec![message_write(DeathMessage(*entity))] ); } #[test] fn cargo_test_detect_deaths() { test_detect_deaths() } fn main() { test_detect_deaths() }
Over all, game logic just becomes easier to reason about, especially ex post facto. I hope you enjoy writing systems this way, and that they bring you more joy when the time comes for you to maintain them.
Output and Effect Composition
This chapter will cover the ways that effects compose, starting with the most basic and getting more advanced.
Combined effects
The canonical form of "effect composition" is the combined effect, which is simply a tuple of effects.
The Effect trait is implemented for tuples where each element of the tuple also implements Effect.
The affect system will perform their effects from left to right.
So, if you want a system that has 2 or more effects of heterogenous type, you can just return their tuple:
use bevy::prelude::*; use bevy_pipe_affect::prelude::*; #[derive(Resource)] struct Score(u32); fn setup() -> impl Effect { ( command_spawn(Camera2d::default()), command_insert_resource(Score(0)), ) } fn main() { bevy::ecs::system::assert_is_system(setup.pipe(affect)) }
Effect iterators
Effect is implemented for a couple of important iterators, Option and Vec.
There's also the affect_many effect, which can wrap any iterator.
So, if you want a system that has 2 or more effects of homogenous type, you can return them as a Vec:
//! This test is mostly used to demonstrate testing in the book use bevy::prelude::*; use bevy_pipe_affect::prelude::*; #[derive(Component)] struct Health(u32); #[derive(Debug, PartialEq, Eq, Message)] struct DeathMessage(Entity); fn detect_deaths(query: Query<(Entity, &Health)>) -> Vec<MessageWrite<DeathMessage>> { query .iter() .flat_map(|(entity, health)| { if health.0 == 0 { Some(DeathMessage(entity)) } else { None } }) .map(message_write) .collect() } use bevy::ecs::system::RunSystemOnce; #[derive(Resource)] struct UnhealthyEntity(Entity); fn test_detect_deaths() { let mut world = World::new(); // We still need to setup the initial state of the world. let _setup = world .run_system_once( (|| { command_spawn_and(Health(100), |_| { command_spawn_and(Health(0), |entity| { command_insert_resource(UnhealthyEntity(entity)) }) }) }) .pipe(affect) .pipe(ApplyDeferred), ) .unwrap(); // Now we can just assert against system output instead of state changes let dead_entity_messages = world.run_system_once(detect_deaths).unwrap(); let UnhealthyEntity(entity) = world.get_resource::<UnhealthyEntity>().unwrap(); assert_eq!( dead_entity_messages, vec![message_write(DeathMessage(*entity))] ); } #[test] fn cargo_test_detect_deaths() { test_detect_deaths() } fn main() { bevy::ecs::system::assert_is_system(detect_deaths.pipe(affect)); }
EffectOut
bevy_pipe_affect sort of hijacks bevy's system piping.
So, at first glance, it may seem like there's no way to go about typical system pipe usage while making effects.
The EffectOut type aims to give system piping back to the people.
It also provides some composibility of its own that may be useful beyond systems.
More on this in the following sections.
Structurally, it's just an effect field containing an effect, and an out field containing additional output.
You may be interested to know that the higher-order systems provided by bevy_pipe_affect actually only ever expect a type that can convert into EffectOut, not just a mere Effect:
use bevy::prelude::*; use bevy_pipe_affect::prelude::*; #[derive(Resource)] struct Score(u32); #[derive(Deref, DerefMut, Resource)] struct StartTime(f32); fn update_score(time: Res<Time>, start_time: Res<StartTime>) -> EffectOut<ResSet<Score>, f32> { let level_time = time.elapsed_secs() - **start_time; effect_out(res_set(Score(level_time as u32)), level_time) } fn main() { bevy::ecs::system::assert_is_system(update_score.pipe(affect)) }
Notice that we can still pipe update_score into affect, even though update_score returns an EffectOut instead of an Effect.
However, be aware that affect will actually have an output too; the f32 is passed along.
This would prevent us from scheduling the system (without further piping to drop the f32).
However, it is inconsequential if the out type is ().
EffectOut::and_then
EffectOuts compose in a few different ways, with the main goal of letting users process the out field while continuing to collect effects.
For example, the and_then method takes a function for processing the out into another Effect/EffectOut, and returns an EffectOut with the effect combining the original/new effect, and an out being the new output.
This code example shows and_then being used to process the EffectOut returned by one function into more effects:
use bevy::prelude::*; use bevy_pipe_affect::prelude::*; // A simple marker component for players. #[derive(Component)] struct Player<const N: usize>; // A logical component for player level. #[derive(Component, Deref, DerefMut)] struct FightLevel(u32); /// A simple message we can write with effects. #[derive(Message)] struct Log(String); /// Will be used as an intermediate result of the fight logic. enum FightOutcome { Player1Wins, Player2Wins, Draw, } /// This is not a system! It describes the logic for the upcoming fight system and also has a logging effect. fn fight_outcome( player_1: &FightLevel, player_2: &FightLevel, ) -> EffectOut<MessageWrite<Log>, FightOutcome> { if **player_1 > **player_2 { effect_out( message_write(Log("player 1 wins!".to_string())), FightOutcome::Player1Wins, ) } else if **player_1 < **player_2 { effect_out( message_write(Log("player 2 wins!".to_string())), FightOutcome::Player2Wins, ) } else { effect_out( message_write(Log("fight came to a draw!".to_string())), FightOutcome::Draw, ) } } /// This is a system! It defines the effects of a player 1 and 2 fighting (logging the result and despawning). fn fight( player_1: Single<(Entity, &FightLevel), With<Player<1>>>, player_2: Single<(Entity, &FightLevel), With<Player<2>>>, ) -> EffectOut<(MessageWrite<Log>, Option<EntityCommandDespawn>), ()> { let (player_1_entity, player_1_level) = *player_1; let (player_2_entity, player_2_level) = *player_2; // here's an EffectOut-returning call fight_outcome(player_1_level, player_2_level) // and now we're composing it w/ a closure processing the `out` .and_then(|outcome| match outcome { FightOutcome::Player1Wins => Some(entity_command_despawn(player_2_entity)), FightOutcome::Player2Wins => Some(entity_command_despawn(player_1_entity)), FightOutcome::Draw => None, // we can return an EffectOut here, but a mere Effect works too (in this case Option<EntityCommandDespawn>) }) // we could continue composing it with more `.and_then`s if we had more output to process into effects. } fn main() { bevy::ecs::system::assert_is_system(fight.pipe(affect)) }
You may notice that, if we did want to create the "despawn" effects in the fight_outcome system, we'd have to complicate its function signature with more Entity inputs.
This composition of EffectOuts keeps each function smaller and simpler, but allows for a grander logic that is much more complicated and powerful.
Rust users will recognize this API as being similar in name and purpose to Option::and_then and Result::and_then, and they'd roughly be correct.
Functional programmers, on the other hand, may recognize this API as being similar to a monad's bind operation.
It is kind of like that, but not quite.
While the out: O type gets mapped in a monadic way, the effect: E type changes to be a tuple of the new and old effects.
If it were truly monadic, only one type parameter (out: O) of EffectOut would be changed by bind, not both.
EffectOut::and_extend
and_then may not be monadic, but there is another EffectOut composition function that is.
If the effect: E of the original EffectOut is an extendable iterator, and the new effect is an iterator, they can be concatenated with and_extend.
In this excerpt from the sokoban example, we take advantage of this to write a system with recursive logic.
This recursion is only possible because the recursive function's typing stays consistent.
I.e., the effect: E type parameter doesn't need to change with and_extend like it does with and_then:
use bevy::prelude::*; use bevy_pipe_affect::prelude::*; /// Component defining the logical position of a sokoban entity. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Component, Deref, DerefMut)] pub struct Position(pub IVec2); /// Component defining the weight of a sokoban block (blocks too heavy cannot be pushed). #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Component, Deref, DerefMut)] pub struct Weight(pub u32); /// An observer event for triggering the push system #[derive(Event)] pub struct PushEntity { pub direction: IVec2, pub entity: Entity, } /// This recursive function creates the effects for pushing entities and also sums their weights. fn push_and_weigh( positions: &Query<(Entity, &Position, &Weight)>, position_pushed: Position, direction: IVec2, ) -> EffectOut<Vec<EntityComponentsSet<(Position,)>>, Weight> { match positions .iter() .find(|(_, position, _)| **position == position_pushed) { // base case None => effect_out(vec![], Weight(0)), // recursive case Some((entity, _, weight)) => { let new_position = Position(*position_pushed + direction); push_and_weigh(&positions, new_position.clone(), direction) // This is monadic EffectOut composition! .and_extend(|acc_weight| { effect_out( vec![entity_components_set(entity, (new_position,))], Weight(*acc_weight + **weight), ) }) } } } /// This observer system is the entrypoint for the above recursive pushing logic. pub fn push( push: On<PushEntity>, positions: Query<(Entity, &Position, &Weight)>, ) -> Vec<EntityComponentsSet<(Position,)>> { let (_first_entity, position_pushed, _weight) = positions.get(push.entity).unwrap(); // We only use `EffectOut` for intermediate computation, and return a normal `Effect` in the system. let EffectOut { effect: pushes, out: weight, } = push_and_weigh(&positions, *position_pushed, push.direction); if *weight > 10 { // too heavy, do nothing. vec![] } else { pushes } } /// Spawns an observer for the push system. pub fn spawn_push_observer() -> impl Effect { command_spawn(Observer::new(push.pipe(affect))) } fn main() { bevy::ecs::system::assert_is_system(push.pipe(affect)) }
This is a bit of a side-note, but notice how this system takes advantage of the fact that we are not actually performing side effects directly when using Effects.
We create a hypothetical set of entity movements with push_and_weigh, but those movements aren't performed until the affect system runs.
So, we can decide to discard them for whatever reason between now and then (in this case, the reason being that the total weight is too heavy).
EffectOut::and_then_compose
So, EffectOut::and_then and EffectOut::and_extend define two common ways to process output into more effects, and compose those effects.
What about less common effect composition strategies?
Well, EffectOut::and_then_compose simply allows the user to pass in an effect composition function, with a signature like Fn(E1, E2) -> E3.
It is actually used internally by and_then and and_extend.
Some common effect composition functions are provided in the crate's effect_composition module.
EffectOut iterators
Often, you will find yourself iterating through queries or event readers, and trying to produce effects during that iteration.
As you enjoy FP, you'll probably be trying to do this by mapping the iterator to effects.
This works well for mere Effects, you can just collect into a Vec and call it a day.
However, you may be dealing with EffectOuts, and a system returning Vec<EffectOut> cannot be piped into affect.
The crate provides a simple answer for this issue.
If the effect and out are both extendable iterators, then you can collect an iterator of EffectOuts into an EffectOut of iterators.
This is actually demonstrated in the code example of the next section.
System-level composition
EffectOut::and_then, EffectOut::and_extend, and EffectOut::and_then_compose are all methods for processing the out of an effect out into more effects and composing them.
Their functionality is also available at the system level, so you can return an EffectOut from one system, and then process its output into more effects with another.
These are the in_and_then, in_and_extend, and in_and_then_compose system combinators, respectively.
They each accept a system that takes the out: O type as system input and returns an effect, and they each return a system that accepts the whole EffectOut as system input and composes the effects.
Here, we use in_and_then to compose a system that creates explosions while calculating the explosion size with a system that plays the explosion sound effect.
use bevy::prelude::*; use bevy_pipe_affect::prelude::*; use bevy::audio::Volume; #[derive(Deref, DerefMut, Component)] struct InternalPressure(u32); #[derive(Deref, DerefMut, Resource)] struct ExplosionSound(Handle<AudioSource>); fn explosion( explodable: Query<(Entity, &InternalPressure)>, ) -> EffectOut<Vec<EntityCommandDespawn>, u32> { explodable .iter() .flat_map(|(entity, internal_pressure)| { let explosion_size = internal_pressure.saturating_sub(100); if explosion_size > 0 { Some(effect_out(entity_command_despawn(entity), explosion_size)) } else { None } }) .collect::<EffectOut<_, Vec<_>>>() // this maps the `out` value (functoriality!) .map(|sizes| sizes.into_iter().sum()) } fn explosion_sound(In(explosion_size): In<u32>, sound: Res<ExplosionSound>) -> Option<impl Effect> { if explosion_size > 0 { Some(command_spawn(( AudioPlayer::new(sound.clone()), PlaybackSettings::default().with_volume(Volume::Linear(explosion_size as f32)), ))) } else { None } } fn main() { bevy::ecs::system::assert_is_system(explosion.pipe(in_and_then(explosion_sound)).pipe(affect)); }
Notice how the explosion and explosion_sound systems have completely disjoint system parameters.
Just like functions, the benefit of composing the effects of multiple systems in this way is often that you can simplify each individual system.
As mentioned before, if there remains an out type at the end of all this piping, then it will be passed out of the affect system, allowing for output processing to continue.
Spawn a Relationship Synchronously
Bevy relationships may be spawned using specialized commands, like with_children or with_related_entities.
bevy_pipe_affect APIs are more minimal, but these situations can still be handled ergonomically:
- Return an effect that spawns the entity that will be the
RelationshipTargetwithcommand_spawn_and. In theParent/ChildOfrelationship, this will become theParententity. That component does not need to be provided, it will be created by Bevy. - Provide a closure to the second argument of the
command_spawn_andcall that returns anothercommand_spawn-effect that will spawn theRelationshipentity. In theParent/ChildOfrelationship, this is theChildOfentity. This time, you do need to provide that component with theEntityprovided to the closure (this will beRelationshipTargetEntity, spawned in step 1).
The relationship example does this, while also bundling some sprites/marker components:
#[derive(Component)] struct Spinny; use bevy::prelude::*; use bevy_pipe_affect::prelude::*; fn spawn_relationship() -> impl Effect { // We will give both entities a sprite, which we can load using this effect // We can use its handle to create more effects with a closure asset_server_load_and("player.png", |image_handle| { // Similarly, this effect will spawn our components // Then, we use the resulting Entity to create more effects with a closure command_spawn_and( ( Spinny, Sprite::from_image(image_handle.clone()), Transform::from_scale(Vec3::splat(10.0)), ), |parent| { command_spawn(( ChildOf(parent), // This is where the relationship happens! Spinny, Sprite::from_image(image_handle), Transform::from_xyz(20.0, 20.0, 0.0).with_scale(Vec3::splat(0.5)), )) }, ) }) } fn main() { bevy::ecs::system::assert_is_system(spawn_relationship.pipe(affect)) }
Spawn and Trigger an Observer
While bevy_pipe_affect effects are somewhat minimal and don't include a command_add_observer, observers still integrate well with effects.
The following code examples are pulled from the observer cargo example.
- Create an event type (
InflateEventin this example)
use bevy::prelude::*; use bevy_pipe_affect::prelude::*; #[derive(Event)] pub struct InflateEvent; #[derive(Component)] pub struct Inflatable; fn inflate(_event: On<InflateEvent>) -> impl Effect + use<> { components_set_filtered_with::<_, _, With<Inflatable>>(|(transform,): (Transform,)| { (transform.with_scale(transform.scale * 1.1),) }) } pub fn spawn_observer() -> impl Effect { command_spawn(Observer::new(inflate.pipe(affect))) } pub fn trigger_observer(input: Res<ButtonInput<KeyCode>>) -> Option<CommandTrigger<InflateEvent>> { if input.just_pressed(KeyCode::Space) { Some(command_trigger(InflateEvent)) } else { None } } fn main() {}
- Create a system that accepts an
On<InflateEvent>and returns an effect that will be ran by the observer.
use bevy::prelude::*; use bevy_pipe_affect::prelude::*; #[derive(Event)] pub struct InflateEvent; #[derive(Component)] pub struct Inflatable; fn inflate(_event: On<InflateEvent>) -> impl Effect + use<> { components_set_filtered_with::<_, _, With<Inflatable>>(|(transform,): (Transform,)| { (transform.with_scale(transform.scale * 1.1),) }) } pub fn spawn_observer() -> impl Effect { command_spawn(Observer::new(inflate.pipe(affect))) } pub fn trigger_observer(input: Res<ButtonInput<KeyCode>>) -> Option<CommandTrigger<InflateEvent>> { if input.just_pressed(KeyCode::Space) { Some(command_trigger(InflateEvent)) } else { None } } fn main() { bevy::ecs::system::assert_is_system(inflate.pipe(affect)) }
- Create a system that spawns an
Observercomponent withcommand_spawn, using the previous system.pipe(affect)-ed as the observer system.
use bevy::prelude::*; use bevy_pipe_affect::prelude::*; #[derive(Event)] pub struct InflateEvent; #[derive(Component)] pub struct Inflatable; fn inflate(_event: On<InflateEvent>) -> impl Effect + use<> { components_set_filtered_with::<_, _, With<Inflatable>>(|(transform,): (Transform,)| { (transform.with_scale(transform.scale * 1.1),) }) } pub fn spawn_observer() -> impl Effect { command_spawn(Observer::new(inflate.pipe(affect))) } pub fn trigger_observer(input: Res<ButtonInput<KeyCode>>) -> Option<CommandTrigger<InflateEvent>> { if input.just_pressed(KeyCode::Space) { Some(command_trigger(InflateEvent)) } else { None } } fn main() { bevy::ecs::system::assert_is_system(spawn_observer.pipe(affect)) }
- Trigger your observer with the
command_triggereffect.
use bevy::prelude::*; use bevy_pipe_affect::prelude::*; #[derive(Event)] pub struct InflateEvent; #[derive(Component)] pub struct Inflatable; fn inflate(_event: On<InflateEvent>) -> impl Effect + use<> { components_set_filtered_with::<_, _, With<Inflatable>>(|(transform,): (Transform,)| { (transform.with_scale(transform.scale * 1.1),) }) } pub fn spawn_observer() -> impl Effect { command_spawn(Observer::new(inflate.pipe(affect))) } pub fn trigger_observer(input: Res<ButtonInput<KeyCode>>) -> Option<CommandTrigger<InflateEvent>> { if input.just_pressed(KeyCode::Space) { Some(command_trigger(InflateEvent)) } else { None } } fn main() { bevy::ecs::system::assert_is_system(trigger_observer.pipe(affect)) }