use crate::memory::InlineVec;
use crate::components::{
BodyDynamics, Collider, GlobalTransform, Lifetime, MeshRenderer, ModelRenderer, Pickup,
PropInstance, RenderHandle, SkeletonPose, Spawner, Transform,
};
use crate::ecs::asset_id::AssetId;
use crate::ecs::{Entity, EntityByName, FrameVec, PipelineContext};
pub fn spawn_from_template(
ctx: &mut PipelineContext,
template: Entity,
name: Option<AssetId>,
transform: Transform,
lifetime: Option<f32>,
mut clone_slot: impl FnMut(usize, [[f32; 4]; 4]) -> Option<usize>,
) -> Option<Entity> {
let src_slots: InlineVec<u32> = ctx.get::<RenderHandle>(template).map(|h| h.draws.clone())?;
if src_slots.is_empty() {
return None;
}
let model = transform.model_matrix();
let mut draws = InlineVec::new();
for src in src_slots {
let new_slot = clone_slot(src as usize, model)?;
draws.push(new_slot as u32);
}
let mesh_renderer = ctx.get::<MeshRenderer>(template).cloned();
let model_renderer = ctx.get::<ModelRenderer>(template).cloned();
let collider = ctx.get::<Collider>(template).cloned();
let body_dynamics = ctx.get::<BodyDynamics>(template).copied();
let pickup = ctx.get::<Pickup>(template).is_some();
let prop_instance = ctx.get::<PropInstance>(template).is_some();
let entity = ctx.components.spawn();
ctx.insert(entity, transform);
ctx.insert(entity, GlobalTransform(model));
ctx.insert(entity, RenderHandle { draws });
if let Some(renderer) = mesh_renderer {
ctx.insert(entity, renderer);
} else if let Some(renderer) = model_renderer {
ctx.insert(entity, renderer);
}
if let Some(collider) = collider {
ctx.insert(entity, collider);
}
if let Some(body_dynamics) = body_dynamics {
ctx.insert(entity, body_dynamics);
}
if pickup {
ctx.insert(entity, Pickup);
}
if prop_instance {
ctx.insert(entity, PropInstance);
}
if let Some(secs) = lifetime {
ctx.insert(entity, Lifetime { remaining: secs });
}
if let Some(name) = name
&& let Some(by_name) = ctx.resource_mut::<EntityByName>()
{
by_name.0.insert(name, entity);
}
Some(entity)
}
pub fn spawn_skinned_from_template(
ctx: &mut PipelineContext,
template: Entity,
name: Option<AssetId>,
transform: Transform,
lifetime: Option<f32>,
mut acquire_slot: impl FnMut(usize, [[f32; 4]; 4]) -> Option<usize>,
) -> Option<Entity> {
let template_pose = ctx.get::<SkeletonPose>(template)?;
let model = transform.model_matrix();
let skinned_index = acquire_slot(template_pose.skinned_index, model)?;
let pose = template_pose.clone_for_slot(skinned_index);
let prop_instance = ctx.get::<PropInstance>(template).is_some();
let entity = ctx.components.spawn();
ctx.insert(entity, transform);
ctx.insert(entity, pose);
if prop_instance {
ctx.insert(entity, PropInstance);
}
if let Some(secs) = lifetime {
ctx.insert(entity, Lifetime { remaining: secs });
}
if let Some(name) = name
&& let Some(by_name) = ctx.resource_mut::<EntityByName>()
{
by_name.0.insert(name, entity);
}
Some(entity)
}
#[derive(Clone, Copy)]
pub struct DueSpawn {
pub template: AssetId,
pub transform: Transform,
pub lifetime: Option<f32>,
}
pub fn tick_spawners<'a>(ctx: &mut PipelineContext<'a>, dt: f32) -> FrameVec<'a, DueSpawn> {
let frame = ctx.frame;
let mut fired = frame.vec::<(Entity, AssetId, f32, u32)>(ctx.query::<Spawner>().len());
for (entity, spawner) in ctx.query_mut_with_entity::<Spawner>() {
if spawner.interval <= 0.0 {
continue;
}
spawner.elapsed += dt;
let mut count = 0;
while spawner.elapsed >= spawner.interval {
spawner.elapsed -= spawner.interval;
spawner.count += 1;
count += 1;
}
if count > 0 {
fired.push((entity, spawner.template, spawner.lifetime, count));
}
}
let mut due = frame.vec::<DueSpawn>(
fired
.iter()
.map(|&(.., count)| count as usize)
.sum::<usize>(),
);
for &(entity, template, lifetime, count) in fired.iter() {
let transform = ctx.get::<Transform>(entity).copied().unwrap_or_default();
for _ in 0..count {
due.push(DueSpawn {
template,
transform,
lifetime: (lifetime > 0.0).then_some(lifetime),
});
}
}
due
}
pub fn tick_lifetimes<'a>(ctx: &mut PipelineContext<'a>, dt: f32) -> FrameVec<'a, Entity> {
let mut expired = ctx.frame.vec::<Entity>(ctx.query::<Lifetime>().len());
for (entity, life) in ctx.query_mut_with_entity::<Lifetime>() {
life.remaining -= dt;
if life.remaining <= 0.0 {
expired.push(entity);
}
}
expired
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::collections::BTreeMap;
use alloc::vec;
use alloc::vec::Vec;
use crate::ecs::{
Arena, ComponentStorage, FrameContext, NoPayloads, Resources, SkinnedMeshHandle,
};
use crate::gfx::profile::FrameProfile;
use crate::gfx::skeleton::Skeleton;
fn run<R>(body: impl FnOnce(&mut PipelineContext) -> R) -> R {
let mut components = ComponentStorage::default();
let mut blob = NoPayloads;
let mut profile = FrameProfile::default();
let mut resources = Resources::new();
let scratch = Arena::with_capacity(64 * 1024);
let mut ctx = PipelineContext {
components: &mut components,
blob: &mut blob,
profile: &mut profile,
resources: &mut resources,
frame: FrameContext::new(&scratch),
};
body(&mut ctx)
}
#[derive(Default)]
struct Slots {
free: Vec<usize>,
len: usize,
}
impl Slots {
fn with_len(len: usize) -> Slots {
Slots {
free: Vec::new(),
len,
}
}
fn allocate(&mut self) -> usize {
match self.free.pop() {
Some(slot) => slot,
None => {
self.len += 1;
self.len - 1
}
}
}
fn free(&mut self, slot: usize) {
self.free.push(slot);
}
}
#[derive(Default)]
struct Pool {
free: BTreeMap<usize, Vec<usize>>,
owner: BTreeMap<usize, usize>,
}
impl Pool {
fn reserve(&mut self, template: usize, instance: usize) {
self.owner.insert(instance, template);
self.free.entry(template).or_default().push(instance);
}
fn acquire(&mut self, template: usize) -> Option<usize> {
self.free.get_mut(&template).and_then(|slots| slots.pop())
}
fn release(&mut self, instance: usize) {
if let Some(&template) = self.owner.get(&instance) {
self.free.entry(template).or_default().push(instance);
}
}
}
#[test]
fn spawned_copy_carries_the_template_physics_components() {
run(|ctx| {
ctx.insert_resource(EntityByName::default());
let template = ctx.components.spawn();
ctx.insert(template, Transform::default());
ctx.insert(
template,
MeshRenderer {
mesh: None,
material: None,
texture: None,
cull_distance: 0.0,
},
);
ctx.insert(template, RenderHandle { draws: [0].into() });
ctx.insert(
template,
Collider(crate::components::PropCollider {
radius: 0.4,
..Default::default()
}),
);
ctx.insert(
template,
BodyDynamics {
mass: 2.5,
..Default::default()
},
);
ctx.insert(template, Pickup);
let mut alloc = Slots::with_len(1);
let spawned = spawn_from_template(
ctx,
template,
None,
Transform::default(),
None,
|_src, _model| Some(alloc.allocate()),
)
.expect("spawn");
assert_eq!(
ctx.get::<Collider>(spawned).map(|c| c.0.radius),
Some(0.4),
"the collider is copied"
);
assert_eq!(
ctx.get::<BodyDynamics>(spawned).map(|b| b.mass),
Some(2.5),
"the dynamic-body parameters are copied"
);
assert!(ctx.get::<Pickup>(spawned).is_some(), "the tag is copied");
});
}
#[test]
fn freed_draw_slot_is_reused_by_the_next_spawn() {
run(|ctx| {
ctx.insert_resource(EntityByName::default());
let template = ctx.components.spawn();
ctx.insert(template, Transform::default());
ctx.insert(
template,
MeshRenderer {
mesh: None,
material: None,
texture: None,
cull_distance: 0.0,
},
);
ctx.insert(template, RenderHandle { draws: [0].into() });
let mut alloc = Slots::with_len(1);
let first = spawn_from_template(
ctx,
template,
Some(AssetId(1)),
Transform::default(),
Some(0.5),
|_src, _model| Some(alloc.allocate()),
)
.expect("first spawn");
let first_slot = ctx.get::<RenderHandle>(first).unwrap().draws.clone();
assert_eq!(first_slot, vec![1], "first spawn appended slot 1");
let expired = tick_lifetimes(ctx, 1.0);
assert_eq!(&*expired, &[first], "the short-lived spawn expired");
let freed: Vec<u32> = ctx.get::<RenderHandle>(first).unwrap().draws.to_vec();
for slot in &freed {
alloc.free(*slot as usize);
}
ctx.despawn(first);
assert!(ctx.get::<RenderHandle>(first).is_none(), "first despawned");
let second = spawn_from_template(
ctx,
template,
Some(AssetId(2)),
Transform::default(),
None,
|_src, _model| Some(alloc.allocate()),
)
.expect("second spawn");
let second_slot = ctx.get::<RenderHandle>(second).unwrap().draws.clone();
assert_eq!(
second_slot, freed,
"the freed draw slot must be recycled by the next spawn"
);
});
}
#[test]
fn skinned_spawn_claims_and_recycles_a_pooled_slot() {
run(|ctx| {
ctx.insert_resource(EntityByName::default());
let template = ctx.components.spawn();
ctx.insert(
template,
SkeletonPose::new(SkinnedMeshHandle(10), 0, Skeleton::new(Vec::new())),
);
let mut pool = Pool::default();
pool.reserve(0, 1);
pool.reserve(0, 2);
let first = spawn_skinned_from_template(
ctx,
template,
Some(AssetId(11)),
Transform::default(),
Some(0.5),
|template_idx, _model| pool.acquire(template_idx),
)
.expect("first skinned spawn");
let first_slot = ctx.get::<SkeletonPose>(first).unwrap().skinned_index;
assert_eq!(
ctx.get::<SkeletonPose>(first).unwrap().mesh_id,
SkinnedMeshHandle(10),
"the instance shares the template's mesh id so it animates with it"
);
let expired = tick_lifetimes(ctx, 1.0);
assert_eq!(&*expired, &[first]);
pool.release(first_slot);
ctx.despawn(first);
let second = spawn_skinned_from_template(
ctx,
template,
None,
Transform::default(),
None,
|template_idx, _model| pool.acquire(template_idx),
)
.expect("second skinned spawn");
assert_eq!(
ctx.get::<SkeletonPose>(second).unwrap().skinned_index,
first_slot,
"the freed skinned slot must be recycled by the next spawn"
);
});
}
#[test]
fn skinned_spawn_with_exhausted_pool_returns_none() {
run(|ctx| {
let template = ctx.components.spawn();
ctx.insert(
template,
SkeletonPose::new(SkinnedMeshHandle(10), 0, Skeleton::new(Vec::new())),
);
let mut pool = Pool::default();
let spawned = spawn_skinned_from_template(
ctx,
template,
None,
Transform::default(),
None,
|template_idx, _model| pool.acquire(template_idx),
);
assert!(spawned.is_none(), "an exhausted pool drops the spawn");
});
}
#[test]
fn spawn_registers_the_instance_by_name() {
run(|ctx| {
ctx.insert_resource(EntityByName::default());
let template = ctx.components.spawn();
ctx.insert(template, Transform::default());
ctx.insert(template, RenderHandle { draws: [0].into() });
let mut alloc = Slots::with_len(1);
let spawned = spawn_from_template(
ctx,
template,
Some(AssetId(42)),
Transform::default(),
None,
|_src, _model| Some(alloc.allocate()),
)
.expect("spawn");
let by_name = ctx.resource::<EntityByName>().unwrap();
assert_eq!(by_name.get(AssetId(42)), Some(spawned));
});
}
#[test]
fn spawner_emits_one_copy_per_interval_elapsed() {
run(|ctx| {
let spawner = ctx.components.spawn();
ctx.insert(
spawner,
Transform {
position: [1.0, 2.0, 3.0],
..Transform::default()
},
);
ctx.insert(
spawner,
Spawner {
template: AssetId(7),
interval: 1.0,
lifetime: 2.0,
elapsed: 0.0,
count: 0,
},
);
assert!(tick_spawners(ctx, 0.5).is_empty());
let due = tick_spawners(ctx, 0.6);
assert_eq!(due.len(), 1);
assert_eq!(due[0].template, AssetId(7));
assert_eq!(due[0].lifetime, Some(2.0));
assert_eq!(due[0].transform.position, [1.0, 2.0, 3.0]);
assert_eq!(tick_spawners(ctx, 2.5).len(), 2);
assert_eq!(ctx.get::<Spawner>(spawner).unwrap().count, 3);
});
}
#[test]
fn spawner_with_nonpositive_interval_is_inert() {
run(|ctx| {
let spawner = ctx.components.spawn();
ctx.insert(spawner, Transform::default());
ctx.insert(
spawner,
Spawner {
template: AssetId(7),
interval: 0.0,
lifetime: 0.0,
elapsed: 0.0,
count: 0,
},
);
assert!(tick_spawners(ctx, 100.0).is_empty());
});
}
#[test]
fn tick_only_expires_elapsed_lifetimes() {
run(|ctx| {
let short = ctx.components.spawn();
ctx.insert(short, Lifetime { remaining: 0.1 });
let long = ctx.components.spawn();
ctx.insert(long, Lifetime { remaining: 5.0 });
let expired = tick_lifetimes(ctx, 0.2);
assert_eq!(&*expired, &[short], "only the short lifetime expired");
assert_eq!(ctx.get::<Lifetime>(long).unwrap().remaining, 4.8);
});
}
}