Creating Custom Components

Advanced

Learn how to define, register, and serialize custom ECS components with editor integration.

Components are pure data containers in Web Engine's ECS architecture. This guide covers creating custom components, registering schemas with the ComponentRegistry, and integrating with the editor UI.

Component Basics#

Components are defined using bitECS and registered with ComponentRegistry for editor metadata:

BasicComponent.ts
typescript
import { defineComponent, Types } from "bitecs";
import { ComponentRegistry } from "@web-engine-dev/core";
// 1. Define the bitECS component
export const Health = defineComponent({
current: Types.f32,
max: Types.f32,
});
// 2. Register schema with editor metadata
ComponentRegistry.register("Health", {
name: "Health",
description: "Entity health and damage tracking",
category: "Gameplay",
component: Health,
props: {
current: { type: "float", default: 100, min: 0 },
max: { type: "float", default: 100, min: 0 },
},
});

Data-Oriented Storage

Components are stored in TypedArrays for cache efficiency

Type Safety

Full TypeScript support with schema validation

Editor Integration

Automatic UI generation from property schemas

Serialization

Components are automatically serialized to scene JSON

Component Schema Definition#

ComponentSchema.ts
typescript
interface ComponentSchema {
name: string; // Component identifier
description?: string; // Human-readable description
category?: string; // Editor category (Physics, Rendering, etc.)
component?: ComponentType; // bitECS component definition
version?: number; // Schema version for migrations
props: Record<string, PropSchema>; // Property definitions
canDisable?: boolean; // Whether component can be disabled
}

Property Types#

ComponentRegistry supports a wide range of property types for different use cases:

PropertyTypes.ts
typescript
type PropType =
| "float" // Single floating-point number
| "int" // Integer number
| "string" // Text string
| "boolean" // True/false checkbox
| "vec2" // 2D vector (x, y)
| "vec3" // 3D vector (x, y, z)
| "vec4" // 4D vector (x, y, z, w)
| "vec2int" // 2D integer vector
| "vec3int" // 3D integer vector
| "color" // RGB/RGBA color picker
| "quaternion" // Rotation quaternion
| "matrix3" // 3x3 matrix
| "matrix4" // 4x4 matrix
| "curve" // Animation curve
| "enum" // Dropdown selection
| "asset" // Asset reference (legacy)
| "animation_clip" // Animation clip reference
| "entity" // Entity reference
| "array" // Array of values
| "assetRef" // UUID-based asset reference
| "labels" // Tag/label editor
| "renderLayers" // Render layer bitmask
| "tags"; // Tag bitmask

Basic Property Schemas#

Numeric Properties#

NumericProps.ts
typescript
ComponentRegistry.register("Movement", {
name: "Movement",
props: {
speed: {
type: "float",
default: 5.0,
min: 0,
max: 100,
step: 0.1, // Slider step size
label: "Speed", // Display label
},
jumpCount: {
type: "int",
default: 2,
min: 1,
max: 5,
},
},
});

Vector Properties#

VectorProps.ts
typescript
props: {
position: {
type: "vec3",
default: [0, 0, 0],
label: "Position",
},
velocity: {
type: "vec3",
default: [0, 0, 0],
},
color: {
type: "color",
default: [1, 1, 1, 1], // RGBA
},
rotation: {
type: "quaternion",
default: [0, 0, 0, 1], // x, y, z, w
},
}

Enum Properties#

EnumProps.ts
typescript
props: {
bodyType: {
type: "enum",
options: ["0", "1", "2"],
labels: ["Dynamic", "Static", "Kinematic"],
default: "0",
},
quality: {
type: "enum",
options: ["low", "medium", "high", "ultra"],
default: "medium",
},
}

Advanced Property Types#

Asset References#

AssetRefProps.ts
typescript
props: {
model: {
type: "assetRef",
acceptTypes: ["model"], // Only accept model assets
showPreview: true, // Show thumbnail in editor
allowClear: true, // Allow clearing the reference
label: "Model Asset",
},
texture: {
type: "assetRef",
acceptTypes: ["texture"],
showPreview: true,
},
sounds: {
type: "assetRef",
acceptTypes: ["audio"],
},
}

Array Properties#

ArrayProps.ts
typescript
props: {
waypoints: {
type: "array",
items: { type: "vec3" }, // Array of vec3
maxItems: 10,
minItems: 2,
defaultItem: [0, 0, 0],
reorderable: true, // Enable drag-to-reorder
collapsible: true, // Enable collapse/expand
},
damageMultipliers: {
type: "array",
items: { type: "float", min: 0, max: 10 },
maxItems: 5,
},
}

Labels/Tags#

LabelProps.ts
typescript
props: {
tags: {
type: "labels",
suggestions: ["enemy", "player", "npc", "boss"],
maxLabels: 5,
label: "Entity Tags",
},
layers: {
type: "renderLayers", // 32-bit bitmask for layers
default: 1, // Layer 0 enabled
},
}

Complete Component Example#

InventoryComponent.ts
typescript
import { defineComponent, Types } from "bitecs";
import { ComponentRegistry } from "@web-engine-dev/core";
// 1. Define bitECS component structure
export const Inventory = defineComponent({
capacity: Types.ui16,
gold: Types.ui32,
// Note: Arrays and complex types need custom serialization
});
// 2. Register schema with ComponentRegistry
ComponentRegistry.register("Inventory", {
name: "Inventory",
description: "Player inventory system",
category: "Gameplay",
component: Inventory,
version: 1,
canDisable: true,
props: {
capacity: {
type: "int",
default: 20,
min: 1,
max: 100,
label: "Max Items",
},
gold: {
type: "int",
default: 0,
min: 0,
label: "Gold Amount",
},
items: {
type: "array",
items: {
type: "assetRef",
acceptTypes: ["prefab"],
},
maxItems: 20,
reorderable: true,
collapsible: true,
label: "Items",
},
},
});

