Skip to main content

khora_sdk/
game_world.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! The `GameWorld` facade — a safe, typed entry point for managing
16//! the ECS world and asset registry without exposing internal engine types.
17//!
18//! This follows the pattern of every major game engine: users interact with
19//! entities and components through a controlled API, never touching the raw
20//! `World` or `Assets` directly.
21
22use khora_core::asset::{AssetHandle, AssetUUID};
23use khora_core::ecs::entity::EntityId;
24use khora_core::renderer::api::scene::Mesh;
25use khora_data::ecs::{
26    Camera, Children, Component, ComponentBundle, GlobalTransform, HandleComponent, Parent, Query,
27    QueryMut, Transform, World, WorldQuery,
28};
29
30/// A high-level facade over the internal ECS `World` and `Assets` registry.
31///
32/// `GameWorld` is the primary interface for game developers to create and
33/// manage entities, components, and assets. It hides the raw types from
34/// `khora-data` behind a clean, stable API surface.
35///
36/// # Examples
37///
38/// ```rust
39/// use khora_sdk::GameWorld;
40/// use khora_sdk::prelude::ecs::{Camera, GlobalTransform, Name, Transform};
41/// use khora_sdk::prelude::math::Vec3;
42///
43/// let mut world = GameWorld::new();
44///
45/// // Spawn a camera.
46/// world.spawn_camera(Camera::new_perspective(
47///     std::f32::consts::FRAC_PI_4, 16.0 / 9.0, 0.1, 1000.0,
48/// ));
49///
50/// // Spawn an entity from a component bundle.
51/// let entity = world.spawn((
52///     Transform::from_translation(Vec3::new(0.0, 1.0, 0.0)),
53///     GlobalTransform::default(),
54///     Name::new("hero"),
55/// ));
56/// assert!(world.get_transform(entity).is_some());
57/// ```
58pub struct GameWorld {
59    /// The internal ECS world.
60    world: World,
61}
62
63impl Default for GameWorld {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl GameWorld {
70    /// Creates a new `GameWorld` with an empty world and asset registry.
71    pub fn new() -> Self {
72        Self {
73            world: World::new(),
74        }
75    }
76
77    /// Creates a `GameWorld` from an existing ECS `World`.
78    /// Used for restoring a snapshot in play mode.
79    pub fn from_world(world: World) -> Self {
80        Self { world }
81    }
82
83    // ─────────────────────────────────────────────────────────────────────
84    // Entity Lifecycle
85    // ─────────────────────────────────────────────────────────────────────
86
87    /// Spawns a new entity with the given component bundle.
88    ///
89    /// Returns the [`EntityId`] of the newly created entity.
90    ///
91    /// # Examples
92    ///
93    /// ```rust
94    /// use khora_sdk::GameWorld;
95    /// use khora_sdk::prelude::ecs::{GlobalTransform, Transform};
96    ///
97    /// let mut world = GameWorld::new();
98    /// let entity = world.spawn((Transform::identity(), GlobalTransform::default()));
99    /// assert!(world.get_transform(entity).is_some());
100    /// ```
101    pub fn spawn<B: ComponentBundle>(&mut self, bundle: B) -> EntityId {
102        self.world.spawn(bundle)
103    }
104
105    /// Removes an entity and all its components from the world.
106    ///
107    /// Returns `true` if the entity existed and was removed.
108    pub fn despawn(&mut self, entity: EntityId) -> bool {
109        self.world.despawn(entity)
110    }
111
112    // ─────────────────────────────────────────────────────────────────────
113    // Camera Helpers
114    // ─────────────────────────────────────────────────────────────────────
115
116    /// Spawns a camera entity with a [`Camera`] component and an identity
117    /// [`GlobalTransform`].
118    ///
119    /// This is the recommended way to add a camera to the scene. The
120    /// `RenderAgent` will automatically discover cameras during its
121    /// extraction phase.
122    ///
123    /// Returns the [`EntityId`] of the camera entity.
124    pub fn spawn_camera(&mut self, camera: Camera) -> EntityId {
125        self.world.spawn((camera, GlobalTransform::identity()))
126    }
127
128    // ─────────────────────────────────────────────────────────────────────
129    // Asset Management
130    // ─────────────────────────────────────────────────────────────────────
131
132    /// Adds a mesh to the asset registry and returns a handle component.
133    ///
134    /// The returned `HandleComponent<Mesh>` can be attached to entities
135    /// to give them a visible mesh. The `RenderAgent` will automatically
136    /// upload the mesh to GPU when the entity is rendered.
137    ///
138    /// # Arguments
139    /// * `mesh` - The CPU-side mesh data.
140    ///
141    /// # Returns
142    /// A `HandleComponent<Mesh>` that references the stored mesh.
143    ///
144    /// # Examples
145    ///
146    /// ```rust,ignore
147    /// // `mesh` is CPU-side geometry you have built or loaded.
148    /// let handle = world.add_mesh(mesh);
149    /// let entity = world.spawn((Transform::identity(), handle));
150    /// ```
151    ///
152    /// For the common case of built-in shapes, prefer the procedural helpers
153    /// [`spawn_plane`](crate::spawn_plane), [`spawn_cube_at`](crate::spawn_cube_at),
154    /// and [`spawn_sphere`](crate::spawn_sphere), which attach the mesh for you.
155    pub fn add_mesh(&mut self, mesh: Mesh) -> HandleComponent<Mesh> {
156        let uuid = AssetUUID::new();
157        let handle = AssetHandle::new(mesh);
158        HandleComponent { handle, uuid }
159    }
160
161    // ─────────────────────────────────────────────────────────────────────
162    // Component Access
163    // ─────────────────────────────────────────────────────────────────────
164
165    /// Adds a component to an existing entity.
166    ///
167    /// If the entity already has a component of this type, the old value
168    /// is replaced.
169    pub fn add_component<C: Component>(&mut self, entity: EntityId, component: C) {
170        if let Err(e) = self.world.add_component(entity, component) {
171            log::warn!(
172                "GameWorld::add_component<{}>({:?}) failed: {:?}",
173                std::any::type_name::<C>(),
174                entity,
175                e
176            );
177        }
178    }
179
180    /// Removes a single component `C` from `entity`. Other components on
181    /// the same entity (in any domain) are preserved — this is a surgical
182    /// removal, not a domain wipe. Backed by [`World::remove_component`].
183    ///
184    /// No-op (silently logged) if the entity is dead or doesn't carry `C`.
185    pub fn remove_component<C: Component>(&mut self, entity: EntityId) {
186        if let Err(e) = self.world.remove_component::<C>(entity) {
187            log::trace!(
188                "GameWorld::remove_component<{}>({:?}) skipped: {:?}",
189                std::any::type_name::<C>(),
190                entity,
191                e
192            );
193        }
194    }
195
196    // ─────────────────────────────────────────────────────────────────────
197    // Queries
198    // ─────────────────────────────────────────────────────────────────────
199
200    /// Creates a read-only query over the world.
201    ///
202    /// # Examples
203    ///
204    /// ```rust
205    /// use khora_sdk::GameWorld;
206    /// use khora_sdk::prelude::ecs::{GlobalTransform, Name, Transform};
207    ///
208    /// let mut world = GameWorld::new();
209    /// world.spawn((Transform::identity(), GlobalTransform::default(), Name::new("a")));
210    ///
211    /// // Iterate every entity that has both a Transform and a Name.
212    /// for (transform, name) in world.query::<(&Transform, &Name)>() {
213    ///     let _ = (transform.translation, &name.0);
214    /// }
215    /// ```
216    pub fn query<'a, Q: WorldQuery>(&'a self) -> Query<'a, Q> {
217        self.world.query::<Q>()
218    }
219
220    /// Creates a mutable query over the world.
221    ///
222    /// # Examples
223    ///
224    /// ```rust
225    /// use khora_sdk::GameWorld;
226    /// use khora_sdk::prelude::ecs::{GlobalTransform, Transform};
227    /// use khora_sdk::prelude::math::Vec3;
228    ///
229    /// let mut world = GameWorld::new();
230    /// world.spawn((Transform::identity(), GlobalTransform::default()));
231    ///
232    /// // Nudge every transform up by one unit.
233    /// for (transform,) in world.query_mut::<(&mut Transform,)>() {
234    ///     transform.translation = transform.translation + Vec3::Y;
235    /// }
236    /// ```
237    pub fn query_mut<'a, Q: WorldQuery>(&'a mut self) -> QueryMut<'a, Q> {
238        self.world.query_mut::<Q>()
239    }
240
241    // ─────────────────────────────────────────────────────────────────────
242    // Convenience Methods
243    // ─────────────────────────────────────────────────────────────────────
244
245    /// Spawns an entity with just a transform component.
246    ///
247    /// Returns the [`EntityId`] of the newly created entity.
248    pub fn spawn_entity(&mut self, transform: &Transform) -> EntityId {
249        let global = GlobalTransform::at_position(transform.translation);
250        self.world.spawn((*transform, global))
251    }
252
253    /// Returns an iterator over all entity IDs in the world.
254    pub fn iter_entities(&self) -> impl Iterator<Item = EntityId> + '_ {
255        self.world.iter_entities()
256    }
257
258    /// Gets a mutable reference to a transform component.
259    ///
260    /// Returns `None` if the entity doesn't exist or has no transform.
261    pub fn get_transform_mut(&mut self, entity: EntityId) -> Option<&mut Transform> {
262        self.world.get_mut::<Transform>(entity)
263    }
264
265    /// Gets a reference to a transform component.
266    ///
267    /// Returns `None` if the entity doesn't exist or has no transform.
268    pub fn get_transform(&self, entity: EntityId) -> Option<&Transform> {
269        self.world.get::<Transform>(entity)
270    }
271
272    /// Gets a mutable reference to any component.
273    ///
274    /// Returns `None` if the entity doesn't exist or has no such component.
275    pub fn get_component_mut<C: Component>(&mut self, entity: EntityId) -> Option<&mut C> {
276        self.world.get_mut::<C>(entity)
277    }
278
279    /// Gets a reference to any component.
280    ///
281    /// Returns `None` if the entity doesn't exist or has no such component.
282    pub fn get_component<C: Component>(&self, entity: EntityId) -> Option<&C> {
283        self.world.get::<C>(entity)
284    }
285
286    // ─────────────────────────────────────────────────────────────────────
287    // Transform Synchronization
288    // ─────────────────────────────────────────────────────────────────────
289
290    /// Synchronizes the GlobalTransform component from the Transform component.
291    ///
292    /// This should be called after modifying a Transform to ensure the changes
293    /// are visible to the rendering system. This is a convenience method that
294    /// copies the local transform to the global transform.
295    ///
296    /// # Example
297    /// ```rust
298    /// use khora_sdk::GameWorld;
299    /// use khora_sdk::prelude::ecs::{GlobalTransform, Transform};
300    /// use khora_sdk::prelude::math::Vec3;
301    ///
302    /// let mut world = GameWorld::new();
303    /// let entity = world.spawn((Transform::identity(), GlobalTransform::default()));
304    ///
305    /// // Move the entity, then sync so the renderer sees the new pose.
306    /// if let Some(transform) = world.get_transform_mut(entity) {
307    ///     transform.translation = transform.translation + Vec3::Y;
308    /// }
309    /// world.sync_global_transform(entity);
310    /// ```
311    pub fn sync_global_transform(&mut self, entity: EntityId) {
312        if let Some(transform) = self.world.get::<Transform>(entity) {
313            let matrix = transform.to_mat4();
314            if let Some(global) = self.world.get_mut::<GlobalTransform>(entity) {
315                *global = GlobalTransform::new(matrix);
316            }
317        }
318    }
319
320    /// Updates an entity's transform and immediately syncs it to GlobalTransform.
321    ///
322    /// This is a convenience method that combines getting the transform,
323    /// applying a modification function, and syncing to GlobalTransform.
324    ///
325    /// # Example
326    /// ```rust
327    /// use khora_sdk::GameWorld;
328    /// use khora_sdk::prelude::ecs::{GlobalTransform, Transform};
329    /// use khora_sdk::prelude::math::Vec3;
330    ///
331    /// let mut world = GameWorld::new();
332    /// let entity = world.spawn((Transform::identity(), GlobalTransform::default()));
333    ///
334    /// world.update_transform(entity, |t| {
335    ///     t.translation = t.translation + Vec3::Y;
336    /// });
337    /// ```
338    pub fn update_transform<F>(&mut self, entity: EntityId, f: F)
339    where
340        F: FnOnce(&mut Transform),
341    {
342        if let Some(transform) = self.world.get_mut::<Transform>(entity) {
343            f(transform);
344        }
345        self.sync_global_transform(entity);
346    }
347
348    /// Reparents `child` under `new_parent`, or detaches it (root) when
349    /// `new_parent` is `None`.
350    ///
351    /// Maintains both the `Parent` component on `child` and the `Children`
352    /// list on the involved parents. Refuses cycles silently (a no-op).
353    pub fn set_parent(&mut self, child: EntityId, new_parent: Option<EntityId>) {
354        // Refuse cycles: new_parent must not be a descendant of child.
355        if let Some(np) = new_parent {
356            if np == child || self.is_descendant_of(np, child) {
357                log::warn!(
358                    "set_parent: refused cycle (child={:?}, new_parent={:?})",
359                    child,
360                    np
361                );
362                return;
363            }
364        }
365
366        // 1. Remove `child` from its previous parent's `Children`, if any.
367        let old_parent = self.world.get::<Parent>(child).map(|p| p.0);
368        if let Some(old) = old_parent {
369            if let Some(children) = self.world.get_mut::<Children>(old) {
370                children.0.retain(|c| *c != child);
371            }
372        }
373
374        // 2. Update `Parent` on `child` (set or remove).
375        match new_parent {
376            Some(np) => {
377                if let Some(existing) = self.world.get_mut::<Parent>(child) {
378                    existing.0 = np;
379                } else {
380                    self.add_component(child, Parent(np));
381                }
382            }
383            None => {
384                // Surgical: drop only the `Parent` component, keep the
385                // entity's other Spatial components (Transform,
386                // GlobalTransform, Name, …) intact.
387                self.remove_component::<Parent>(child);
388            }
389        }
390
391        // 3. Add `child` to the new parent's `Children` (creating it if needed).
392        if let Some(np) = new_parent {
393            if let Some(children) = self.world.get_mut::<Children>(np) {
394                if !children.0.contains(&child) {
395                    children.0.push(child);
396                }
397            } else {
398                self.add_component(np, Children(vec![child]));
399            }
400        }
401    }
402
403    /// Returns whether `candidate` is a descendant of `ancestor` in the
404    /// scene hierarchy. Used by `set_parent` to refuse cycle-creating reparents.
405    fn is_descendant_of(&self, candidate: EntityId, ancestor: EntityId) -> bool {
406        let mut current = candidate;
407        // Bound the traversal to avoid infinite loops on a malformed hierarchy.
408        for _ in 0..1024 {
409            let Some(parent) = self.world.get::<Parent>(current) else {
410                return false;
411            };
412            if parent.0 == ancestor {
413                return true;
414            }
415            current = parent.0;
416        }
417        log::warn!("is_descendant_of: hierarchy traversal exceeded depth bound");
418        false
419    }
420
421    /// Builds an authored, inline material reference to attach to entities.
422    ///
423    /// The returned [`MaterialRef::Inline`](khora_data::ecs::MaterialRef) embeds
424    /// the material value directly in the scene. The resolver turns it into a
425    /// runtime material handle, which the GPU projection uploads. To reference a
426    /// `.kmat` asset instead, attach `MaterialRef::Asset(uuid)`.
427    ///
428    /// # Arguments
429    /// * `material` - The CPU-side material data (e.g., `StandardMaterial`).
430    ///
431    /// # Returns
432    /// A `MaterialRef::Inline` referencing the given material.
433    pub fn add_material<M: khora_core::asset::Material>(
434        &mut self,
435        material: M,
436    ) -> khora_data::ecs::MaterialRef {
437        khora_data::ecs::MaterialRef::inline(Box::new(material))
438    }
439
440    // ─────────────────────────────────────────────────────────────────────
441    // Internal — used by the SDK, not exposed to users
442    // ─────────────────────────────────────────────────────────────────────
443
444    /// Returns a shared reference to the underlying ECS [`World`].
445    ///
446    /// Useful for serialization and other low-level operations that need
447    /// direct access to the world outside the `GameWorld` API surface.
448    pub fn inner_world(&self) -> &World {
449        &self.world
450    }
451
452    /// Returns an exclusive reference to the underlying ECS [`World`].
453    ///
454    /// Useful for serialization and other low-level operations that need
455    /// direct access to the world outside the `GameWorld` API surface.
456    pub fn inner_world_mut(&mut self) -> &mut World {
457        &mut self.world
458    }
459}