bevy_yoetz/advisor.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
use bevy::ecs::query::{QueryData, WorldQuery};
use bevy::ecs::system::EntityCommands;
use bevy::prelude::*;
#[doc(inline)]
pub use bevy_yoetz_macros::YoetzSuggestion;
/// An action suggestion for the AI agent to consider.
///
/// Avoid implementing this trait manually - prefer using the
/// [`YoetzSuggestion`](bevy_yoetz_macros::YoetzSuggestion) derive macro.
///
/// `enum`s that implement this trait are mainly used as the generic parameter for [`YoetzAdvisor`]
/// and as the data passed to it. A [`YoetzPlugin`](crate::YoetzPlugin) parametrized on them should
/// also be added to the Bevy application.
pub trait YoetzSuggestion: 'static + Sized + Send + Sync {
/// The key identifies a suggestion even when its data changes. The
/// [`YoetzSuggestion`](bevy_yoetz_macros::YoetzSuggestion) derive macro generates a key that
/// is a "subset" of the `enum` - it contains all the variants, but each variant only contains
/// the fields marked as `#[yoetz(key)]`.
type Key: 'static + Send + Sync + Clone + PartialEq;
/// A query that allows access to all possible behavior components.
///
/// The query generated by the [`YoetzSuggestion`](bevy_yoetz_macros::YoetzSuggestion) derive
/// macro is unsightly and there it never a reason to use it manually.
type OmniQuery: QueryData;
/// Generate a [`Key`](Self::Key) that identifies the suggestion.
fn key(&self) -> Self::Key;
/// Remove the behavior components that were created by a suggestion with the specified key.
fn remove_components(key: &Self::Key, cmd: &mut EntityCommands);
/// Add behavior components created from the suggestion.
fn add_components(self, cmd: &mut EntityCommands);
/// Update the existing behavior components from the suggestion's data.
///
/// The method generated by the [`YoetzSuggestion`](bevy_yoetz_macros::YoetzSuggestion) derive
/// macro will only update the fields marked with `#[yoetz(input)]`. Fields marked with
/// `#[yoetz(state)]` will not be updated because the action systems are allowed to store their
/// own state there (or just maintain the initial state from when the behavior was chosen), and
/// fields marked with `#[yoetz(key)]` will not be updated because when they change the
/// [`Key`](Self::Key) changes and the components themselves will be re-inserted rather than
/// updated.
fn update_into_components(
self,
components: &mut <Self::OmniQuery as WorldQuery>::Item<'_>,
) -> Result<(), Self>;
}
/// Controls an entity's AI by listening to [`YoetzSuggestion`]s and updating the entity's behavior
/// components.
#[derive(Component)]
pub struct YoetzAdvisor<S: YoetzSuggestion> {
/// Added to score of any suggestion that matches the currently active behavior. This can be
/// used to reduce the "flickering" when multiple suggestions are flocking around the same
/// score.
pub consistency_bonus: f32,
active_key: Option<S::Key>,
top_suggestion: Option<(f32, S)>,
}
impl<S: YoetzSuggestion> YoetzAdvisor<S> {
/// Create a new advisor with the specified [`consistency_bonus`](Self::consistency_bonus).
pub fn new(consistency_bonus: f32) -> Self {
Self {
consistency_bonus,
active_key: None,
top_suggestion: None,
}
}
/// The [`Key`](YoetzSuggestion::Key) of the currently active behavior.
///
/// This can be used to implement a state machine behavior, where the code that suggests a
/// behavior can look at the current state.
pub fn active_key(&self) -> &Option<S::Key> {
&self.active_key
}
/// Suggest a behavior for the AI to consider.
///
/// A suggestion should be sent every frame as long as it is valid - once it stops being sent
/// it will immediately be replaced by another suggestion.
pub fn suggest(&mut self, score: f32, suggestion: S) {
if let Some((current_score, _)) = self.top_suggestion.as_ref() {
let bonus = if self
.active_key
.as_ref()
.map(|key| *key == suggestion.key())
.unwrap_or(false)
{
self.consistency_bonus
} else {
0.0
};
if score + bonus < *current_score {
return;
}
}
self.top_suggestion = Some((score, suggestion));
}
}
pub fn update_advisor<S: YoetzSuggestion>(
mut query: Query<(Entity, &mut YoetzAdvisor<S>, S::OmniQuery)>,
mut commands: Commands,
) {
for (entity, mut advisor, mut components) in query.iter_mut() {
let Some((_, mut suggestion)) = advisor.top_suggestion.take() else {
continue;
};
let key = suggestion.key();
let mut cmd;
if let Some(old_key) = advisor.active_key.as_ref() {
if *old_key == key {
let update_result = suggestion.update_into_components(&mut components);
if let Err(update_result) = update_result {
warn!(
"Components were wrong - will not update, add them with a command instead"
);
suggestion = update_result;
} else {
continue;
}
}
cmd = commands.entity(entity);
S::remove_components(old_key, &mut cmd)
} else {
cmd = commands.entity(entity);
}
suggestion.add_components(&mut cmd);
advisor.active_key = Some(key);
}
}