diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..b65046afe8868c9621fb6ee595921f117f29a439 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,22 @@ +# Add the contents of this file to `config.toml` to enable "fast build" configuration. Please read the notes below. + +# NOTE: For maximum performance, build using a nightly compiler +# If you are using rust stable, remove the "-Zshare-generics=y" below (as well as "-Csplit-debuginfo=unpacked" when building on macOS). + +[target.x86_64-unknown-linux-gnu] +linker = "/usr/bin/clang" +rustflags = ["-Clink-arg=-fuse-ld=lld"] + +# NOTE: you must manually install https://github.com/michaeleisel/zld on mac. you can easily do this with the "brew" package manager: +# `brew install michaeleisel/zld/zld` +[target.x86_64-apple-darwin] +rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/bin/zld"] + +[target.x86_64-pc-windows-msvc] +linker = "rust-lld.exe" +rustflags = [] + +# Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only' +# In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains. +#[profile.dev] +#debug = 1 diff --git a/Cargo.toml b/Cargo.toml index c8b18134f81cb7cd9300e5d370fbc9d5d58e1651..f360cc725751d1426be4eb4e056d352397428557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,18 +6,16 @@ edition = "2018" description = "Rusty Utility AI library" license-file = "LICENSE.md" readme = "README.md" -keywords = ["utility-ai", "specs", "ecs"] +keywords = ["utility-ai", "bevy", "ai", "ecs"] categories = ["game-development"] repository = "https://github.com/zkat/big-brain" homepage = "https://github.com/zkat/big-brain" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -big-brain-derive = "0.1" -specs = { version = "0.16.1", features = ["parallel", "shred-derive", "specs-derive"] } +big-brain-derive = { path = "./big-brain-derive", version = "0.1" } serde = "1.0.111" typetag = "0.1.5" specs-derive = "0.4.1" ron = "0.6.0" +bevy = "0.5.0" diff --git a/README.md b/README.md index 64e4c9a78ad9074b2a0222dac6aac812a53c4fb9..bedf86d902995a4f3679fa2819e86d2c18eed0ab 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ `big-brain` is a [Utility AI](https://en.wikipedia.org/wiki/Utility_system) -library for games, built on the [`specs` ECS](https://docs.rs/specs). +library for games, built for the [Bevy Game Engine](https://bevyengine.org/) It lets you define complex, intricate AI behaviors for your entities based on their perception of the world. Definitions are almost entirely data-driven, @@ -17,39 +17,30 @@ First, you define actions and considerations, which are just plain old `specs` ### Considerations -`Consideration`s are entities that look at the world and evaluate into `Utility`s. +`Consideration`s are entities that look at the world and evaluate into `Utility` values. ```rust -use specs::{Component, Entity, ReadStorage, System, WriteStorage}; -use big_brain::{Consideration, Utility}; +use bevy::prelude::*; +use big_brain::*; -use crate::components; - -#[derive(Debug, Component, Consideration)] -pub struct Hunger { - pub actor: Entity, +#[derive(Debug, Consideration)] +pub struct ThirstConsideration { #[consideration(default)] pub evaluator: PowerEvaluator, #[consideration(param)] pub weight: f32, } -pub struct ConsiderHunger; -impl<'a> System<'a> for ConsiderHunger { - type SystemData = ( - ReadStorage<'a, components::Hunger>, - WriteStorage<'a, Hunger>, - WriteStorage<'a, Utility>, - ); - - fn run(&mut self, (hungers, mut considerers, mut utilities): Self::SystemData) { - for (conser, util) in (&mut considerers, &mut utilities).join() { - if let Some(hunger) = hungers.get(conser.actor.clone()) { - *util = Utility { - value: conser.evaluator.evaluate(hunger.hunger), - weight: conser.weight, - }; - } +pub fn thirst_consideration_system( + thirsts: Query<&Thirst>, + mut query: Query<(&Parent, &ThirstConsideration, &mut Utility)>, +) { + for (Parent(actor), conser, mut util) in query.iter_mut() { + if let Ok(thirst) = thirsts.get(*actor) { + *util = Utility { + value: conser.evaluator.evaluate(thirst.thirst), + weight: conser.weight, + }; } } } @@ -60,40 +51,25 @@ impl<'a> System<'a> for ConsiderHunger { `Action`s are the actual things your entities will _do_. ```rust -use specs::{Component, Entity, System, WriteStorage}; -use big_brain::{Action, ActionState}; - -use crate::components; - -#[derive(Debug, Clone, Component, Action)] -pub struct Eat { - pub actor: Entity, - #[action(default)] - pub foo: f32, - #[action(param)] - pub reduce_by: f32, -} - -pub struct EatSystem; -impl<'a> System<'a> for EatSystem { - type SystemData = ( - WriteStorage<'a, components::Hunger>, - WriteStorage<'a, Eat>, - WriteStorage<'a, ActionState>, - ); - fn run(&mut self, (mut hungers, mut eat_actions, mut states): Self::SystemData) { - for (state, eat_action) in (&mut states, &mut eat_actions).join() { - if let Some(hunger) = hungers.get_mut(eat_action.actor.clone()) { - match state { - ActionState::Requested => { - hunger.hunger -= eat_action.reduce_by; - *state = ActionState::Success; - } - ActionState::Cancelled => { - *state = ActionState::Failure; - } - _ => {} +#[derive(Debug, Action)] +pub struct DrinkAction {} + +fn drink_action_system( + mut thirsts: Query<&mut Thirst>, + mut query: Query<(&Parent, &DrinkAction, &mut ActionState)>, +) { + for (Parent(actor), _drink_action, mut state) in query.iter_mut() { + if let Ok(mut thirst) = thirsts.get_mut(*actor) { + match *state { + ActionState::Requested => { + thirst.thirst = 10.0; + println!("drank some water"); + *state = ActionState::Success; + } + ActionState::Cancelled => { + *state = ActionState::Failure; } + _ => {} } } } @@ -102,7 +78,7 @@ impl<'a> System<'a> for EatSystem { ### Thinker Definition -Finally, you can define the `Thinker`: +Finally, you can use it when define the `Thinker`: ```ron // behavior.ron diff --git a/big-brain-derive/Cargo.toml b/big-brain-derive/Cargo.toml index 6df78726b47af1dd2fcb80ef826e79cc618c52e6..9df5616fcf3c59656b4ac3204cc4c16bfe11c7f1 100644 --- a/big-brain-derive/Cargo.toml +++ b/big-brain-derive/Cargo.toml @@ -16,6 +16,6 @@ proc-macro = true [dependencies] syn = "1.0.33" quote = "1.0.7" -big-brain = "0.1.0" +# big-brain = { path = "../", version = "0.1.0" } proc-macro2 = "1.0.18" darling = "0.10.2" diff --git a/big-brain-derive/src/action.rs b/big-brain-derive/src/action.rs index e0f58786c303e56db2dee233c57d737070da5a28..e1c01c6da5c64da72573ce814534961f9eaeea3c 100644 --- a/big-brain-derive/src/action.rs +++ b/big-brain-derive/src/action.rs @@ -1,7 +1,6 @@ use darling::{ast, FromDeriveInput, FromField, ToTokens}; use proc_macro2::TokenStream; use quote::quote; -use syn; #[derive(Debug, FromDeriveInput)] #[darling(attributes(action), supports(struct_named))] @@ -70,10 +69,10 @@ impl ToTokens for Action { } }); let ts = quote! { - mod big_brain_builder { + mod big_brain_action_builder { use super::#ident as Comp; - use big_brain::{typetag, serde::Deserialize, Action, ActionManager, ecs::{Entity, Entities, LazyUpdate}, ActionEnt}; + use big_brain::{typetag, serde::Deserialize, Action, ActionManager, bevy::prelude::{Entity, Commands}, ActionEnt}; #[derive(Debug, Deserialize)] struct #ident { @@ -82,19 +81,19 @@ impl ToTokens for Action { #[typetag::deserialize] impl Action for #ident { - fn build(self: Box<Self>, actor: Entity, action_ent: ActionEnt, ents: &Entities, lazy: &LazyUpdate) -> Box<dyn ActionManager> { + fn build(self: Box<Self>, actor: Entity, action_ent: ActionEnt, cmd: &mut Commands) -> Box<dyn ActionManager> { self } } impl ActionManager for #ident { - fn activate(&self, actor: Entity, action_ent: ActionEnt, lazy: &LazyUpdate) { - lazy.insert(action_ent.0.clone(), Comp { + fn activate(&self, actor: Entity, action_ent: ActionEnt, cmd: &mut Commands) { + cmd.entity(action_ent.0).insert(Comp { #(#field_assignments),* }); } - fn deactivate(&self, action_ent: ActionEnt, lazy: &LazyUpdate) { - lazy.remove::<Comp>(action_ent.0); + fn deactivate(&self, action_ent: ActionEnt, cmd: &mut Commands) { + cmd.entity(action_ent.0).remove::<Comp>(); } } } diff --git a/big-brain-derive/src/consideration.rs b/big-brain-derive/src/consideration.rs index 5c60270341c6f7990055c03a34d1c1efd8c117eb..14fdb96c80b2d38ac473540cd1482680dd2f3f17 100644 --- a/big-brain-derive/src/consideration.rs +++ b/big-brain-derive/src/consideration.rs @@ -1,7 +1,6 @@ use darling::{ast, FromDeriveInput, FromField, ToTokens}; use proc_macro2::TokenStream; use quote::quote; -use syn; #[derive(Debug, FromDeriveInput)] #[darling(attributes(consideration), supports(struct_named))] @@ -70,10 +69,11 @@ impl ToTokens for Consideration { } }); let ts = quote! { - mod big_brain_builder { + mod big_brain_cons_builder { use super::#ident as Comp; - use big_brain::{typetag, serde::Deserialize, Consideration, ecs::{Entity, Entities, LazyUpdate}, ConsiderationEnt}; + use big_brain::{typetag, serde::Deserialize, Consideration, bevy::prelude::{Entity, Commands}, ConsiderationEnt}; + // use typetag; #[derive(Debug, Deserialize)] struct #ident { @@ -81,10 +81,11 @@ impl ToTokens for Consideration { } #[typetag::deserialize] impl Consideration for #ident { - fn build(&self, actor: Entity, ents: &Entities, lazy: &LazyUpdate) -> ConsiderationEnt { - let ent = ConsiderationEnt(ents.create()); - lazy.insert(ent.clone().0, big_brain::Utility::default()); - lazy.insert(ent.clone().0, Comp { + fn build(&self, actor: Entity, cmd: &mut Commands) -> ConsiderationEnt { + let ent = ConsiderationEnt(cmd.spawn().id()); + cmd.entity(ent.0) + .insert(big_brain::Utility::default()) + .insert(Comp { #(#field_assignments),* }); ent diff --git a/examples/thirst.rs b/examples/thirst.rs new file mode 100644 index 0000000000000000000000000000000000000000..b7a29dd025185e61e0a50331a7ced9e256b8b6e9 --- /dev/null +++ b/examples/thirst.rs @@ -0,0 +1,164 @@ +use bevy::prelude::*; +use big_brain::{evaluators::*, *}; + +// First, we define a "Thirst" component and associated system. This is NOT +// THE AI. It's a plain old system that just makes an entity "thirstier" over +// time. This is what the AI will later interact with. +// +// There's nothing special here. It's a plain old Bevy component. +#[derive(Debug)] +pub struct Thirst { + pub per_second: f32, + pub thirst: f32, +} + +impl Thirst { + pub fn new(thirst: f32, per_second: f32) -> Self { + Self { thirst, per_second } + } +} + +pub fn thirst_system(time: Res<Time>, mut thirsts: Query<&mut Thirst>) { + for mut thirst in thirsts.iter_mut() { + thirst.thirst += + thirst.per_second * (time.delta().as_micros() as f32 / 1000000.0); + println!("Getting thirstier...{}%", thirst.thirst); + } +} + +// The second step is to define an action. What can the AI do, and how does it +// do it? This is the first bit involving Big Brain itself, and there's a few +// pieces you need: + +// First, you need an Action struct, and derive Action. +// +// These actions will be spawned and queued by the game engine when their +// conditions trigger (we'll configure what these are later). +#[derive(Debug, Action)] +pub struct DrinkAction {} + +// Associated with that DrinkAction, you then need to have a system that will +// actually execute those actions when they're "spawned" by the Big Brain +// engine. +// +// In our case, we want the Thirst components, since we'll be changing those. +// Additionally, we want to pick up the DrinkAction components, as well as +// their associated ActionState. Note that the DrinkAction belongs to a +// *separate entity* from the owner of the Thirst component! +fn drink_action_system( + mut thirsts: Query<&mut Thirst>, + // We grab the Parent here, because individual Actions are parented to the + // entity "doing" the action. + // + // ActionState is an enum that described the specific run-state the action + // is in. You can think of Actions as state machines. They get requested, + // they can be cancelled, they can run to completion, etc. Cancellations + // usually happen because the target action changed (due to a different + // Consideration winning). But you can also cancel the actions yourself by + // setting the state in the Action system. + mut query: Query<(&Parent, &DrinkAction, &mut ActionState)>, +) { + for (Parent(actor), _drink_action, mut state) in query.iter_mut() { + // Use the drink_action's actor to look up the corresponding Thirst. + if let Ok(mut thirst) = thirsts.get_mut(*actor) { + match *state { + ActionState::Requested => { + thirst.thirst = 10.0; + println!("drank some water"); + *state = ActionState::Success; + } + ActionState::Cancelled => { + *state = ActionState::Failure; + } + _ => {} + } + } + } +} + +// Then, we have something called "Considerations". These are special +// components that run in the background, calculating a "Utility" value, which +// is what Big Brain will use to pick which actions to execute. +// +// Additionally, though, we pull in an evaluator and define a weight. Which is +// just mathy stuff you can tweak to get the behavior you want. More on this +// in the docs (later), but for now, just put them in there and trust the +// system. :) +#[derive(Debug, Consideration)] +pub struct ThirstConsideration { + #[consideration(default)] + pub evaluator: PowerEvaluator, + #[consideration(param)] + pub weight: f32, +} + +// Look familiar? Similar dance to Actions here. +pub fn thirst_consideration_system( + thirsts: Query<&Thirst>, + // Same dance with the Parent here, but now we've added a Utility! + mut query: Query<(&Parent, &ThirstConsideration, &mut Utility)>, +) { + for (Parent(actor), conser, mut util) in query.iter_mut() { + if let Ok(thirst) = thirsts.get(*actor) { + // This is really what the job of a Consideration is. To calculate + // a generic Utility value that the Big Brain engine will compare + // against others, over time, and use to make decisions. This is + // generally "the higher the better", and "first across the finish + // line", but that's all configurable! You can customize that to + // your heart's extent using Measures and Pickers. + // + // A note: You don't actually *need* evaluators/weights. You can + // literally just use linear values here and set thresholds + // accordingly. The evaluator is just there to give the value a + // bit of a curve. + *util = Utility { + value: conser.evaluator.evaluate(thirst.thirst), + weight: conser.weight, + }; + } + } +} + +// Now that we hav eall that defined, it's time to add a Thinker to an entity! +// The Thinker is the actual "brain" behind all the AI. Every entity you want +// to have AI behavior should have one *or more* Thinkers attached to it. +// Thinkers are configured using RON right now, with a DSL that makes it easy +// to define, in data, the actual behavior you want. +pub fn init_entities(mut cmd: Commands) { + let actor = cmd.spawn().insert(Thirst::new(80.0, 2.0)).id(); + + // Here's a very simple one that only has one consideration and one + // associated action. But you can have more of them, and even nest them by + // using more Thinkers (which are actually themselves Actions). See + // basic.ron in examples/ for a more involved Thinker definition. + // + // Ultimately, these Thinkers are meant to be usable by non-programmers: + // You, the developer, create Actions and Considerations, and someone else + // is then able to put them all together like LEGOs into all sorts of + // intricate logic. + Thinker::load_from_str( + r#" +( + picker: {"FirstToScore": (threshold: 80.0)}, + choices: [( + consider: [{"ThirstConsideration": (weight: 2.0)}], + then: {"DrinkAction": ()}, + )], +) +"#, + ) + .build(actor, &mut cmd); +} + +fn main() { + // Once all that's done, we just add our systems and off we go! + App::build() + .add_plugins(DefaultPlugins) + .add_startup_system(init_entities.system()) + .add_system(thirst_system.system()) + .add_system(thirst_consideration_system.system()) + .add_system(drink_action_system.system()) + // Don't forget the Thinker system itself! This is the heart of it all! + .add_system(big_brain::thinker_system.system()) + .run(); +} diff --git a/src/actions.rs b/src/actions.rs index 0a8904b32b953bb91144c5cbe045ed0222e9b439..3d7d8d0757addaaf048bcb87592e2db527b5f650 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,12 +1,11 @@ -use specs::{Component, DenseVecStorage, Entities, Entity, LazyUpdate, ReadStorage, WriteStorage}; -use typetag; +use bevy::prelude::*; use crate::ActionEnt; -#[derive(Debug, Component)] +#[derive(Debug)] pub struct ActionManagerWrapper(pub(crate) Box<dyn ActionManager>); -#[derive(Debug, Component, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum ActionState { Init, Requested, @@ -21,34 +20,12 @@ impl ActionState { Self::default() } - pub fn get<'a>(state_ent: &ActionEnt, states: &'a ReadStorage<ActionState>) -> &'a Self { - states - .get(state_ent.0.clone()) - .expect("ActionState doesn't exist?") - } - - pub fn get_mut<'a>( - state_ent: &ActionEnt, - states: &'a mut WriteStorage<ActionState>, - ) -> &'a mut Self { - states - .get_mut(state_ent.0.clone()) - .expect("ActionState doesn't exist?") - } - - pub(crate) fn build( - builder: Box<dyn Action>, - actor: Entity, - ents: &Entities, - lazy: &LazyUpdate, - ) -> ActionEnt { - let action_ent = ActionEnt(ents.create()); - let ent_clone = action_ent.clone(); - lazy.insert(ent_clone.0, ActionState::default()); - lazy.insert( - ent_clone.0, - ActionManagerWrapper(builder.build(actor, action_ent.clone(), ents, lazy)), - ); + pub(crate) fn build(builder: Box<dyn Action>, actor: Entity, cmd: &mut Commands) -> ActionEnt { + let action_ent = ActionEnt(cmd.spawn().id()); + let manager_wrapper = ActionManagerWrapper(builder.build(actor, action_ent, cmd)); + cmd.entity(action_ent.0) + .insert(ActionState::default()) + .insert(manager_wrapper); action_ent } } @@ -68,12 +45,11 @@ pub trait Action: std::fmt::Debug + Send + Sync { self: Box<Self>, actor: Entity, action_ent: ActionEnt, - ents: &Entities, - lazy: &LazyUpdate, + cmd: &mut Commands, ) -> Box<dyn ActionManager>; } pub trait ActionManager: std::fmt::Debug + Send + Sync { - fn activate(&self, actor: Entity, action: ActionEnt, lazy: &LazyUpdate); - fn deactivate(&self, action: ActionEnt, lazy: &LazyUpdate); + fn activate(&self, actor: Entity, action: ActionEnt, cmd: &mut Commands); + fn deactivate(&self, action: ActionEnt, cmd: &mut Commands); } diff --git a/src/choices.rs b/src/choices.rs index bff8e2ff8895a46147f38d860af733d35e83b3c1..efd21c1a0050aec136519cb2253cf9d4e44b22db 100644 --- a/src/choices.rs +++ b/src/choices.rs @@ -1,5 +1,5 @@ +use bevy::prelude::*; use serde::Deserialize; -use specs::{Entities, Entity, LazyUpdate, ReadStorage}; use crate::{ actions::{Action, ActionState}, @@ -16,13 +16,13 @@ pub struct Choice { pub action_state: ActionEnt, } impl Choice { - pub fn calculate<'a>(&self, utilities: &ReadStorage<'a, Utility>) -> f32 { + pub fn calculate(&self, utilities: &Query<&Utility>) -> f32 { self.measure.calculate( self.utilities .iter() .map(|choice_cons| { utilities - .get(choice_cons.0.clone()) + .get(choice_cons.0) .expect("Where did the utility go?") }) .collect(), @@ -36,16 +36,16 @@ pub struct ChoiceBuilder { pub then: Box<dyn Action>, } impl ChoiceBuilder { - pub fn build(self, actor: Entity, ents: &Entities, lazy: &LazyUpdate) -> Choice { + pub fn build(self, actor: Entity, cmd: &mut Commands) -> Choice { let action = self.then; Choice { measure: Box::new(WeightedMeasure), utilities: self .consider .iter() - .map(|cons| cons.build(actor.clone(), ents, lazy)) + .map(|cons| cons.build(actor, cmd)) .collect(), - action_state: ActionState::build(action, actor, ents, lazy), + action_state: ActionState::build(action, actor, cmd), } } } diff --git a/src/considerations.rs b/src/considerations.rs index 3bfc10c1e23efab9e2ecad781eec189d365ff920..78687d8993f28a7b411bc7406363a6e72dcfa5c6 100644 --- a/src/considerations.rs +++ b/src/considerations.rs @@ -1,19 +1,17 @@ -use specs::{Component, DenseVecStorage, Entities, Entity, LazyUpdate}; -use typetag; +use bevy::prelude::*; use crate::ConsiderationEnt; -#[derive(Debug, Clone, Default, Component)] +#[derive(Debug, Clone, Default)] pub struct Utility { pub value: f32, pub weight: f32, } - /** This trait defines new considerations. In general, you should use the [derive macro](derive.Consideration.html) instead. */ #[typetag::deserialize] pub trait Consideration: std::fmt::Debug + Sync + Send { - fn build(&self, entity: Entity, ents: &Entities, lazy: &LazyUpdate) -> ConsiderationEnt; + fn build(&self, entity: Entity, cmd: &mut Commands) -> ConsiderationEnt; } diff --git a/src/evaluators.rs b/src/evaluators.rs index 5b6e039a07f535f6b71b74e5fc8def0f5f622cd9..9f5f591f699dfec04de7bfeb0650a04c99222d76 100644 --- a/src/evaluators.rs +++ b/src/evaluators.rs @@ -1,5 +1,4 @@ use serde::{Deserialize, Serialize}; -use typetag; #[typetag::serde] pub trait Evaluator: std::fmt::Debug + Sync + Send { diff --git a/src/lib.rs b/src/lib.rs index b008edc12cf58fb9ee3e459d7b1502211051e6d3..f887f8ebebff305ec955556737155a04e8234387 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,66 +1,6 @@ -//! `big-brain` is a [Utility -//! AI](https://en.wikipedia.org/wiki/Utility_system) library for implementing -//! rich, complex artificial intelligence mainly in video games using the -//! [`specs`](https://docs.rs/specs) ECS system. -//! -//! `big-brain` not only allows you to define these complex behaviors, but it -//! allows you to define them in a data-oriented format, such as -//! [`RON`](https://docs.rs/ron), potentially allowing non-programmers to -//! define many of these behaviors themselves. -//! -//! In general, the only things that need to be programmed are -//! [Actions](derive.Action.html) and -//! [Considerations](derive.Consideration.html). Everything else is included -//! with `big-brain`. -//! -//! For example, this is what a basic thinker might look like: -//! -//! ```ignore -//! // basic_needs.ron -//! ( -//! // The first Choice to score above the threshold will be executed. -//! picker: {"FirstToScore": (threshold: 80.0)}, -//! // A list of choices, with their considerations evaluated in order, -//! // and picked using the Picker. -//! choices: [( -//! consider: [{"Bladder": ()}], -//! then: {"Pee": ()}, -//! ), ( -//! consider: [{"Hunger": ()}], -//! // Thinkers are also actions, so you can nest them indefinitely. -//! then: {"Thinker": ( -//! picker: {"FirstToScore": (threshold: 80.0)}, -//! choices: [( -//! consider: [{"FoodInRange": (range: 10.0)}], -//! then: {"EatFood": (range: 10.0)} -//! ), ( -//! consider: [{"FoodNearby": (range: 1000.0)}], -//! then: {"WalkToFood": (range: 1000.0)} -//! )] -//! )}, -//! )], -//! // If no Choice goes over the threshold, we just... wander around -//! otherwise: Some({"Meander": ()}) -//! ) -//! ``` -//! -//! You would then load up the component into a `specs` entity like so: -//! -//! ```no_run -//! use big_brain::ThinkerBuilder; -//! use specs::{Entities, Entity, LazyUpdate, World, WorldExt}; -//! let mut world = World::new(); -//! let entity = world.create_entity().build(); -//! world.exec(|(entities, lazy): (Entities, Read<LazyUpdate>)| { -//! ThinkerBuilder::load_from("./path/to/basic.ron").build(entity, &entities, &lazy); -//! }); -//! ``` - -use specs::{World, WorldExt}; - +pub use bevy; pub use big_brain_derive::*; pub use serde; -pub use specs as ecs; pub use typetag; pub use actions::*; @@ -75,13 +15,4 @@ pub mod pickers; mod actions; mod choices; mod considerations; -mod stopwatch; mod thinker; - -pub fn register(world: &mut World) { - world.register::<Utility>(); - world.register::<ActionState>(); - world.register::<ActionManagerWrapper>(); - world.register::<ActiveThinker>(); - world.register::<Thinker>(); -} diff --git a/src/measures.rs b/src/measures.rs index e822d19ad0e469f97f47b693e01e5ef5975af3a7..7866ef3c8274cc21c2908dd9e8d368f65a84beaf 100644 --- a/src/measures.rs +++ b/src/measures.rs @@ -1,5 +1,4 @@ use serde::{Deserialize, Serialize}; -use typetag; use crate::Utility; diff --git a/src/pickers.rs b/src/pickers.rs index 0d380807e7c463a6d0a9a0eb1e4cba49e6f13f1a..463ab5a6ac51c1c31f290ade56459b435f7b19fa 100644 --- a/src/pickers.rs +++ b/src/pickers.rs @@ -1,16 +1,12 @@ +use bevy::prelude::*; + use serde::{Deserialize, Serialize}; -use specs::ReadStorage; -use typetag; use crate::{choices::Choice, considerations::Utility, thinker::ActionEnt}; #[typetag::serde] pub trait Picker: std::fmt::Debug + Sync + Send { - fn pick<'a>( - &mut self, - _choices: &Vec<Choice>, - _utilities: &ReadStorage<'a, Utility>, - ) -> Option<ActionEnt>; + fn pick(&self, _choices: &[Choice], _utilities: &Query<&Utility>) -> Option<ActionEnt>; } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -20,15 +16,11 @@ pub struct FirstToScore { #[typetag::serde] impl Picker for FirstToScore { - fn pick<'a>( - &mut self, - choices: &Vec<Choice>, - utilities: &ReadStorage<'a, Utility>, - ) -> Option<ActionEnt> { + fn pick(&self, choices: &[Choice], utilities: &Query<&Utility>) -> Option<ActionEnt> { for choice in choices { let value = choice.calculate(utilities); if value >= self.threshold { - return Some(choice.action_state.clone()); + return Some(choice.action_state); } } None diff --git a/src/stopwatch.rs b/src/stopwatch.rs deleted file mode 100644 index 2fa90b9c713f8594333e135c80dba788d31d71cc..0000000000000000000000000000000000000000 --- a/src/stopwatch.rs +++ /dev/null @@ -1,70 +0,0 @@ -// This file is taken from amethyst_core -use std::time::{Duration, Instant}; - -/// A stopwatch which accurately measures elapsed time. -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum Stopwatch { - /// Initial state with an elapsed time value of 0 seconds. - Waiting, - /// Stopwatch has started counting the elapsed time since this `Instant` - /// and accumuluated time from previous start/stop cycles `Duration`. - Started(Duration, Instant), - /// Stopwatch has been stopped and reports the elapsed time `Duration`. - Ended(Duration), -} - -impl Default for Stopwatch { - fn default() -> Stopwatch { - Stopwatch::Waiting - } -} - -impl Stopwatch { - /// Creates a new stopwatch. - pub fn new() -> Stopwatch { - Default::default() - } - - /// Retrieves the elapsed time. - pub fn elapsed(&self) -> Duration { - match *self { - Stopwatch::Waiting => Duration::new(0, 0), - Stopwatch::Started(dur, start) => dur + start.elapsed(), - Stopwatch::Ended(dur) => dur, - } - } - - /// Stops, resets, and starts the stopwatch again. - pub fn restart(&mut self) { - *self = Stopwatch::Started(Duration::new(0, 0), Instant::now()); - } - - /// Starts, or resumes, measuring elapsed time. If the stopwatch has been - /// started and stopped before, the new results are compounded onto the - /// existing elapsed time value. - /// - /// Note: Starting an already running stopwatch will do nothing. - pub fn start(&mut self) { - match *self { - Stopwatch::Waiting => self.restart(), - Stopwatch::Ended(dur) => { - *self = Stopwatch::Started(dur, Instant::now()); - } - _ => {} - } - } - - /// Stops measuring elapsed time. - /// - /// Note: Stopping a stopwatch that isn't running will do nothing. - pub fn stop(&mut self) { - if let Stopwatch::Started(dur, start) = *self { - *self = Stopwatch::Ended(dur + start.elapsed()); - } - } - - /// Clears the current elapsed time value. - pub fn reset(&mut self) { - *self = Stopwatch::Waiting; - } -} diff --git a/src/thinker.rs b/src/thinker.rs index 7a61fc9a2e754e52d24d2fc949c4751525ec5e8f..41c979a087bc0b3ec48e1edf66fd32f4c31122ec 100644 --- a/src/thinker.rs +++ b/src/thinker.rs @@ -2,38 +2,36 @@ use std::fs::File; use std::path::Path; use std::time::{Duration, Instant}; +use bevy::prelude::*; use serde::Deserialize; -use specs::{ - Component, DenseVecStorage, Entities, Entity, Join, LazyUpdate, Read, ReadStorage, System, - WriteStorage, -}; use crate::{ - actions::{Action, ActionManager, ActionManagerWrapper, ActionState}, + actions::{self, Action, ActionManager, ActionManagerWrapper, ActionState}, choices::{Choice, ChoiceBuilder}, considerations::Utility, pickers::Picker, - stopwatch::Stopwatch, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct ActionEnt(pub Entity); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct ConsiderationEnt(pub Entity); -#[derive(Component, Debug)] +#[derive(Debug)] pub struct Thinker { pub picker: Box<dyn Picker>, pub otherwise: Option<ActionEnt>, pub choices: Vec<Choice>, - pub actor: Entity, pub current_action: Option<ActionEnt>, - pub timer: Stopwatch, } impl Thinker { - pub fn load_from<P: AsRef<Path>>(path: P) -> builder::Thinker { + pub fn load_from_str<S: AsRef<str>>(string: S) -> builder::Thinker { + ron::de::from_str(string.as_ref()).expect("Failed to parse RON") + } + + pub fn load_from_path<P: AsRef<Path>>(path: P) -> builder::Thinker { let f = File::open(&path).expect("Failed to open file"); ron::de::from_reader(f).expect("Failed to read .ron file") } @@ -50,10 +48,11 @@ mod builder { } impl builder::Thinker { - pub fn build(self, actor: Entity, ents: &Entities, lazy: &LazyUpdate) -> ActionEnt { - let action_ent = ActionState::build(Box::new(self), actor, ents, lazy); - lazy.insert(action_ent.0.clone(), ActiveThinker(true)); - lazy.insert(action_ent.0.clone(), ActionState::Requested); + pub fn build(self, actor: Entity, cmd: &mut Commands) -> ActionEnt { + let action_ent = ActionState::build(Box::new(self), actor, cmd); + cmd.entity(action_ent.0) + .insert(ActiveThinker(true)) + .insert(ActionState::Requested); action_ent } } @@ -64,51 +63,49 @@ impl Action for builder::Thinker { self: Box<Self>, actor: Entity, action_ent: ActionEnt, - ents: &Entities, - lazy: &LazyUpdate, + cmd: &mut Commands, ) -> Box<dyn ActionManager> { - lazy.insert( - action_ent.0.clone(), - Thinker { - picker: self.picker, - actor, - choices: self - .choices - .into_iter() - .map(|choice| choice.build(actor.clone(), &ents, &lazy)) - .collect(), - otherwise: self - .otherwise - .map(|builder| ActionState::build(builder, actor.clone(), ents, lazy)), - current_action: None, - timer: Stopwatch::new(), - }, - ); + let choices = self + .choices + .into_iter() + .map(|choice| choice.build(actor, cmd)) + .collect(); + let otherwise = self + .otherwise + .map(|builder| ActionState::build(builder, actor, cmd)); + cmd.entity(action_ent.0).insert(Thinker { + picker: self.picker, + choices, + otherwise, + current_action: None, + }); + cmd.entity(actor).push_children(&[action_ent.0]); Box::new(ThinkerManager) } } -#[derive(Debug, Component)] +#[derive(Debug)] pub struct ActiveThinker(bool); #[derive(Debug)] pub struct ThinkerManager; impl ActionManager for ThinkerManager { - fn activate(&self, _: Entity, action_ent: ActionEnt, lazy: &LazyUpdate) { - lazy.insert(action_ent.0, ActiveThinker(false)); - lazy.insert(action_ent.0, ActionState::Requested); + fn activate(&self, _: Entity, action_ent: ActionEnt, cmd: &mut Commands) { + cmd.entity(action_ent.0) + .insert(ActiveThinker(false)) + .insert(ActionState::Requested); } - fn deactivate(&self, action_ent: ActionEnt, lazy: &LazyUpdate) { - lazy.remove::<ActiveThinker>(action_ent.0); + fn deactivate(&self, action_ent: ActionEnt, cmd: &mut Commands) { + cmd.entity(action_ent.0).remove::<ActiveThinker>(); } } -pub struct ThinkerSystem { +pub struct ThinkerIterations { index: usize, max_duration: Duration, } -impl ThinkerSystem { +impl ThinkerIterations { pub fn new(max_duration: Duration) -> Self { Self { index: 0, @@ -116,121 +113,116 @@ impl ThinkerSystem { } } } +impl Default for ThinkerIterations { + fn default() -> Self { + Self::new(Duration::from_millis(10)) + } +} -impl<'a> System<'a> for ThinkerSystem { - type SystemData = ( - Entities<'a>, - WriteStorage<'a, Thinker>, - ReadStorage<'a, ActiveThinker>, - ReadStorage<'a, Utility>, - WriteStorage<'a, ActionState>, - ReadStorage<'a, ActionManagerWrapper>, - Read<'a, LazyUpdate>, - ); - fn run( - &mut self, - (ents, mut thinkers, active_thinkers, utilities, mut action_states, builder_wrappers, lazy): Self::SystemData, - ) { - let start = Instant::now(); - for (thinker, thinker_ent, active_thinker) in (&mut thinkers, &ents, &active_thinkers) - .join() - .skip(self.index) - { - self.index += 1; +pub fn thinker_system( + mut cmd: Commands, + mut iterations: Local<ThinkerIterations>, + mut thinker_q: Query<(Entity, &Parent, &mut Thinker, &ActiveThinker)>, + utilities: Query<&Utility>, + mut action_states: Query<&mut actions::ActionState>, + builder_wrappers: Query<&ActionManagerWrapper>, +) { + let start = Instant::now(); + for (thinker_ent, Parent(actor), mut thinker, active_thinker) in thinker_q.iter_mut().skip(iterations.index) { + iterations.index += 1; - let thinker_state = action_states - .get(thinker_ent.clone()) - .expect("Where is it?") - .clone(); - match thinker_state { - ActionState::Init | ActionState::Success | ActionState::Failure => { - if let ActiveThinker(true) = active_thinker { - let act_state = action_states.get_mut(thinker_ent.clone()).expect("???"); - *act_state = ActionState::Requested; - } + let thinker_state = action_states + .get_mut(thinker_ent) + .expect("Where is it?") + .clone(); + match thinker_state { + ActionState::Init | ActionState::Success | ActionState::Failure => { + if let ActiveThinker(true) = active_thinker { + let mut act_state = action_states.get_mut(thinker_ent).expect("???"); + *act_state = ActionState::Requested; } - ActionState::Cancelled => { - if let Some(current) = &mut thinker.current_action { - let state = action_states.get(current.0.clone()).expect("Couldn't find a component corresponding to the current action. This is definitely a bug.").clone(); - match state { - ActionState::Success | ActionState::Failure => { - let act_state = - action_states.get_mut(thinker_ent.clone()).expect("???"); - *act_state = state.clone(); - let state = action_states.get_mut(current.0.clone()).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); - *state = ActionState::Init; - thinker.current_action = None; - } - _ => { - let state = action_states.get_mut(current.0.clone()).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); - *state = ActionState::Cancelled; - } + } + ActionState::Cancelled => { + if let Some(current) = &mut thinker.current_action { + let state = action_states.get_mut(current.0).expect("Couldn't find a component corresponding to the current action. This is definitely a bug.").clone(); + match state { + ActionState::Success | ActionState::Failure => { + let mut act_state = action_states.get_mut(thinker_ent).expect("???"); + *act_state = state.clone(); + let mut state = action_states.get_mut(current.0).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); + *state = ActionState::Init; + thinker.current_action = None; + } + _ => { + let mut state = action_states.get_mut(current.0).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); + *state = ActionState::Cancelled; } } } - ActionState::Requested | ActionState::Executing => { - if let Some(picked_action_ent) = - thinker.picker.pick(&thinker.choices, &utilities) - { - // Think about what action we're supposed to be taking. We do this - // every tick, because we might change our mind. - // ...and then execute it (details below). - exec_picked_action( - thinker_ent, - thinker, - &picked_action_ent, - &mut action_states, - &builder_wrappers, - &lazy, - ); - } else if let Some(default_action_ent) = &thinker.otherwise { - // Otherwise, let's just execute the default one! (if it's there) - let default_action_ent = default_action_ent.clone(); - exec_picked_action( - thinker_ent, - thinker, - &default_action_ent, - &mut action_states, - &builder_wrappers, - &lazy, - ); - } else if let Some(current) = &mut thinker.current_action { - // If we didn't pick anything, and there's no default action, - // we need to see if there's any action currently executing, - // and cancel it. We also use this opportunity to clean up - // stale action components so they don't slow down joins. - let state = action_states.get_mut(current.0.clone()).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); - let factory = builder_wrappers.get(current.0.clone()).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); - match state { - ActionState::Init | ActionState::Success | ActionState::Failure => { - factory.0.deactivate(current.clone(), &lazy); - *state = ActionState::Init; - thinker.current_action = None; - } - _ => { - *state = ActionState::Cancelled; - } + } + ActionState::Requested | ActionState::Executing => { + if let Some(picked_action_ent) = thinker.picker.pick(&thinker.choices, &utilities) { + // Think about what action we're supposed to be taking. We do this + // every tick, because we might change our mind. + // ...and then execute it (details below). + exec_picked_action( + &mut cmd, + thinker_ent, + *actor, + &mut thinker, + &picked_action_ent, + &mut action_states, + &builder_wrappers, + ); + } else if let Some(default_action_ent) = &thinker.otherwise { + // Otherwise, let's just execute the default one! (if it's there) + let default_action_ent = *default_action_ent; + exec_picked_action( + &mut cmd, + thinker_ent, + *actor, + &mut thinker, + &default_action_ent, + &mut action_states, + &builder_wrappers, + ); + } else if let Some(current) = &mut thinker.current_action { + // If we didn't pick anything, and there's no default action, + // we need to see if there's any action currently executing, + // and cancel it. We also use this opportunity to clean up + // stale action components so they don't slow down joins. + let mut state = action_states.get_mut(current.0).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); + let factory = builder_wrappers.get(current.0).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); + match *state { + actions::ActionState::Init + | actions::ActionState::Success + | actions::ActionState::Failure => { + factory.0.deactivate(*current, &mut cmd); + *state = ActionState::Init; + thinker.current_action = None; + } + _ => { + *state = ActionState::Cancelled; } } } } - thinker.timer.reset(); - thinker.timer.start(); - if self.index % 500 == 0 && start.elapsed() > self.max_duration { - return; - } } - self.index = 0; + if iterations.index % 500 == 0 && start.elapsed() > iterations.max_duration { + return; + } } + iterations.index = 0; } fn exec_picked_action( + cmd: &mut Commands, thinker_ent: Entity, - thinker: &mut Thinker, + actor: Entity, + thinker: &mut Mut<Thinker>, picked_action_ent: &ActionEnt, - states: &mut WriteStorage<ActionState>, - builder_wrappers: &ReadStorage<ActionManagerWrapper>, - lazy: &Read<LazyUpdate>, + states: &mut Query<&mut ActionState>, + builder_wrappers: &Query<&ActionManagerWrapper>, ) { // If we do find one, then we need to grab the corresponding // component for it. The "action" that `picker.pick()` returns @@ -248,26 +240,26 @@ fn exec_picked_action( // So we've picked a different action than we were // currently executing. Just like before, we grab the // actual Action component (and we assume it exists). - let curr_action_state = states.get_mut(current.0.clone()).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); + let mut curr_action_state = states.get_mut(current.0).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); // If the action is executing, or was requested, we // need to cancel it to make sure it stops. The Action // system will take care of resetting its state as // needed. - match curr_action_state { + match *curr_action_state { ActionState::Executing | ActionState::Requested => { *curr_action_state = ActionState::Cancelled; - let thinker_state = states.get_mut(thinker_ent.clone()).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); + let mut thinker_state = states.get_mut(thinker_ent).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); *thinker_state = ActionState::Cancelled; } ActionState::Init | ActionState::Success | ActionState::Failure => { - let current_action_factory = builder_wrappers.get(current.0.clone()).expect("Couldn't find an Action component corresponding to an Action entity. This is definitely a bug."); + let current_action_factory = builder_wrappers.get(current.0).expect("Couldn't find an Action component corresponding to an Action entity. This is definitely a bug."); current_action_factory .0 - .deactivate(picked_action_ent.clone(), &lazy); + .deactivate(*picked_action_ent, cmd); let old_state = curr_action_state.clone(); *curr_action_state = ActionState::Init; - *current = picked_action_ent.clone(); - let thinker_state = states.get_mut(thinker_ent.clone()).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); + *current = *picked_action_ent; + let mut thinker_state = states.get_mut(thinker_ent).expect("Couldn't find a component corresponding to the current action. This is definitely a bug."); *thinker_state = old_state; } ActionState::Cancelled => {} @@ -278,18 +270,13 @@ fn exec_picked_action( // it as Requested if for some reason it had finished // but the Action System hasn't gotten around to // cleaning it up. - let picked_action_state = states.get_mut(picked_action_ent.0.clone()).expect("Couldn't find an Action component corresponding to an Action entity. This is definitely a bug."); - match picked_action_state { - ActionState::Init => { - let picked_action_factory = builder_wrappers.get(picked_action_ent.0.clone()).expect("Couldn't find an Action component corresponding to an Action entity. This is definitely a bug."); - picked_action_factory.0.activate( - thinker.actor.clone(), - picked_action_ent.clone(), - lazy, - ); - *picked_action_state = ActionState::Requested; - } - _ => {} + let mut picked_action_state = states.get_mut(picked_action_ent.0).expect("Couldn't find an Action component corresponding to an Action entity. This is definitely a bug."); + if *picked_action_state == ActionState::Init { + let picked_action_factory = builder_wrappers.get(picked_action_ent.0).expect("Couldn't find an Action component corresponding to an Action entity. This is definitely a bug."); + picked_action_factory + .0 + .activate(actor, *picked_action_ent, cmd); + *picked_action_state = ActionState::Requested; } } } else { @@ -297,12 +284,12 @@ fn exec_picked_action( // current_action in the thinker. The logic here is pretty // straightforward -- we set the action, Request it, and // that's it. - let picked_action_factory = builder_wrappers.get(picked_action_ent.0.clone()).expect("Couldn't find an Action component corresponding to an Action entity. This is definitely a bug."); - let picked_action_state = states.get_mut(picked_action_ent.0.clone()).expect("Couldn't find an Action component corresponding to an Action entity. This is definitely a bug."); + let picked_action_factory = builder_wrappers.get(picked_action_ent.0).expect("Couldn't find an Action component corresponding to an Action entity. This is definitely a bug."); + let mut picked_action_state = states.get_mut(picked_action_ent.0).expect("Couldn't find an Action component corresponding to an Action entity. This is definitely a bug."); picked_action_factory .0 - .activate(thinker.actor.clone(), picked_action_ent.clone(), lazy); - thinker.current_action = Some(picked_action_ent.clone()); + .activate(actor, *picked_action_ent, cmd); + thinker.current_action = Some(*picked_action_ent); *picked_action_state = ActionState::Requested; } }