Quick answer: Fetch the bus via GetNode<EventBus>("/root/EventBus") inside _Ready, not in field initializers. Rebuild the C# project after editing autoload setup.
An event bus autoload coordinates score/health updates across scenes. A C# enemy class calls bus.EmitScored(10) on death — NullReferenceException. Bus reference was captured before the autoload finished loading.
The Bug Pattern
public partial class Enemy : Node3D
{
private EventBus _bus = (EventBus)Godot.Engine.GetMainLoop(); // runs too early
public override void _Ready()
{
_bus.EmitScored(10); // _bus is null
}
}
Field initializers run during construction, before scene tree finishes setup. Autoload may not be ready.
Correct Lookup
private EventBus _bus;
public override void _Ready()
{
_bus = GetNode<EventBus>("/root/EventBus");
_bus.EmitScored(10);
}
By _Ready, autoload nodes are in /root and accessible. GetNode resolves the path.
Lazy Property
private EventBus _bus;
private EventBus Bus => _bus ??= GetNode<EventBus>("/root/EventBus");
First access fetches the bus, subsequent uses return cached. Works in any callback after the node enters the tree.
Cross-Autoload Order
Autoloads load in the order listed in Project Settings → Autoload. If AutoloadA._Ready references AutoloadB, B must be above A. Otherwise defer:
public override void _Ready()
{
CallDeferred(nameof(WireBus));
}
private void WireBus()
{
var bus = GetNode<EventBus>("/root/EventBus");
bus.Scored += OnScored;
}
Rebuild After Autoload Edit
Adding/renaming autoloads triggers a project.godot change. C# generated wrappers may need a rebuild: Build menu → Build Solution. Stale wrappers = null at runtime.
Verifying
Spawn enemy, kill it, verify Scored signal fires and listeners react. No null refs across scene reloads. Stress test: spawn 1000 enemies and kill them all; no missed signals.
“Autoload references belong in _Ready, not constructors. Lazy properties or explicit GetNode keep cross-autoload code clean.”
For larger projects, generate a strongly-typed Globals.cs that caches each autoload by GetNode call — one place to update if you rename, one type-safe accessor per autoload.