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}