Classifications

The three GS_Core contract kinds in depth — TypeBase (Strategy), Emit (Observance), and Exchange (Mediator) — each with its authoring how-to, plus the naming guard and the hoisting mechanics authors need.

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

  1. Derive from the Strategy base — class MyReactor : public GS_Core::ReactorType.
  2. Reflect it to SerializeContext (and EditContext for inspector fields), exactly like any O3DE class.
  3. Done — it appears in the type-picker dropdown of every component that holds a vector of that base. No registration call.

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

  1. Handle the bus — class MyThing : public GS_Core::PulseReactorEmissionBus::Handler (C++), or drop the bus’s event-handler node into a ScriptCanvas graph.
  2. Connect at the right address — BusConnect(entityId) for per-entity buses, BusConnect() for global broadcasts.
  3. Override only the events you need. Disconnect when done.

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

  1. Call the Exchange bus with a result — GS_PhantomCam::CamCoreExchangeBus::BroadcastResult(out, &GS_PhantomCam::CamCoreExchange::GetCamCore).
  2. A single provider answers. If no provider is present, the result is the bus’s default.

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.

PieceLives 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 reflectionproducing gem
The command/query half (*Requests) until proven cross-gemproducing 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.

See Also


Get GS_Core

GS_Core — Explore this gem on the product page and add it to your project.