Probably Playable
Back to Codons

Event Bus

Stable

Stop your AI from creating spaghetti code — clean system communication

by Probably Playable — v1.0.0

Description

The problem

When you vibe-code a game with AI, each feature gets built in isolation. The AI creates a combat system, then a UI, then a sound manager — but it wires them together with direct imports and nested callbacks. Scene A imports Scene B imports Manager C. One change breaks three files. Ask the AI to add a new feature and it rewrites half your code. This is spaghetti, and it gets worse with every prompt.

The solution

The Event Bus gives your AI a clean pattern to follow. Instead of importing modules into each other, systems talk through events:

// Sound — doesn't import Combat at all
EventBus.on('ENEMY_KILLED', () => sound.play('explosion'));

// UI — doesn’t import Combat either EventBus.on(‘ENEMY_KILLED’, (e) => updateScore(e.points));

// Combat — just announces what happened EventBus.emit(‘ENEMY_KILLED’, { id: enemy.id, points: 100 });

Three systems, zero imports between them. Adding a feature = adding a listener.

Why vibe-coders need this

  • → Your AI won't create circular dependencies — events flow one way
  • → Adding features means adding listeners, not editing existing code
  • → The event constants file acts as a map of your game that both you and your AI can read
  • → Built-in cleanup prevents the memory leaks your AI won't think about

API

Just 5 methods: on, off, once, emit, removeAll. Works with Phaser, Three.js, or plain JS. Zero dependencies.

Dependencies

None — zero external dependencies.

Phaser Three.js Vanilla JS Any Framework
Technical Details
Version
1.0.0
Status
Stable
License
MIT
Size
5 KB
Author
Probably Playable
Updated
2025-04-04
eventsarchitecturedecouplingpub-submessagingstate-management
AI Integration Skill

Drop into .claude/skills/ — your AI handles the rest.

AI Skill codon.skill.md

Event Bus — Integration Skill

Use this skill when the user asks to "add event system", "decouple components",

"pub/sub", "event bus", "event-driven architecture", or "inter-system communication".

WHAT IT DOES

A lightweight, typed publish/subscribe event system. Components emit and listen to

named events through a central bus, eliminating direct dependencies between game systems.

REQUIREMENTS

  • Framework: Any (Phaser, Three.js, vanilla JS, Node.js)
  • Language: TypeScript or JavaScript
  • Dependencies: None
  • Files: 2 TypeScript files (~120 LOC total)

FILE STRUCTURE


event-bus/
  EventBus.ts       — Singleton event emitter with emit/on/off/once/removeAll
  GameEvents.ts     — String constants for all event names (customize per project)

INSTALL

  1. Copy the event-bus/ folder into your project's src/ directory (e.g., src/events/).
    1. Import wherever needed:
    2. 
      import { EventBus } from './events/EventBus';
      import { GameEvents } from './events/GameEvents';
      

      INTEGRATION

      Basic usage:

      
      import { EventBus } from './events/EventBus';
      
      // Subscribe to an event
      EventBus.on('ENEMY_KILLED', (enemy: Enemy, killer: Tower) => {
        score += enemy.points;
        updateScoreUI(score);
      });
      
      // Emit an event from anywhere
      EventBus.emit('ENEMY_KILLED', deadEnemy, tower);
      
      // One-time listener
      EventBus.once('GAME_OVER', () => showGameOverScreen());
      
      // Unsubscribe
      const handler = (wave: number) => console.log(`Wave ${wave}`);
      EventBus.on('WAVE_START', handler);
      EventBus.off('WAVE_START', handler);
      

      Define typed event constants:

      
      // GameEvents.ts — customize these for YOUR game
      export const GameEvents = {
        // Combat
        ENEMY_SPAWNED: 'ENEMY_SPAWNED',
        ENEMY_KILLED: 'ENEMY_KILLED',
        PROJECTILE_FIRED: 'PROJECTILE_FIRED',
      
        // Progression
        WAVE_START: 'WAVE_START',
        WAVE_COMPLETE: 'WAVE_COMPLETE',
        LEVEL_UP: 'LEVEL_UP',
      
        // UI
        SCORE_UPDATED: 'SCORE_UPDATED',
        MENU_OPENED: 'MENU_OPENED',
        DIALOG_SHOWN: 'DIALOG_SHOWN',
      
        // System
        SAVE_GAME: 'SAVE_GAME',
        LOAD_GAME: 'LOAD_GAME',
        SETTINGS_CHANGED: 'SETTINGS_CHANGED',
      } as const;
      
      export type GameEvent = typeof GameEvents[keyof typeof GameEvents];
      

      Scene cleanup (prevents memory leaks):

      
      // In Phaser scene shutdown:
      class BattleScene extends Phaser.Scene {
        create() {
          EventBus.on(GameEvents.ENEMY_KILLED, this.onEnemyKilled, this);
          EventBus.on(GameEvents.WAVE_COMPLETE, this.onWaveComplete, this);
        }
      
        shutdown() {
          // Remove all listeners bound to this scene
          EventBus.removeAll(this);
        }
      }
      

      With Three.js:

      
      // In a Three.js game loop
      class EnemyManager {
        constructor() {
          EventBus.on('FRAME_UPDATE', this.update.bind(this));
        }
      
        spawnEnemy(type: string, position: THREE.Vector3) {
          const enemy = new Enemy(type, position);
          this.enemies.push(enemy);
          EventBus.emit('ENEMY_SPAWNED', { type, position, id: enemy.id });
        }
      
        destroy() {
          EventBus.removeAll(this);
        }
      }
      

      API REFERENCE

      EventBus (singleton)

      MethodSignatureDescription
      emitemit(event: string, ...args: any[]): voidFire event to all registered listeners
      onon(event: string, callback: Function, context?: any): voidSubscribe to event
      offoff(event: string, callback: Function, context?: any): voidUnsubscribe from event
      onceonce(event: string, callback: Function, context?: any): voidSubscribe for one event only
      removeAllremoveAll(context?: any): voidRemove all listeners, optionally filtered by context

      CONFIGURATION

      No configuration needed. The EventBus is a singleton — import and use.

      To customize event names, edit GameEvents.ts with your game's domain events.

      GOTCHAS

      1. Always clean up listeners — When a scene, component, or manager is destroyed, call EventBus.removeAll(this) or individually off() each listener. Leaked listeners cause memory leaks and ghost behavior.
        1. Event name typos — Always use the GameEvents constants object instead of raw strings. This gives you autocomplete and catches typos at compile time.
          1. Listener order is not guaranteed — Don't rely on the order in which listeners fire. If you need ordering, use a priority queue pattern on top.
            1. Circular event chains — Be careful with A emitting B which emits A. Add guards or use once() to prevent infinite loops.
              1. Singleton scope — There's one global bus. In a multi-game setup (e.g., iframe embeds), each game needs its own EventBus instance. Create with new EventEmitter() instead of the singleton.
                1. Phaser compatibility — If already using Phaser, the EventBus can use Phaser.Events.EventEmitter as its base class. For non-Phaser projects, a plain implementation is included.

This codon is provided "as is" without warranty of any kind, express or implied. Probably Playable assumes no responsibility for any damages, data loss, security vulnerabilities, or defects arising from its use. You are solely responsible for reviewing, testing, and validating this code before integrating it into your project. Use at your own risk.