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}