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
6struct Position { x: f32, y: f32 }
9struct Velocity { vx: f32, vy: f32 }
10#[allow(dead_code)]
11struct Life { current: f32, max: f32 }
12struct EntityMarker; #[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, 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 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 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 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 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 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 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 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 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 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 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}