Why TimeSignalBus is a Resource
The short version
TimeSignalBus extends Resource (which itself extends RefCounted) so it can be [Export]ed in the Godot inspector. This lets beginners wire a shared signal bus via drag-and-drop instead of falling back to singletons or opaque code-only construction.
If it were RefCounted alone, inspector export would be impossible. If it were Node, it would have to live in the scene tree, adding unnecessary transform overhead and tree-order dependencies.
The problem we were solving
In early versions of the plugin, TimeSignalBus was RefCounted. This created two friction points:
No inspector support — You could not drag a
TimeSignalBusinto an[Export]slot. Beginners had to create it in_Ready()and pass it around in code, which is invisible in the editor.Duplicate buses — Because there was no visible shared instance, every demo script that needed a bus created its own with
new TimeSignalBus(). Two different systems each made their own bus, wired them to the same runtime, and suddenly emitted every signal twice.
Making it a Resource solves both: one .tres (or one exported field) = one shared reference = no accidental duplication.
What "Resource" actually means here
In Godot, Resource is the base class for reference-counted data objects that can be shared, saved, and loaded. The inheritance chain is:
GodotObject
└── RefCounted
└── Resource
└── ... (custom resources)
TimeSignalBus sits at the Resource level. It is not meant to be saved to disk — its subscriptions are purely runtime state. We use Resource solely for:
[Export]support in C# scripts- Inspector visibility — you can see the field and assign it
- Shared-reference semantics — multiple nodes can hold the same instance naturally
What Resource does NOT mean here
- ❌ It is not a persistent asset you version-control as
.tres(you can, but it holds no meaningful serialized state). - ❌ It is not a
Node— it does not appear in the scene tree, have transforms, or process frames. - ❌ It is not copied per-node — it is shared by reference, which is the entire point.
The recommended wiring pattern
We enforce this pattern in all official demos:
1. Root node exports the bus
public partial class GameRoot : Node
{
/// <summary>
/// Shared signal bus. Assign one instance in the inspector
/// and every child that calls Configure() will share it.
/// </summary>
[Export] public TimeSignalBus? SignalBus { get; set; }
public override void _Ready()
{
// Fallback if nothing assigned in the inspector
SignalBus ??= new TimeSignalBus();
var provider = new WorldTimeProvider();
provider.Initialize(gameRuntime);
SignalBus.Initialize(provider.EventBus);
// Pass the shared bus to every interested child
foreach (var child in GetChildren())
{
if (child is ITimeSignalBusListener listener)
listener.Configure(SignalBus);
}
}
}
2. Child nodes receive it explicitly
public partial class ClockWidget : Label, ITimeSignalBusListener
{
private TimeSignalBus? _signalBus;
/// <summary>
/// Receives the shared bus from the parent. Call this from the root's _Ready().
/// </summary>
public void Configure(TimeSignalBus signalBus)
{
// Defensive: unwire any previous bus to avoid duplicate subscriptions
if (_signalBus != null)
_signalBus.OnTimeOfDayChanged -= OnTimeOfDayChanged;
_signalBus = signalBus;
_signalBus.OnTimeOfDayChanged += OnTimeOfDayChanged;
}
private void OnTimeOfDayChanged(HoursTime newTime, HoursTime previousTime)
{
Text = $"{newTime.Hours:D2}:{newTime.Minutes:D2}";
}
public override void _ExitTree()
{
if (_signalBus != null)
_signalBus.OnTimeOfDayChanged -= OnTimeOfDayChanged;
}
}
Why explicit Configure() instead of [Export] on every child?
You could export TimeSignalBus on every child node, but that leads to:
- Drudgery — manually assigning the same field on 20 nodes.
- Inconsistency — forgetting one node creates a null-reference bug.
- Tight coupling — child nodes know they need a bus, but the root no longer controls the wiring.
The root-export + child-configure pattern keeps dependencies visible in code while still giving beginners the inspector-friendly entry point at the top level.
⚠️ Critical caveat: shared state
Because Resource is reference-counted, every node holding the same TimeSignalBus instance sees the same subscriptions.
What goes wrong
If two different roots each call Initialize() on the same shared bus with different event sources:
// Root A
SharedBus.Initialize(runtimeA.EventBus);
// Root B (later, same shared instance)
SharedBus.Initialize(runtimeB.EventBus);
Root B's call replaces the internal _eventBus reference. Root A's runtime is now disconnected. All nodes still hold the same bus, but it only talks to runtimeB.
How to avoid it
- Initialize once — typically in your bootstrap or root
_Ready(). - Pass, don't recreate — child nodes receive the already-initialized bus via
Configure(). - Never re-Initialize a shared bus unless you are deliberately switching sources.
- Need isolation? Create separate instances:
var uiBus = new TimeSignalBus(); var aiBus = new TimeSignalBus(); uiBus.Initialize(uiRuntime.EventBus); aiBus.Initialize(aiRuntime.EventBus);
GDScript equivalent
GDScript can use the same pattern. Because TimeSignalBus is a [GlobalClass], it appears in the editor:
# root.gd
@export var signal_bus: TimeSignalBus
func _ready():
signal_bus = signal_bus if signal_bus else TimeSignalBus.new()
signal_bus.initialize(provider.event_bus)
for child in get_children():
if child.has_method("configure"):
child.configure(signal_bus)
# clock_widget.gd
var _signal_bus: TimeSignalBus
func configure(bus: TimeSignalBus):
if _signal_bus != null:
_signal_bus.time_of_day_changed.disconnect(_on_time_of_day_changed)
_signal_bus = bus
_signal_bus.time_of_day_changed.connect(_on_time_of_day_changed)
func _on_time_of_day_changed(new_hour, new_minute, new_second, prev_hour, prev_minute, prev_second):
text = "%02d:%02d" % [new_hour, new_minute]
func _exit_tree():
if _signal_bus != null:
_signal_bus.time_of_day_changed.disconnect(_on_time_of_day_changed)
FAQ
Q: Should I make my own event bus classes Resource too?
A: Only if they need inspector export or drag-and-drop sharing. For purely internal C# objects, RefCounted is fine. The rule of thumb: if a beginner needs to wire it in a scene, make it Resource.
Q: Can I save a TimeSignalBus to a .tres file?
A: Technically yes, but it is pointless — all meaningful state (event subscriptions) is runtime-only and will be empty when loaded. Treat it as a transient shared reference, not a persistent asset.
Q: What about TimeProvider? Should that also be Resource?
A: TimeProvider is intentionally RefCounted because it is typically created and managed by a bootstrap class, not assigned in the inspector. If your game needs to export a TimeProvider in scenes, consider wrapping it or creating a TimeProviderResource facade.
Q: Does making it Resource hurt performance?
A: No. Resource adds negligible overhead over RefCounted. The base class is lightweight; the bulk of the work is still your event subscriptions and signal emissions.