pebble/ecs/schedule.rs
1use std::any::TypeId;
2
3use crate::ecs::{
4 commands::{ResourceCommandQueue, TriggerQueue},
5 resources::Resources,
6 system_param::{IntoSystem, System, SystemChain, SystemConfig},
7};
8
9/// An ordered list of systems, run together — one `Schedule` backs each
10/// [`SystemStage`](crate::ecs::system::SystemStage). Running it also
11/// flushes any deferred `Commands` from the systems that just ran.
12///
13/// Systems normally run in the order they were added. Call `.after(...)`/
14/// `.before(...)` directly on a system (see
15/// [`IntoSystemConfig`](crate::ecs::system_param::IntoSystemConfig)) to
16/// constrain it relative to another system known to the schedule — added
17/// earlier or later, registration order doesn't matter, only the
18/// constraint does. `.priority(...)` breaks ties between systems with no
19/// `after`/`before` relationship to each other — higher runs first — but
20/// never overrides an explicit constraint. `.chain()` on a tuple of systems
21/// (see [`Chain`](crate::ecs::system_param::Chain), registered with
22/// [`add_systems`](Schedule::add_systems)) forces them to run in that exact
23/// relative order, and can itself be given `.after(...)`/`.before(...)`/
24/// `.priority(...)`, applied to the whole chain:
25///
26/// ```ignore
27/// schedule
28/// .add_system(spawn_enemies)
29/// .add_system(move_enemies.after(spawn_enemies))
30/// .add_system(render.after(move_enemies))
31/// .add_system(hud.priority(10))
32/// .add_systems((physics_step, resolve_collisions).chain().before(render));
33/// ```
34#[derive(Default)]
35pub struct Schedule {
36 systems: Vec<(TypeId, Box<dyn System>, i32)>,
37 /// `(dependent, dependency)` — `dependent` must run after `dependency`.
38 constraints: Vec<(TypeId, TypeId)>,
39 order: Vec<usize>,
40 order_dirty: bool,
41}
42
43impl Schedule {
44 /// Appends `system` to the end of this schedule. `system` may be a bare
45 /// system, or one wrapped with `.after(...)`/`.before(...)`/`.priority(...)`
46 /// — see [`IntoSystemConfig`](crate::ecs::system_param::IntoSystemConfig).
47 pub fn add_system<S, Params>(&mut self, system: impl Into<SystemConfig<S, Params>>) -> &mut Self
48 where
49 Params: 'static,
50 S: IntoSystem<Params> + 'static,
51 {
52 let (id, system, priority, constraints) = system.into().into_parts();
53 self.systems.push((id, system, priority));
54 self.constraints.extend(constraints);
55 self.order_dirty = true;
56 self
57 }
58
59 /// Appends every system in `chain` (built with `.chain()` on a tuple of
60 /// systems — see [`Chain`](crate::ecs::system_param::Chain)) to the end
61 /// of this schedule.
62 pub fn add_systems(&mut self, chain: SystemChain) -> &mut Self {
63 let (systems, constraints) = chain.into_parts();
64 self.systems.extend(systems);
65 self.constraints.extend(constraints);
66 self.order_dirty = true;
67 self
68 }
69
70 /// Topologically sorts systems to satisfy every `after`/`before`
71 /// constraint. Among systems with no constraint relative to each other,
72 /// higher `priority` runs first; ties within the same priority break by
73 /// original `add_system` order. Panics if the constraints form a cycle;
74 /// silently ignores a constraint that names a system never added to
75 /// this schedule.
76 fn compute_order(&self) -> Vec<usize> {
77 let n = self.systems.len();
78 let index_of = |id: TypeId| self.systems.iter().position(|(sid, _, _)| *sid == id);
79
80 let mut in_degree = vec![0usize; n];
81 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); n];
82
83 for &(dependent_id, dependency_id) in &self.constraints {
84 if let (Some(dependent), Some(dependency)) =
85 (index_of(dependent_id), index_of(dependency_id))
86 && dependent != dependency
87 {
88 dependents[dependency].push(dependent);
89 in_degree[dependent] += 1;
90 }
91 }
92
93 let mut remaining: Vec<usize> = (0..n).collect();
94 let mut order = Vec::with_capacity(n);
95
96 while !remaining.is_empty() {
97 // among ready systems (in_degree 0), pick the highest priority;
98 // ties keep the first one found, preserving `add_system` order
99 // since `remaining` is only ever shrunk, never reordered.
100 let mut best: Option<(usize, i32)> = None;
101 for (pos, &i) in remaining.iter().enumerate() {
102 if in_degree[i] != 0 {
103 continue;
104 }
105 let priority = self.systems[i].2;
106 if best.is_none_or(|(_, best_priority)| priority > best_priority) {
107 best = Some((pos, priority));
108 }
109 }
110 let ready = best.map(|(pos, _)| pos).expect("system ordering constraints form a cycle");
111 let picked = remaining.remove(ready);
112 order.push(picked);
113 for &dependent in &dependents[picked] {
114 in_degree[dependent] -= 1;
115 }
116 }
117
118 order
119 }
120
121 /// Runs every system in order, then flushes deferred entity spawns,
122 /// resource commands, and triggered observers from this run.
123 pub fn run(&mut self, world: &mut hecs::World, resources: &mut Resources) {
124 if self.order_dirty {
125 self.order = self.compute_order();
126 self.order_dirty = false;
127 }
128
129 for &index in &self.order {
130 self.systems[index].1.run(world, &*resources);
131 }
132
133 // sync entity commands
134 resources.get_mut::<hecs::CommandBuffer>().run_on(world);
135
136 // sync resource commands
137 if resources.contains::<ResourceCommandQueue>() {
138 let commands = std::mem::take(&mut resources.get_mut::<ResourceCommandQueue>().0);
139 for command in commands {
140 command(resources);
141 }
142 }
143
144 // sync triggered observers
145 if resources.contains::<TriggerQueue>() {
146 let triggers = std::mem::take(&mut resources.get_mut::<TriggerQueue>().0);
147 for trigger in triggers {
148 trigger(world, resources);
149 }
150 }
151 }
152}