Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
//! Prefabs — named templates that spawn an entity already carrying a set of
|
||||
//! components.
|
||||
//!
|
||||
//! The engine deliberately has **no parallel "object type" system**: an entity
|
||||
//! *is* its set of components. A [`Prefab`] is therefore nothing more than a
|
||||
//! named bundle of **(component name, value)** specs, applied on spawn through
|
||||
//! the [`TypeRegistry`]. "Spawn a Cube" means "spawn an entity, then set its
|
||||
//! `MeshRenderer` to a cube" — the same name-keyed path the editor and scripts
|
||||
//! already use, so prefabs are pure data (serializable, dual-editable) rather
|
||||
//! than code.
|
||||
//!
|
||||
//! This is what makes the editor's add-menu **data-driven**: the menu lists the
|
||||
//! prefabs in a [`PrefabRegistry`] instead of hard-coding one button per type.
|
||||
//!
|
||||
//! ```
|
||||
//! use oxide_engine::prelude::*;
|
||||
//! use oxide_engine::prefab::{ComponentSpec, Prefab, PrefabRegistry};
|
||||
//! use oxide_engine::reflect::TypeRegistry;
|
||||
//!
|
||||
//! // A registry that knows how to round-trip MeshRenderer by name.
|
||||
//! let mut types = TypeRegistry::new();
|
||||
//! types.register_reflected::<MeshRenderer>("MeshRenderer");
|
||||
//!
|
||||
//! // A "Cube" prefab: an entity carrying a default MeshRenderer (shape = Cube).
|
||||
//! let mut prefabs = PrefabRegistry::new();
|
||||
//! prefabs.register(
|
||||
//! Prefab::new("Cube")
|
||||
//! .with(ComponentSpec::of("MeshRenderer", &MeshRenderer::default()).unwrap()),
|
||||
//! );
|
||||
//!
|
||||
//! let mut scene = Scene::new();
|
||||
//! let cube = prefabs.spawn("Cube", &mut scene, &types).unwrap();
|
||||
//! assert!(types.has(scene.world(), cube, "MeshRenderer").unwrap());
|
||||
//! ```
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use hecs::Entity;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::math::Transform;
|
||||
use crate::reflect::TypeRegistry;
|
||||
use crate::scene::Scene;
|
||||
|
||||
/// One component a prefab attaches: a registered type **name** plus its value
|
||||
/// serialized as **RON** — the same representation [`TypeRegistry::set_ron`]
|
||||
/// consumes.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ComponentSpec {
|
||||
/// The component's registered name in the [`TypeRegistry`].
|
||||
pub type_name: String,
|
||||
/// The component value as RON.
|
||||
pub ron: String,
|
||||
}
|
||||
|
||||
impl ComponentSpec {
|
||||
/// A spec from a name and an already-serialized RON string.
|
||||
pub fn new(type_name: impl Into<String>, ron: impl Into<String>) -> Self {
|
||||
Self {
|
||||
type_name: type_name.into(),
|
||||
ron: ron.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A spec built by serializing a concrete component `value`. Returns `None`
|
||||
/// if it cannot be serialized to RON.
|
||||
pub fn of<T: Serialize>(type_name: impl Into<String>, value: &T) -> Option<Self> {
|
||||
ron::to_string(value)
|
||||
.ok()
|
||||
.map(|ron| Self::new(type_name, ron))
|
||||
}
|
||||
}
|
||||
|
||||
/// A named spawn template: a node name plus the components to attach beyond the
|
||||
/// node-baked ones.
|
||||
///
|
||||
/// Every spawned entity already carries `Node`, `Transform`, and `Layer`
|
||||
/// (auto-attached by [`Scene::spawn`]); a prefab's [`components`](Self::components)
|
||||
/// are layered on top. A spec named `"Transform"` overrides the identity
|
||||
/// transform `spawn` starts with, so a prefab can place itself.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Prefab {
|
||||
/// The name given to the spawned node (also the registry key).
|
||||
pub name: String,
|
||||
/// Components attached on spawn, applied in order.
|
||||
pub components: Vec<ComponentSpec>,
|
||||
}
|
||||
|
||||
impl Prefab {
|
||||
/// An empty prefab (spawns a bare node with just the node-baked components).
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
components: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a component spec (builder style).
|
||||
pub fn with(mut self, spec: ComponentSpec) -> Self {
|
||||
self.components.push(spec);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A registry of prefabs keyed by name — the data-driven source for the
|
||||
/// editor's "add an entity that already carries these components" menu.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PrefabRegistry {
|
||||
prefabs: BTreeMap<String, Prefab>,
|
||||
}
|
||||
|
||||
impl PrefabRegistry {
|
||||
/// An empty registry.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Registers `prefab` under its [`name`](Prefab::name). Re-registering the
|
||||
/// same name replaces the entry.
|
||||
pub fn register(&mut self, prefab: Prefab) {
|
||||
self.prefabs.insert(prefab.name.clone(), prefab);
|
||||
}
|
||||
|
||||
/// The prefab registered under `name`, if any.
|
||||
pub fn get(&self, name: &str) -> Option<&Prefab> {
|
||||
self.prefabs.get(name)
|
||||
}
|
||||
|
||||
/// Whether a prefab is registered under `name`.
|
||||
pub fn contains(&self, name: &str) -> bool {
|
||||
self.prefabs.contains_key(name)
|
||||
}
|
||||
|
||||
/// The registered prefab names, sorted — what an add-menu lists.
|
||||
pub fn names(&self) -> impl Iterator<Item = &str> + '_ {
|
||||
self.prefabs.keys().map(String::as_str)
|
||||
}
|
||||
|
||||
/// The number of registered prefabs.
|
||||
pub fn len(&self) -> usize {
|
||||
self.prefabs.len()
|
||||
}
|
||||
|
||||
/// Whether no prefabs are registered.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.prefabs.is_empty()
|
||||
}
|
||||
|
||||
/// Spawns the named prefab as a **root** entity, applying its component
|
||||
/// specs through `registry`. Returns the new entity, or `None` if `name`
|
||||
/// isn't registered.
|
||||
///
|
||||
/// Application is best-effort: a spec whose type isn't registered or whose
|
||||
/// RON doesn't parse is skipped (the entity is still created with whatever
|
||||
/// applied). Use [`unknown_specs`](Self::unknown_specs) to validate a prefab
|
||||
/// against a registry up front.
|
||||
pub fn spawn(&self, name: &str, scene: &mut Scene, registry: &TypeRegistry) -> Option<Entity> {
|
||||
let prefab = self.prefabs.get(name)?;
|
||||
let entity = scene.spawn(prefab.name.clone(), Transform::IDENTITY);
|
||||
apply(prefab, entity, scene, registry);
|
||||
Some(entity)
|
||||
}
|
||||
|
||||
/// Like [`spawn`](Self::spawn) but parents the new entity under `parent`.
|
||||
pub fn spawn_child(
|
||||
&self,
|
||||
name: &str,
|
||||
parent: Entity,
|
||||
scene: &mut Scene,
|
||||
registry: &TypeRegistry,
|
||||
) -> Option<Entity> {
|
||||
let prefab = self.prefabs.get(name)?;
|
||||
let entity = scene.spawn_child(parent, prefab.name.clone(), Transform::IDENTITY);
|
||||
apply(prefab, entity, scene, registry);
|
||||
Some(entity)
|
||||
}
|
||||
|
||||
/// The type names a prefab references that `registry` doesn't know — empty
|
||||
/// when the prefab will spawn fully. Handy for surfacing authoring typos.
|
||||
pub fn unknown_specs(&self, name: &str, registry: &TypeRegistry) -> Vec<String> {
|
||||
self.prefabs
|
||||
.get(name)
|
||||
.map(|p| {
|
||||
p.components
|
||||
.iter()
|
||||
.filter(|s| !registry.is_registered(&s.type_name))
|
||||
.map(|s| s.type_name.clone())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a prefab's component specs onto an already-spawned `entity`.
|
||||
fn apply(prefab: &Prefab, entity: Entity, scene: &mut Scene, registry: &TypeRegistry) {
|
||||
for spec in &prefab.components {
|
||||
// Best-effort: an unknown type or malformed RON simply doesn't apply,
|
||||
// leaving the rest of the prefab intact.
|
||||
let _ = registry.set_ron(scene.world_mut(), entity, &spec.type_name, &spec.ron);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::render::{MeshRenderer, PrimitiveShape};
|
||||
use crate::scene::Node;
|
||||
|
||||
fn types() -> TypeRegistry {
|
||||
let mut r = TypeRegistry::new();
|
||||
r.register_reflected::<Transform>("Transform");
|
||||
r.register_reflected::<MeshRenderer>("MeshRenderer");
|
||||
r
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_lists_names_sorted_and_looks_up() {
|
||||
let mut prefabs = PrefabRegistry::new();
|
||||
prefabs.register(Prefab::new("Sphere"));
|
||||
prefabs.register(Prefab::new("Cube"));
|
||||
assert_eq!(prefabs.names().collect::<Vec<_>>(), vec!["Cube", "Sphere"]);
|
||||
assert!(prefabs.contains("Cube"));
|
||||
assert!(prefabs.get("Cube").is_some());
|
||||
assert_eq!(prefabs.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_attaches_specced_components() {
|
||||
let types = types();
|
||||
let mut prefabs = PrefabRegistry::new();
|
||||
let mesh = MeshRenderer {
|
||||
shape: PrimitiveShape::Sphere,
|
||||
..MeshRenderer::default()
|
||||
};
|
||||
prefabs
|
||||
.register(Prefab::new("Ball").with(ComponentSpec::of("MeshRenderer", &mesh).unwrap()));
|
||||
|
||||
let mut scene = Scene::new();
|
||||
let e = prefabs.spawn("Ball", &mut scene, &types).unwrap();
|
||||
|
||||
// Node name comes from the prefab; the spec'd component is attached.
|
||||
assert_eq!(scene.world().get::<&Node>(e).unwrap().name, "Ball");
|
||||
let got = scene.world().get::<&MeshRenderer>(e).unwrap();
|
||||
assert_eq!(got.shape, PrimitiveShape::Sphere);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_child_parents_under_the_target() {
|
||||
let types = types();
|
||||
let mut prefabs = PrefabRegistry::new();
|
||||
prefabs.register(Prefab::new("Child"));
|
||||
|
||||
let mut scene = Scene::new();
|
||||
let parent = scene.spawn("parent", Transform::IDENTITY);
|
||||
let child = prefabs
|
||||
.spawn_child("Child", parent, &mut scene, &types)
|
||||
.unwrap();
|
||||
assert_eq!(scene.parent(child), Some(parent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_spec_overrides_the_identity_spawn() {
|
||||
let types = types();
|
||||
let mut prefabs = PrefabRegistry::new();
|
||||
let placed = Transform::from_translation(crate::math::Vec3::new(1.0, 2.0, 3.0));
|
||||
prefabs
|
||||
.register(Prefab::new("Placed").with(ComponentSpec::of("Transform", &placed).unwrap()));
|
||||
|
||||
let mut scene = Scene::new();
|
||||
let e = prefabs.spawn("Placed", &mut scene, &types).unwrap();
|
||||
let t = scene.local_transform(e).unwrap();
|
||||
assert!((t.translation - crate::math::Vec3::new(1.0, 2.0, 3.0)).length() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_prefab_name_spawns_nothing() {
|
||||
let types = types();
|
||||
let prefabs = PrefabRegistry::new();
|
||||
let mut scene = Scene::new();
|
||||
assert!(prefabs.spawn("Nope", &mut scene, &types).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_specs_are_reported_and_skipped() {
|
||||
let types = types(); // knows Transform + MeshRenderer
|
||||
let mut prefabs = PrefabRegistry::new();
|
||||
prefabs.register(
|
||||
Prefab::new("Mixed")
|
||||
.with(ComponentSpec::of("MeshRenderer", &MeshRenderer::default()).unwrap())
|
||||
.with(ComponentSpec::new("Ghost", "()")),
|
||||
);
|
||||
assert_eq!(prefabs.unknown_specs("Mixed", &types), vec!["Ghost"]);
|
||||
|
||||
// Spawn still succeeds; the known component applies, the ghost is skipped.
|
||||
let mut scene = Scene::new();
|
||||
let e = prefabs.spawn("Mixed", &mut scene, &types).unwrap();
|
||||
assert!(types.has(scene.world(), e, "MeshRenderer").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefab_round_trips_through_ron() {
|
||||
let mut prefabs = PrefabRegistry::new();
|
||||
prefabs.register(
|
||||
Prefab::new("Cube")
|
||||
.with(ComponentSpec::of("MeshRenderer", &MeshRenderer::default()).unwrap()),
|
||||
);
|
||||
let ron = ron::to_string(&prefabs).unwrap();
|
||||
let back: PrefabRegistry = ron::from_str(&ron).unwrap();
|
||||
assert_eq!(prefabs, back);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user