Skip to main content

stress_test/
stress_test.rs

1use jengine::engine::{Color, Game, jEngine, KeyCode};
2use jengine::ecs::{Entity, World};
3use jengine::input::{ActionMap, InputSource};
4use jengine::{DEFAULT_TILESET, DEFAULT_FONT_METADATA, DEFAULT_TILE_W, DEFAULT_TILE_H};
5
6// ── Components ───────────────────────────────────────────────────────────────
7
8struct Position { x: f32, y: f32 }
9struct Velocity { vx: f32, vy: f32 }
10#[allow(dead_code)]
11struct Life { current: f32, max: f32 }
12struct EntityMarker; // Marker for the "crowd" entities
13
14// ── Stress Test State ────────────────────────────────────────────────────────
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17enum StressAction {
18    IncEntities,
19    DecEntities,
20    IncParticles,
21    DecParticles,
22    Nova,
23    Quit,
24}
25
26struct StressTest {
27    world: World,
28    actions: ActionMap<StressAction>,
29    font_loaded: bool,
30    particle_spawn_rate: usize, // particles per frame
31    entity_target_count: usize,
32}
33
34impl StressTest {
35    fn new() -> Self {
36        let mut actions = ActionMap::new();
37        actions.bind(StressAction::IncEntities,  InputSource::Key(KeyCode::KeyW));
38        actions.bind(StressAction::DecEntities,  InputSource::Key(KeyCode::KeyQ));
39        actions.bind(StressAction::IncParticles, InputSource::Key(KeyCode::KeyS));
40        actions.bind(StressAction::DecParticles, InputSource::Key(KeyCode::KeyA));
41        actions.bind(StressAction::Nova,         InputSource::Key(KeyCode::Space));
42        actions.bind(StressAction::Quit,         InputSource::Key(KeyCode::Escape));
43
44        Self {
45            world: World::new(),
46            actions,
47            font_loaded: false,
48            particle_spawn_rate: 10,
49            entity_target_count: 100,
50        }
51    }
52
53    fn spawn_particle(&mut self, x: f32, y: f32, vx: f32, vy: f32) {
54        let e = self.world.spawn();
55        self.world.insert(e, Position { x, y });
56        self.world.insert(e, Velocity { vx, vy });
57        self.world.insert(e, Life { current: 1.5, max: 1.5 });
58    }
59
60    fn spawn_entity(&mut self, sw: f32, sh: f32) {
61        let e = self.world.spawn();
62        let x = (pseudo_rand(self.world.entity_count() as u64) * sw) as u32;
63        let y = (pseudo_rand(self.world.entity_count() as u64 + 7) * sh) as u32;
64        
65        // We use the grid path for entities to stress the grid vertex building
66        // But since we want them to move smoothly, we use ParticlePosition/Velocity 
67        // and just draw them as sprites or particles for this test.
68        self.world.insert(e, EntityMarker);
69        self.world.insert(e, Position { x: x as f32, y: y as f32 });
70        let vx = (pseudo_rand(e.id() as u64) - 0.5) * 100.0;
71        let vy = (pseudo_rand(e.id() as u64 + 1) - 0.5) * 100.0;
72        self.world.insert(e, Velocity { vx, vy });
73    }
74}
75
76fn pseudo_rand(seed: u64) -> f32 {
77    let x = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
78    (x >> 33) as f32 / u32::MAX as f32
79}
80
81impl Game for StressTest {
82    fn on_enter(&mut self, engine: &mut jEngine) {
83        engine.audio.load_sound("UI_click", "resources/audio/UI_click.wav");
84    }
85
86    fn update(&mut self, engine: &mut jEngine) {
87        let dt = engine.dt();
88        let sw = engine.renderer.window.inner_size().width as f32;
89        let sh = engine.renderer.window.inner_size().height as f32;
90
91        if self.actions.is_pressed(StressAction::Quit, &engine.input) {
92            engine.play_sound("UI_click");
93            engine.request_quit();
94        }
95
96        // ── Adjust Load ──
97        if self.actions.is_held(StressAction::IncEntities, &engine.input) { self.entity_target_count += 5; }
98        if self.actions.is_held(StressAction::DecEntities, &engine.input) { self.entity_target_count = self.entity_target_count.saturating_sub(5); }
99        if self.actions.is_held(StressAction::IncParticles, &engine.input) { self.particle_spawn_rate += 2; }
100        if self.actions.is_held(StressAction::DecParticles, &engine.input) { self.particle_spawn_rate = self.particle_spawn_rate.saturating_sub(2); }
101
102        // ── Maintain Entity Count ──
103        let current_entities = self.world.query::<EntityMarker>().count();
104        if current_entities < self.entity_target_count {
105            for _ in 0..10 { self.spawn_entity(sw, sh); }
106        } else if current_entities > self.entity_target_count {
107            let to_kill: Vec<Entity> = self.world.query::<EntityMarker>().take(10).map(|(e, _)| e).collect();
108            for e in to_kill { self.world.despawn(e); }
109        }
110
111        // ── Spawn Particles ──
112        for _ in 0..self.particle_spawn_rate {
113            let vx = (pseudo_rand(engine.tick() + self.world.entity_count() as u64) - 0.5) * 300.0;
114            let vy = (pseudo_rand(engine.tick() + self.world.entity_count() as u64 + 1) - 0.5) * 300.0;
115            self.spawn_particle(sw * 0.5, sh * 0.5, vx, vy);
116        }
117
118        if self.actions.is_pressed(StressAction::Nova, &engine.input) {
119            for i in 0..2000 {
120                let angle = (i as f32 / 2000.0) * std::f32::consts::TAU;
121                let speed = 200.0 + pseudo_rand(i as u64) * 400.0;
122                self.spawn_particle(sw * 0.5, sh * 0.5, angle.cos() * speed, angle.sin() * speed);
123            }
124            engine.camera_shake(15.0);
125        }
126
127        // ── System: Movement (Entities) ──
128        for (_e, (pos, vel, _marker)) in self.world.query_multi_mut::<(Position, Velocity, EntityMarker)>() {
129            pos.x += vel.vx * dt;
130            pos.y += vel.vy * dt;
131
132            // Bounce entities off walls
133            if pos.x < 0.0 || pos.x > sw { vel.vx *= -1.0; pos.x = pos.x.clamp(0.0, sw); }
134            if pos.y < 0.0 || pos.y > sh { vel.vy *= -1.0; pos.y = pos.y.clamp(0.0, sh); }
135        }
136
137        // ── System: Movement & Life (Particles) ──
138        let mut dead = Vec::new();
139        for (e, (pos, vel, life)) in self.world.query_multi_mut::<(Position, Velocity, Life)>() {
140            pos.x += vel.vx * dt;
141            pos.y += vel.vy * dt;
142
143            life.current -= dt;
144            if life.current <= 0.0 { dead.push(e); }
145        }
146        for e in dead { self.world.despawn(e); }
147    }
148
149    fn render(&mut self, engine: &mut jEngine) {
150        if !self.font_loaded {
151            if let Ok(font) = jengine::renderer::text::Font::from_mtsdf_json(DEFAULT_FONT_METADATA) {
152                engine.renderer.set_mtsdf_distance_range(font.distance_range);
153                engine.ui.text.set_font(font);
154            }
155            self.font_loaded = true;
156        }
157
158        engine.clear();
159
160        // ── Draw Entities (as small circles for speed) ──
161        for (_e, (_m, pos)) in self.world.query_multi::<(EntityMarker, Position)>() {
162            engine.draw_particle(pos.x, pos.y, Color([0.4, 0.7, 1.0, 1.0]), 4.0);
163        }
164
165        // ── Draw Particles ──
166        for (_e, (_l, pos)) in self.world.query_multi::<(Life, Position)>() {
167            engine.draw_particle(pos.x, pos.y, Color([1.0, 0.5, 0.2, 0.8]), 2.0);
168        }
169
170        // ── Stats UI ──
171        let th = engine.tile_height() as f32;
172        
173        engine.ui.ui_rect(0.0, 0.0, 350.0, th * 8.0, Color([0.0, 0.0, 0.0, 0.7]));
174        
175        let mut y = 10.0;
176        let lines = [
177            format!("STRESS TEST - [Space] for Nova"),
178            format!("Entities:  {} (Q/W to adj)", self.entity_target_count),
179            format!("P-Rate:    {} / frame (A/S to adj)", self.particle_spawn_rate),
180            format!("Total ECS: {}", self.world.entity_count()),
181            format!("FPS:       {:.1}", 1.0 / engine.dt().max(0.001)),
182        ];
183
184        for line in lines {
185            engine.ui.ui_text(10.0, y, &line, Color::WHITE, Color::TRANSPARENT, None);
186            y += th;
187        }
188    }
189}
190
191fn main() {
192    jEngine::builder()
193        .with_title("jengine — Performance Stress Test")
194        .with_size(1280, 720)
195        .with_tileset(DEFAULT_TILESET, DEFAULT_TILE_W, DEFAULT_TILE_H)
196        .run(StressTest::new());
197}