Core components
Entity component system
Component
20 min
overview components are the data containers in the ir engine's entity component system (ecs) they represent specific aspects or characteristics of entities, such as position, health, appearance, or behavior parameters unlike traditional object oriented approaches where data and behavior are combined in classes, components in ecs are pure data structures with no inherent logic or behavior this separation allows for a more modular, flexible, and performance optimized approach to building complex applications by attaching different combinations of components to entities, developers can create diverse object types without deep inheritance hierarchies core concepts component purpose components serve several essential purposes in the ecs architecture data storage they contain the specific data that defines an aspect of an entity type definition they establish what kind of information an entity possesses aspect identification they mark entities as having particular characteristics composition building blocks they enable the creation of complex entities through combination by focusing solely on data storage, components maintain a clear separation of concerns within the ecs architecture component structure each component type has a defined structure that specifies what data fields it contains the data type of each field default values for fields when not explicitly set any validation or transformation rules this structure is defined through a schema, which serves as a blueprint for creating component instances implementation defining components components are defined using the definecomponent function, which registers a new component type with the system // import the necessary functions and types import { definecomponent, s } from '@ir engine/ecs'; // define a position component with x and y coordinates const positioncomponent = definecomponent({ name 'positioncomponent', // human readable name for debugging schema s object({ // schema defining the data structure x s number({ default 0 }), // x coordinate with default value 0 y s number({ default 0 }) // y coordinate with default value 0 }) }); // define a velocity component for movement const velocitycomponent = definecomponent({ name 'velocitycomponent', schema s object({ x s number({ default 0 }), // x velocity with default value 0 y s number({ default 0 }) // y velocity with default value 0 }) }); // define a health component const healthcomponent = definecomponent({ name 'healthcomponent', schema s object({ current s number({ default 100 }), // current health with default 100 maximum s number({ default 100 }) // maximum health with default 100 }) }); the definecomponent function takes a configuration object with a name and schema registers the component type with the ecs system sets up internal storage for component data returns a reference to the component type for later use the schema system (represented by s ) provides a way to define the structure of component data, including field types, default values, and validation rules this will be explored in more detail in the component schema docid\ bviluktf76s5iifqwxob4 chapter component operations once component types are defined, several operations can be performed with them attaching components to entities // import necessary functions import { createentity, setcomponent } from '@ir engine/ecs'; // assume positioncomponent and velocitycomponent are defined as above // create a new entity const ballentity = createentity(); // attach a position component to the entity setcomponent(ballentity, positioncomponent, { x 100, y 200 }); // attach a velocity component to the entity setcomponent(ballentity, velocitycomponent, { x 5, y 3 }); the setcomponent function takes an entity id, a component type, and component data creates or updates the component data for the specified entity registers the component as being attached to the entity retrieving component data // import necessary functions import { getcomponent } from '@ir engine/ecs'; // assume ballentity has positioncomponent attached // get the position component data for the entity const position = getcomponent(ballentity, positioncomponent); // access the component data console log(`ball position (${position x}, ${position y})`); // output ball position (100, 200) the getcomponent function takes an entity id and a component type retrieves the component data for the specified entity returns the data as an object with the structure defined in the schema checking component existence // import necessary functions import { hascomponent } from '@ir engine/ecs'; // assume ballentity has positioncomponent but not healthcomponent // check if the entity has a position component const hasposition = hascomponent(ballentity, positioncomponent); console log(`has position ${hasposition}`); // output has position true // check if the entity has a health component const hashealth = hascomponent(ballentity, healthcomponent); console log(`has health ${hashealth}`); // output has health false the hascomponent function takes an entity id and a component type checks if the entity has the specified component attached returns a boolean indicating the presence of the component removing components // import necessary functions import { removecomponent } from '@ir engine/ecs'; // assume ballentity has velocitycomponent attached // remove the velocity component from the entity removecomponent(ballentity, velocitycomponent); // verify the component was removed const stillhasvelocity = hascomponent(ballentity, velocitycomponent); console log(`still has velocity ${stillhasvelocity}`); // output still has velocity false the removecomponent function takes an entity id and a component type detaches the component from the entity cleans up any associated data component storage internals under the hood, components are typically stored in a memory efficient manner using a pattern called "structure of arrays" (soa) sequencediagram participant app as application code participant api as ecs api participant registry as component registry participant storage as component storage app >>api definecomponent(positioncomponent, schema) api >>registry register component type registry >>storage allocate arrays for x and y values storage >>registry arrays allocated registry >>api component type registered api >>app return component type reference app >>api setcomponent(entity1, positioncomponent, {x 10, y 20}) api >>registry get storage for positioncomponent registry >>api return x and y arrays api >>storage store x\[entity1]=10, y\[entity1]=20 storage >>api data stored app >>api getcomponent(entity1, positioncomponent) api >>registry get storage for positioncomponent registry >>api return x and y arrays api >>storage retrieve x\[entity1], y\[entity1] storage >>api return 10, 20 api >>app return {x 10, y 20} instead of storing each entity's components as separate objects, the system typically maintains arrays for each component field (e g , positionx\[] , positiony\[] ) uses entity ids as indices into these arrays stores component data directly in these arrays this approach improves cache locality for better performance reduces memory fragmentation enables efficient batch processing of components minimizes garbage collection overhead a simplified conceptual implementation might look like // simplified conceptual implementation (not actual code) class componentstorage { // arrays to store component data private positionx = new float32array(max entities); private positiony = new float32array(max entities); // bitset to track which entities have this component private hascomponent = new uint8array(max entities); // set component data for an entity setpositioncomponent(entity, data) { this positionx\[entity] = data x; this positiony\[entity] = data y; this hascomponent\[entity] = 1; } // get component data for an entity getpositioncomponent(entity) { if (this hascomponent\[entity]) { return { x this positionx\[entity], y this positiony\[entity] }; } return undefined; } // check if an entity has this component haspositioncomponent(entity) { return this hascomponent\[entity] === 1; } // remove component from an entity removepositioncomponent(entity) { this hascomponent\[entity] = 0; } } in practice, the ir engine often uses optimized libraries like bitecs to handle this storage efficiently practical examples creating a game character components can be combined to create complex game objects // create a player entity const playerentity = createentity(); // add position and movement components setcomponent(playerentity, positioncomponent, { x 50, y 50 }); setcomponent(playerentity, velocitycomponent, { x 0, y 0 }); // add health and status components setcomponent(playerentity, healthcomponent, { current 100, maximum 100 }); setcomponent(playerentity, statuscomponent, { isinvulnerable false, stunned false }); // add input control component setcomponent(playerentity, playerinputcomponent, { movespeed 5, jumpheight 10 }); // add visual representation component setcomponent(playerentity, spritecomponent, { texture 'player sprite', width 32, height 32 }); this approach allows for flexible entity creation by mixing and matching components as needed component based state changes components can be added or removed to change an entity's state // make the player temporarily invulnerable const status = getcomponent(playerentity, statuscomponent); status isinvulnerable = true; setcomponent(playerentity, statuscomponent, status); // add a temporary power up effect setcomponent(playerentity, powerupcomponent, { type 'speed boost', duration 10, multiplier 2 0 }); // later, when the power up expires removecomponent(playerentity, powerupcomponent); this dynamic composition allows entities to change their behavior and characteristics at runtime benefits of components the component based approach provides several key advantages data oriented design components focus on structured data, which aligns well with modern cpu architecture composition over inheritance entities are composed of components rather than inheriting from class hierarchies flexibility new entity types can be created by combining existing components in different ways modularity components can be developed and tested independently performance the structure of arrays storage pattern improves cache locality and processing efficiency clarity each component has a clear, single responsibility these benefits make components a powerful tool for building complex, performant applications next steps while components provide the data for entities, they don't contain the logic to process that data the next chapter explores how systems use components to implement behavior and logic next system docid\ zjzqkggef0ni3hx2bsm6t