pub(crate) use unlit_ecs::{Entity, World};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Frame {
pub delta_time: f32,
pub index: u32,
pub size: (u32, u32),
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct FrameClock {
pub time: f32,
pub delta_time: f32,
pub index: u32,
}
impl FrameClock {
pub fn advance(&mut self, delta_time: f32, index: u32) {
self.time += delta_time;
self.delta_time = delta_time;
self.index = index;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct FrameBehaviourOrder(pub i32);
pub type FrameCallback = Box<dyn FnMut(&World, Entity, &mut Frame)>;
#[derive(Debug, Default)]
pub struct FrameSkip(pub bool);
impl FrameSkip {
pub fn skip(world: &World) {
Self::set(world, true);
}
#[must_use]
pub fn is_skipped(world: &World) -> bool {
world
.root_scope()
.query::<&FrameSkip>()
.any(|frame_skip| frame_skip.0)
}
fn reset(world: &World) {
Self::set(world, false);
}
fn set(world: &World, value: bool) {
for entity in Self::entities(world) {
let _ = world
.root_scope()
.query_one::<&mut FrameSkip>(entity)
.map(|mut skip| skip.0 = value);
}
}
fn entities(world: &World) -> Vec<Entity> {
world
.root_scope()
.query::<(&FrameSkip, Entity)>()
.map(|(_frame_skip, entity)| entity)
.collect()
}
}
pub trait FrameBehaviour: 'static {
fn order(&self) -> FrameBehaviourOrder;
fn run(&mut self, world: &World, entity: Entity, frame: &mut Frame);
}
pub(crate) struct BehaviourSlot {
callback: FrameCallback,
order: FrameBehaviourOrder,
}
impl BehaviourSlot {
fn new(order: FrameBehaviourOrder, callback: FrameCallback) -> Self {
Self { callback, order }
}
fn order(&self) -> FrameBehaviourOrder {
self.order
}
fn run(&mut self, world: &World, entity: Entity, frame: &mut Frame) {
(self.callback)(world, entity, frame);
}
}
macro_rules! frame_behaviour {
($name:ident, $doc:expr) => {
#[doc = $doc]
pub struct $name($crate::frame::BehaviourSlot);
impl $name {
#[doc = concat!("Wrap `f` as an [`", stringify!($name), "`].")]
pub fn new(
f: impl FnMut(
&$crate::frame::World,
$crate::frame::Entity,
&mut $crate::frame::Frame,
) + 'static,
) -> Self {
Self($crate::frame::BehaviourSlot::new(
$crate::frame::FrameBehaviourOrder::default(),
Box::new(f),
))
}
#[doc = concat!("Wrap `f` as an [`", stringify!($name), "`] that runs at `order`.")]
pub fn with_order(
order: $crate::frame::FrameBehaviourOrder,
f: impl FnMut(
&$crate::frame::World,
$crate::frame::Entity,
&mut $crate::frame::Frame,
) + 'static,
) -> Self {
Self($crate::frame::BehaviourSlot::new(order, Box::new(f)))
}
pub fn order(&self) -> $crate::frame::FrameBehaviourOrder {
self.0.order()
}
pub fn run(
&mut self,
world: &$crate::frame::World,
entity: $crate::frame::Entity,
frame: &mut $crate::frame::Frame,
) {
self.0.run(world, entity, frame);
}
}
impl $crate::frame::FrameBehaviour for $name {
fn order(&self) -> $crate::frame::FrameBehaviourOrder {
self.0.order()
}
fn run(
&mut self,
world: &$crate::frame::World,
entity: $crate::frame::Entity,
frame: &mut $crate::frame::Frame,
) {
self.0.run(world, entity, frame);
}
}
};
}
frame_behaviour!(
OnFrame,
"Runs once per frame of a windowed application.\n\nDispatch it with [`dispatch_frame`]."
);
pub fn dispatch_frame(world: &mut World, frame: &mut Frame) {
FrameSkip::reset(world);
world
.root_scope()
.query::<&mut FrameClock>()
.for_each(|mut clock| clock.advance(frame.delta_time, frame.index));
dispatch::<OnFrame>(world, frame);
if let Err(error) = world.apply() {
log::error!("a command queued by a behaviour failed: {error}");
}
}
fn dispatch<B: FrameBehaviour>(world: &World, frame: &mut Frame) {
let mut order: Vec<(FrameBehaviourOrder, Entity)> = world
.root_scope()
.query::<(&B, Entity)>()
.map(|(b, entity)| (b.order(), entity))
.collect();
order.sort_unstable_by_key(|(order, _entity)| *order);
for (_, entity) in order {
world
.root_scope()
.query_one::<&mut B>(entity)
.map(|mut behaviour| behaviour.run(world, entity, frame))
.unwrap_or_else(|_| panic!("the behaviour entity exists: {entity:?}"));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::cell::RefCell;
use std::rc::Rc;
use unlit_ecs::bundle;
fn frame() -> Frame {
Frame {
delta_time: 0.0,
index: 0,
size: (0, 0),
}
}
const END: FrameBehaviourOrder = FrameBehaviourOrder(100);
const START: FrameBehaviourOrder = FrameBehaviourOrder(-100);
#[test]
fn behaviours_run_in_their_declared_order() {
let mut world = World::new();
let log: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let later = {
let log = log.clone();
OnFrame::with_order(FrameBehaviourOrder(10), move |_world, _entity, _frame| {
log.borrow_mut().push(2)
})
};
let earlier = {
let log = log.clone();
OnFrame::with_order(FrameBehaviourOrder(5), move |_world, _entity, _frame| {
log.borrow_mut().push(1)
})
};
world.root_scope_mut().spawn(bundle![later]);
world.root_scope_mut().spawn(bundle![earlier]);
dispatch_frame(&mut world, &mut frame());
assert_eq!(*log.borrow(), vec![1, 2]);
}
#[test]
fn the_default_band_runs_between_the_stated_orders() {
let mut world = World::new();
let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
for (order, name) in [(Some(END), "end"), (None, "update"), (Some(START), "start")] {
let log = log.clone();
let behaviour = match order {
Some(order) => OnFrame::with_order(order, move |_world, _entity, _frame| {
log.borrow_mut().push(name)
}),
None => OnFrame::new(move |_world, _entity, _frame| log.borrow_mut().push(name)),
};
world.root_scope_mut().spawn(bundle![behaviour]);
}
dispatch_frame(&mut world, &mut frame());
assert_eq!(*log.borrow(), vec!["start", "update", "end"]);
}
#[test]
fn a_behaviour_receives_the_frame() {
let mut world = World::new();
let seen: Rc<RefCell<Option<Frame>>> = Rc::new(RefCell::new(None));
{
let seen = seen.clone();
world
.root_scope_mut()
.spawn(bundle![OnFrame::new(move |_world, _entity, frame| {
*seen.borrow_mut() = Some(*frame);
}),]);
}
let mut frame = Frame {
delta_time: 0.5,
index: 7,
size: (320, 240),
};
dispatch_frame(&mut world, &mut frame);
assert_eq!(
*seen.borrow(),
Some(Frame {
delta_time: 0.5,
index: 7,
size: (320, 240)
})
);
}
#[test]
fn a_frame_behaviour_may_advance_the_index() {
let mut world = World::new();
world
.root_scope_mut()
.spawn(bundle![OnFrame::new(|_world, _entity, frame| {
frame.index += 1;
}),]);
let mut frame = frame();
dispatch_frame(&mut world, &mut frame);
assert_eq!(frame.index, 1);
}
#[test]
fn dispatch_applies_what_a_behaviour_queued() {
struct Marker;
let mut world = World::new();
world
.root_scope_mut()
.spawn(bundle![OnFrame::new(|world, _entity, _frame| {
let _ = world.root_scope().queue().spawn(bundle![Marker]);
}),]);
dispatch_frame(&mut world, &mut frame());
assert_eq!(world.root_scope().query::<&Marker>().count(), 1);
}
#[test]
fn a_stated_order_runs_after_the_default_band() {
let mut world = World::new();
let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
{
let log = log.clone();
world.root_scope_mut().spawn(bundle![OnFrame::with_order(
END,
move |_world, _entity, _frame| {
log.borrow_mut().push("end");
}
),]);
}
{
let log = log.clone();
world
.root_scope_mut()
.spawn(bundle![OnFrame::new(move |_world, _entity, _frame| {
log.borrow_mut().push("update");
}),]);
}
dispatch_frame(&mut world, &mut frame());
assert_eq!(*log.borrow(), vec!["update", "end"]);
}
#[test]
fn skip_marks_the_frame_and_is_skipped_reads_it() {
let mut world = World::new();
world.root_scope_mut().spawn(bundle![FrameSkip::default()]);
assert!(!FrameSkip::is_skipped(&world));
FrameSkip::skip(&world);
assert!(FrameSkip::is_skipped(&world));
}
#[test]
fn skip_is_a_no_op_without_a_flag_component() {
let world = World::new();
FrameSkip::skip(&world);
assert!(!FrameSkip::is_skipped(&world));
}
#[test]
fn dispatch_clears_a_skip_left_by_the_previous_frame() {
let mut world = World::new();
let first = Rc::new(Cell::new(true));
world.root_scope_mut().spawn(bundle![FrameSkip::default()]);
{
let first = first.clone();
world
.root_scope_mut()
.spawn(bundle![OnFrame::new(move |world, _entity, _frame| {
assert!(!FrameSkip::is_skipped(world), "the flag starts whole");
if first.replace(false) {
FrameSkip::skip(world);
}
}),]);
}
dispatch_frame(&mut world, &mut frame());
assert!(FrameSkip::is_skipped(&world));
dispatch_frame(&mut world, &mut frame());
assert!(!FrameSkip::is_skipped(&world));
}
#[test]
fn the_clock_follows_the_frame_it_is_dispatched_with() {
let mut world = World::new();
let clock = world.root_scope_mut().spawn(bundle![FrameClock::default()]);
let seen: Rc<RefCell<Vec<FrameClock>>> = Rc::new(RefCell::new(Vec::new()));
{
let seen = seen.clone();
world
.root_scope_mut()
.spawn(bundle![OnFrame::new(move |world, _entity, _frame| {
let clock = *world
.root_scope()
.query_one::<&FrameClock>(clock)
.expect("the clock is spawned");
seen.borrow_mut().push(clock);
}),]);
}
let mut frame = Frame {
delta_time: 0.25,
index: 3,
size: (0, 0),
};
dispatch_frame(&mut world, &mut frame);
frame.delta_time = 0.5;
frame.index = 4;
dispatch_frame(&mut world, &mut frame);
let seen = seen.borrow();
assert_eq!(
seen[0],
FrameClock {
time: 0.25,
delta_time: 0.25,
index: 3,
},
"the first frame's clock starts from zero"
);
assert_eq!(
seen[1],
FrameClock {
time: 0.75,
delta_time: 0.5,
index: 4,
},
"time accumulates the deltas the loop states"
);
}
#[test]
fn dispatch_without_a_clock_still_runs_every_behaviour() {
let mut world = World::new();
let ran = Rc::new(Cell::new(0));
{
let ran = ran.clone();
world
.root_scope_mut()
.spawn(bundle![OnFrame::new(move |_world, _entity, _frame| {
ran.set(ran.get() + 1);
}),]);
}
dispatch_frame(&mut world, &mut frame());
assert_eq!(ran.get(), 1);
}
}