Creating Custom Components
AdvancedLearn 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:
import { defineComponent, Types } from "bitecs";import { ComponentRegistry } from "@web-engine-dev/core"; // 1. Define the bitECS componentexport const Health = defineComponent({ current: Types.f32, max: Types.f32,}); // 2. Register schema with editor metadataComponentRegistry.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#
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:
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 bitmaskBasic Property Schemas#
Numeric Properties#
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#
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#
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#
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#
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#
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#
import { defineComponent, Types } from "bitecs";import { ComponentRegistry } from "@web-engine-dev/core"; // 1. Define bitECS component structureexport const Inventory = defineComponent({ capacity: Types.ui16, gold: Types.ui32, // Note: Arrays and complex types need custom serialization}); // 2. Register schema with ComponentRegistryComponentRegistry.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#
import { ComponentRegistry } from "@web-engine-dev/core"; // Register a new componentComponentRegistry.register("MyComponent", { name: "MyComponent", description: "Does something cool", category: "Custom", props: { value: { type: "float", default: 0 }, },}); // Check if component existsif (ComponentRegistry.has("MyComponent")) { console.log("Component registered");} // Get component schemaconst schema = ComponentRegistry.get("MyComponent");console.log(schema?.description); // Get all componentsconst allComponents = ComponentRegistry.getAll(); // Get components by categoryconst physicsComponents = Array.from(allComponents.values()) .filter(c => c.category === "Physics"); // Validate component dataconst 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:
// 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 automaticallyComponent 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#
// Validation happens automatically during registrationComponentRegistry.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 validationconst 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: falsefor critical components like Transform
Advanced Component Example#
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", }, },});