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_core::EngineContext;
26use khora_data::ecs::{
27 Camera, Component, ComponentBundle, GlobalTransform, HandleComponent, Query, QueryMut,
28 Transform, World, WorldQuery,
29};
30use std::any::Any;
31
32/// A high-level facade over the internal ECS `World` and `Assets` registry.
33///
34/// `GameWorld` is the primary interface for game developers to create and
35/// manage entities, components, and assets. It hides the raw types from
36/// `khora-data` behind a clean, stable API surface.
37///
38/// # Examples
39///
40/// ```rust,ignore
41/// fn setup(&mut self, world: &mut GameWorld) {
42/// // Spawn a camera
43/// world.spawn_camera(Camera::new_perspective(
44/// std::f32::consts::FRAC_PI_4, 16.0 / 9.0, 0.1, 1000.0,
45/// ));
46///
47/// // Spawn a custom entity
48/// world.spawn((Transform::identity(), MyComponent { speed: 10.0 }));
49/// }
50/// ```
51pub struct GameWorld {
52 /// The internal ECS world.
53 world: World,
54}
55
56impl GameWorld {
57 /// Creates a new `GameWorld` with an empty world and asset registry.
58 pub(crate) fn new() -> Self {
59 Self {
60 world: World::new(),
61 }
62 }
63
64 // ─────────────────────────────────────────────────────────────────────
65 // Entity Lifecycle
66 // ─────────────────────────────────────────────────────────────────────
67
68 /// Spawns a new entity with the given component bundle.
69 ///
70 /// Returns the [`EntityId`] of the newly created entity.
71 ///
72 /// # Examples
73 ///
74 /// ```rust,ignore
75 /// let entity = world.spawn((Transform::identity(), Velocity::default()));
76 /// ```
77 pub fn spawn<B: ComponentBundle>(&mut self, bundle: B) -> EntityId {
78 self.world.spawn(bundle)
79 }
80
81 /// Removes an entity and all its components from the world.
82 ///
83 /// Returns `true` if the entity existed and was removed.
84 pub fn despawn(&mut self, entity: EntityId) -> bool {
85 self.world.despawn(entity)
86 }
87
88 // ─────────────────────────────────────────────────────────────────────
89 // Camera Helpers
90 // ─────────────────────────────────────────────────────────────────────
91
92 /// Spawns a camera entity with a [`Camera`] component and an identity
93 /// [`GlobalTransform`].
94 ///
95 /// This is the recommended way to add a camera to the scene. The
96 /// `RenderAgent` will automatically discover cameras during its
97 /// extraction phase.
98 ///
99 /// Returns the [`EntityId`] of the camera entity.
100 pub fn spawn_camera(&mut self, camera: Camera) -> EntityId {
101 self.world.spawn((camera, GlobalTransform::identity()))
102 }
103
104 // ─────────────────────────────────────────────────────────────────────
105 // Asset Management
106 // ─────────────────────────────────────────────────────────────────────
107
108 /// Adds a mesh to the asset registry and returns a handle component.
109 ///
110 /// The returned `HandleComponent<Mesh>` can be attached to entities
111 /// to give them a visible mesh. The `RenderAgent` will automatically
112 /// upload the mesh to GPU when the entity is rendered.
113 ///
114 /// # Arguments
115 /// * `mesh` - The CPU-side mesh data.
116 ///
117 /// # Returns
118 /// A `HandleComponent<Mesh>` that references the stored mesh.
119 ///
120 /// # Examples
121 ///
122 /// ```rust,ignore
123 /// let plane_mesh = create_plane(10.0, 0.0);
124 /// let handle = world.add_mesh(plane_mesh);
125 /// let entity = world.spawn((Transform::identity(), handle));
126 /// ```
127 pub fn add_mesh(&mut self, mesh: Mesh) -> HandleComponent<Mesh> {
128 let uuid = AssetUUID::new();
129 let handle = AssetHandle::new(mesh);
130 HandleComponent { handle, uuid }
131 }
132
133 // ─────────────────────────────────────────────────────────────────────
134 // Component Access
135 // ─────────────────────────────────────────────────────────────────────
136
137 /// Adds a component to an existing entity.
138 ///
139 /// If the entity already has a component of this type, the old value
140 /// is replaced.
141 pub fn add_component<C: Component>(&mut self, entity: EntityId, component: C) {
142 let _ = self.world.add_component(entity, component);
143 }
144
145 /// Removes a component (and its semantic domain) from an entity.
146 ///
147 /// This is an O(1) operation — the data is orphaned and cleaned up
148 /// later by the garbage collector agent.
149 pub fn remove_component<C: Component>(&mut self, entity: EntityId) {
150 self.world.remove_component_domain::<C>(entity);
151 }
152
153 // ─────────────────────────────────────────────────────────────────────
154 // Queries
155 // ─────────────────────────────────────────────────────────────────────
156
157 /// Creates a read-only query over the world.
158 ///
159 /// # Examples
160 ///
161 /// ```rust,ignore
162 /// for (pos, vel) in world.query::<(&Transform, &Velocity)>() {
163 /// // iterate matching entities
164 /// }
165 /// ```
166 pub fn query<'a, Q: WorldQuery>(&'a self) -> Query<'a, Q> {
167 self.world.query::<Q>()
168 }
169
170 /// Creates a mutable query over the world.
171 ///
172 /// # Examples
173 ///
174 /// ```rust,ignore
175 /// for (pos,) in world.query_mut::<(&mut Transform,)>() {
176 /// pos.translate(Vec3::Y * delta);
177 /// }
178 /// ```
179 pub fn query_mut<'a, Q: WorldQuery>(&'a mut self) -> QueryMut<'a, Q> {
180 self.world.query_mut::<Q>()
181 }
182
183 // ─────────────────────────────────────────────────────────────────────
184 // Convenience Methods
185 // ─────────────────────────────────────────────────────────────────────
186
187 /// Spawns an entity with just a transform component.
188 ///
189 /// Returns the [`EntityId`] of the newly created entity.
190 pub fn spawn_entity(&mut self, transform: &Transform) -> EntityId {
191 let global = GlobalTransform::at_position(transform.translation);
192 self.world.spawn((*transform, global))
193 }
194
195 /// Returns an iterator over all entity IDs in the world.
196 pub fn iter_entities(&self) -> impl Iterator<Item = EntityId> + '_ {
197 self.world.iter_entities()
198 }
199
200 /// Gets a mutable reference to a transform component.
201 ///
202 /// Returns `None` if the entity doesn't exist or has no transform.
203 pub fn get_transform_mut(&mut self, entity: EntityId) -> Option<&mut Transform> {
204 self.world.get_mut::<Transform>(entity)
205 }
206
207 /// Gets a reference to a transform component.
208 ///
209 /// Returns `None` if the entity doesn't exist or has no transform.
210 pub fn get_transform(&self, entity: EntityId) -> Option<&Transform> {
211 self.world.get::<Transform>(entity)
212 }
213
214 /// Gets a mutable reference to any component.
215 ///
216 /// Returns `None` if the entity doesn't exist or has no such component.
217 pub fn get_component_mut<C: Component>(&mut self, entity: EntityId) -> Option<&mut C> {
218 self.world.get_mut::<C>(entity)
219 }
220
221 /// Gets a reference to any component.
222 ///
223 /// Returns `None` if the entity doesn't exist or has no such component.
224 pub fn get_component<C: Component>(&self, entity: EntityId) -> Option<&C> {
225 self.world.get::<C>(entity)
226 }
227
228 // ─────────────────────────────────────────────────────────────────────
229 // Transform Synchronization
230 // ─────────────────────────────────────────────────────────────────────
231
232 /// Synchronizes the GlobalTransform component from the Transform component.
233 ///
234 /// This should be called after modifying a Transform to ensure the changes
235 /// are visible to the rendering system. This is a convenience method that
236 /// copies the local transform to the global transform.
237 ///
238 /// # Example
239 /// ```rust,ignore
240 /// // Move entity
241 /// if let Some(transform) = world.get_transform_mut(entity) {
242 /// transform.translation += Vec3::Y * delta;
243 /// }
244 /// // Sync to rendering
245 /// world.sync_global_transform(entity);
246 /// ```
247 pub fn sync_global_transform(&mut self, entity: EntityId) {
248 if let Some(transform) = self.world.get::<Transform>(entity) {
249 let matrix = transform.to_mat4();
250 if let Some(global) = self.world.get_mut::<GlobalTransform>(entity) {
251 *global = GlobalTransform::new(matrix);
252 }
253 }
254 }
255
256 /// Updates an entity's transform and immediately syncs it to GlobalTransform.
257 ///
258 /// This is a convenience method that combines getting the transform,
259 /// applying a modification function, and syncing to GlobalTransform.
260 ///
261 /// # Example
262 /// ```rust,ignore
263 /// world.update_transform(entity, |t| {
264 /// t.translation += Vec3::Y * delta;
265 /// });
266 /// ```
267 pub fn update_transform<F>(&mut self, entity: EntityId, f: F)
268 where
269 F: FnOnce(&mut Transform),
270 {
271 if let Some(transform) = self.world.get_mut::<Transform>(entity) {
272 f(transform);
273 }
274 self.sync_global_transform(entity);
275 }
276
277 /// Adds a material to the asset registry and returns a handle component.
278 ///
279 /// The returned `MaterialComponent` can be attached to entities
280 /// to give them a visible material.
281 ///
282 /// # Arguments
283 /// * `material` - The CPU-side material data (e.g., StandardMaterial).
284 ///
285 /// # Returns
286 /// A `MaterialComponent` that references the stored material.
287 pub fn add_material<M: khora_core::asset::Material>(
288 &mut self,
289 material: M,
290 ) -> khora_data::ecs::MaterialComponent {
291 let uuid = AssetUUID::new();
292 let handle = AssetHandle::new(Box::new(material) as Box<dyn khora_core::asset::Material>);
293 khora_data::ecs::MaterialComponent { handle, uuid }
294 }
295
296 // ─────────────────────────────────────────────────────────────────────
297 // Internal — used by the SDK, not exposed to users
298 // ─────────────────────────────────────────────────────────────────────
299
300 /// Builds an [`EngineContext`] for the DCC agent update cycle.
301 ///
302 /// This type-erases `World` and `Assets` into `dyn Any` pointers,
303 /// which agents downcast internally. Users never call this.
304 pub(crate) fn as_engine_context(
305 &mut self,
306 services: khora_core::ServiceRegistry,
307 ) -> EngineContext<'_> {
308 EngineContext {
309 world: Some(&mut self.world as &mut dyn Any),
310 services,
311 }
312 }
313}