Skip to content

Commit

Permalink
Optimize entity spawning with an entity pool
Browse files Browse the repository at this point in the history
  • Loading branch information
benfrankel committed Dec 9, 2023
1 parent 905b01d commit c8e38c0
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 104 deletions.
6 changes: 4 additions & 2 deletions src/physics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Velocity>()
.register_type::<PhysicsSettings>()
.init_resource::<PhysicsSettings>()
.add_systems(Update, apply_velocity.in_set(AppSet::Simulate));
}
}

pub const UNIT_SPEED: f32 = 8.0;

#[derive(Resource, Default)]
#[derive(Resource, Reflect, Default)]
#[reflect(Resource)]
pub struct PhysicsSettings {
pub speed_multiplier: f32,
}

#[derive(Component, Reflect)]
#[derive(Component, Reflect, Default, Clone, Copy)]
pub struct Velocity(pub Vec3);

fn apply_velocity(
Expand Down
139 changes: 92 additions & 47 deletions src/simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ mod sprite_pack;

use std::f32::consts::TAU;

use bevy::ecs::event::ManualEventReader;
use bevy::prelude::*;
use rand::thread_rng;
use rand::Rng;

use crate::physics::Velocity;
Expand All @@ -14,7 +16,6 @@ use crate::state::editor_screen::SceneViewBounds;
use crate::state::editor_screen::WrapWithinSceneView;
use crate::state::AppState;
use crate::ui::CodeTyper;
use crate::util::OverflowDespawnQueue;
use crate::AppRoot;
use crate::AppSet;

Expand All @@ -25,14 +26,12 @@ pub struct SimulationPlugin;
impl Plugin for SimulationPlugin {
fn build(&self, app: &mut App) {
app.register_type::<SpawnEvent>()
.register_type::<IsEntityCap>()
.add_plugins(sprite_pack::SpritePackPlugin)
.add_event::<SpawnEvent>()
.add_event::<LinesAddedEvent>()
.init_resource::<Simulation>()
.init_resource::<PassiveCodeTyper>()
.init_resource::<PassiveEntitySpawner>()
.add_systems(Startup, spawn_entity_caps)
.add_systems(
Update,
(
Expand Down Expand Up @@ -112,28 +111,85 @@ impl Default for Simulation {
}
}

/// Maximum number of entities that can be spawned in the scene view for a single SpawnEvent.
const MAX_SPAWN_PER_EVENT: usize = 32;
struct EntityPool {
entities: Vec<Entity>,
old_idx: usize,
}

#[derive(Event, Reflect)]
impl Default for EntityPool {
fn default() -> Self {
Self {
entities: Vec::with_capacity(20_000),
old_idx: 0,
}
}
}

impl EntityPool {
fn recycle(&mut self) -> Entity {
self.old_idx += 1;
if self.old_idx > self.entities.len() {
self.old_idx -= self.entities.len();
}
self.entities[self.old_idx - 1]
}
}

/// Maximum number of entities that can be spawned in the scene view in a single SpawnEvent.
const MAX_SPAWN_PER_EVENT: usize = 20;

#[derive(Event, Reflect, Clone, Copy)]
pub struct SpawnEvent {
pub position: Vec2,
pub count: f64,
}

fn spawn_entities(
mut commands: Commands,
mut events: EventReader<SpawnEvent>,
root: Res<AppRoot>,
mut simulation: ResMut<Simulation>,
sprite_pack_assets: Res<SpritePackAssets>,
mut entity_cap_query: Query<&mut OverflowDespawnQueue, With<IsEntityCap>>,
world: &mut World,
mut pool: Local<EntityPool>,
mut reader: Local<ManualEventReader<SpawnEvent>>,
) {
let mut rng = rand::thread_rng();
for event in events.read() {
simulation.entities += event.count;
// Fill entity pool initially
let capacity = pool.entities.capacity() - pool.entities.len();
if capacity > 0 {
pool.entities.extend(
world.spawn_batch(
std::iter::repeat((
Name::new("Entity"),
// NOTE: Workaround for SpatialBundle not impling Clone
(
Visibility::Hidden,
InheritedVisibility::default(),
ViewVisibility::default(),
Transform::default(),
GlobalTransform::default(),
),
WrapWithinSceneView,
Velocity::default(),
TextureAtlasSprite::default(),
Handle::<TextureAtlas>::default(),
))
.take(capacity),
),
);

let parent = world.resource::<AppRoot>().world;
for entity in pool.entities.iter().copied() {
world.entity_mut(entity).set_parent(parent);
}
}

let mut rng = thread_rng();
for event in reader
.read(world.resource::<Events<_>>())
.copied()
.collect::<Vec<_>>()
{
world.resource_mut::<Simulation>().entities += event.count;

let simulation = world.resource::<Simulation>();
let spawn_count = MAX_SPAWN_PER_EVENT.min(event.count as usize);
let mut bundles = vec![];
for _ in 0..spawn_count {
let angle = rng.gen_range(0.0..=TAU);
let direction = Vec2::from_angle(angle);
Expand All @@ -144,47 +200,36 @@ fn spawn_entities(
let offset = rng.gen_range(simulation.spawn_offset_min..=simulation.spawn_offset_max)
* direction;
let position = (event.position + offset).extend(0.0);
let transform = Transform::from_translation(position);

let size = rng.gen_range(simulation.entity_size_min..=simulation.entity_size_max);
let size = Vec2::splat(size);

let entity = commands
.spawn((
Name::new("Entity"),
SpatialBundle::from_transform(Transform::from_translation(position)),
Velocity(velocity),
WrapWithinSceneView,
))
.set_parent(root.world)
.id();
simulation.sprite_pack.apply(
&mut commands,
entity,
&sprite_pack_assets,
size,
&mut rng,
);
let (sprite, texture) =
simulation
.sprite_pack
.bundle(world.resource::<SpritePackAssets>(), size, &mut rng);

bundles.push((
Visibility::Inherited,
transform,
Velocity(velocity),
sprite,
texture,
));
}

for mut despawn_queue in &mut entity_cap_query {
despawn_queue.push(entity);
}
for (visibility, transform, velocity, sprite, texture) in bundles {
let mut entity = world.entity_mut(pool.recycle());
*entity.get_mut::<Visibility>().unwrap() = visibility;
*entity.get_mut::<Transform>().unwrap() = transform;
*entity.get_mut::<Velocity>().unwrap() = velocity;
*entity.get_mut::<TextureAtlasSprite>().unwrap() = sprite;
*entity.get_mut::<Handle<TextureAtlas>>().unwrap() = texture;
}
}
}

#[derive(Component, Reflect)]
struct IsEntityCap;

const HARD_CAP: usize = 8000;

fn spawn_entity_caps(mut commands: Commands) {
commands.spawn((
Name::new("HardEntityCap"),
OverflowDespawnQueue::new(HARD_CAP),
IsEntityCap,
));
}

/// Resource for handling passive code generation.
#[derive(Resource)]
pub struct PassiveCodeTyper {
Expand Down
19 changes: 9 additions & 10 deletions src/simulation/sprite_pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,16 @@ impl SpritePack {
};
}

pub fn apply(
pub fn bundle(
&self,
commands: &mut Commands,
entity: Entity,
assets: &SpritePackAssets,
size: Vec2,
mut rng: impl Rng,
) {
if let Some(&skin) = self.skins.choose(&mut rng) {
commands.entity(entity).insert(skin.bundle(assets, size));
};
) -> (TextureAtlasSprite, Handle<TextureAtlas>) {
self.skins
.choose(&mut rng)
.map(|skin| skin.bundle(assets, size))
.unwrap_or_default()
}

pub fn replace_skin_set(&mut self, skin_set: SkinSet, rng: impl Rng) {
Expand Down Expand Up @@ -205,9 +204,9 @@ fn apply_sprite_pack(
Vec2::splat(rng.gen_range(simulation.entity_size_min..=simulation.entity_size_max))
});

simulation
.sprite_pack
.apply(&mut commands, entity, &assets, size, &mut rng);
commands
.entity(entity)
.insert(simulation.sprite_pack.bundle(&assets, size, &mut rng));
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/state/editor_screen/scene_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ fn update_scene_view_bounds(
bounds.max = rect.max.extend(camera_z - proj.near);
}

#[derive(Component, Reflect)]
#[derive(Component, Reflect, Clone, Copy)]
pub struct WrapWithinSceneView;

fn wrap_sprites_within_scene_view(
Expand Down
1 change: 0 additions & 1 deletion src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use bevy::prelude::*;
use format_num::format_num;

pub use crate::util::despawn::DespawnSet;
pub use crate::util::despawn::OverflowDespawnQueue;

pub struct UtilPlugin;

Expand Down
44 changes: 1 addition & 43 deletions src/util/despawn.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::collections::VecDeque;

use bevy::prelude::*;
use bevy::utils::HashSet;

Expand All @@ -11,47 +9,7 @@ impl Plugin for DespawnPlugin {
fn build(&self, app: &mut App) {
app.register_type::<DespawnSet>()
.init_resource::<DespawnSet>()
.add_systems(
Update,
(apply_overflow_despawn_queue, apply_despawn_set)
.chain()
.in_set(AppSet::Despawn),
);
}
}

#[derive(Component, Reflect)]
pub struct OverflowDespawnQueue {
pub entities: VecDeque<Entity>,
pub cap: usize,
}

impl OverflowDespawnQueue {
pub fn new(cap: usize) -> Self {
Self {
entities: VecDeque::with_capacity(2 * cap),
cap,
}
}

pub fn push(&mut self, entity: Entity) {
self.entities.push_back(entity);
}

pub fn drain_overflow(&mut self) -> impl '_ + Iterator<Item = Entity> {
let overflow = self.entities.len().saturating_sub(self.cap);
self.entities.drain(0..overflow)
}
}

fn apply_overflow_despawn_queue(
mut despawn: ResMut<DespawnSet>,
mut queue_query: Query<&mut OverflowDespawnQueue>,
) {
for mut queue in &mut queue_query {
for entity in queue.drain_overflow() {
despawn.recursive(entity);
}
.add_systems(Update, apply_despawn_set.in_set(AppSet::Despawn));
}
}

Expand Down

0 comments on commit c8e38c0

Please sign in to comment.