AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Ecs Events

skill-bullish0x-gamestudio-ecs-events · by bullish0x

Event-driven architecture in ECS including event buses, typed events, event queues, and event-based system communication

No reviews yet
0 installs
27 views
0.0% view→install

Install

$ agentstack add skill-bullish0x-gamestudio-ecs-events

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-bullish0x-gamestudio-ecs-events)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Ecs Events? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

ECS Events

When to Use

Use this skill when:

  • Systems need to communicate without tight coupling
  • Implementing reactive game logic
  • Broadcasting state changes
  • Triggering animations or effects
  • Handling user input events
  • Coordinating between systems

Core Principles

  1. Loose Coupling: Systems don't directly reference each other
  2. Type Safety: Events are strongly typed
  3. Queue-Based: Events processed in order
  4. Frame-Delayed: Events from frame N processed in frame N+1
  5. No Return Values: Events are fire-and-forget
  6. Hierarchical: Support event bubbling and capturing

Event System Implementation

1. Event Base Classes

// events/Event.ts
export interface Event {
  readonly type: string;
  readonly timestamp: number;
  cancelled?: boolean;
}

export abstract class BaseEvent implements Event {
  readonly timestamp: number;
  cancelled = false;

  constructor() {
    this.timestamp = performance.now();
  }

  get type(): string {
    return this.constructor.name;
  }

  cancel(): void {
    this.cancelled = true;
  }
}

// Example events
export class EntityDestroyedEvent extends BaseEvent {
  constructor(public readonly entity: Entity) {
    super();
  }
}

export class CollisionEvent extends BaseEvent {
  constructor(
    public readonly entityA: Entity,
    public readonly entityB: Entity,
    public readonly point: Vector3,
    public readonly normal: Vector3
  ) {
    super();
  }
}

export class DamageEvent extends BaseEvent {
  constructor(
    public readonly target: Entity,
    public readonly source: Entity | null,
    public readonly amount: number,
    public readonly damageType: string
  ) {
    super();
  }
}

export class InputEvent extends BaseEvent {
  constructor(
    public readonly action: string,
    public readonly pressed: boolean
  ) {
    super();
  }
}

2. Event Bus

// events/EventBus.ts
type EventHandler = (event: T) => void;
type EventType = new (...args: any[]) => T;

export class EventBus {
  private listeners = new Map>>();
  private eventQueue: Event[] = [];
  private processingQueue: Event[] = [];
  private isProcessing = false;

  // Subscribe to events
  on(
    eventType: EventType,
    handler: EventHandler
  ): () => void {
    const typeName = eventType.name;
    let handlers = this.listeners.get(typeName);

    if (!handlers) {
      handlers = new Set();
      this.listeners.set(typeName, handlers);
    }

    handlers.add(handler);

    // Return unsubscribe function
    return () => {
      const handlers = this.listeners.get(typeName);
      if (handlers) {
        handlers.delete(handler);
        if (handlers.size === 0) {
          this.listeners.delete(typeName);
        }
      }
    };
  }

  // Unsubscribe from events
  off(
    eventType: EventType,
    handler: EventHandler
  ): void {
    const typeName = eventType.name;
    const handlers = this.listeners.get(typeName);
    if (handlers) {
      handlers.delete(handler);
      if (handlers.size === 0) {
        this.listeners.delete(typeName);
      }
    }
  }

  // Emit event (queued for next frame)
  emit(event: T): void {
    if (this.isProcessing) {
      // If we're currently processing events, add to next frame's queue
      this.eventQueue.push(event);
    } else {
      this.eventQueue.push(event);
    }
  }

  // Emit event immediately (use sparingly)
  emitImmediate(event: T): void {
    this.dispatchEvent(event);
  }

  // Process all queued events
  processEvents(): void {
    if (this.isProcessing) {
      console.warn('Already processing events');
      return;
    }

    // Swap queues
    this.processingQueue = this.eventQueue;
    this.eventQueue = [];
    this.isProcessing = true;

    // Process all events
    for (const event of this.processingQueue) {
      if (!event.cancelled) {
        this.dispatchEvent(event);
      }
    }

    this.processingQueue = [];
    this.isProcessing = false;
  }

