Component
The Component in EliCS is a core element of the ECS (Entity-Component-System) architecture. It defines the data structure for specific aspects of entities and is designed for optimal performance by centralizing data storage in highly optimized structures. Components are not instantiated as individual objects attached to entities; instead, their data is stored in contiguous arrays (such as TypedArray
s) to maximize memory efficiency and processing speed.
Features
- Typed Data Storage: Stores component data in
TypedArray
or similar optimized structures for efficient memory usage. - Schema Definition: Developers define a schema for each component type to enforce data structure, types, and default values.
- Separation of Data and Logic: Component data is maintained in centralized storage rather than as individual objects, reducing overhead and improving cache performance.
- Flexible Data Types: Supports various data types (numerical, vectors, strings, and objects) to suit different application needs.
Implementation Overview
Under the hood, the Component class is optimized for performance by storing all data in centralized arrays. Instead of each entity having its own component instance, the component class maintains a set of arrays (e.g., a Float32Array
for numerical data) where each index corresponds to an entity's component data. This design:
- Reduces Memory Overhead: By avoiding the creation of numerous small objects.
- Improves Cache Locality: Contiguous memory storage allows faster iteration and vectorized operations.
- Enables Batch Operations: Bulk data operations can be performed directly on the arrays, offering significant performance improvements in data-intensive applications.
- Centralized Management: Makes it easier to enforce consistent data structures and perform optimizations during runtime.
Usage
The following examples demonstrate how to define a component, attach it to an entity, and access or modify its data.
Defining a Component
Define a component with the createComponent
function by providing a schema that defines the data structure and default values.
import { createComponent, Types } from 'elics';
const schema = {
isAlive: { type: Types.Boolean, default: true },
position: { type: Types.Vec3, default: [0, 0, 0] },
health: { type: Types.Float32, default: 100 },
uuid: { type: Types.String, default: '' },
object3D: { type: Types.Object, default: null },
};
const EnemyComponent = createComponent(schema);
Component Schema
The schema defines the data structure and default values for the component as a TypedSchema. Each key in the schema corresponds to a property, for example:
const schema = {
isAlive: { type: Types.Boolean, default: true },
// ... more properties
};
This example schema tells EliCS that the EnemyComponent
has a property isAlive
of type Boolean
with a default value of true
.
Tracking Component Lifecycle
If you need to track when components are added to or removed from entities, you can use Query subscriptions. This provides a more flexible and decoupled approach than component-specific callbacks:
const enemyQuery = world.queryManager.registerQuery({
required: [EnemyComponent],
});
// Track when entities gain the EnemyComponent
enemyQuery.subscribe('qualify', (entity) => {
console.log(`EnemyComponent attached to entity ${entity.index}`);
});
// Track when entities lose the EnemyComponent
enemyQuery.subscribe('disqualify', (entity) => {
const object3D = entity.getValue(EnemyComponent, 'object3D');
if (object3D) {
object3D.removeFromParent();
}
});
This approach allows you to:
- Track multiple components with a single query
- Have multiple listeners for the same component lifecycle events
- Keep component definitions pure and focused on data
- Easily enable/disable lifecycle tracking without modifying component code
Registering a Component
Before using a component with entities or queries, it must be registered with the World
instance.
world.registerComponent(EnemyComponent);
The registration process initializes the component's data storage based on the defined schema. If the component has not been registered, attaching it to an entity or using it to form queries will result in an error.
Attaching Components to an Entity
Attach the component to an entity using the addComponent
method. You can optionally provide initial data to override default values.
entity.addComponent(EnemyComponent, {
isAlive: 1,
position: [10, 20, 30],
health: 50,
uuid: 'abc123',
object3D: someObject3D,
});
Accessing and Modifying Component Data
Directly access the underlying storage arrays for performance-critical operations. The entity’s index is used to retrieve or update data.
const index = entity.index;
// Accessing values
const isAlive = EnemyComponent.data['isAlive'][index];
const position = EnemyComponent.data['position'].subarray(
index * 3,
index * 3 + 3,
);
const health = EnemyComponent.data['health'][index];
// Modifying values
EnemyComponent.data['health'][index] = 75;
EnemyComponent.data['position'].set([5, 10, 15], index * 3);
EnemyComponent.data['isAlive'][index] = 0; // Mark as not alive
API Documentation
This section provides detailed information about the Component API in EliCS.
INFO
Components are not instantiated per entity. Instead, the component object's properties manage a centralized data store that is shared across all entities. Components are registered with the World
, which initializes their storage based on the defined schema.
createComponent Function
Creates a new component with the specified schema.
function createComponent<T extends Types>(schema: TypedSchema<T>): Component<T>;
- Parameters:
schema
: The schema defining the data structure and default values for the component.
- Returns: A new
Component
instance based on the provided schema.
Component.schema
Defines the structure and default values for the component's data:
readonly schema: TypedSchema<Types>;
Component.data
Defines the component's data in optimized arrays:
readonly data: { [key: keyof schema]: TypedArray | Array<any> };
the data
property stores the component's data in optimized arrays (one for each property in the schema). Numerical and vector data are typically stored in a TypedArray
, while strings and objects are stored in regular JavaScript arrays.