Specialized components
World editor
Entity component system integration
22 min
overview the entity component system (ecs) integration is a fundamental element of the ir engine's world editor that provides a flexible architecture for defining and manipulating objects in 3d worlds it implements a composition based approach where entities are constructed from modular components rather than rigid inheritance hierarchies by separating data (components) from logic (systems) and identity (entities), this architecture enables intuitive visual editing of complex objects while maintaining high performance this chapter explores the implementation, workflow, and benefits of the ecs integration within the world editor core concepts entity architecture the entity component system follows a composition based architecture entities unique identifiers that represent objects in the scene components data containers that define specific aspects of an entity systems logic processors that operate on entities with specific components queries filters that find entities matching specific component combinations events notifications that trigger when entity or component states change this architecture provides a flexible foundation for object definition and manipulation component types the editor supports various component types for entity definition transform defines position, rotation, and scale in 3d space mesh references 3d geometry for visual representation material defines surface appearance properties light creates various light sources (point, directional, spot) camera defines viewpoints and projection properties physics adds collision and physical behavior audio attaches sound sources and listeners script adds custom behavior through code or visual scripts these components can be combined in any configuration to create diverse entity types editor integration the editor provides specialized interfaces for ecs interaction properties panel displays and edits component data component browser lists available components for addition entity hierarchy shows parent child relationships between entities selection system manages the currently selected entities undo/redo tracks component changes for reversible operations this integration creates an intuitive visual interface for the underlying ecs architecture implementation entity management the entity management system handles entity creation and organization // simplified from src/services/entityservices ts import { definestate, getmutablestate } from '@ir engine/hyperflux'; import { createentity, removeentity, hascomponent, getcomponent } from '@ir engine/ecs'; import { scenestate } from ' /scenestate'; / state management for entities / export const entitystate = definestate({ name 'entitystate', // initial state initial () => ({ entities \[] as entity\[], selectedentityids \[] as string\[] }), // create a new entity createentity (parentid? string) => { const state = getmutablestate(entitystate); const scenestate = getmutablestate(scenestate); // create a new entity const entity = createentity(); // add transform component addcomponent(entity, 'transform', { position { x 0, y 0, z 0 }, rotation { x 0, y 0, z 0 }, scale { x 1, y 1, z 1 } }); // add to parent if specified if (parentid) { const parententity = state entities value find(e => e id === parentid); if (parententity) { const hierarchycomponent = getcomponent(parententity, 'hierarchy') || { children \[] }; hierarchycomponent children push(entity id); addcomponent(parententity, 'hierarchy', hierarchycomponent); } } // add to entity list state entities merge(\[entity]); // mark scene as modified scenestate ismodified set(true); return entity; }, // remove an entity removeentity (entityid string) => { const state = getmutablestate(entitystate); const scenestate = getmutablestate(scenestate); // find the entity const entity = state entities value find(e => e id === entityid); if (!entity) return false; // remove from parent's hierarchy state entities value foreach(e => { if (hascomponent(e, 'hierarchy')) { const hierarchy = getcomponent(e, 'hierarchy'); const childindex = hierarchy children indexof(entityid); if (childindex >= 0) { hierarchy children splice(childindex, 1); addcomponent(e, 'hierarchy', hierarchy); } } }); // remove the entity removeentity(entity); // update entity list state entities set(state entities value filter(e => e id !== entityid)); // deselect if selected if (state selectedentityids value includes(entityid)) { state selectedentityids set( state selectedentityids value filter(id => id !== entityid) ); } // mark scene as modified scenestate ismodified set(true); return true; }, // select entities selectentities (entityids string\[], addtoselection boolean = false) => { const state = getmutablestate(entitystate); if (addtoselection) { // add to current selection const newselection = \[ state selectedentityids value]; entityids foreach(id => { if (!newselection includes(id)) { newselection push(id); } }); state selectedentityids set(newselection); } else { // replace current selection state selectedentityids set(entityids); } } }); this service provides methods for creating and removing entities manages parent child relationships between entities handles entity selection for editing updates the scene state when entities change maintains a list of all entities in the scene component editing the component editing system provides interfaces for modifying component data // simplified from src/panels/properties/propertyeditor tsx import react, { useeffect, usestate } from 'react'; import { usehookstate } from '@hookstate/core'; import { entitystate } from ' / /services/entityservices'; import { componentregistry } from ' / /services/componentregistry'; import { hascomponent, getcomponent, addcomponent, removecomponent } from '@ir engine/ecs'; import { addcomponentbutton } from ' /addcomponentbutton'; import { componentheader } from ' /componentheader'; / property editor for entity components @param props component properties @returns property editor component / export const propertyeditor react fc = () => { // get selected entity const entitystate = usehookstate(entitystate state); const selectedids = entitystate selectedentityids value; // handle multi selection const ismultiselect = selectedids length > 1; // if nothing selected, show empty state if (selectedids length === 0) { return \<div classname="property editor empty">no entity selected\</div>; } // get the primary selected entity const primaryentityid = selectedids\[0]; const entity = entitystate entities value find(e => e id === primaryentityid); if (!entity) { return \<div classname="property editor empty">selected entity not found\</div>; } // get all components on the entity const entitycomponents = getentitycomponents(entity); return ( \<div classname="property editor"> {/ entity header with name /} \<entityheader entity={entity} ismultiselect={ismultiselect} /> {/ component editors /} {entitycomponents map(componenttype => ( \<componenteditorwrapper key={componenttype} entity={entity} componenttype={componenttype} ismultiselect={ismultiselect} /> ))} {/ add component button /} \<addcomponentbutton entity={entity} existingcomponents={entitycomponents} /> \</div> ); }; / component editor wrapper @param props component properties @returns component editor wrapper / const componenteditorwrapper react fc<{ entity entity; componenttype string; ismultiselect boolean; }> = ({ entity, componenttype, ismultiselect }) => { // get component data const componentdata = getcomponent(entity, componenttype); // get editor for this component type const componenteditor = componentregistry geteditor(componenttype); // if no editor is registered for this component type if (!componenteditor) { return ( \<div classname="component editor"> \<componentheader name={componenttype} entity={entity} componenttype={componenttype} /> \<div classname="component editor unsupported"> no editor available for this component type \</div> \</div> ); } // handle component removal const handleremovecomponent = () => { removecomponent(entity, componenttype); // mark scene as modified // implementation details omitted for brevity }; return ( \<div classname="component editor"> \<componentheader name={componenttype} entity={entity} componenttype={componenttype} onremove={handleremovecomponent} /> \<componenteditor entity={entity} componenttype={componenttype} data={componentdata} ismultiselect={ismultiselect} /> \</div> ); }; this component retrieves the selected entity from the entity state gets all components attached to the entity renders specialized editors for each component type provides controls for adding and removing components handles multi selection for editing multiple entities component registry the component registry manages available component types and their editors // simplified from src/services/componentregistry ts import { definestate, getmutablestate } from '@ir engine/hyperflux'; import { transformeditor } from ' /components/editors/transformeditor'; import { mesheditor } from ' /components/editors/mesheditor'; import { lighteditor } from ' /components/editors/lighteditor'; import { cameraeditor } from ' /components/editors/cameraeditor'; / registry for component types and editors / export const componentregistry = definestate({ name 'componentregistry', // initial state initial () => ({ // component definitions components { transform { name 'transform', description 'position, rotation, and scale in 3d space', category 'basic', editor transformeditor, defaultdata { position { x 0, y 0, z 0 }, rotation { x 0, y 0, z 0 }, scale { x 1, y 1, z 1 } } }, mesh { name 'mesh', description '3d model geometry', category 'rendering', editor mesheditor, defaultdata { url '', visible true } }, directionallight { name 'directional light', description 'light that shines in a specific direction', category 'lighting', editor lighteditor, defaultdata { color '#ffffff', intensity 1, castshadow true } }, camera { name 'camera', description 'viewpoint for rendering', category 'rendering', editor cameraeditor, defaultdata { fov 60, near 0 1, far 1000, isactive false } } // additional component definitions }, // component categories categories \[ { id 'basic', name 'basic' }, { id 'rendering', name 'rendering' }, { id 'lighting', name 'lighting' }, { id 'physics', name 'physics' }, { id 'audio', name 'audio' }, { id 'scripting', name 'scripting' } ] }), // get component definition getcomponent (componenttype string) => { const state = getmutablestate(componentregistry); return state components\[componenttype]; }, // get editor for component type geteditor (componenttype string) => { const state = getmutablestate(componentregistry); const component = state components\[componenttype]; return component ? component editor null; }, // get default data for component type getdefaultdata (componenttype string) => { const state = getmutablestate(componentregistry); const component = state components\[componenttype]; return component ? { component defaultdata } {}; }, // get components by category getcomponentsbycategory (categoryid string) => { const state = getmutablestate(componentregistry); const result = \[]; for (const \[type, component] of object entries(state components)) { if (component category === categoryid) { result push({ type, component }); } } return result; } }); this service defines available component types and their metadata organizes components into categories for browsing provides default data for component initialization maps component types to their specialized editors offers methods for querying component information transform editor the transform editor provides controls for manipulating spatial properties // simplified from src/components/editors/transformeditor tsx import react from 'react'; import { vector3input } from '@ir engine/ui/src/components/inputs/vector3input'; import { usehookstate } from '@hookstate/core'; import { scenestate } from ' / /services/scenestate'; import { getcomponent, updatecomponent } from '@ir engine/ecs'; / editor for transform components @param props component properties @returns transform editor component / export const transformeditor react fc<{ entity entity; data any; ismultiselect boolean; }> = ({ entity, data, ismultiselect }) => { const scenestate = usehookstate(scenestate state); // handle position change const handlepositionchange = (position vector3) => { const transform = getcomponent(entity, 'transform'); updatecomponent(entity, 'transform', { transform, position }); // mark scene as modified scenestate ismodified set(true); }; // handle rotation change const handlerotationchange = (rotation vector3) => { const transform = getcomponent(entity, 'transform'); updatecomponent(entity, 'transform', { transform, rotation }); // mark scene as modified scenestate ismodified set(true); }; // handle scale change const handlescalechange = (scale vector3) => { const transform = getcomponent(entity, 'transform'); updatecomponent(entity, 'transform', { transform, scale }); // mark scene as modified scenestate ismodified set(true); }; return ( \<div classname="transform editor"> \<div classname="editor row"> \<label>position\</label> \<vector3input value={data position} onchange={handlepositionchange} disabled={ismultiselect} /> \</div> \<div classname="editor row"> \<label>rotation\</label> \<vector3input value={data rotation} onchange={handlerotationchange} disabled={ismultiselect} /> \</div> \<div classname="editor row"> \<label>scale\</label> \<vector3input value={data scale} onchange={handlescalechange} disabled={ismultiselect} /> \</div> \</div> ); }; this component provides input fields for position, rotation, and scale updates the transform component when values change marks the scene as modified for save tracking handles multi selection state for editing multiple entities uses specialized vector3input components for coordinate editing add component dialog the add component dialog allows adding new components to entities // simplified from src/panels/properties/addcomponentbutton tsx import react, { usestate } from 'react'; import { button, dialog, list, listitem, searchinput } from '@ir engine/ui'; import { componentregistry } from ' / /services/componentregistry'; import { addcomponent } from '@ir engine/ecs'; import { scenestate } from ' / /services/scenestate'; / button and dialog for adding components to entities @param props component properties @returns add component button component / export const addcomponentbutton react fc<{ entity entity; existingcomponents string\[]; }> = ({ entity, existingcomponents }) => { // dialog state const \[isopen, setisopen] = usestate(false); const \[searchquery, setsearchquery] = usestate(''); // get component categories const categories = componentregistry state categories value; // handle component selection const handleselectcomponent = (componenttype string) => { // get default data for the component const defaultdata = componentregistry getdefaultdata(componenttype); // add component to entity addcomponent(entity, componenttype, defaultdata); // mark scene as modified const scenestate = getmutablestate(scenestate); scenestate ismodified set(true); // close dialog setisopen(false); }; // filter components based on search query const filtercomponents = (components) => { if (!searchquery) return components; return components filter(component => component name tolowercase() includes(searchquery tolowercase()) || component description tolowercase() includes(searchquery tolowercase()) ); }; return ( <> \<button classname="add component button" onclick={() => setisopen(true)} \> add component \</button> \<dialog isopen={isopen} onclose={() => setisopen(false)} title="add component" \> \<searchinput value={searchquery} onchange={(e) => setsearchquery(e target value)} placeholder="search components " /> \<div classname="component categories"> {categories map(category => { // get components for this category const components = filtercomponents( componentregistry getcomponentsbycategory(category id) ); // skip empty categories if (components length === 0) return null; return ( \<div key={category id} classname="component category"> \<h3>{category name}\</h3> \<list> {components map(component => { // skip components that already exist on the entity if (existingcomponents includes(component type)) { return null; } return ( \<listitem key={component type} onclick={() => handleselectcomponent(component type)} \> \<div classname="component item"> \<div classname="component name">{component name}\</div> \<div classname="component description"> {component description} \</div> \</div> \</listitem> ); })} \</list> \</div> ); })} \</div> \</dialog> \</> ); }; this component displays a button that opens the add component dialog shows available components organized by category provides search functionality for finding components filters out components already on the entity adds selected components with default values ecs workflow the complete ecs workflow follows this sequence sequencediagram participant user participant hierarchypanel participant entitystate participant propertiespanel participant componentregistry participant ecscore user >>hierarchypanel clicks "create entity" hierarchypanel >>entitystate createentity() entitystate >>ecscore createentity() ecscore >>entitystate return new entity entitystate >>ecscore addcomponent(entity, 'transform', { }) entitystate >>hierarchypanel update with new entity hierarchypanel >>user show new entity in hierarchy user >>hierarchypanel selects entity hierarchypanel >>entitystate selectentities(\[entityid]) entitystate >>propertiespanel notify of selection change propertiespanel >>ecscore getentitycomponents(entity) ecscore >>propertiespanel return component list loop for each component propertiespanel >>componentregistry geteditor(componenttype) componentregistry >>propertiespanel return component editor propertiespanel >>propertiespanel render component editor end propertiespanel >>user display entity properties user >>propertiespanel clicks "add component" propertiespanel >>componentregistry show component list user >>componentregistry selects component type componentregistry >>ecscore addcomponent(entity, componenttype, defaultdata) ecscore >>propertiespanel notify of component addition propertiespanel >>user show new component editor user >>propertiespanel modifies component property propertiespanel >>ecscore updatecomponent(entity, componenttype, newdata) ecscore >>user update visual representation in scene this diagram illustrates the user creates an entity, which initializes it with a transform component the user selects an entity, which displays its components in the properties panel the user adds a component, which attaches it to the entity with default values the user modifies a component property, which updates the entity's behavior integration with other components the ecs integration connects with several other components of the world editor gizmo system the ecs integrates with the gizmo system for visual manipulation // example of gizmo system integration import { entitystate } from ' /services/entityservices'; import { hascomponent, getcomponent } from '@ir engine/ecs'; import { transformgizmosystem } from ' /systems/transformgizmosystem'; / updates the transform gizmo based on selection / export const updatetransformgizmo = () => { const entitystate = getmutablestate(entitystate); const selectedids = entitystate selectedentityids value; // get the transform gizmo system const transformgizmosystem = getsystem(transformgizmosystem); // if nothing selected, hide gizmo if (selectedids length === 0) { transformgizmosystem hide(); return; } // get the primary selected entity const primaryentityid = selectedids\[0]; const entity = entitystate entities value find(e => e id === primaryentityid); // if entity has a transform component, show gizmo if (entity && hascomponent(entity, 'transform')) { const transform = getcomponent(entity, 'transform'); transformgizmosystem show(entity, transform); } else { transformgizmosystem hide(); } }; this integration monitors entity selection changes checks if selected entities have transform components shows or hides gizmos based on component presence updates gizmo position and orientation to match entity transforms enables visual manipulation of entity properties scene serialization the ecs integrates with scene serialization for saving and loading // example of scene serialization integration import { entitystate } from ' /services/entityservices'; import { getcomponent, getallcomponents } from '@ir engine/ecs'; / serializes entities for scene saving @returns serialized entity data / export const serializeentities = () => { const entitystate = getmutablestate(entitystate); const entities = entitystate entities value; // serialize each entity const serializedentities = entities map(entity => { // get all components on the entity const components = getallcomponents(entity); // serialize each component const serializedcomponents = {}; for (const \[type, data] of object entries(components)) { serializedcomponents\[type] = data; } // return serialized entity return { id entity id, name getcomponent(entity, 'name')? value || 'entity', components serializedcomponents }; }); return serializedentities; }; this integration extracts component data from entities for serialization preserves entity ids and relationships converts component data to a format suitable for storage supports the scene saving and loading process enables persistence of entity configurations visual scripting the ecs integrates with the visual scripting system for behavior definition // example of visual scripting integration import { entitystate } from ' /services/entityservices'; import { addcomponent, getcomponent, updatecomponent } from '@ir engine/ecs'; import { visualscriptstate } from ' /services/visualscriptstate'; / attaches a visual script to an entity @param entityid id of the target entity @param scriptid id of the visual script @returns success status / export const attachvisualscript = (entityid string, scriptid string) boolean => { // get the entity const entitystate = getmutablestate(entitystate); const entity = entitystate entities value find(e => e id === entityid); if (!entity) { return false; } // get the script const script = visualscriptstate getscriptbyid(scriptid); if (!script) { return false; } // check if entity already has a script component if (hascomponent(entity, 'script')) { // update existing script component const scriptcomponent = getcomponent(entity, 'script'); updatecomponent(entity, 'script', { scriptcomponent, scriptid scriptid }); } else { // add new script component addcomponent(entity, 'script', { scriptid scriptid, enabled true, parameters {} }); } return true; }; this integration connects visual scripts to entities through script components updates script references when changes occur maintains parameters for script customization enables behavior definition through the visual scripting system provides a visual interface for entity behavior programming benefits of ecs integration the entity component system integration provides several key advantages flexibility enables creation of diverse entity types through component composition modularity separates data (components) from logic (systems) for better organization performance optimizes processing by operating on components in batches extensibility allows easy addition of new component types and systems visualization provides intuitive interfaces for component editing reusability enables sharing and reuse of component configurations maintainability simplifies debugging by isolating functionality in components these benefits create a powerful and intuitive foundation for 3d world creation next steps with an understanding of the entity component system integration, the next chapter explores how gizmos provide visual tools for manipulating entities in the 3d space next gizmos docid\ ytyi0ifpnqxbjwb6ymjnl