Classifications
Categories:
The interfaces layer collects exactly three kinds of cross-gem contract. Each kind has a single, recognisable extension story — the suffix tells you which one you are looking at (see the glance test). This page covers each kind in depth and shows how to author against it.
For the contract concept and why the layer exists, start at the Interfaces overview.
Contents
TypeBase — Strategy
“Inherit this and you’re plugged in.”
A pickable polymorphic base class. An author writes a subclass, reflects it, and it auto-appears in the editor’s type-picker for any component that holds a list of that base. There is no registration call and no manifest — the O3DE SerializeContext drives the picker directly from the reflected inheritance.
TypeBase contracts are the only compiled contracts: they carry reflection (.h + .cpp) and are registered in the Core reflection aggregator so the base is reflected ahead of any gem’s subtypes.
“TypeBase” is the category name we use to talk about them — the classes themselves keep their domain name (PulseType, DialogueCondition, UiMotionTrack, …). That saved name string is the on-disk identity and is frozen: renaming it breaks existing assets.
Examples: PulseType / ReactorType (interaction pulses), WorldTriggerType / TriggerSensorType (world triggers), DialogueCondition / DialoguePerformance / DialogueEffect (dialogue graph nodes), UiMotionTrack / FeedbackMotionTrack (UI & FX motion tracks).
How to: add a new type
- Derive from the Strategy base —
class MyReactor : public GS_Core::ReactorType. - Reflect it to SerializeContext (and EditContext for inspector fields), exactly like any O3DE class.
- Done — it appears in the type-picker dropdown of every component that holds a
vectorof that base. No registration call.
Frozen name
The reflected class name string is the saved-data identity. Once shipped it must never be renamed, or assets that reference it fail to load. Rename the C++ identifier freely; keep the reflected name string stable.Emit — Observance
“Wait for this signal, then act.”
A one-way observation bus. A producer emits a signal; any number of consumers listen. If the producer is not present at runtime, the signal simply never fires — consumers degrade to silence rather than erroring.
Code shape: an EBus interface named *Emissions, aliased *EmissionBus. Each event has an empty default body, so a handler only implements the events it cares about.
Examples: PossessionEmissions (a controller possessed a unit / a unit was possessed), PulseReactorEmissions (a reactor received a pulse), TimeEmissions (world tick, day/night changed), UIPageEmissions (a page shown/hidden). The full list is in the Contract Reference.
Addressing: per-entity vs global
- By EntityId — listen to a specific entity (interaction, UI interactable, motion, possession buses). Connect at the address of the entity you care about.
- Global broadcast — listen for any occurrence of a state change (
TimeEmissions,CinematicEmissions,DialogueSequenceEmissions,UIPageEmissions). Connect to the broadcast.
The possession bus fires controller-vantage events on the controller entity and unit-vantage events on the unit entity.
Fire on resolved state, not on request
Emits fire at the moment the observable state actually changed, not when a command was issued. You can trust their timing:
- “Active camera changed” fires at blend-complete, not when a new camera was requested.
- “Page shown / hidden” fires after the show/hide animation resolves, not on the call.
- “Unit released” carries the released entity (captured before the reference clears), so the payload is meaningful rather than already-nulled.
How to: react to an event
- Handle the bus —
class MyThing : public GS_Core::PulseReactorEmissionBus::Handler(C++), or drop the bus’s event-handler node into a ScriptCanvas graph. - Connect at the right address —
BusConnect(entityId)for per-entity buses,BusConnect()for global broadcasts. - Override only the events you need. Disconnect when done.
ScriptCanvas names are preserved
When a bus was hoisted from a gem into Core, the gem keeps reflecting it to ScriptCanvas under its original bus name. Existing graphs keep resolving — the C++ type moved, the script-facing name did not. The one exception is Possession, a deliberate redesign whose name did change (see Hoisting Mechanics).Exchange — Mediator
“Ask, a provider answers.” — mostly forward-looking
A two-way request/fulfill (query) bus: a consumer asks, a single provider answers. An Exchange is created only when a bidirectional cross-gem need is proven — a pull, not a push. Until then a feature’s command surface stays an in-gem *Requests bus.
Code shape: *Exchange / *ExchangeBus.
Shipped today: only CamCoreExchange (GetCamCore — “which entity is the active camera core?”). It pairs with the CamCore lifecycle Emit and data Emit to form an observable-service trio: learn it exists (Emit) → query it (Exchange) → observe its updates (Emit).
Planned (not yet shipped): Play/Stop a sequence / UI motion / feedback emit / soundtrack / audio event; Get/Set a graph parameter; a terrain surface-info query for footstep sounds; CamManager queries. Each planned Play/Stop Exchange will pair with a completion Emit — command via Exchange, observe via Emit.
How to: query a provider
- Call the Exchange bus with a result —
GS_PhantomCam::CamCoreExchangeBus::BroadcastResult(out, &GS_PhantomCam::CamCoreExchange::GetCamCore). - A single provider answers. If no provider is present, the result is the bus’s default.
Verify before authoring
Exchange is the least-built kind. Treat anything beyondCamCoreExchange as a design sketch and confirm against source before writing against it.Hoisting Mechanics
The defining idea: a contract lives in GS_Core, but the feature gem owns and operates it. Only the neutral C++ interface moves to Core; everything that needs the producer present stays home.
| Piece | Lives in |
|---|---|
The EBus interface (*Emissions / *Exchange) or the Strategy base (*Type) | GS_Core |
| The emit / broadcast call sites (where the signal fires) | producing gem |
| The ScriptCanvas binder + its BehaviorContext reflection | producing gem |
The command/query half (*Requests) until proven cross-gem | producing gem |
An observation handler is inert without its emitter, so binding the signal into ScriptCanvas only makes sense where the emitter exists — the producing gem reflects it. GS_Core just publishes the shape.
Transparent hoist vs deliberate redesign
- Transparent hoist (same events, new home) — ScriptCanvas name, handler UUID, and translation assets are all preserved. No graph breakage. This is the norm (Time, Interaction, UI, Cinematics emits all did this).
- Deliberate redesign (events split or renamed) — the ScriptCanvas name intentionally changes and old graphs break by design. The one case is Possession: the single “possessed” event (which fired on both possess and depossess) was split into explicit possess + release, across controller and unit vantages. It is now
PossessionEmissionBus.
Possession — graphs need rewriting
Any C++, graph, or example referencing the oldUnitControllerNotificationBus / PossessedTargetUnit or UnitNotificationBus::UnitPossessed is describing a bus that no longer carries possession. Rewrite to the four-event PossessionEmissionBus (OnPossessedUnit, OnReleasedUnit, OnPossessedByController, OnReleasedByController). Note the unit standby events stayed in-gem on UnitNotificationBus.See Also
- Interfaces overview — the contract concept and the glance test.
- Contract Reference — every contract by capability + producer, with status.
- Framework API: GS_Core — the gem that houses this layer.
Get GS_Core
GS_Core — Explore this gem on the product page and add it to your project.