Prefabs
Master prefabs: reusable entity templates that enable efficient instantiation, nesting, overrides, and variants. Learn prefab creation, serialization, and best practices.
Prefabs are reusable entity templates that define a blueprint for creating entities with pre-configured components and hierarchies. They enable consistent entity creation, support nesting and overrides, and dramatically reduce repetitive setup code.
What is a Prefab?#
A prefab is a serialized definition of an entity hierarchy with component data. When you instantiate a prefab, Web Engine creates new entities with the defined components and applies any instance-specific overrides.
// Prefab definition structureinterface PrefabDefinition { name: string; components: Record<string, unknown>; // Component data children?: PrefabDefinition[]; // Nested children prefabRef?: string; // Reference to another prefab overrides?: PrefabOverride; // Template-level overrides} // Example: Crate prefabconst cratePrefab: PrefabDefinition = { name: 'Crate', components: { Transform: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1], }, RigidBody: { mass: 10.0, friction: 0.5, restitution: 0.2, type: 0, // Dynamic }, Collider: { type: 0, // Box size: [0.5, 0.5, 0.5], }, Mesh: { geometryType: 0, // Box castShadow: 1, }, },};Reusability
Define once, instantiate many times. Perfect for common objects like props, enemies, pickups.
Variants
Create variations with overrides. Red barrel, blue barrel—same base prefab, different properties.
Nesting
Prefabs can contain other prefabs, enabling complex hierarchies and modular design.
Hot Updates
Modify prefab definitions and existing instances automatically inherit changes.
Creating Prefabs from Entities#
You can create prefabs from existing entities in the scene. The prefab system captures all components and hierarchy data.
import { PrefabManager } from '@web-engine/core'; // Create an entity with componentsconst eid = addEntity(world);addComponent(world, Transform, eid);addComponent(world, RigidBody, eid);addComponent(world, Collider, eid);addComponent(world, Mesh, eid); // Set component dataTransform.position.y[eid] = 1.0;RigidBody.mass[eid] = 5.0;Collider.size.x[eid] = 0.5;Collider.size.y[eid] = 0.5;Collider.size.z[eid] = 0.5; // Create prefab from entityconst prefabId = PrefabManager.createFromEntity(world, eid, 'Barrel'); // Now you can spawn instancesconst barrel1 = PrefabManager.spawn(world, prefabId, [0, 1, 0]);const barrel2 = PrefabManager.spawn(world, prefabId, [5, 1, 0]);const barrel3 = PrefabManager.spawn(world, prefabId, [10, 1, 0]);Prefabs Capture Hierarchies
When creating a prefab from an entity with children, the entire hierarchy is captured. Parent-child relationships are preserved on instantiation.
Instantiating Prefabs#
Spawn prefab instances with optional position and override parameters. Each instance is a new set of entities linked to the prefab definition.
Basic Spawning#
import { PrefabManager } from '@web-engine/core'; // Spawn at origin (0, 0, 0)const instance1 = PrefabManager.spawn(world, prefabId); // Spawn at specific positionconst instance2 = PrefabManager.spawn(world, prefabId, [10, 0, 5]); // Spawn returns root entity IDconsole.log(instance2); // Entity ID of root nodeSpawning with Overrides#
Provide instance-specific component overrides when spawning. Overrides apply on top of the prefab's base values.
// Spawn with component overridesconst heavyCrate = PrefabManager.spawn( world, cratePrefabId, [0, 1, 0], { components: { RigidBody: { props: { mass: 50.0 }, // Override mass to 50 }, Mesh: { props: { geometryType: 1 }, // Change to sphere }, }, }); // Spawn with added componentsconst glowingCrate = PrefabManager.spawn( world, cratePrefabId, [5, 1, 0], { components: { Light: { add: true, // Add Light component not in base prefab props: { type: 0, // Point light intensity: 2.0, color: [1, 0.8, 0.5], }, }, }, });Override Persistence
Instance overrides are stored with the entity in the PrefabInstanceBinding metadata. When the scene is saved, overrides are preserved. When the prefab definition changes, non-overridden properties update automatically.
Prefab Overrides System#
The override system allows fine-grained control over prefab instances. You can override individual properties, remove components, or add new ones.
Property-Level Overrides#
// Override interfaceinterface ComponentOverride { props?: Record<string, ComponentPropertyValue>; // Override properties removedProps?: string[]; // Remove properties remove?: boolean; // Remove entire component add?: boolean; // Add new component} // Example: Override just the massconst override: PrefabOverride = { components: { RigidBody: { props: { mass: 100.0 }, // Only mass changes }, },}; // All other RigidBody properties (friction, restitution, etc.)// remain from the prefab definitionRemoving Components#
// Remove RigidBody from instance (make it static)const staticVersion: PrefabOverride = { components: { RigidBody: { remove: true, // Remove entire RigidBody component }, Collider: { remove: true, // Remove Collider too }, },}; const staticCrate = PrefabManager.spawn( world, cratePrefabId, [0, 0, 0], staticVersion); // staticCrate has no physics - just Transform and MeshAdding Components#
// Add components not in base prefabconst enhancedOverride: PrefabOverride = { components: { Health: { add: true, props: { current: 100, max: 100, regenRate: 0.5, }, }, Lifetime: { add: true, props: { duration: 30.0, elapsed: 0.0, }, }, },};Nested Prefabs#
Prefabs can reference other prefabs, enabling modular composition and reuse. This is powerful for building complex entities from smaller building blocks.
// Base prefabsconst wheelPrefab: PrefabDefinition = { name: 'Wheel', components: { Transform: { scale: [0.5, 0.5, 0.5] }, Mesh: { geometryType: 2 }, // Cylinder RigidBody: { mass: 2.0 }, },}; const enginePrefab: PrefabDefinition = { name: 'Engine', components: { Transform: {}, Mesh: { geometryType: 0 }, },}; // Car prefab referencing wheel and engine prefabsconst carPrefab: PrefabDefinition = { name: 'Car', components: { Transform: {}, RigidBody: { mass: 1000.0 }, Collider: { type: 0, size: [2, 1, 4] }, }, children: [ { name: 'FrontLeftWheel', prefabRef: 'wheel-uuid', // Reference to wheelPrefab components: { Transform: { position: [-1, -0.5, 1.5] }, }, }, { name: 'FrontRightWheel', prefabRef: 'wheel-uuid', components: { Transform: { position: [1, -0.5, 1.5] }, }, }, { name: 'RearLeftWheel', prefabRef: 'wheel-uuid', components: { Transform: { position: [-1, -0.5, -1.5] }, }, }, { name: 'RearRightWheel', prefabRef: 'wheel-uuid', components: { Transform: { position: [1, -0.5, -1.5] }, }, }, { name: 'Engine', prefabRef: 'engine-uuid', components: { Transform: { position: [0, 0, 2] }, }, }, ],}; // When you spawn carPrefab, it resolves all nested prefab referencesconst myCar = PrefabManager.spawn(world, carPrefabId, [0, 1, 0]);// Creates: 1 car body + 4 wheels + 1 engine = 6 entities totalCircular Reference Detection
The prefab system detects and prevents circular references. If prefab A references prefab B, and B references A, the system will throw an error during registration or instantiation.
Prefab Variants#
Create prefab variants by defining template-level overrides. This allows you to have multiple prefabs that share a base while differing in specific properties.
// Base enemy prefabconst enemyBasePrefab: PrefabDefinition = { name: 'EnemyBase', components: { Transform: {}, Mesh: { geometryType: 1 }, // Sphere RigidBody: { mass: 50 }, Health: { current: 100, max: 100 }, },}; // Fast variant - less health, faster movementconst fastEnemyPrefab: PrefabDefinition = { name: 'FastEnemy', prefabRef: 'enemy-base-uuid', overrides: { components: { Health: { props: { current: 50, max: 50 }, // Half health }, CharacterController: { add: true, props: { speed: 8.0 }, // Fast movement }, }, },}; // Tank variant - more health, slower, biggerconst tankEnemyPrefab: PrefabDefinition = { name: 'TankEnemy', prefabRef: 'enemy-base-uuid', overrides: { components: { Health: { props: { current: 300, max: 300 }, // Triple health }, Transform: { props: { scale: [2, 2, 2] }, // Twice as big }, RigidBody: { props: { mass: 200 }, // Heavy }, CharacterController: { add: true, props: { speed: 2.0 }, // Slow movement }, }, },};Variants inherit changes from their base prefab. If you update the base enemy's Mesh component, both FastEnemy and TankEnemy automatically receive the update (unless they override that specific property).
Prefab Serialization#
Prefabs are stored as assets in the project. When you save a scene with prefab instances, only the instance binding and overrides are saved — not the full entity data.
Prefab Assets#
// Prefab stored as assetinterface PrefabAsset { id: string; // Unique prefab UUID name: string; // Display name version: number; // Schema version definition: PrefabDefinition; metadata: { createdAt: number; modifiedAt: number; thumbnail?: string; };} // Save prefab as assetAssetRegistry.registerPrefab(cratePrefabAsset);Instance Serialization#
// Prefab instance binding stored with entityinterface PrefabInstanceBinding { prefabId: string; // UUID of source prefab nodePath: string; // Path within prefab hierarchy ("0", "0.1", etc.) overrides?: PrefabOverride;} // When serializing scene entitiesconst serializedEntity = { uuid: 'entity-123', name: 'Crate Instance', components: { // Only overridden component data is serialized RigidBody: { mass: 50.0, // Overridden value }, }, prefab: { prefabId: 'crate-prefab-uuid', nodePath: '0', overrides: { components: { RigidBody: { props: { mass: 50.0 }, }, }, }, },}; // When loading, the prefab definition is resolved// and overrides are applied on topSpace Savings
Prefab instances save significant disk space. Instead of storing full component data for every entity, only the prefab reference and overrides are saved. A scene with 1000 identical crates stores 1 prefab definition + 1000 lightweight instance bindings.
Updating Prefabs#
When you modify a prefab definition, existing instances can automatically inherit the changes. This powerful feature enables iteration without manual updates.
// Update prefab definitionPrefabManager.updateDefinition(prefabId, { components: { RigidBody: { friction: 0.8, // Changed from 0.5 }, },}); // Option 1: Apply updates to all instances immediatelyPrefabManager.applyUpdatesToInstances(prefabId); // Option 2: Apply updates to specific instancePrefabManager.applyUpdatesToInstance(world, entityUuid); // Only non-overridden properties are updated// If an instance overrode 'friction', it keeps its custom valueOverride Protection
Instance overrides take precedence over prefab updates. If you've customized a property on an instance, updating the prefab won't change that property. This prevents accidentally losing instance-specific customizations.
Instance Tracking#
The prefab system tracks all active instances of each prefab. This enables querying, bulk updates, and debugging.
// Get all instances of a prefabconst instances = PrefabManager.getInstances(prefabId); console.log(`Prefab has ${instances.length} active instances`); instances.forEach((instance) => { console.log(`Entity ${instance.eid}: ${instance.entityUuid}`); console.log(` Node path: ${instance.nodePath}`); console.log(` Has overrides: ${instance.hasOverrides}`); console.log(` Created at: ${new Date(instance.createdAt)}`);}); // Get instance info for specific entityconst binding = PrefabManager.getInstanceBinding(entityUuid); if (binding) { console.log(`Entity is instance of: ${binding.prefabId}`); console.log(`Path: ${binding.nodePath}`); console.log(`Overrides:`, binding.overrides);}Best Practices#
- Use prefabs for repeated entities — Props, enemies, pickups, projectiles benefit from prefabs
- Keep prefabs focused — Small, composable prefabs are easier to maintain than monolithic ones
- Leverage nesting — Build complex prefabs from simpler prefab components
- Use variants for similar entities — Share base definition, customize with overrides
- Version prefab schemas — Include version numbers for migration when structure changes
- Test circular references — Always validate prefabs don't reference themselves indirectly
- Document overrides — Comment why instance overrides exist to prevent confusion
- Minimize overrides — Excessive overrides defeat the purpose of prefabs; consider creating variants instead
Common Patterns#
Object Pooling#
// Prefabs work great with object poolsclass ProjectilePool { private prefabId: PrefabId; private pool: number[] = []; spawn(position: [number, number, number]) { let eid: number; if (this.pool.length > 0) { // Reuse pooled entity eid = this.pool.pop()!; Transform.position.x[eid] = position[0]; Transform.position.y[eid] = position[1]; Transform.position.z[eid] = position[2]; } else { // Spawn new from prefab eid = PrefabManager.spawn(world, this.prefabId, position); } return eid; } recycle(eid: number) { this.pool.push(eid); }}Procedural Generation#
// Use prefabs for procedural contentfunction generateDungeon(rooms: number) { const roomPrefabs = [ 'room-small-uuid', 'room-medium-uuid', 'room-large-uuid', ]; for (let i = 0; i < rooms; i++) { const prefabId = roomPrefabs[Math.floor(Math.random() * roomPrefabs.length)]; const x = i * 20; const z = Math.random() * 10 - 5; // Spawn with random variant PrefabManager.spawn(world, prefabId, [x, 0, z], { components: { Light: { props: { intensity: 0.5 + Math.random() * 0.5, color: [Math.random(), Math.random(), Math.random()], }, }, }, }); }}Editor Integration#
// Create prefab from editor selectionfunction createPrefabFromSelection(selectedEntities: number[]) { const rootEntity = selectedEntities[0]; // Capture entity hierarchy const prefabId = PrefabManager.createFromEntity( world, rootEntity, 'New Prefab' ); // Save as asset AssetRegistry.registerPrefab({ id: prefabId, name: 'New Prefab', version: 1, definition: PrefabManager.getDefinition(prefabId), metadata: { createdAt: Date.now(), modifiedAt: Date.now(), }, }); console.log(`Created prefab: ${prefabId}`);}