Skip to content

Component System

In ECS architecture, Components are carriers of data and behavior. Components define the properties and functionality that entities possess, and are the core building blocks of ECS architecture.

Components are concrete classes that inherit from the Component abstract base class, used for:

  • Storing entity data (such as position, velocity, health, etc.)
  • Defining behavior methods related to the data
  • Providing lifecycle callback hooks
  • Supporting serialization and debugging
import { Component, ECSComponent } from '@esengine/ecs-framework';
@ECSComponent('Position')
class Position extends Component {
x: number = 0;
y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('Health')
class Health extends Component {
current: number;
max: number;
constructor(max: number = 100) {
super();
this.max = max;
this.current = max;
}
// Components can contain behavior methods
takeDamage(damage: number): void {
this.current = Math.max(0, this.current - damage);
}
heal(amount: number): void {
this.current = Math.min(this.max, this.current + amount);
}
isDead(): boolean {
return this.current <= 0;
}
}

@ECSComponent is a required decorator for component classes, providing type identification and metadata management.

FeatureDescription
Type IdentificationProvides stable type name that remains correct after code obfuscation
Serialization SupportUses this name as type identifier during serialization/deserialization
Component RegistrationAuto-registers to ComponentRegistry, assigns unique bitmask
Debug SupportShows readable component names in debug tools and logs
@ECSComponent(typeName: string)
  • typeName: Component’s type name, recommended to use same or similar name as class name
// ✅ Correct usage
@ECSComponent('Velocity')
class Velocity extends Component {
dx: number = 0;
dy: number = 0;
}
// ✅ Recommended: Keep type name consistent with class name
@ECSComponent('PlayerController')
class PlayerController extends Component {
speed: number = 5;
}
// ❌ Wrong usage - no decorator
class BadComponent extends Component {
// Components defined this way may have issues in production:
// 1. Class name changes after minification, can't serialize correctly
// 2. Component not registered to framework, queries may fail
}

When components need serialization support, use @ECSComponent and @Serializable together:

import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
@ECSComponent('Player')
@Serializable({ version: 1 })
class PlayerComponent extends Component {
@Serialize()
name: string = '';
@Serialize()
level: number = 1;
// Fields without @Serialize() won't be serialized
private _cachedData: any = null;
}

Note: @ECSComponent’s typeName and @Serializable’s typeId can differ. If @Serializable doesn’t specify typeId, it defaults to @ECSComponent’s typeName.

Each component’s type name should be unique:

// ❌ Wrong: Two components using the same type name
@ECSComponent('Health')
class HealthComponent extends Component { }
@ECSComponent('Health') // Conflict!
class EnemyHealthComponent extends Component { }
// ✅ Correct: Use different type names
@ECSComponent('PlayerHealth')
class PlayerHealthComponent extends Component { }
@ECSComponent('EnemyHealth')
class EnemyHealthComponent extends Component { }

Each component has some built-in properties:

@ECSComponent('ExampleComponent')
class ExampleComponent extends Component {
someData: string = "example";
onAddedToEntity(): void {
console.log(`Component ID: ${this.id}`); // Unique component ID
console.log(`Entity ID: ${this.entityId}`); // Owning entity's ID
}
}

Components store the owning entity’s ID (entityId), not a direct entity reference. This is a reflection of ECS’s data-oriented design, avoiding circular references.

In practice, entity and component interactions should be handled in Systems, not within components:

@ECSComponent('Health')
class Health extends Component {
current: number;
max: number;
constructor(max: number = 100) {
super();
this.max = max;
this.current = max;
}
isDead(): boolean {
return this.current <= 0;
}
}
@ECSComponent('Damage')
class Damage extends Component {
value: number;
constructor(value: number) {
super();
this.value = value;
}
}
// Recommended: Handle logic in System
class DamageSystem extends EntitySystem {
constructor() {
super(new Matcher().all(Health, Damage));
}
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health)!;
const damage = entity.getComponent(Damage)!;
health.current -= damage.value;
if (health.isDead()) {
entity.destroy();
}
// Remove Damage component after applying damage
entity.removeComponent(damage);
}
}
}