Component Registration#

ComponentRegistration.ts
typescript
import { ComponentRegistry } from "@web-engine-dev/core";
// Register a new component
ComponentRegistry.register("MyComponent", {
name: "MyComponent",
description: "Does something cool",
category: "Custom",
props: {
value: { type: "float", default: 0 },
},
});
// Check if component exists
if (ComponentRegistry.has("MyComponent")) {
console.log("Component registered");
}
// Get component schema
const schema = ComponentRegistry.get("MyComponent");
console.log(schema?.description);
// Get all components
const allComponents = ComponentRegistry.getAll();
// Get components by category
const physicsComponents = Array.from(allComponents.values())
.filter(c => c.category === "Physics");
// Validate component data
const validationResult = ComponentRegistry.validateData("MyComponent", {
value: 42,
});
if (!validationResult.valid) {
validationResult.errors.forEach(err => {
console.error(`${err.property}: ${err.message}`);
});
}

Component Serialization#

Components are automatically serialized to/from scene JSON. The serialization system uses the component schema to convert between ECS data and JSON:

ComponentSerialization.ts
typescript
// Scene JSON format
{
"entities": [
{
"id": "entity-1",
"name": "Player",
"components": {
"Transform": {
"position": [0, 1, 0],
"rotation": [0, 0, 0, 1],
"scale": [1, 1, 1]
},
"Health": {
"current": 100,
"max": 100
},
"Inventory": {
"capacity": 20,
"gold": 150,
"items": [
"item-uuid-1",
"item-uuid-2"
]
}
}
}
]
}
// Components are deserialized on scene load
// and serialized on scene save automatically

Component Categories#

Organize components into categories for the editor UI:

  • Core - Transform, Name, Parent/Child relationships
  • Rendering - MeshRenderer, Light, Camera
  • Physics - RigidBody, Collider, CharacterController
  • Audio - AudioSource, AudioListener, AudioZone
  • Animation - Animator, SkinnedMesh
  • Gameplay - Health, Inventory, Quest, Dialogue
  • AI - NavMeshAgent, Behavior, StateMachine
  • Network - NetworkIdentity, NetworkTransform
  • Logic - ScriptBehavior, LogicGraph
  • Environment - Water, Foliage, Terrain

Component Validation#

ComponentValidation.ts
typescript
// Validation happens automatically during registration
ComponentRegistry.register("MyComponent", {
name: "MyComponent",
props: {
speed: {
type: "float",
min: 0,
max: 100,
required: true, // Field is required
},
target: {
type: "entity",
required: false, // Field is optional
},
},
});
// Manual validation
const result = ComponentRegistry.validateData("MyComponent", {
speed: 150, // Out of range
});
if (!result.valid) {
// [{ property: "speed", message: "Value must be <= 100", code: "MAX" }]
console.error(result.errors);
}

Best Practices#

Component Design

  • Keep components small and focused on a single responsibility
  • Use bitECS Types for optimal memory layout
  • Add min/max constraints for numeric fields to prevent invalid values
  • Provide sensible defaults for all properties
  • Use descriptive labels for better editor UX
  • Version your schemas for future migrations
  • Group related components with the same category
  • Use canDisable: false for critical components like Transform

Advanced Component Example#

WeaponComponent.ts
typescript
import { defineComponent, Types } from "bitecs";
import { ComponentRegistry } from "@web-engine-dev/core";
export const Weapon = defineComponent({
damage: Types.f32,
fireRate: Types.f32,
ammoCount: Types.ui16,
maxAmmo: Types.ui16,
reloadTime: Types.f32,
// ... more fields
});
ComponentRegistry.register("Weapon", {
name: "Weapon",
description: "Weapon system with ammo and fire rate",
category: "Gameplay",
component: Weapon,
version: 2,
props: {
damage: {
type: "float",
default: 25,
min: 0,
max: 1000,
step: 5,
label: "Damage per Hit",
},
fireRate: {
type: "float",
default: 10,
min: 0.1,
max: 60,
step: 0.5,
label: "Rounds per Minute",
},
ammoCount: {
type: "int",
default: 30,
min: 0,
label: "Current Ammo",
},
maxAmmo: {
type: "int",
default: 30,
min: 1,
max: 200,
label: "Magazine Size",
},
reloadTime: {
type: "float",
default: 2.0,
min: 0.1,
max: 10,
step: 0.1,
label: "Reload Time (s)",
},
weaponModel: {
type: "assetRef",
acceptTypes: ["model"],
showPreview: true,
label: "Weapon Model",
},
fireSound: {
type: "assetRef",
acceptTypes: ["audio"],
label: "Fire Sound",
},
muzzleFlash: {
type: "assetRef",
acceptTypes: ["prefab"],
label: "Muzzle Flash Effect",
},
damageTypes: {
type: "labels",
suggestions: ["kinetic", "explosive", "energy", "poison"],
maxLabels: 3,
label: "Damage Types",
},
},
});
Advanced | Web Engine Docs | Web Engine Docs