Camera Input Reader
Categories:
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.

Contents
- OrbitInputProvider Bus
- Per-Axis Input Style
- Authored Fields
- Runtime State
- Event Handling
- Poll Handling
- ResetPendingInput Invariant
- DynamicOrbitBody Consumption
- Sensitivity vs Halflife vs Speed
- Authoring Workflow
- See Also
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:
| Style | Source | Behavior |
|---|---|---|
Axis | Joystick / continuous analog | Last-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. |
Delta | Mouse / per-frame deltas | Accumulating 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
| Field | Default | Purpose |
|---|---|---|
m_yawEventName | "OrbitYaw" | The named event in the InputProfile that yaw should listen for. |
m_pitchEventName | "OrbitPitch" | Pitch event name. |
m_yawStyle | Axis | Per-axis style (Axis or Delta). |
m_pitchStyle | Axis | Per-axis style. |
m_yawDeltaSensitivity | 0.05 | Pixels-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_pitchDeltaSensitivity | 0.05 | Same, pitch. |
m_invertYaw | false | Invert yaw axis. |
m_invertPitch | false | Invert 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:
| Control | Lives on | Effect | When to tune |
|---|---|---|---|
Sensitivity (m_yawDeltaSensitivity / m_pitchDeltaSensitivity) | Camera Input Reader, Delta mode only | Converts 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) | DynamicOrbitBody | Degrees / 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) | DynamicOrbitBody | Solver 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
- Place
GS_CameraInputReaderComponenton the cam entity (or its child — the bus is per-entity addressed, so the entity ID must matchctx.camEntityId, which is the cam entity itself). - Assign a
GS_InputProfileasset in the parent’s slot (inherited fromGS_InputReaderComponent). - Author profile events named
OrbitYawandOrbitPitch(or rename the reader’sm_yawEventName/m_pitchEventNameto match your profile). - Set per-axis style:
- Mouse-driven cams → both axes
Delta. Tunem_*DeltaSensitivityto taste. - Joystick-driven cams → both axes
Axis. Sensitivity is ignored. - Mixed (mouse for yaw, gamepad stick for pitch) → mix as needed.
- Mouse-driven cams → both axes
- Optionally toggle
m_invertYaw/m_invertPitch.
See Also
Consumer:
Related PhantomCam pages:
- Phantom Cameras — Execution States — dormancy / reactivation lifecycle that motivates
ResetPendingInput. - Stage Pipeline — CameraContext —
ctx.snapThisFrameand the input-integration gate.
Parent input system:
Get GS_PhantomCam
GS_PhantomCam — Explore this gem on the product page and add it to your project.