This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Camera Input Reader

GS_CameraInputReaderComponent and OrbitInputProvider bus — translates input profile events into per-tick yaw / pitch deltas for DynamicOrbitBody. Per-axis Axis vs Delta style, sensitivity, buffer drain, ResetPendingInput invariant.

The Camera Input Reader translates GS_InputProfile events into per-tick yaw / pitch deltas for the DynamicOrbitBody Body stage. It subclasses GS_Core::GS_InputReaderComponent (inherits profile binding, channel filtering, deadzone handling) and publishes results on the OrbitInputProvider bus that the orbit body polls each tick.

It mirrors the GS_PlayerControllerInputReaderComponent pattern from gs_unit: the parent resolves which named events to fire; the subclass routes those events to the system that consumes them.

GS_CameraInputReaderComponent in the O3DE Inspector

 

Contents


OrbitInputProvider Bus

The Camera Input Reader implements OrbitInputProvider on the cam’s entity address. The DynamicOrbitBody binds to this bus at first Evaluate and polls it every tick.

class OrbitInputProvider : public AZ::EBusTraits
{
public:
    using BusIdType = AZ::EntityId;
    static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
    static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;

    virtual void GetOrbitInputDelta(float& outYawDelta, float& outPitchDelta) const = 0;
    virtual void ResetPendingInput() = 0;
};

Per-entity addressed, single-handler. The reader component must live on the cam entity (or a child whose ancestor walk resolves to the cam) so the bus address matches ctx.camEntityId.


Per-Axis Input Style

Each axis (yaw, pitch) authors an independent style. The reader handles the two source semantics differently:

StyleSourceBehavior
AxisJoystick / continuous analogLast-write-wins state. Held axis = continuous rotation. The input profile fires StateUpdated each frame and the reader overwrites; release fires value 0 and rotation stops.
DeltaMouse / per-frame deltasAccumulating buffer reset on poll. Per-frame pixel deltas are scaled by sensitivity and accumulated. On poll, the reader returns the accumulated value AND resets the buffer to zero. Mouse rest = no rotation.

A reader can mix styles — joystick yaw + mouse pitch, for example — by setting m_yawStyle = Axis and m_pitchStyle = Delta.


Authored Fields

FieldDefaultPurpose
m_yawEventName"OrbitYaw"The named event in the InputProfile that yaw should listen for.
m_pitchEventName"OrbitPitch"Pitch event name.
m_yawStyleAxisPer-axis style (Axis or Delta).
m_pitchStyleAxisPer-axis style.
m_yawDeltaSensitivity0.05Pixels-per-frame → axis units. Only used in Delta mode. Default 0.05 means a 20-pixel mouse swing in one frame produces 1.0 axis unit (= one tick at the body’s full m_yawSpeed).
m_pitchDeltaSensitivity0.05Same, pitch.
m_invertYawfalseInvert yaw axis.
m_invertPitchfalseInvert pitch axis.

GS_InputProfile asset assignment is inherited from GS_InputReaderComponent — author the profile separately and reference it in the component’s standard input-profile slot.


Runtime State

mutable float m_pendingYaw   = 0.0f;
mutable float m_pendingPitch = 0.0f;

mutable so the const GetOrbitInputDelta override can reset Delta-mode buffers on poll. Axis-mode buffers are written from HandleFireInput and read back unchanged.


Event Handling

When an InputProfile event fires:

HandleFireInput(eventName, value):
    if eventName == m_yawEventName:
        v = m_invertYaw ? -value : value
        if m_yawStyle == Axis:
            m_pendingYaw = v                              // last-write-wins
        else /* Delta */:
            m_pendingYaw += v * m_yawDeltaSensitivity     // accumulate

    elif eventName == m_pitchEventName:
        v = m_invertPitch ? -value : value
        if m_pitchStyle == Axis:
            m_pendingPitch = v
        else /* Delta */:
            m_pendingPitch += v * m_pitchDeltaSensitivity

Poll Handling

When the body polls the bus:

GetOrbitInputDelta(out outYawDelta, out outPitchDelta):
    outYawDelta   = m_pendingYaw
    outPitchDelta = m_pendingPitch

    if m_yawStyle   == Delta:  m_pendingYaw   = 0.0      // drain on read
    if m_pitchStyle == Delta:  m_pendingPitch = 0.0