  private dispatchEvent(event: Event): void {
    const handlers = this.listeners.get(event.type);
    if (handlers) {
      for (const handler of handlers) {
        handler(event);
        if (event.cancelled) break;
      }
    }
  }

  // Clear all listeners
  clear(): void {
    this.listeners.clear();
    this.eventQueue = [];
    this.processingQueue = [];
  }

  // Get statistics
  getStats(): { listeners: number; queuedEvents: number } {
    let totalListeners = 0;
    this.listeners.forEach((handlers) => {
      totalListeners += handlers.size;
    });

    return {
      listeners: totalListeners,
      queuedEvents: this.eventQueue.length,
    };
  }
}

3. Event-Driven Systems

// systems/EventDrivenSystem.ts
export abstract class EventDrivenSystem extends UpdateSystem {
  protected eventBus: EventBus;
  private unsubscribers: Array void> = [];

  constructor(eventBus: EventBus) {
    super();
    this.eventBus = eventBus;
  }

  // Helper to subscribe to events
  protected subscribe(
    eventType: EventType,
    handler: EventHandler
  ): void {
    const unsubscribe = this.eventBus.on(eventType, handler);
    this.unsubscribers.push(unsubscribe);
  }

  cleanup(): void {
    // Unsubscribe from all events
    this.unsubscribers.forEach((unsub) => unsub());
    this.unsubscribers = [];
  }
}

// Example: Health system that listens to damage events
import { Health } from '../components/Health';
import { DamageEvent } from '../events/DamageEvent';

export class HealthSystem extends EventDrivenSystem {
  constructor(eventBus: EventBus) {
    super(eventBus);
    this.subscribe(DamageEvent, (event) => this.onDamage(event));
  }

