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 structure
interface 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 prefab
const 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.

Creating a prefab from an entity
typescript
import { PrefabManager } from '@web-engine/core';
// Create an entity with components
const eid = addEntity(world);
addComponent(world, Transform, eid);
addComponent(world, RigidBody, eid);
addComponent(world, Collider, eid);
addComponent(world, Mesh, eid);
// Set component data
Transform.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 entity
const prefabId = PrefabManager.createFromEntity(world, eid, 'Barrel');
// Now you can spawn instances
const 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 position
const instance2 = PrefabManager.spawn(world, prefabId, [10, 0, 5]);
// Spawn returns root entity ID
console.log(instance2); // Entity ID of root node

Spawning with Overrides#

Provide instance-specific component overrides when spawning. Overrides apply on top of the prefab's base values.

// Spawn with component overrides
const 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 components
const 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 interface
interface 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 mass
const override: PrefabOverride = {
components: {
RigidBody: {
props: { mass: 100.0 }, // Only mass changes
},
},
};
// All other RigidBody properties (friction, restitution, etc.)
// remain from the prefab definition

Removing 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 Mesh

Adding Components#

// Add components not in base prefab
const 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 prefabs
const 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 prefabs
const 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 references
const myCar = PrefabManager.spawn(world, carPrefabId, [0, 1, 0]);
// Creates: 1 car body + 4 wheels + 1 engine = 6 entities total

Circular 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 prefab
const enemyBasePrefab: PrefabDefinition = {
name: 'EnemyBase',
components: {
Transform: {},
Mesh: { geometryType: 1 }, // Sphere
RigidBody: { mass: 50 },
Health: { current: 100, max: 100 },
},
};
// Fast variant - less health, faster movement
const 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, bigger
const 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 asset
interface 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 asset
AssetRegistry.registerPrefab(cratePrefabAsset);

Instance Serialization#

// Prefab instance binding stored with entity
interface PrefabInstanceBinding {
prefabId: string; // UUID of source prefab
nodePath: string; // Path within prefab hierarchy ("0", "0.1", etc.)
overrides?: PrefabOverride;
}
// When serializing scene entities
const 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 top

Space 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 definition
PrefabManager.updateDefinition(prefabId, {
components: {
RigidBody: {
friction: 0.8, // Changed from 0.5
},
},
});
// Option 1: Apply updates to all instances immediately
PrefabManager.applyUpdatesToInstances(prefabId);
// Option 2: Apply updates to specific instance
PrefabManager.applyUpdatesToInstance(world, entityUuid);
// Only non-overridden properties are updated
// If an instance overrode 'friction', it keeps its custom value

Override 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 prefab
const 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 entity
const 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 pools
class 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 content
function 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 selection
function 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}`);
}
Prefabs | Web Engine Docs