Install
$ agentstack add skill-bullish0x-gamestudio-ecs-events ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
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 →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
- Loose Coupling: Systems don't directly reference each other
- Type Safety: Events are strongly typed
- Queue-Based: Events processed in order
- Frame-Delayed: Events from frame N processed in frame N+1
- No Return Values: Events are fire-and-forget
- 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.
Write a review
Versions
- v0.1.0 Imported from the upstream source.