bevy_yoetz/
lib.rs

1//! Yoetz - A Rule Based AI Plugin for the Bevy Game Engine
2//!
3//! Yoetz ("advisor" in Hebrew) is a rule based AI plugin for Bevy, structured around the following
4//! tenets:
5//!
6//! 1. There is no need to build special data structures for calculating the transitions and the
7//!    scores when representing these mechanisms as code inside user systems is both more flexible
8//!    and simpler to use.
9//! 2. The systems that check the rules need to be able to pass data to the systems act upon the
10//!    decisions.
11//! 3. Enacting the decision should be done with the ECS. If the action that the rules mechanism
12//!    decided to do is reflected by components, it becomes easy to write different systems that
13//!    perform the various possible actions.
14//!
15//! # Quick Start
16//!
17//! Define the various actions the AI can do with an enum that derives [`YoetzSuggestion`], and add
18//! a [`YoetzPlugin`] for it:
19//!
20//! ```no_run
21//! # use bevy::prelude::*;
22//! # use bevy_yoetz::prelude::*;
23//! # let mut app = App::new();
24//! app.add_plugins(YoetzPlugin::<AiBehavior>::new(FixedUpdate));
25//!
26//! #[derive(YoetzSuggestion)]
27//! enum AiBehavior {
28//!     DoNothing,
29//!     Attack {
30//!         #[yoetz(key)]
31//!         target_to_attack: Entity,
32//!     },
33//! }
34//! ```
35//!
36//! Give [`YoetzAdvisor`](crate::advisor::YoetzAdvisor) to the AI controlled entities:
37//!
38//! ```no_run
39//! # use bevy::prelude::*;
40//! # use bevy_yoetz::prelude::*;
41//! # let mut commands: Commands = panic!();
42//! # #[derive(YoetzSuggestion)] enum AiBehavior { VariantSoThatItWontBeEmpty }
43//! # #[derive(Component)] struct OtherComponentsForThisEntity;
44//! commands.spawn((
45//!     // The argument to `new` is a bonus for maintaining the current action.
46//!     YoetzAdvisor::<AiBehavior>::new(2.0),
47//!     OtherComponentsForThisEntity,
48//! ));
49//! ```
50//!
51//! Add under [`YoetzSystemSet::Suggest`] systems that check for the various rules and generate
52//! suggestions with scores:
53//!
54//! ```no_run
55//! # use bevy::prelude::*;
56//! # use bevy_yoetz::prelude::*;
57//! # #[derive(YoetzSuggestion)]
58//! # enum AiBehavior {
59//! #     DoNothing,
60//! #     Attack {
61//! #         #[yoetz(key)]
62//! #         target_to_attack: Entity,
63//! #     },
64//! # }
65//! # let mut app = App::new();
66//! app.add_systems(
67//!     FixedUpdate,
68//!     (
69//!         make_ai_entities_do_nothing,
70//!         give_targets_to_ai_entities,
71//!     )
72//!         .in_set(YoetzSystemSet::Suggest),
73//! );
74//!
75//! fn make_ai_entities_do_nothing(mut query: Query<&mut YoetzAdvisor<AiBehavior>>) {
76//!     for mut advisor in query.iter_mut() {
77//!         // A constant suggestion, so that if nothing else beats this score the entity will
78//!         // still have a behavior to execute.
79//!         advisor.suggest(0.0, AiBehavior::DoNothing);
80//!     }
81//! }
82//!
83//! # #[derive(Component)] struct Attackable;
84//! fn give_targets_to_ai_entities(
85//!     mut query: Query<(&mut YoetzAdvisor<AiBehavior>, &GlobalTransform)>,
86//!     targets_query: Query<(Entity, &GlobalTransform), With<Attackable>>,
87//! ) {
88//!     for (mut advisor, ai_transform) in query.iter_mut() {
89//!         for (target_entity, target_transorm) in targets_query.iter() {
90//!             let distance = ai_transform.translation().distance(target_transorm.translation());
91//!             advisor.suggest(
92//!                 // The closer the target, the more desirable it is to attack it. If the
93//!                 // distance is more than 10, the score will get below 0 and the DoNothing
94//!                 // suggestion will be used instead.
95//!                 10.0 - distance,
96//!                 AiBehavior::Attack {
97//!                     target_to_attack: target_entity,
98//!                 },
99//!             );
100//!         }
101//!     }
102//! }
103//! ```
104//!
105//! Add under [`YoetzSystemSet::Act`] systems that performs these actions. These systems use
106//! components that are generated by the [`YoetzSuggestion`](bevy_yoetz_macros::YoetzSuggestion)
107//! macro and are added and removed automatically by [`YoetzPlugin`]:
108//!
109//! ```no_run
110//! # use bevy::prelude::*;
111//! # use bevy_yoetz::prelude::*;
112//! # #[derive(YoetzSuggestion)]
113//! # enum AiBehavior {
114//! #     DoNothing,
115//! #     Attack {
116//! #         #[yoetz(key)]
117//! #         target_to_attack: Entity,
118//! #     },
119//! # }
120//! # let mut app = App::new();
121//! app.add_systems(
122//!     FixedUpdate,
123//!     (
124//!         perform_do_nothing,
125//!         perform_attack,
126//!     )
127//!         .in_set(YoetzSystemSet::Act),
128//! );
129//!
130//! fn perform_do_nothing(query: Query<&AiBehaviorDoNothing>) {
131//!     for _do_nothing in query.iter() {
132//!         // Do... nothing. This whole function is kind of pointless.
133//!     }
134//! }
135//!
136//! # #[derive(Component)] struct Attacker;
137//! # impl Attacker { fn attack(&mut self, _target: Entity) {} }
138//! fn perform_attack(mut query: Query<(&mut Attacker, &AiBehaviorAttack)>) {
139//!     for (mut attacker, attack_behavior) in query.iter_mut() {
140//!         attacker.attack(attack_behavior.target_to_attack);
141//!     }
142//! }
143mod advisor;
144
145use std::marker::PhantomData;
146
147use bevy::ecs::schedule::{InternedScheduleLabel, ScheduleLabel};
148use bevy::prelude::*;
149
150use self::advisor::update_advisor;
151use self::prelude::YoetzSuggestion;
152
153pub use bevy;
154
155pub mod prelude {
156    #[doc(inline)]
157    pub use crate::advisor::{YoetzAdvisor, YoetzSuggestion};
158    #[doc(inline)]
159    pub use crate::{YoetzPlugin, YoetzSystemSet};
160}
161
162/// Add systems for processing a [`YoetzSuggestion`].
163pub struct YoetzPlugin<S: YoetzSuggestion> {
164    schedule: InternedScheduleLabel,
165    _phantom: PhantomData<fn(S)>,
166}
167
168impl<S: YoetzSuggestion> YoetzPlugin<S> {
169    /// Create a `YoetzPlugin` that cranks the [`YoetzAdvisor`](crate::advisor::YoetzAdvisor) in
170    /// the given schedule.
171    ///
172    /// The update will be done between [`YoetzSystemSet::Suggest`] and [`YoetzSystemSet::Act`] in
173    /// that schedule.
174    pub fn new(schedule: impl ScheduleLabel) -> Self {
175        Self {
176            schedule: schedule.intern(),
177            _phantom: PhantomData,
178        }
179    }
180}
181
182impl<S: 'static + YoetzSuggestion> Plugin for YoetzPlugin<S> {
183    fn build(&self, app: &mut App) {
184        app.configure_sets(
185            self.schedule,
186            (
187                YoetzSystemSet::Suggest,
188                YoetzInternalSystemSet::Think,
189                YoetzSystemSet::Act,
190            )
191                .chain(),
192        );
193        app.add_systems(
194            self.schedule,
195            update_advisor::<S>.in_set(YoetzInternalSystemSet::Think),
196        );
197    }
198}
199
200/// System sets to put suggestion systems and action systems in.
201#[derive(Debug, Clone, PartialEq, Eq, Hash, SystemSet)]
202pub enum YoetzSystemSet {
203    /// Systems that suggest behaviors (by calling
204    /// [`YoetzAdvisor::suggest`](advisor::YoetzAdvisor::suggest)) should go in this set.
205    Suggest,
206    /// Systems that enact behaviors (by querying for the behavior structs generated by the
207    /// [`YoetzSuggestion`](bevy_yoetz_macros::YoetzSuggestion) macro) should go in this set.
208    Act,
209}
210
211#[doc(hidden)]
212#[derive(Debug, Clone, PartialEq, Eq, Hash, SystemSet)]
213pub enum YoetzInternalSystemSet {
214    Think,
215}