Skip to main content

libdqg/
world.rs

1use glam::{Mat4, Quat, Vec3};
2use serde::{Deserialize, Serialize};
3
4use crate::camera::Camera;
5use crate::ecs::entity::EntityAllocator;
6use crate::ecs::{ComponentStore, Entity};
7use crate::renderer::{Model, Sprite};
8use crate::scripting::ScriptList;
9use crate::transform::Transformable;
10
11/// Position/rotation/scale, kept as separate fields (rather than a raw [`Mat4`]) so an editor
12/// inspector can show and edit them independently. The ECS component is the source of truth;
13/// [`Renderable::sync_transform`] copies it into the actual GPU-facing [`Sprite`]/[`Model`]
14/// matrix each frame via [`Transformable::set_transform`].
15#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
16pub struct Transform {
17    pub position: Vec3,
18    pub rotation: Quat,
19    pub scale: Vec3,
20}
21
22impl Default for Transform {
23    fn default() -> Self {
24        Self { position: Vec3::ZERO, rotation: Quat::IDENTITY, scale: Vec3::ONE }
25    }
26}
27
28impl Transform {
29    pub fn to_mat4(&self) -> Mat4 {
30        Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
31    }
32}
33
34#[derive(Clone)]
35pub struct Name(pub String);
36
37/// What an entity draws as. An entity is a sprite or a model, never both, so this is one enum
38/// rather than two parallel component stores that would otherwise need to be kept in sync.
39#[derive(Clone)]
40pub enum Renderable {
41    Sprite(Sprite),
42    Model(Model),
43}
44
45impl Renderable {
46    /// Writes `transform` into the underlying [`Sprite`]/[`Model`]'s own matrix, overwriting any
47    /// transform accumulated on it directly. Call once per entity per frame before drawing.
48    pub fn sync_transform(&mut self, transform: &Transform) {
49        let matrix = transform.to_mat4();
50        match self {
51            Renderable::Sprite(sprite) => {
52                sprite.set_transform(matrix);
53            }
54            Renderable::Model(model) => {
55                model.set_transform(matrix);
56            }
57        }
58    }
59
60    /// Bounding-sphere radius in local (pre-transform) space, for picking.
61    pub fn bounding_radius(&self) -> f32 {
62        match self {
63            Renderable::Sprite(sprite) => 0.5 * sprite.width.hypot(sprite.height),
64            Renderable::Model(model) => model.bounding_radius,
65        }
66    }
67
68    /// Bounding-sphere center in local (pre-transform) space, for picking.
69    pub fn bounding_center(&self) -> Vec3 {
70        match self {
71            Renderable::Sprite(sprite) => Vec3::new(sprite.width * 0.5, sprite.height * 0.5, 0.0),
72            Renderable::Model(model) => model.bounding_center,
73        }
74    }
75}
76
77/// A camera lens attached to an entity. `active` marks the single camera entity Play mode should
78/// look through — a bool flag rather than a separate "active camera entity" reference, since
79/// entities aren't referenced by stable ID across a scene save/load (scripts look entities up by
80/// name for the same reason). The UI is responsible for keeping at most one entity's flag set
81/// (radio-button semantics, see `editor::draw_camera_editor`); `World::active_camera` defends
82/// against more than one somehow being set by just using the first it finds.
83#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
84pub struct CameraComponent {
85    /// Vertical field of view, in degrees — same convention as [`Camera::fov`].
86    pub fov: f32,
87    pub znear: f32,
88    pub zfar: f32,
89    pub active: bool,
90}
91
92impl Default for CameraComponent {
93    fn default() -> Self {
94        Self { fov: 45.0, znear: 0.1, zfar: 100.0, active: false }
95    }
96}
97
98impl CameraComponent {
99    /// Looks down local -Z, rotated by `transform.rotation` into world space, then converted to
100    /// the yaw/pitch [`Camera`] stores via [`Camera::yaw_pitch_from_forward`] — any roll in
101    /// `transform.rotation` is dropped, since the FPS-style `Camera` has no roll to express it
102    /// with. `aspect` is viewport-derived, not stored on the component (mirrors how
103    /// `Renderer::resize` keeps `Camera::aspect` current rather than baking it into camera data).
104    pub fn derive_camera(&self, transform: &Transform, aspect: f32) -> Camera {
105        let forward = transform.rotation * Vec3::NEG_Z;
106        let (yaw, pitch) = Camera::yaw_pitch_from_forward(forward);
107        Camera {
108            position: transform.position,
109            yaw,
110            pitch,
111            aspect,
112            fov: self.fov,
113            znear: self.znear,
114            zfar: self.zfar,
115        }
116    }
117}
118
119/// A spatial container of entities, distinct from [`crate::scene::Scene`] (which means
120/// "game state/screen" in this framework, not a place). Each entity is a [`Transform`] plus a
121/// [`Renderable`] and an optional [`Name`].
122///
123/// `Clone` is cheap and GPU-allocation-free: a cloned [`Renderable`]'s `wgpu::Buffer`/
124/// `wgpu::BindGroup` handles are refcount copies of the same GPU resource, not duplicates (wgpu
125/// itself derives `Clone` for both). The editor relies on this to snapshot the whole `World`
126/// before entering Play and restore it wholesale on Stop.
127#[derive(Clone)]
128pub struct World {
129    allocator: EntityAllocator,
130    pub transforms: ComponentStore<Transform>,
131    pub names: ComponentStore<Name>,
132    pub renderables: ComponentStore<Renderable>,
133    pub scripts: ComponentStore<ScriptList>,
134    pub cameras: ComponentStore<CameraComponent>,
135    /// Marks an entity as surviving a scene change (`WorldCommand::ChangeScene`) instead of being
136    /// torn down with the rest of the outgoing scene — Unity's `DontDestroyOnLoad`. A plain
137    /// `ComponentStore<()>` marker, mirroring how `CameraComponent::active` flags "the" active
138    /// camera, since most entities never need it. Deliberately not part of `EntityRecord`/
139    /// `SceneFile` — like `DontDestroyOnLoad` itself, it's a runtime-only script call
140    /// (`entity.set_persistent(true)`), not an authorable/serialized property.
141    pub persistent: ComponentStore<()>,
142    pub camera: Camera,
143}
144
145impl World {
146    pub fn new(camera: Camera) -> Self {
147        Self {
148            allocator: EntityAllocator::new(),
149            transforms: ComponentStore::new(),
150            names: ComponentStore::new(),
151            renderables: ComponentStore::new(),
152            scripts: ComponentStore::new(),
153            cameras: ComponentStore::new(),
154            persistent: ComponentStore::new(),
155            camera,
156        }
157    }
158
159    pub fn spawn_empty(&mut self, name: impl Into<String>, transform: Transform) -> Entity {
160        let entity = self.allocator.spawn();
161        self.names.insert(entity, Name(name.into()));
162        self.transforms.insert(entity, transform);
163        entity
164    }
165
166    pub fn set_renderable(&mut self, entity: Entity, renderable: Renderable) {
167        self.renderables.insert(entity, renderable);
168    }
169
170    pub fn clear_renderable(&mut self, entity: Entity) {
171        self.renderables.remove(entity);
172    }
173
174    pub fn set_camera(&mut self, entity: Entity, component: CameraComponent) {
175        self.cameras.insert(entity, component);
176    }
177
178    pub fn clear_camera(&mut self, entity: Entity) {
179        self.cameras.remove(entity);
180    }
181
182    /// Marks (or unmarks) `entity` as persistent — see [`World::persistent`]'s doc comment.
183    pub fn set_persistent(&mut self, entity: Entity, persistent: bool) {
184        if persistent {
185            self.persistent.insert(entity, ());
186        } else {
187            self.persistent.remove(entity);
188        }
189    }
190
191    pub fn is_persistent(&self, entity: Entity) -> bool {
192        self.persistent.get(entity).is_some()
193    }
194
195    pub fn despawn(&mut self, entity: Entity) {
196        self.allocator.despawn(entity);
197        self.names.remove(entity);
198        self.transforms.remove(entity);
199        self.renderables.remove(entity);
200        self.scripts.remove(entity);
201        self.cameras.remove(entity);
202        self.persistent.remove(entity);
203    }
204
205    pub fn is_alive(&self, entity: Entity) -> bool {
206        self.allocator.is_alive(entity)
207    }
208
209    /// Every live entity (every entity has a [`Transform`], so iterating that store enumerates
210    /// them all — unlike `renderables`, which only some entities have).
211    pub fn iter_entities(&self) -> impl Iterator<Item = Entity> + '_ {
212        self.transforms.iter().map(|(entity, _)| entity)
213    }
214
215    /// Despawns every entity except ones marked persistent — the "full teardown" half of a scene
216    /// change (see `WorldCommand::ChangeScene`'s handling in `ScriptRuntime::drain_commands`).
217    /// Collects into a `Vec` first since `despawn` mutates the same `transforms` store
218    /// `iter_entities` reads from.
219    pub fn despawn_non_persistent(&mut self) {
220        let doomed: Vec<Entity> = self.iter_entities().filter(|&e| !self.is_persistent(e)).collect();
221        for entity in doomed {
222            self.despawn(entity);
223        }
224    }
225
226    /// Syncs every entity's ECS [`Transform`] into its [`Renderable`]'s own GPU-facing matrix.
227    /// Call once per frame before drawing.
228    pub fn sync_transforms(&mut self) {
229        for (entity, renderable) in self.renderables.iter_mut() {
230            if let Some(transform) = self.transforms.get(entity) {
231                renderable.sync_transform(transform);
232            }
233        }
234    }
235
236    /// The derived [`Camera`] for whichever entity has [`CameraComponent::active`] set, if any —
237    /// what Play mode pushes to the renderer in place of the edit-mode fly camera. `aspect` is
238    /// threaded through rather than read off `self.camera` so the caller can supply the current
239    /// viewport's aspect the same frame it's computed.
240    ///
241    /// Returns `None` if no entity is marked active (or the active entity's `Transform` is
242    /// somehow missing) — callers should fall back to whatever camera they'd otherwise use.
243    pub fn active_camera(&self, aspect: f32) -> Option<Camera> {
244        let (entity, component) = self.cameras.iter().find(|(_, c)| c.active)?;
245        let transform = self.transforms.get(entity)?;
246        Some(component.derive_camera(transform, aspect))
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use crate::scripting::{ScriptAttachment, ScriptList};
254    use std::path::PathBuf;
255
256    fn test_camera() -> Camera {
257        Camera { position: Vec3::ZERO, yaw: 0.0, pitch: 0.0, aspect: 1.0, fov: 45.0, znear: 0.1, zfar: 100.0 }
258    }
259
260    #[test]
261    fn despawn_non_persistent_keeps_persistent_entities_alive() {
262        let mut world = World::new(test_camera());
263        let persistent = world.spawn_empty("Persistent", Transform::default());
264        let transient = world.spawn_empty("Transient", Transform::default());
265        world.set_persistent(persistent, true);
266
267        world.despawn_non_persistent();
268
269        assert!(world.is_alive(persistent));
270        assert!(!world.is_alive(transient));
271    }
272
273    #[test]
274    fn despawn_clears_the_persistent_flag() {
275        let mut world = World::new(test_camera());
276        let entity = world.spawn_empty("Persistent", Transform::default());
277        world.set_persistent(entity, true);
278
279        world.despawn(entity);
280
281        assert!(!world.is_persistent(entity));
282    }
283
284    #[test]
285    fn despawn_clears_attached_scripts() {
286        let mut world = World::new(test_camera());
287        let entity = world.spawn_empty("Scripted", Transform::default());
288        world.scripts.insert(
289            entity,
290            ScriptList(vec![ScriptAttachment { path: PathBuf::from("scripts/move.rhai"), enabled: true }]),
291        );
292
293        world.despawn(entity);
294
295        assert!(world.scripts.get(entity).is_none());
296    }
297
298    #[test]
299    fn despawn_clears_attached_camera() {
300        let mut world = World::new(test_camera());
301        let entity = world.spawn_empty("Cam", Transform::default());
302        world.set_camera(entity, CameraComponent::default());
303
304        world.despawn(entity);
305
306        assert!(world.cameras.get(entity).is_none());
307    }
308
309    #[test]
310    fn derive_camera_looks_down_local_negative_z() {
311        let component = CameraComponent::default();
312        let camera = component.derive_camera(&Transform::default(), 1.0);
313
314        assert!(camera.forward().abs_diff_eq(Vec3::NEG_Z, 1e-5));
315    }
316
317    /// Locks the rotation convention `CameraComponent::derive_camera` uses so a future refactor
318    /// can't silently flip which way a camera entity faces.
319    #[test]
320    fn derive_camera_respects_rotation() {
321        let component = CameraComponent::default();
322        let transform = Transform { rotation: Quat::from_rotation_y(90.0_f32.to_radians()), ..Transform::default() };
323
324        let camera = component.derive_camera(&transform, 1.0);
325
326        assert!(camera.forward().abs_diff_eq(Vec3::NEG_X, 1e-4));
327    }
328
329    #[test]
330    fn active_camera_returns_none_when_nothing_is_marked_active() {
331        let mut world = World::new(test_camera());
332        let entity = world.spawn_empty("Cam", Transform::default());
333        world.set_camera(entity, CameraComponent { active: false, ..CameraComponent::default() });
334
335        assert!(world.active_camera(1.0).is_none());
336    }
337
338    #[test]
339    fn active_camera_derives_from_the_marked_entity() {
340        let mut world = World::new(test_camera());
341        let position = Vec3::new(1.0, 2.0, 3.0);
342        let entity = world.spawn_empty("Cam", Transform { position, ..Transform::default() });
343        world.set_camera(entity, CameraComponent { fov: 60.0, znear: 0.5, zfar: 50.0, active: true });
344
345        let camera = world.active_camera(1.7).expect("an active camera entity exists");
346
347        assert_eq!(camera.position, position);
348        assert_eq!(camera.fov, 60.0);
349        assert_eq!(camera.znear, 0.5);
350        assert_eq!(camera.zfar, 50.0);
351        assert_eq!(camera.aspect, 1.7);
352    }
353
354    /// Guards the editor's Play/Stop snapshot-and-restore, which relies on `World::clone()`
355    /// producing an independent copy (see [`World`]'s doc comment) — a future change that made
356    /// some field shallow/shared (e.g. wrapping a component store in an `Rc` for some other
357    /// reason) would silently break Stop's revert-to-pre-Play guarantee without this test
358    /// catching it. Doesn't exercise a [`Renderable`] directly (that needs a live `Renderer`/GPU
359    /// device to construct, impractical in a unit test), but `Renderable`'s own GPU-backed
360    /// fields are cheap handle clones by construction — `wgpu::Buffer`/`BindGroup`/`Texture` all
361    /// derive `Clone` themselves — so cloning one just copies a handle, never GPU state; what
362    /// this test guards is the plain-data component stores instead.
363    #[test]
364    fn clone_is_independent_of_the_original() {
365        let mut world = World::new(test_camera());
366        let entity = world.spawn_empty("Original", Transform { position: Vec3::new(1.0, 2.0, 3.0), ..Transform::default() });
367        world.scripts.insert(
368            entity,
369            ScriptList(vec![ScriptAttachment { path: PathBuf::from("scripts/move.rhai"), enabled: true }]),
370        );
371
372        let mut clone = world.clone();
373
374        // Mutate the clone in every way Play can mutate a World: move, rename, despawn, and
375        // spawn a brand new entity.
376        clone.transforms.get_mut(entity).unwrap().position = Vec3::ZERO;
377        clone.names.get_mut(entity).unwrap().0 = "Renamed".to_string();
378        clone.scripts.get_mut(entity).unwrap().0[0].enabled = false;
379        let new_entity = clone.spawn_empty("SpawnedDuringPlay", Transform::default());
380
381        assert_eq!(world.transforms.get(entity).unwrap().position, Vec3::new(1.0, 2.0, 3.0));
382        assert_eq!(world.names.get(entity).unwrap().0, "Original");
383        assert!(world.scripts.get(entity).unwrap().0[0].enabled);
384        assert!(!world.is_alive(new_entity), "an entity spawned only on the clone shouldn't exist on the original");
385    }
386}