Event Bus logo

Event Bus

Community
jame581
event-bus

Use when implementing decoupled communication between nodes — global EventBus autoload with typed signals

Overview

Publisherjame581
RepositoryGodotPrompter
Skill nameevent-bus
Stars
808
Forks
47
Bundled files
3
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 3 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by jame581 on GitHub. Read the source before you install it.

Installation

Install the Event Bus AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/jame581/GodotPrompter/tree/master/skills/event-bus
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/jame581/GodotPrompter.git /tmp/GodotPrompter
mkdir -p .claude/skills
cp -r /tmp/GodotPrompter/skills/event-bus .claude/skills/event-bus
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Event Bus in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Event Bus on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Event Bus is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Event Bus in Godot 4.3+

A global signal hub that lets unrelated nodes communicate without holding references to each other. All examples target Godot 4.3+ with no deprecated APIs.

Related skills: component-system for direct signal communication between components, csharp-signals for C#-specific signal patterns, dependency-injection for alternative decoupling approaches, ability-system for an EventBus usage example with ability events.


1. What is an Event Bus

An EventBus is a singleton autoload that acts as a central registry for signals. Instead of nodes connecting directly to each other, every node connects to (or emits on) the shared EventBus. This removes the need for one node to hold a reference to another.

Without EventBus             With EventBus
──────────────               ──────────────────────────
NodeA ──signal──► NodeB      NodeA ──emit──► EventBus ──signal──► NodeB
                                                       ──signal──► NodeC
                                                       ──signal──► NodeD

Flow diagram

┌─────────┐   emit(player_died)   ┌───────────┐   player_died   ┌──────────┐
│  NodeA  │ ────────────────────► │ EventBus  │ ───────────────► │  NodeB   │
│(Player) │                       │(Autoload) │                  │   (UI)   │
└─────────┘                       └───────────┘ ───────────────► └──────────┘
                                                  player_died    ┌──────────┐
                                                                 │  NodeC   │
                                                                 │(AudioMgr)│
                                                                 └──────────┘

NodeA emits the signal. NodeB and NodeC each connected to EventBus independently. Neither knows the other exists.


2. When to Use vs Direct Signals

ScenarioRecommended approach
Parent notifying its own childDirect signal or method call
Child notifying its parentDirect signal (bubble up)
Two nodes with the same parentDirect signal via parent
Completely unrelated nodes in the treeEvent bus
UI reacting to gameplay state changesEvent bus
Audio manager reacting to game eventsEvent bus
Data manager / save system reactingEvent bus
Tight, performance-sensitive inner loopDirect method call

Rule of thumb: if you would otherwise need get_node("../../SomeDistantNode") or a hard-coded NodePath, the event bus is a better fit.


3. Basic EventBus

Create res://autoloads/event_bus.gd (or EventBus.cs), then register it in Project → Project Settings → Globals → Autoload with the name EventBus.

GDScript (autoloads/event_bus.gd)

gdscript
extends Node

## Emitted when the player character has died.
signal player_died

## Emitted whenever the score changes.
signal score_changed(new_score: int)

## Emitted when a level finishes successfully.
signal level_completed(level_id: int)

## Emitted when the player picks up a collectible.
signal item_collected(item_name: String)

## Emitted when the player's health changes.
signal health_changed(current: int, maximum: int)

C# (Autoloads/EventBus.cs)

csharp
using Godot;

/// <summary>
/// Global signal hub. Register as an autoload named "EventBus".
/// </summary>
public partial class EventBus : Node
{
    /// <summary>Emitted when the player character has died.</summary>
    [Signal] public delegate void PlayerDiedEventHandler();

    /// <summary>Emitted whenever the score changes.</summary>
    [Signal] public delegate void ScoreChangedEventHandler(int newScore);

    /// <summary>Emitted when a level finishes successfully.</summary>
    [Signal] public delegate void LevelCompletedEventHandler(int levelId);

    /// <summary>Emitted when the player picks up a collectible.</summary>
    [Signal] public delegate void ItemCollectedEventHandler(string itemName);

    /// <summary>Emitted when the player's health changes.</summary>
    [Signal] public delegate void HealthChangedEventHandler(int current, int maximum);
}

4. Connecting to Events

Consumers connect in _ready(). In C#, always disconnect in _ExitTree() to avoid dangling delegates and memory leaks.

GDScript

gdscript
extends CanvasLayer

# GDScript connections are reference-counted and cleaned up automatically
# when the node is freed, but explicit disconnection is still good practice
# for long-lived nodes that reconnect frequently.

func _ready() -> void:
    EventBus.player_died.connect(_on_player_died)
    EventBus.score_changed.connect(_on_score_changed)
    EventBus.health_changed.connect(_on_health_changed)


func _exit_tree() -> void:
    EventBus.player_died.disconnect(_on_player_died)
    EventBus.score_changed.disconnect(_on_score_changed)
    EventBus.health_changed.disconnect(_on_health_changed)


func _on_player_died() -> void:
    $DeathScreen.show()


func _on_score_changed(new_score: int) -> void:
    $ScoreLabel.text = "Score: %d" % new_score


func _on_health_changed(current: int, maximum: int) -> void:
    $HealthBar.value = float(current) / float(maximum) * 100.0

C#

csharp
using Godot;

public partial class HudLayer : CanvasLayer
{
    private EventBus _eventBus;

