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);
    }
}