Axis-mode buffers are NOT drained on poll — the body’s next tick reads the same held value (consistent with “held stick = continuous rotation”). Delta-mode buffers are drained because mouse-rest must mean no rotation.


ResetPendingInput Invariant

ResetPendingInput():
    m_pendingYaw   = 0.0
    m_pendingPitch = 0.0

Called by the DynamicOrbitBody when ctx.snapThisFrame == true. Critical for cams with alwaysUpdate = false:

When such a cam is dormant, mouse motion still accumulates into m_pendingYaw / m_pendingPitch via Delta-mode event handling (the input profile keeps firing). Without the snap-time reset, the cam would “burst” all the accumulated input the moment it became active.

This is defense-in-depth paired with the per-tick drain. The body polls every tick (regardless of whether it integrates the result), so Delta buffers don’t accumulate while the cam ticks but is dormant. ResetPendingInput clears any stragglers at the moment activation snap occurs.

See Phantom Cameras — Execution States for the dormancy + reactivation lifecycle.


DynamicOrbitBody Consumption

// In DynamicOrbitBody::Evaluate:

// 1. Ensure bus binding.
EnsureBusConnected(ctx.camEntityId);

// 2. Poll input deltas every tick (regardless of integration eligibility,
//    so Delta-mode buffers drain whether we integrate or not).
float yawDelta = 0.0f, pitchDelta = 0.0f;
OrbitInputProviderBus::Event(ctx.camEntityId,
    &OrbitInputProvider::GetOrbitInputDelta, yawDelta, pitchDelta);

// 3. Gate integration on input-eligibility predicate.
const bool integrateInput = (ctx.hasFocus || ctx.alwaysUpdate || ctx.blendingOut);

if (integrateInput)
{
    m_targetYaw   += yawDelta   * AZ::DegToRad(m_yawSpeed)   * dt;
    m_targetPitch += pitchDelta * AZ::DegToRad(m_pitchSpeed) * dt;
    // Apply WrapYawToPi + pitch clamp.
}

// 4. On snap: reset reader buffers.
if (ctx.snapThisFrame)
{
    OrbitInputProviderBus::Event(ctx.camEntityId,
        &OrbitInputProvider::ResetPendingInput);
}

Sensitivity vs Halflife vs Speed

A common authoring confusion — three different controls, three different purposes:

ControlLives onEffectWhen to tune
Sensitivity (m_yawDeltaSensitivity / m_pitchDeltaSensitivity)Camera Input Reader, Delta mode onlyConverts raw input units (pixels) → axis units. “How much axis movement per unit of physical input.”Mouse feel — pixel-to-rotation conversion.
Speed (m_yawSpeed / m_pitchSpeed)DynamicOrbitBodyDegrees / sec per unit axis input. “How fast the cam rotates per unit of axis movement.”Overall rotation speed at full stick / full mouse delta.
Halflife (m_blendHalflife)DynamicOrbitBodySolver damping — how fast the cam catches up to the target angles. “How responsive the cam feels.”Visual snap vs glide on the cam’s response.

Don’t conflate. Sensitivity controls input mapping; Speed controls rotation amount per input; Halflife controls visual responsiveness.


Authoring Workflow

  1. Place GS_CameraInputReaderComponent on the cam entity (or its child — the bus is per-entity addressed, so the entity ID must match ctx.camEntityId, which is the cam entity itself).
  2. Assign a GS_InputProfile asset in the parent’s slot (inherited from GS_InputReaderComponent).
  3. Author profile events named OrbitYaw and OrbitPitch (or rename the reader’s m_yawEventName / m_pitchEventName to match your profile).
  4. Set per-axis style:
    • Mouse-driven cams → both axes Delta. Tune m_*DeltaSensitivity to taste.
    • Joystick-driven cams → both axes Axis. Sensitivity is ignored.
    • Mixed (mouse for yaw, gamepad stick for pitch) → mix as needed.
  5. Optionally toggle m_invertYaw / m_invertPitch.

See Also

Consumer:

Related PhantomCam pages:

Parent input system:


Get GS_PhantomCam

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