The Trait System is a composition-based architecture that allows entities to gain functionality by adding modular traits. Each trait encapsulates specific behavior and can be mixed and matched to create complex entities.
Traits are reusable components that provide specific functionality to entities. Instead of inheritance, entities use composition to combine multiple traits, making the system flexible and maintainable.
Each trait follows a standard structure:
export class ExampleTrait {
private entity: GameObject;
private someProperty: any;
constructor(entity: GameObject, initialValue: any) {
this.entity = entity;
this.someProperty = initialValue;
}
// Trait-specific methods
public doSomething(): void {
// Trait functionality
}
// Cleanup when trait is removed
public destroy(): void {
// Cleanup logic
}
// Static type guard
static is(entity: GameObject): boolean {
try {
entity.getTrait('example');
return true;
} catch {
return false;
}
}
}// Add trait to entity
entity.addTrait('transform', new TransformTrait(entity, game, x, y, 'global'));
entity.addTrait('container', new ContainerTrait(entity, transformTrait));
entity.addTrait('network', new NetworkTrait(entity, game, networkConfig));
// Access trait
const transform = entity.getTrait('transform');
const position = transform.position.position;
// Check if entity has trait
if (TransformTrait.is(entity)) {
// Safe to use transform trait methods
}- Purpose: Handles entity position, rotation, and scale
- Key Properties:
position,rotation,scale,size - Usage: Required for all entities that exist in world space
const transform = TransformTrait.createLarge(game, x, y, 'global');
entity.addTrait('position', transform);
// Access position
const { x, y } = transform.position.position;- Purpose: Manages PIXI.js display objects and rendering
- Key Properties:
container(PIXI Container) - Usage: Required for all visual entities
const container = new ContainerTrait(entity, transformTrait);
entity.addTrait('container', container);
// Add sprites to container
container.container.addChild(sprite);- Purpose: Handles multiplayer synchronization
- Key Properties:
syncConfig, network connection management - Usage: Added automatically by factory system for networked entities
const networkTrait = new NetworkTrait(entity, game, {
syncTraits: ['position', 'placeable'],
syncFrequency: 'batched',
priority: 'normal'
});
entity.addTrait('network', networkTrait);- Purpose: Manages entity placement state and validation
- Key Properties:
isPlaced, placement callbacks - Usage: For entities that can be placed in the world
const placeable = new PlaceableTrait(entity, false, () => {
// Callback when entity is placed
entity.getTrait('ghostable').ghostMode = false;
});
entity.addTrait('placeable', placeable);- Purpose: Handles preview/ghost mode rendering
- Key Properties:
ghostMode, visual transparency - Usage: For entities that show placement previews
const ghostable = new GhostableTrait(entity, true); // Start in ghost mode
entity.addTrait('ghostable', ghostable);
// Toggle ghost mode
GhostableTrait.setGhostMode(entity, false);Traits can be synchronized across the network:
const networkConfig: NetworkSyncConfig = {
syncTraits: ['position', 'placeable'], // Which traits to sync
syncFrequency: 'batched', // How often to sync
priority: 'normal', // Sync priority
persistent: true // Server persistence
};- Immediate: Changes sync instantly to server
- Batched: Changes are batched and sent periodically
- Manual: Sync only when explicitly triggered
Create custom traits for specific functionality:
export class InventoryTrait {
private entity: GameObject;
private items: Array<Item> = [];
private capacity: number;
constructor(entity: GameObject, capacity: number) {
this.entity = entity;
this.capacity = capacity;
}
public addItem(item: Item): boolean {
if (this.items.length >= this.capacity) return false;
this.items.push(item);
return true;
}
public removeItem(itemId: string): Item | null {
const index = this.items.findIndex(item => item.id === itemId);
if (index === -1) return null;
return this.items.splice(index, 1)[0];
}
public getItems(): Array<Item> {
return [...this.items];
}
static is(entity: GameObject): boolean {
try {
entity.getTrait('inventory');
return true;
} catch {
return false;
}
}
}Traits can interact with each other:
export class MovementTrait {
private entity: GameObject;
public moveTo(x: number, y: number): void {
// Update position trait
const transform = this.entity.getTrait('position');
transform.position.position = { x, y, type: 'global' };
// Update container position for rendering
if (ContainerTrait.is(this.entity)) {
const container = this.entity.getTrait('container');
container.container.x = x;
container.container.y = y;
}
// Sync to server if networked
if (NetworkTrait.is(this.entity)) {
// Network trait will handle syncing position changes
}
}
}// Safe trait access with type guards
if (TransformTrait.is(entity) && ContainerTrait.is(entity)) {
const transform = entity.getTrait('position');
const container = entity.getTrait('container');
// Both traits are available
container.container.x = transform.position.position.x;
}
// Utility functions for common trait combinations
export function isRenderableEntity(entity: GameObject): boolean {
return TransformTrait.is(entity) && ContainerTrait.is(entity);
}
export function isNetworkedEntity(entity: GameObject): boolean {
return NetworkTrait.is(entity);
}- Document trait dependencies clearly
- Validate required traits in constructors
- Use type guards for safe trait access
- Always implement
destroy()methods - Clean up event listeners and references
- Remove from external systems (EntityManager, etc.)
- Keep trait state isolated and focused
- Communicate between traits through the entity
- Avoid tight coupling between traits
- Use static type guards for frequent checks
- Batch trait updates when possible
- Consider trait pooling for frequently created/destroyed entities
- Automatically manages trait lifecycles
- Calls
destroy()on all traits when entity is removed - Handles trait registration and cleanup
- NetworkTrait automatically syncs specified traits
- Trait changes trigger network updates based on sync configuration
- Server validates trait state changes
- ContainerTrait manages PIXI.js display objects
- Integrates with layer management for proper z-ordering
- Handles transform inheritance and updates
The trait system provides a flexible, maintainable foundation for entity behavior that scales from simple objects to complex game systems.