Files
Oxide/engine/src/prefab.rs
T
Homer Simpson f56a1eea3b 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>
2026-07-05 20:41:02 +02:00

312 lines
11 KiB
Rust

//! 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);
}
}