Repository navigation
Phase 18 EBus
Mirrored from
PHASE_18_EBUS.mdin the repo root. Edit the source file and re-sync this page rather than editing only here.
Status: Phase 18-A and Phase 18-E (managed handlers, formerly 18-B) shipped. Phase 18-C (managed-defined bus contracts) and Phase 18-D (editor surfaces) remain spec. Scope: A + B (re-scoped as 18-E) + C + D (everything: consume engine EBuses, write handlers, define new buses, ScriptCanvas / inspector surfacing). Estimated effort: the originally planned big-bang commit was broken into incremental phases; 18-A + 18-E shipped across the autumn 2025–spring 2026 audit pass, 18-C and 18-D remain open.
2026-05-19 Status note. The actual implementation deviated from the original spec below in one important way: instead of an
EBusHandler<TBus>abstract base class (the §3.B "managed handlers" approach), the shipped design uses class-level attributes + a Roslyn source generator (renumbered internally as Phase 18-E). The user surface is now:[EBus("TickBus")] public partial class GameClock : ScriptComponent { public override void OnCreate() { ConnectToTickBus(); } public override void OnDestroy() { DisconnectFromTickBus(); } [EBusHandler("OnTick")] private void HandleTick(float deltaTime, ulong frameId) { ... } }The generator (
Code/Tools/SourceGenerators/EBusHandlerGenerator.cs) emitsConnectTo<BusName>/DisconnectFrom<BusName>partial methods plus a private dispatch shim that unmarshals JSON args from the C++ side into typed parameters before invoking the user method. On the C++ side,Code/Source/Scripting/Reflection/GenericDispatcher.cppregisters aBehaviorEBusHandlerper managed handler, installs a generic hook on every event, and routes invocations back through Coral toO3DE.Reflection.EBusHandlerRegistry.DispatchEvent, keyed by a managed token.Why the deviation: the source-generator approach gives users the same authoring ergonomics as Unity/Unreal-style component callbacks without paying the cost of a managed virtual-dispatch table per bus. The original
EBusHandler<TBus>base-class approach is still viable as a follow-up if a "dynamic, no-codegen" path is ever requested, but in practice every consumer wants the attribute-driven API and the codegen handles arity / type matching at compile time, so users get a real compile error on a misspelled bus or wrong-arity handler instead of a runtime "method not found" log.The remaining sections (§3.A through §10) below are the original spec. They're kept as a historical record of the design space, with §3.B re-read against the as-shipped attribute-driven design. §3.C (managed-defined buses) and §3.D (editor surfaces) remain open.
EBus is O3DE's primary inter-system messaging primitive. The current gem gives C# scripts access to two narrow slices of it:
| Capability today | Where |
|---|---|
Receive OnTick and OnTransformChanged callbacks from C++ scripts that derive from ScriptComponent. |
Hardcoded in CSharpScriptComponent::Activate. |
| Dynamic invocation of "any" BehaviorContext method by name. |
NativeReflection.InvokeStaticMethod / InvokeInstanceMethod. |
Both gaps:
-
NativeReflection.BroadcastEBusEvent(busName, eventName, args)is documented in the README and surfaced to scripts, but the C++ dispatcher (GenericDispatcher::BroadcastEBusEvent) is the hardcoded{"error":"Not fully implemented"}stub from the 2026-05-15 audit (defect #14, partial fix in Phase 4 to surface the unimplemented dispatch but not implement it). - There is no managed equivalent of
TransformNotificationBus::Handler::BusConnectfor arbitrary buses. A script can't be a handler for, say,MyGame::PlayerDeathBuswithout writing a new C++ component first. - C# code can't define its own bus contracts. No way to say "here's an interface, any system (C++, Lua, ScriptCanvas, another C#) can implement or broadcast it."
- ScriptCanvas sees BehaviorContext-reflected types automatically; managed-defined buses don't exist there yet because nothing puts them in BehaviorContext.
Phase 18 closes all four gaps in one coordinated set of changes.
Today Phase 18
─────────────────────────────────────────────────────────────────────
Call engine bus event NativeReflection. EngineBus.Broadcast.Event(args)
BroadcastEBusEvent (typed)
(stub, returns error) + dynamic fallback works
─────────────────────────────────────────────────────────────────────
Receive engine bus Subclass-only for public class H : EBusHandler<TBus>
OnTick / OnTransformChanged + handler.Connect(addr)
─────────────────────────────────────────────────────────────────────
Define managed bus Not possible [EBus] interface IMyBus
+ auto-generated dispatcher
─────────────────────────────────────────────────────────────────────
Managed bus in Not possible Bus auto-registers in
ScriptCanvas / Inspector BehaviorContext;
SC + Inspector pick it up
Four sub-phases. All ship in one commit, but the internal structure is:
┌──────────────────────────────┐
│ Phase 18-D Editor surfaces │
│ (SC, inspector, autodoc) │
└──────────────┬───────────────┘
│ piggybacks
┌──────────────────────┴──────────────────────┐
│ │
┌──────────▼──────────┐ ┌──────────▼──────────┐
│ Phase 18-B │ │ Phase 18-C │
│ managed handlers │ │ managed bus contracts│
│ (consume native) │ │ (publish to native) │
└──────────┬──────────┘ └──────────┬──────────┘
│ │
└──────────────────────┬──────────────────────┘
│
┌──────────▼──────────┐
│ Phase 18-A │
│ dispatch + marshal │
│ (foundation) │
└─────────────────────┘
A is the foundation. B and C build on it in parallel. D drops in once B+C are merged.
Goal: NativeReflection.BroadcastEBusEvent(busName, eventName, args)
and BroadcastResultEBusEvent<T>(...) actually work.
C++ side — replace the stubs in
Code/Source/Scripting/Reflection/GenericDispatcher.cpp lines 1248–1318:
- Look up the bus by name in
BehaviorContext::m_ebuses. - Look up the event by name in the bus's
m_eventsmap. EachBehaviorEBusEventSenderhas theBroadcast,Event,Queue*variants pre-populated. - Build a
BehaviorArgument[]from the JSON-encoded args the managed side sent, using a newConvert::JsonValueToBehaviorParameter(rapidjson::Value&, const BehaviorParameter&)helper. Parallel to EditorPythonBindings'Convert::PythonToBehaviorValueParameter; same set of supported types (primitives, math, EntityId, strings, BehaviorObject handles). - Decide between
BroadcastandEventbased on whether the first arg is a bus id (heuristic) or explicit API split (preferred — see managed API below). - Invoke and marshal the result back into a JSON value.
Managed side —
Assets/Scripts/O3DE.Core/Reflection/NativeReflection.cs:
public static class NativeReflection
{
// Broadcast - sends to every connected handler on the bus.
public static void BroadcastEBusEvent(
string busName, string eventName, params object[] args);
public static T BroadcastResultEBusEvent<T>(
string busName, string eventName, params object[] args);
// Event - sends only to handlers connected at the specified bus id.
public static void SendEBusEvent<TBusId>(
string busName, string eventName, TBusId busId, params object[] args);
public static T SendResultEBusEvent<T, TBusId>(
string busName, string eventName, TBusId busId, params object[] args);
}Old BroadcastEBusEvent(busName, eventName, entityId, args) keeps
working as a deprecation-period shim that calls into SendEBusEvent.
Typed wrappers are emitted by the ClangSharp generator (see §6).
Hand-written NativeReflection API is the "I don't have generated
bindings yet" escape hatch.
Goal: any C# class can be a handler for any reflected EBus.
public class MyTransformWatcher : EBusHandler<TransformNotificationBus>
{
// Method names match the reflected event names on the bus.
public override void OnTransformChanged(Transform local, Transform world)
{
Debug.Log($"Entity {EntityId} moved to {world.Position}");
}
}
public override void OnCreate()
{
m_watcher = new MyTransformWatcher();
m_watcher.Connect(this.EntityId); // ById bus
// ...or m_watcher.Connect() on a Single bus
}
public override void OnDestroy()
{
m_watcher?.Disconnect();
m_watcher = null;
}Implementation strategy — reuse the same plumbing that
EditorPythonBindings::PythonProxyNotificationHandler uses for Python
EBus handlers. That class wraps a Coral::BehaviorEBusHandler derived
type created via bus->m_createHandler and installs a generic hook
that forwards each invocation back to script code (Python in EPB's
case, managed C# in ours).
Concrete pieces:
-
O3DE.EBusHandler<TBus>(managed): abstract base inO3DE.Core. Stores a nativeIntPtrto the underlyingBehaviorEBusHandler*. Connect / Disconnect / IsConnected / Bus accessor. -
O3DESharpManagedEBusHandler(C++): aBehaviorEBusHandlersubclass that owns a managedCoral::ManagedObjectfor the handler instance. ItsInstallGenericHookcallback marshals incomingBehaviorArgument[]into aBehaviorArgument[]representation that crosses the C# boundary (sameConvert::*helpers as 18-A, reused in the reverse direction), invokes the matching method on the managed handler by name, marshals the return value back. -
Lookup table: bus name → managed virtual-method slot indices. For dynamic dispatch (no codegen) we resolve method-by-name on first call and cache per
(bus, methodIndex). For typed wrappers (codegen) we use compile-time slot indices. -
Registration: the bus must already carry
Handler<T>()in its BehaviorContext reflection. Most engine buses do (TransformBus, TickBus, ShapeComponentRequestBus, etc.). The audit-era list of buses we know are reflected lives in the generator'sKnownBusestable; we generate wrappers only for those, and the dynamic API refuses to handle unreflected buses with a clear error. -
Threading: O3DE EBus events fire on the thread the broadcaster uses. Most engine buses are main-thread. Coral's managed dispatch acquires the CoreCLR GIL implicitly. We document that handlers run on the broadcasting thread and must not call back into managed long-running operations from non-main threads.
-
Lifecycle: managed handler instances pin themselves via a strong GC handle while connected; the C++ side stores the
void*from that handle. Disconnect releases the handle. Drop onScriptComponent.OnDestroyis automatic via a tracking list on the base class. Hot reload disconnects all handlers inOnBeforeUserAssemblyReloadand reconnects inOnAfterUserAssemblyReloadon the new types (handled by the existing Phase 13 plumbing, extended).
Goal: C# code can publish a bus that other C#, C++, ScriptCanvas, or Lua can implement and broadcast on.
[EBus(
Name = "MyGame.PlayerEventsBus",
Handler = HandlerPolicy.Multiple,
Address = AddressPolicy.ById,
BusIdType = typeof(ulong))]
public interface IPlayerEvents
{
void OnPlayerSpawned(string name);
void OnPlayerDied(string reason, int score);
// Result-returning events are allowed; first defining handler wins
// unless [EBus(Multiple = true)] uses an aggregator.
int GetCurrentScore();
}
// Broadcast / Event helpers are generated alongside the interface:
PlayerEvents.Broadcast.OnPlayerDied("fell off cliff", 1234);
int score = PlayerEvents.Broadcast.GetCurrentScore();
PlayerEvents.Event(playerEntityId).OnPlayerSpawned("Alice");
// Handle by implementing the interface and registering:
public class ScoreTracker : EBusHandler<IPlayerEvents>, IPlayerEvents
{
public void OnPlayerSpawned(string name) { ... }
public void OnPlayerDied(string reason, int score) { ... }
public int GetCurrentScore() => m_score;
}Implementation strategy:
A managed-defined bus needs to live in BehaviorContext so that
ScriptCanvas, Lua, Python, and native C++ code can see it. We can't
just keep the bus state in managed land. So:
-
C# Source Generator (Roslyn analyzer + generator) runs at
dotnet buildtime on user projects:- Finds every
interface ... : IBus(marker interface) or[EBus]-decorated interface. - Emits a partial class:
-
<Interface>Broadcaststatic dispatcher. -
<Interface>Event(busId)factory. - A
<Interface>Registrationclass with a[ModuleInitializer]that registers the bus in BehaviorContext on assembly load.
-
- Finds every
-
C++ bus shim: a templated
O3DESharpManagedEBus<TInterface>on the C++ side that:- Inherits from
AZ::EBusTraitswith the configured handler / address policies. - Has virtual methods that delegate to the interface's managed definition via reflection at registration time.
- Provides
BehaviorContext::EBus<>registration that exposes the bus to all reflection consumers (SC, Lua, Python, BehaviorContext introspection).
- Inherits from
-
Static-side dispatcher: the generated
<Interface>Broadcaststatic class translates each interface method into a call through the C++ shim'sEBus::Broadcast<&IInterface::OnMethod>(args). Compatible with multi-handler buses (the EBus library handles broadcast fan-out) and result aggregation buses. -
Bus-by-name lookup for callers without the generated wrapper:
NativeReflection.BroadcastEBusEvent("MyGame.PlayerEventsBus", ...)works the same as for engine buses — it's just BehaviorContext metadata. -
Handler discovery: when C# code implements the interface and subclasses
EBusHandler<T>, the runtime resolves the C++ shim via the interface's TypeId, instantiates the managed-side handler that 18-B already builds, and connects via the standardBehaviorEBusHandler::Connect(busId). -
Code-generation hook into the build: add a NuGet reference
O3DESharp.SourceGeneratorstoO3DE.Core.csprojand the user csproj template. Build-time only — no runtime cost.
Trade-off: a true managed-defined bus that doesn't go through BehaviorContext would be slightly faster (no native ↔ managed marshaling for managed-only callers) but invisible to non-managed consumers. The Phase 18 design accepts the marshaling cost in exchange for universal visibility, which is the whole point of "define a bus."
Once 18-A through 18-C are in, ScriptCanvas / inspector pickup is
free because both consume BehaviorContext. Only adds:
-
Doc auto-generation in the binding generator: when emitting the typed C# wrapper for an EBus, scrape Doxygen comments from the bus's header and copy them onto the generated method.
-
EBus picker in the C# Script Component inspector — a drop-down showing every bus the script subscribes to (introspected from the
EBusHandler<T>base classes the script type derives from). Purely informational; no behavior change. -
Tools → C# Scripting → Inspect EBusesdialog: lists every bus + the C# wrappers available + which buses are reflected vs not. Helps users discover what's available without having todotnet buildto find out.
All cross-boundary traffic uses the BehaviorContext marshaler. The following table is the truth table for what works:
| C# type | BehaviorContext type | Marshaling |
|---|---|---|
bool / sbyte / byte / short / ushort / int / uint / long / ulong / float / double |
matching AZ::* primitive |
direct copy |
string |
AZStd::string |
Coral::String::operator std::string() (UTF-16 → UTF-8) |
Vector3 |
AZ::Vector3 |
blittable ([StructLayout(LayoutKind.Sequential)]) — direct memcpy |
Quaternion |
AZ::Quaternion |
blittable, direct memcpy |
EntityId |
AZ::EntityId |
blittable (ulong wrapper), direct |
Transform |
AZ::Transform |
blittable (rotation+translation+scale), direct |
Any [Reflected] POD struct |
matching reflected class | per-field marshaling, generated at compile time |
Class handles (O3DE.Entity, etc.) |
BehaviorObject (raw pointer + TypeId) |
opaque ptr, lifetime managed by C++ side |
byte[] / int[] / etc. |
AZStd::vector<T> (when T is blittable) |
length-prefixed contiguous copy |
List<T> / Dictionary<K,V>
|
not supported in v1 | error at marshaling, with a clear message pointing at workarounds |
delegate / Func<>
|
not supported | "use an explicit EBusHandler<T> subclass" error |
Convert::JsonValueToBehaviorParameter (C++ side) and Convert::BehaviorParameterToJson are the central choke points. Both already exist for the EPB Python flow; we hoist them out of EPB into a shared utility under Code/Source/Scripting/Marshaling/ so our codepath doesn't depend on EditorPythonBindings being loaded.
- EBus events fire on the broadcaster's thread. Most engine buses are
main-thread but
AZ::SystemTickBuscan be any thread; physics buses often fire from the physics simulation thread. - Coral's CoreCLR doesn't require the GIL but managed code running on arbitrary threads needs to be careful with non-thread-safe APIs.
-
Rule we document: treat EBus handlers like Unity's
MonoBehaviourlifecycle methods — assume main-thread unless the bus's documentation says otherwise. - For non-main-thread buses, we provide
O3DE.Dispatcher.RunOnMainThread(Action a)(already implicit in Phase 4's single-Tick-per-frame work) so handlers can marshal back.
Connect / Disconnect lifecycle:
-
EBusHandler<T>constructor allocates a native handle (lazy: first Connect call instantiates). - Connect calls the underlying
BehaviorEBusHandler::Connect(busId). - Disconnect calls the underlying
Disconnect(). -
ScriptComponent.OnDestroywalks a tracking list of handlers created bythisand disconnects each. - Hot reload:
O3DESharpHotReloadNotificationBus::OnBeforeUserAssemblyReloaddisconnects every handler in the gem;OnAfterre-binds via the new types. Handlers without a corresponding type in the new assembly are dropped (with a warning) instead of silently leaking.
The ClangSharp binding generator (Code/Tools/BindingGenerator) gains two new emit passes:
For every AZ::EBus<TInterface, TTraits> declaration in a parsed
header, emit:
// Generated from AzCore/Component/TransformBus.h
public static class TransformBus
{
public static class Broadcast
{
public static void SetLocalTranslation(Vector3 newTranslation)
=> NativeReflection.BroadcastEBusEvent(
"TransformBus", nameof(SetLocalTranslation), newTranslation);
public static Vector3 GetWorldTranslation()
=> NativeReflection.BroadcastResultEBusEvent<Vector3>(
"TransformBus", nameof(GetWorldTranslation));
// ... one method per bus event
}
public static EventDispatcher Event(EntityId entityId) => new(entityId);
public struct EventDispatcher // generated per ById bus
{
private readonly EntityId _busId;
// ... method-per-event, dispatching via SendEBusEvent with _busId
}
public abstract class Handler : EBusHandler<TransformBus>
{
public virtual void OnTransformChanged(Transform local, Transform world) { }
public virtual void OnParentChanged(EntityId oldParent, EntityId newParent) { }
// generator walks the bus's events; each maps to a virtual method
// whose signature is the reflected event signature
}
}For every [EBus]-decorated interface in a user project, emit:
// Generated alongside <Project>.dll into <Project>.cpp + .h
namespace MyGame
{
struct PlayerEventsBusTraits : public AZ::EBusTraits
{
static const AZ::EBusHandlerPolicy HandlerPolicy = ...;
static const AZ::EBusAddressPolicy AddressPolicy = ...;
using BusIdType = ...;
};
class PlayerEventsRequests
{
public:
virtual void OnPlayerSpawned(const AZStd::string&) = 0;
// ... mirror each interface method
};
using PlayerEventsBus = AZ::EBus<PlayerEventsRequests, PlayerEventsBusTraits>;
}Plus a registration call from a CMake-generated ModuleInit block.
- C++ → C# (existing) lives in
Code/Tools/BindingGenerator/O3DESharp.BindingGenerator/. Phase 18 extendsGeneration/CSharpGenerator.csto emit EBus wrappers when the parser hits anEBus<>declaration. - C# → C++ shim is new: a Roslyn source generator under
Code/Tools/BindingGenerator/O3DESharp.SourceGenerators/. Runs as part ofdotnet buildon user projects via a NuGet package reference.
-
Convert::JsonValueToBehaviorParameterround-trips for every marshaling-table entry. Already covered for the Python direction byEditorPythonBindings/Code/Tests/PythonMarshalTests.cpp; we extend it. -
BroadcastEBusEventagainst a fixture bus with each parameter arity (0, 1, 2, 3, N) and bothBroadcastandEventshapes. - Handler lifecycle: Connect, fire event, Disconnect, fire event, expect zero callbacks after disconnect.
- Hot reload disconnects + reconnects without leaking handlers.
- Source generator output snapshot tests for representative
[EBus]interfaces. - Compile-time errors when an interface has unsupported parameter types.
- A script subscribes to
TransformNotificationBus, moves the entity, verifiesOnTransformChangedwas called. - A script defines
[EBus] interface IFoo, another script subscribes to it and broadcasts; verify the handler fired. - A C++ test broadcasts on a managed-defined bus; verify the C# handler received the event.
- ScriptCanvas: drag the C#-defined bus into a graph, fire from a game-mode session, verify the SC node received it.
| Risk | Likelihood | Mitigation |
|---|---|---|
| Bus dispatch threading bugs (handlers running on physics thread blowing up the managed runtime). | Medium | Document the rule; add a debug-build assertion that fires when a managed handler runs on a non-main thread without explicitly opting in. |
| Hot reload + active handlers = dangling pointers if reload mis-orders disconnect/reconnect. | Medium | Phase 13 plumbing already handles the script-instance side. Extend to the handler tracking list with the same Before/After pattern. Add a leak detector that fires in test builds if any handler outlives its owning script. |
| Source generator compile-time cost on large user projects. | Low | Generator only emits for [EBus]-decorated interfaces. Empty projects pay zero cost. |
| Roslyn source generator API churn between .NET versions. | Low–Medium | Target Microsoft.CodeAnalysis.CSharp >= 4.5 (compatible with everything from .NET 6 onward). |
User defines [EBus] on an interface with unsupported parameter types (delegate, generic List). |
Medium | Generator emits a compile error with a clear message pointing at the unsupported parameter. Whitelist of supported types per §4. |
Engine bus lacks Handler<T>() reflection — we can't auto-generate a wrapper. |
High | Emit a static class with Broadcast / Event only (no handler base). Log on first use. Document that a small number of buses are receiver-only. |
BehaviorContext::m_ebuses ordering changes between editor sessions and breaks our cached lookups. |
Low | Lookups are always by string name, never by index. No caching of indices across editor lifecycle. |
| Marshaling layer becomes a bottleneck for hot-path event fan-out. | Low–Medium | Phase 18 ships the correctness layer. Optimization (blittable struct fast path, AOT-friendly delegates, per-event JIT'd marshaling) is a Phase 18.1 follow-up. |
Internal sub-phases, in dependency order:
-
18-A.1 Hoist
Convert::PythonToBehaviorValueParameteretc. out of EditorPythonBindings into a sharedCode/Source/Scripting/Marshaling/BehaviorContextMarshaling.{h,cpp}. Wire EPB to use the shared utility (no behavior change there). -
18-A.2 Implement
GenericDispatcher::BroadcastEBusEvent / SendEBusEvent. AddBroadcastResultEBusEvent+SendResultEBusEvent. Wire up the matching managedNativeReflectionAPI. -
18-A.3 Extend ClangSharp generator to detect
AZ::EBus<>declarations and emit typedBroadcast/Eventwrappers. -
18-B.1 Add
O3DE.EBusHandler<T>abstract base + nativeO3DESharpManagedEBusHandlerclass. -
18-B.2 Extend ClangSharp generator to emit the per-bus
Handlernested type with virtual methods. -
18-B.3 Hook
ScriptComponent.OnDestroyto auto-disconnect handlers. Hook hot-reload notifications to disconnect/reconnect across reload cycles. -
18-C.1 Author the
O3DESharp.SourceGeneratorsRoslyn project. -
18-C.2 Generator emits the static
Broadcast/Eventdispatchers +[ModuleInitializer]BehaviorContext registration for[EBus]-marked interfaces. -
18-C.3 Add the
O3DESharpManagedEBus<TInterface>C++ shim template + registration helper. -
18-D.1 ScriptCanvas pickup happens automatically. Add the
Inspect EBuseseditor menu item. -
18-D.2 Doc comment carryover in the ClangSharp generator.
-
Tests for each sub-phase as it lands. Final commit runs all of them.
After Phase 18, the public surface a script author sees:
namespace O3DE
{
// 18-A: untyped dynamic dispatch (escape hatch).
public static class NativeReflection
{
public static void BroadcastEBusEvent(string busName, string eventName, params object[] args);
public static T BroadcastResultEBusEvent<T>(string busName, string eventName, params object[] args);
public static void SendEBusEvent(string busName, string eventName, object busId, params object[] args);
public static T SendResultEBusEvent<T>(string busName, string eventName, object busId, params object[] args);
}
// 18-B: managed handlers for any reflected bus.
public abstract class EBusHandler<TBus> : IDisposable
{
public bool IsConnected { get; }
public void Connect();
public void Connect<TBusId>(TBusId busId);
public void Disconnect();
public void Dispose();
}
// 18-C: define managed buses.
[AttributeUsage(AttributeTargets.Interface)]
public sealed class EBusAttribute : Attribute
{
public string Name { get; set; }
public HandlerPolicy Handler { get; set; } = HandlerPolicy.Multiple;
public AddressPolicy Address { get; set; } = AddressPolicy.Single;
public Type? BusIdType { get; set; }
}
public enum HandlerPolicy { Single, Multiple, MultipleAndOrdered }
public enum AddressPolicy { Single, ById, ByIdAndOrdered }
}
// 18-A typed wrappers (generated per engine bus):
namespace O3DE.AzCore // or wherever the generator places them
{
public static class TransformBus
{
public static class Broadcast { ... }
public static TransformBusEventDispatcher Event(EntityId id) => new(id);
public abstract class Handler : EBusHandler<TransformBus> { ... }
}
// ... one per discovered EBus<>
}
// 18-C typed wrappers (generated per [EBus]-marked user interface):
namespace MyGame
{
public static partial class PlayerEvents // generated counterpart to IPlayerEvents
{
public static class Broadcast { ... }
public static PlayerEventsDispatcher Event(EntityId id) => new(id);
}
}-
Queued events (
QueueBroadcast,QueueEvent). Less commonly used; add in Phase 18.1 if needed. -
Async / Task-based handler methods. Today's EBus dispatch is
synchronous; introducing
asyncintroduces re-entrancy concerns that deserve their own design pass. - Cross-process / network EBuses (replication). Out of scope — belongs to a future Multiplayer integration phase.
- Performance tuning (per-event JIT'd marshaling, NativeAOT compatibility for blittable fast paths). Phase 18.1.
The three open questions have been answered:
-
Source-generator distribution: NuGet via a local feed in the gem. Build
O3DESharp.SourceGeneratorsas a.nupkg, drop intoCode/Tools/BindingGenerator/nuget/, point user csprojs at the local feed in the csproj template. Standard Roslyn distribution path; updates land via project rebuild. Implementation in §3.C.1. -
Bus ID types: primitives +
EntityIdonly in v1. Marshaling table §4 stands as-is. Struct bus IDs hit a clear runtime error onConnectpointing at the workaround ([Reflected]POD struct via the existing reflection path). Custom struct bus IDs deferred to Phase 18.1 if needed. -
EBusHandler<TBus>as a class. Inheritance +IDisposablefor auto-disconnect onScriptComponent.OnDestroy. Struct variant withrefreturns considered for Phase 18.1 only if profiling shows allocation pressure on hot-path subscriber-heavy scenarios.
No further open decisions block implementation. Sub-phase ordering in §9 is authoritative.