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, 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) {
for entity in Self::entities(world) {
let _ = world.with_mut::<FrameSkip, _>(entity, |skip| skip.0 = true);
}
}
#[must_use]
pub fn is_skipped(world: &World) -> bool {
world.query::<&FrameSkip>().any(|(_, skip)| skip.0)
}
fn reset(world: &World) {
for entity in Self::entities(world) {
let _ = world.with_mut::<FrameSkip, _>(entity, |skip| skip.0 = false);
}
}
fn entities(world: &World) -> Vec<Entity> {
world
.query::<&FrameSkip>()
.map(|(entity, _)| entity)
.collect()
}
}
pub trait FrameBehaviour: 'static {
fn order(&self) -> FrameBehaviourOrder;
fn run(&mut self, world: &World, entity: Entity, frame: &mut Frame);
fn mount_index(&self) -> u64;
fn set_mount_index(&mut self, index: u64);
}
pub(crate) struct BehaviourSlot {
callback: FrameCallback,
order: FrameBehaviourOrder,
mount_index: u64,
}
impl BehaviourSlot {
fn new(order: FrameBehaviourOrder, callback: FrameCallback) -> Self {
Self {
callback,
order,
mount_index: 0,
}
}
fn order(&self) -> FrameBehaviourOrder {
self.order
}
fn mount_index(&self) -> u64 {
self.mount_index
}
fn set_mount_index(&mut self, index: u64) {
self.mount_index = index;
}
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(
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);
}
fn mount_index(&self) -> u64 {
self.0.mount_index()
}
fn set_mount_index(&mut self, index: u64) {
self.0.set_mount_index(index);
}
}
};
}
frame_behaviour!(
OnFrame,
"Runs once per frame of a windowed application.\n\nDispatch it with [`dispatch_frame`]."
);
#[derive(Debug, Default)]
struct FrameMounts(u64);
pub trait WorldFrameExt {
fn spawn_frame_behaviour<B: FrameBehaviour>(&mut self, behaviour: B) -> Entity;
}
impl WorldFrameExt for World {
fn spawn_frame_behaviour<B: FrameBehaviour>(&mut self, mut behaviour: B) -> Entity {
let index = next_mount_index(self);
behaviour.set_mount_index(index);
self.spawn((behaviour,))
}
}
fn next_mount_index(world: &mut World) -> u64 {
let counter = world
.query::<&FrameMounts>()
.next()
.map(|(entity, _)| entity);
let Some(counter) = counter else {
world.spawn((FrameMounts(1),));
return 0;
};
world
.with_mut::<FrameMounts, _>(counter, |mounts| {
let index = mounts.0;
mounts.0 += 1;
index
})
.expect("the mount counter exists")
}
pub fn dispatch_frame(world: &mut World, frame: &mut Frame) {
FrameSkip::reset(world);
dispatch::<OnFrame>(world, frame);
world.apply();
}
fn dispatch<B: FrameBehaviour>(world: &World, frame: &mut Frame) {
let mut order: Vec<(FrameBehaviourOrder, u64, Entity)> = world
.query::<&B>()
.map(|(entity, behaviour)| (behaviour.order(), behaviour.mount_index(), entity))
.collect();
order.sort_unstable_by_key(|(order, mount_index, _)| (*order, *mount_index));
for (_, _, entity) in order {
world
.with_mut::<B, _>(entity, |behaviour| behaviour.run(world, entity, frame))
.expect("the behaviour entity exists");
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::cell::RefCell;
use std::rc::Rc;
fn frame() -> Frame {
Frame {
delta_time: 0.0,
index: 0,
size: (0, 0),
}
}
const UPDATE: FrameBehaviourOrder = FrameBehaviourOrder(0);
const END: 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::new(FrameBehaviourOrder(10), move |_world, _entity, _frame| {
log.borrow_mut().push(2)
})
};
let earlier = {
let log = log.clone();
OnFrame::new(FrameBehaviourOrder(5), move |_world, _entity, _frame| {
log.borrow_mut().push(1)
})
};
world.spawn_frame_behaviour(later);
world.spawn_frame_behaviour(earlier);
dispatch_frame(&mut world, &mut frame());
assert_eq!(*log.borrow(), vec![1, 2]);
}
#[test]
fn equal_orders_keep_their_mount_order() {
let mut world = World::new();
let log: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
for value in [1, 2, 3] {
let log = log.clone();
world.spawn_frame_behaviour(OnFrame::new(UPDATE, move |_world, _entity, _frame| {
log.borrow_mut().push(value);
}));
}
dispatch_frame(&mut world, &mut frame());
assert_eq!(*log.borrow(), vec![1, 2, 3]);
}
#[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.spawn_frame_behaviour(OnFrame::new(UPDATE, 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.spawn_frame_behaviour(OnFrame::new(UPDATE, |_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.spawn_frame_behaviour(OnFrame::new(UPDATE, |world, _entity, _frame| {
let _ = world.queue().spawn((Marker,));
}));
dispatch_frame(&mut world, &mut frame());
assert_eq!(world.query::<&Marker>().count(), 1);
}
#[test]
fn a_later_order_runs_after_an_earlier_one() {
let mut world = World::new();
let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
{
let log = log.clone();
world.spawn_frame_behaviour(OnFrame::new(END, move |_world, _entity, _frame| {
log.borrow_mut().push("end");
}));
}
{
let log = log.clone();
world.spawn_frame_behaviour(OnFrame::new(UPDATE, 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.spawn((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.spawn((FrameSkip::default(),));
{
let first = first.clone();
world.spawn_frame_behaviour(OnFrame::new(UPDATE, 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));
}
}