  update(world: World, deltaTime: number): void {
    // Regular health regeneration
    const entities = world.query([Health]);

    entities.iterate((entity, [health]) => {
      if (health.current  {
  handler: EventHandler;
  priority: number;
}

export class PriorityEventBus extends EventBus {
  private prioritizedListeners = new Map[]>();

  onWithPriority(
    eventType: EventType,
    handler: EventHandler,
    priority: number = 0
  ): () => void {
    const typeName = eventType.name;
    let handlers = this.prioritizedListeners.get(typeName);

    if (!handlers) {
      handlers = [];
      this.prioritizedListeners.set(typeName, handlers);
    }

    handlers.push({ handler, priority });
    // Sort by priority (higher numbers run first)
    handlers.sort((a, b) => b.priority - a.priority);

    return () => {
      const handlers = this.prioritizedListeners.get(typeName);
      if (handlers) {
        const index = handlers.findIndex((h) => h.handler === handler);
        if (index !== -1) {
          handlers.splice(index, 1);
        }
      }
    };
  }

  protected dispatchEvent(event: Event): void {
    const handlers = this.prioritizedListeners.get(event.type);
    if (handlers) {
      for (const { handler } of handlers) {
        handler(event);
        if (event.cancelled) break;
      }
    }
  }
}

5. Entity Events

// events/EntityEventBus.ts
export class EntityEventBus extends EventBus {
  private entityListeners = new Map>>>();

  // Subscribe to events from a specific entity
  onEntity(
    entity: Entity,
    eventType: EventType,
    handler: EventHandler
  ): () => void {
    const typeName = eventType.name;
    let entityHandlers = this.entityListeners.get(entity);

    if (!entityHandlers) {
      entityHandlers = new Map();
      this.entityListeners.set(entity, entityHandlers);
    }

    let handlers = entityHandlers.get(typeName);
    if (!handlers) {
      handlers = new Set();
      entityHandlers.set(typeName, handlers);
    }

    handlers.add(handler);

    return () => {
      const entityHandlers = this.entityListeners.get(entity);
      if (entityHandlers) {
        const handlers = entityHandlers.get(typeName);
        if (handlers) {
          handlers.delete(handler);
        }
      }
    };
  }

  // Emit event from specific entity
  emitFrom(entity: Entity, event: T): void {
    // Emit to entity-specific listeners
    const entityHandlers = this.entityListeners.get(entity);
    if (entityHandlers) {
      const handlers = entityHandlers.get(event.type);
      if (handlers) {
        for (const handler of handlers) {
          handler(event);
          if (event.cancelled) return;
        }
      }
    }

    // Also emit to global listeners
    this.emit(event);
  }

  // Clean up entity listeners
  clearEntity(entity: Entity): void {
    this.entityListeners.delete(entity);
  }
}

6. Event Recorder (Replay/Debug)

// events/EventRecorder.ts
export class EventRecorder {
  private recordings: Event[] = [];
  private isRecording = false;
  private eventBus: EventBus;

  constructor(eventBus: EventBus) {
    this.eventBus = eventBus;
  }

  startRecording(): void {
    this.isRecording = true;
    this.recordings = [];

    // Intercept all events
    const originalEmit = this.eventBus.emit.bind(this.eventBus);
    this.eventBus.emit = (event: Event) => {
      if (this.isRecording) {
        this.recordings.push(this.cloneEvent(event));
      }
      originalEmit(event);
    };
  }

  stopRecording(): Event[] {
    this.isRecording = false;
    return this.recordings;
  }

  replay(events: Event[]): void {
    for (const event of events) {
      this.eventBus.emitImmediate(event);
    }
  }

  save(filename: string): void {
    const json = JSON.stringify(this.recordings, null, 2);
    // Save to file (implementation depends on environment)
  }

  load(json: string): void {
    this.recordings = JSON.parse(json);
  }

  private cloneEvent(event: Event): Event {
    return Object.assign(Object.create(Object.getPrototypeOf(event)), event);
  }
}

Usage Examples

// Example 1: Basic event usage
const eventBus = new EventBus();

// Subscribe to events
eventBus.on(DamageEvent, (event) => {
  console.log(`${event.target.id} took ${event.amount} damage`);
});

// Emit events
eventBus.emit(new DamageEvent(player, enemy, 25, 'physical'));

// Process events (once per frame)
function gameLoop(deltaTime: number): void {
  eventBus.processEvents();
  systemManager.update(world, deltaTime);
}

// Example 2: Unsubscribing
const unsubscribe = eventBus.on(CollisionEvent, (event) => {
  console.log('Collision!');
});

// Later...
unsubscribe(); // Stop listening

// Example 3: Event-driven animation system
class AnimationEventSystem extends EventDrivenSystem {
  constructor(eventBus: EventBus) {
    super(eventBus);
    this.subscribe(DamageEvent, (event) => this.onDamage(event));
    this.subscribe(DeathEvent, (event) => this.onDeath(event));
    this.subscribe(JumpEvent, (event) => this.onJump(event));
  }

  update(world: World, deltaTime: number): void {
    // Normal animation updates
  }

  private onDamage(event: DamageEvent): void {
    const animator = event.target.getComponent(AnimationController);
    if (animator) {
      animator.play('hit', { oneShot: true });
    }
  }

  private onDeath(event: DeathEvent): void {
    const animator = event.entity.getComponent(AnimationController);
    if (animator) {
      animator.play('death', { loop: false });
    }
  }

  private onJump(event: JumpEvent): void {
    const animator = event.entity.getComponent(AnimationController);
    if (animator) {
      animator.play('jump', { oneShot: true });
    }
  }
}

// Example 4: Input event system
class InputEventSystem extends UpdateSystem {
  constructor(private eventBus: EventBus) {
    super();
    this.setupInputListeners();
  }

  private setupInputListeners(): void {
    window.addEventListener('keydown', (e) => {
      this.eventBus.emit(new InputEvent(e.key, true));
    });

    window.addEventListener('keyup', (e) => {
      this.eventBus.emit(new InputEvent(e.key, false));
    });
  }

  update(world: World, deltaTime: number): void {
    // Input system doesn't need update
  }
}

class PlayerControllerSystem extends EventDrivenSystem {
  constructor(eventBus: EventBus) {
    super(eventBus);
    this.subscribe(InputEvent, (event) => this.onInput(event));
  }

  update(world: World, deltaTime: number): void {
    // Update player movement based on input
  }

  private onInput(event: InputEvent): void {
    const player = world.query([Player, Velocity]).first();
    if (!player) return;

    const [, velocity] = player.getComponents(Player, Velocity);

    if (event.action === 'w' && event.pressed) {
      velocity.z = -5;
    } else if (event.action === 's' && event.pressed) {
      velocity.z = 5;
    } else if (event.action === 'Space' && event.pressed) {
      this.eventBus.emit(new JumpEvent(player));
    }
  }
}

// Example 5: Combat system with events
class CombatSystem extends EventDrivenSystem {
  constructor(eventBus: EventBus) {
    super(eventBus);
    this.subscribe(AttackEvent, (event) => this.onAttack(event));
  }

  update(world: World, deltaTime: number): void {
    // Check for attacks
    const attackers = world.query([Transform, Attack]);

    attackers.iterate((entity, [transform, attack]) => {
      attack.cooldown = Math.max(0, attack.cooldown - deltaTime);

      if (attack.wantsToAttack && attack.cooldown === 0) {
        this.eventBus.emit(new AttackEvent(entity, transform.position));
        attack.cooldown = attack.cooldownTime;
      }
    });
  }

  private onAttack(event: AttackEvent): void {
    // Find targets in range
    const spatialQuery = new SpatialQuery(world);
    const targets = spatialQuery.withinRadius(
      event.position,
      5, // Attack range
      [Health, Transform]
    );

    for (const target of targets) {
      if (target !== event.attacker) {
        this.eventBus.emit(new DamageEvent(target, event.attacker, 10, 'physical'));
      }
    }
  }
}

// Example 6: Sound system listening to events
class SoundSystem extends EventDrivenSystem {
  private audioContext: AudioContext;

  constructor(eventBus: EventBus) {
    super(eventBus);
    this.audioContext = new AudioContext();

    this.subscribe(DamageEvent, (event) => this.playSound('hit'));
    this.subscribe(DeathEvent, (event) => this.playSound('death'));
    this.subscribe(JumpEvent, (event) => this.playSound('jump'));
    this.subscribe(CollisionEvent, (event) => this.playSound('collision'));
  }

  update(world: World, deltaTime: number): void {
    // Sound system doesn't need update
  }

  private playSound(name: string): void {
    // Play sound using Web Audio API
  }
}

// Example 7: Event cancellation
eventBus.on(DamageEvent, (event) => {
  const armor = event.target.getComponent(Armor);
  if (armor && armor.isInvulnerable) {
    event.cancel(); // Prevent damage
  }
});

Event Patterns

Pattern 1: Event Chains

// Events can trigger other events
class DeathSystem extends EventDrivenSystem {
  constructor(eventBus: EventBus) {
    super(eventBus);
    this.subscribe(DamageEvent, (event) => {
      const health = event.target.getComponent(Health);
      if (health && health.current  {
      // Death event triggers loot drop event
      this.eventBus.emit(new LootDropEvent(event.entity));

      // And explosion effect event
      const transform = event.entity.getComponent(Transform);
      if (transform) {
        this.eventBus.emit(new ExplosionEvent(transform.position));
      }
    });
  }
}

Pattern 2: Event Filtering

// Only process events that meet criteria
eventBus.on(DamageEvent, (event) => {
  // Only log critical hits
  if (event.amount > 50) {
    console.log('CRITICAL HIT!');
  }
});

// Only process events for specific entity types
eventBus.on(CollisionEvent, (event) => {
  const isPlayer = event.entityA.hasComponent(Player) || event.entityB.hasComponent(Player);
  if (!isPlayer) return; // Ignore non-player collisions

  // Process player collision
});

Pattern 3: Event Aggregation

// Collect events over time for batch processing
cl

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [bullish0x](https://github.com/bullish0x)
- **Source:** [bullish0x/GameStudio](https://github.com/bullish0x/GameStudio)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.