    public override void _Ready()
    {
        _eventBus = GetNode<EventBus>("/root/EventBus");

        // Connect using strongly-typed delegate handlers
        _eventBus.PlayerDied      += OnPlayerDied;
        _eventBus.ScoreChanged    += OnScoreChanged;
        _eventBus.HealthChanged   += OnHealthChanged;
    }

    // IMPORTANT: Always disconnect in _ExitTree() in C#.
    // C# delegates are not automatically cleaned up when a node is freed.
    // Failing to disconnect causes the EventBus to hold a reference to the
    // freed node, leading to memory leaks and InvalidOperationExceptions.
    public override void _ExitTree()
    {
        _eventBus.PlayerDied    -= OnPlayerDied;
        _eventBus.ScoreChanged  -= OnScoreChanged;
        _eventBus.HealthChanged -= OnHealthChanged;
    }

    private void OnPlayerDied()
    {
        GetNode<Control>("DeathScreen").Show();
    }

    private void OnScoreChanged(int newScore)
    {
        GetNode<Label>("ScoreLabel").Text = $"Score: {newScore}";
    }

    private void OnHealthChanged(int current, int maximum)
    {
        GetNode<ProgressBar>("HealthBar").Value = (double)current / maximum * 100.0;
    }
}

5. Emitting Events

Producers call EventBus.<signal_name>.emit(...) (GDScript) or EmitSignal(SignalName.*) (C#). The producer does not know which nodes are listening.

GDScript

gdscript
extends CharacterBody2D

@export var max_health: int = 100
var current_health: int = max_health
var score: int = 0


func take_damage(amount: int) -> void:
    current_health = clampi(current_health - amount, 0, max_health)
    EventBus.health_changed.emit(current_health, max_health)

    if current_health == 0:
        EventBus.player_died.emit()


func add_score(points: int) -> void:
    score += points
    EventBus.score_changed.emit(score)


func collect_item(item_name: String) -> void:
    EventBus.item_collected.emit(item_name)


func complete_level(level_id: int) -> void:
    EventBus.level_completed.emit(level_id)

C#

csharp
using Godot;

public partial class Player : CharacterBody2D
{
    [Export] public int MaxHealth { get; set; } = 100;

    private int _currentHealth;
    private int _score;
    private EventBus _eventBus;

    public override void _Ready()
    {
        _currentHealth = MaxHealth;
        _eventBus = GetNode<EventBus>("/root/EventBus");
    }

    public void TakeDamage(int amount)
    {
        _currentHealth = Mathf.Clamp(_currentHealth - amount, 0, MaxHealth);
        _eventBus.EmitSignal(EventBus.SignalName.HealthChanged, _currentHealth, MaxHealth);

        if (_currentHealth == 0)
            _eventBus.EmitSignal(EventBus.SignalName.PlayerDied);
    }

    public void AddScore(int points)
    {
        _score += points;
        _eventBus.EmitSignal(EventBus.SignalName.ScoreChanged, _score);
    }

    public void CollectItem(string itemName)
    {
        _eventBus.EmitSignal(EventBus.SignalName.ItemCollected, itemName);
    }

    public void CompleteLevel(int levelId)
    {
        _eventBus.EmitSignal(EventBus.SignalName.LevelCompleted, levelId);
    }
}

6. Typed Signal Parameters

Type every signal parameter. An untyped bus degrades into "what shape is this payload?" archaeology at every call site, and typos in parameter counts only surface at runtime. For anything richer than two or three primitives, pass a small Resource or a class_name'd data object rather than growing the parameter list.

Typed signal declarations, payload-object patterns, and the C# [Signal] delegate equivalents: references/typed-signals.md


7. Anti-patterns

Four recurring failures: routing everything through the bus when a parent could just reach its own child (over-decoupling); handlers whose side effects emit further signals, so tracing one event means reading every handler; circular chains, where a listener re-emits the signal it just received and loops forever; and connecting without disconnecting in C#, which leaks the handler for the bus's lifetime.

Each anti-pattern with the failing code, why it hurts, and the fix, in GDScript and C#: references/anti-patterns.md


8. Testing

Use GUT to verify both producer-side emission (watch_signals(event_bus) then assert_signal_emitted_with_parameters(...)) and consumer-side reactions (emit on the bus, then assert on the consumer's state). Always test against the real autoload EventBus retrieved via get_tree().root.get_node("EventBus"), not a fresh instance.

See references/testing.md for full producer-side and consumer-side test files plus a GUT-helper reference table.


9. Checklist

  • EventBus autoload is registered in Project → Project Settings → Globals → Autoload
  • All signals use typed parameters (signal foo(bar: int)) — no untyped signals
  • Every consumer connects in _ready() and disconnects in _exit_tree() (mandatory in C#)
  • Producers emit through EventBus, not by calling consumer methods directly
  • No node holds a direct reference to another unrelated node just to emit or receive signals
  • Complex payloads use a Resource subclass, not a raw Dictionary
  • No handler re-emits the same signal it just received (prevents infinite loops)
  • No event bus signal used where a direct parent-child call or signal is simpler
  • GUT tests cover both emission (producer) and reception (consumer) for critical signals
  • C# handlers verified to disconnect in _ExitTree() before merging

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Event Bus AI skill do?

Use when implementing decoupled communication between nodes — global EventBus autoload with typed signals

Why use Event Bus on TypingMind?

Because you install it once and use it with any model. Event Bus is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Event Bus in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/jame581/GodotPrompter/tree/master/skills/event-bus. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Event Bus?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Event Bus?

As many as you like. As long as a model supports skills, you can use Event Bus with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Event Bus AI skill free?

Yes. It is published on GitHub by jame581 under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