custom/
custom.rs

1use std::{marker::PhantomData, time::Duration};
2
3use bevy_app::{AppExit, ScheduleRunnerPlugin, prelude::*};
4use bevy_ecs::{prelude::*, query::QueryFilter, schedule::ScheduleLabel};
5
6use bevy_sequential_actions::*;
7
8fn main() {
9    App::new()
10        .init_schedule(EvenSchedule)
11        .init_schedule(OddSchedule)
12        .add_plugins((
13            ScheduleRunnerPlugin::run_loop(Duration::from_millis(100)),
14            // Add custom plugin for the even schedule
15            CustomSequentialActionsPlugin::new(EvenSchedule)
16                .with_cleanup()
17                .with_filter::<With<EvenMarker>>(),
18            // Add custom plugin for the odd schedule
19            CustomSequentialActionsPlugin::new(OddSchedule)
20                // No cleanup for odd agents
21                .with_filter::<With<OddMarker>>(),
22        ))
23        .add_systems(Startup, setup)
24        .add_systems(Update, run_custom_schedules)
25        .run();
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Hash, ScheduleLabel)]
29struct EvenSchedule;
30
31#[derive(Debug, Clone, PartialEq, Eq, Hash, ScheduleLabel)]
32struct OddSchedule;
33
34#[derive(Component)]
35struct EvenMarker;
36
37#[derive(Component)]
38struct OddMarker;
39
40fn setup(mut commands: Commands) {
41    // Spawn agent with even marker for even schedule
42    let agent_even = commands.spawn((SequentialActions, EvenMarker)).id();
43    commands
44        .actions(agent_even)
45        .add(PrintForeverAction::new(format!(
46            "Even: is_finished is called every even frame for agent {agent_even}."
47        )));
48
49    // Spawn agent with odd marker for odd schedule
50    let agent_odd = commands.spawn((SequentialActions, OddMarker)).id();
51    commands
52        .actions(agent_odd)
53        .add(PrintForeverAction::new(format!(
54            "Odd:  is_finished is called every odd  frame for agent {agent_odd}."
55        )));
56}
57
58fn run_custom_schedules(
59    world: &mut World,
60    mut frame_count: Local<u32>,
61    mut agent_q: Local<QueryState<Entity, With<SequentialActions>>>,
62) {
63    if *frame_count % 2 == 0 {
64        world.run_schedule(EvenSchedule);
65    } else {
66        world.run_schedule(OddSchedule);
67    }
68
69    if *frame_count == 10 {
70        for agent in agent_q.iter(world).collect::<Vec<_>>() {
71            world.despawn(agent);
72        }
73        world.write_message(AppExit::Success);
74    }
75
76    *frame_count += 1;
77}
78
79struct PrintForeverAction {
80    message: String,
81    agent: Entity,
82}
83
84impl PrintForeverAction {
85    fn new(message: String) -> Self {
86        Self {
87            message,
88            agent: Entity::PLACEHOLDER,
89        }
90    }
91}
92
93impl Action for PrintForeverAction {
94    fn is_finished(&self, _agent: Entity, _world: &World) -> bool {
95        println!("{}", self.message);
96        false
97    }
98    fn on_start(&mut self, agent: Entity, _world: &mut World) -> bool {
99        self.agent = agent;
100        false
101    }
102    fn on_stop(&mut self, _agent: Option<Entity>, _world: &mut World, _reason: StopReason) {}
103    fn on_drop(self: Box<Self>, _agent: Option<Entity>, _world: &mut World, _reason: DropReason) {
104        // Notice that this is not called for odd agents when despawned...
105        println!("Dropping action for agent {}...", self.agent);
106    }
107}
108
109/// Custom plugin for sequential actions.
110///
111/// Action queue advancement will run in the specified schedule `S`,
112/// and only for agents matching the specified query filter `F`.
113/// With `cleanup` enabled, an observer will trigger for despawned agents
114/// that ensures any remaining action is cleaned up.
115struct CustomSequentialActionsPlugin<S: ScheduleLabel, F: QueryFilter> {
116    schedule: S,
117    cleanup: bool,
118    filter: PhantomData<F>,
119}
120
121impl<S: ScheduleLabel> CustomSequentialActionsPlugin<S, ()> {
122    const fn new(schedule: S) -> Self {
123        Self {
124            schedule,
125            cleanup: false,
126            filter: PhantomData,
127        }
128    }
129
130    const fn with_cleanup(mut self) -> Self {
131        self.cleanup = true;
132        self
133    }
134
135    fn with_filter<F: QueryFilter>(self) -> CustomSequentialActionsPlugin<S, F> {
136        CustomSequentialActionsPlugin {
137            schedule: self.schedule,
138            cleanup: self.cleanup,
139            filter: PhantomData,
140        }
141    }
142}
143
144impl<S: ScheduleLabel, F: QueryFilter> CustomSequentialActionsPlugin<S, F> {
145    fn check_actions_exclusive(
146        world: &mut World,
147        mut finished: Local<Vec<Entity>>,
148        mut agent_q: Local<QueryState<(Entity, &CurrentAction), F>>,
149    ) {
150        // Collect all agents with finished action
151        finished.extend(agent_q.iter(world).filter_map(|(agent, current_action)| {
152            current_action
153                .as_ref()
154                .and_then(|action| action.is_finished(agent, world).then_some(agent))
155        }));
156
157        // Do something with the finished list if you want.
158        // Perhaps sort by some identifier for deterministic behavior.
159
160        // Advance the action queue
161        for agent in finished.drain(..) {
162            SequentialActionsPlugin::stop_current_action(agent, StopReason::Finished, world);
163            SequentialActionsPlugin::start_next_action(agent, world);
164        }
165    }
166}
167
168impl Default for CustomSequentialActionsPlugin<Last, ()> {
169    fn default() -> Self {
170        Self::new(Last).with_cleanup()
171    }
172}
173
174impl<S: ScheduleLabel + Clone, F: QueryFilter + Send + Sync + 'static> Plugin
175    for CustomSequentialActionsPlugin<S, F>
176{
177    fn build(&self, app: &mut App) {
178        // Add system for advancing action queue to specified schedule
179        app.add_systems(self.schedule.clone(), Self::check_actions_exclusive);
180
181        // Add observers for cleanup of actions when despawning agents
182        if self.cleanup {
183            app.add_observer(CurrentAction::on_remove_trigger::<F>)
184                .add_observer(ActionQueue::on_remove_trigger::<F>);
185        }
186    }
187}