Camera zone system¶
Overview¶
The camera system provides zone-driven camera control for a single-player game. The player walks through box volumes placed in the level, and the active camera transitions between zone cameras based on overlap and priority. The system supports nested and overlapping zones, two blending modes (position-based and time-based), optional player tracking per zone, and cinematic overrides that temporarily bypass zone control.
The implementation lives in two classes:
URuneCameraSubsystem(WorldSubsystem) -- owns the zone stack, manages blend state, drives the interpolation camera each tick.ACameraZoneActor(Actor) -- defines a single zone volume with its camera, blend parameters, and optional tracking behavior.
No replication. No multiplayer support. Single local player only.
Architecture¶
Ownership Model¶
URuneCameraSubsystem (WorldSubsystem, ticks every frame)
|
|-- OverlappingZones[] priority-sorted stack of active ACameraZoneActors
|-- CinematicStack[] override targets that bypass zone logic
|-- InterpCamera hidden ACameraActor driven by Lerp each tick
|-- SourceZone live source for position-based blends (zone below top)
|
+-- ACameraZoneActor (placed in level, one per zone)
|-- ZoneBounds UBoxComponent, overlap trigger
|-- ZoneCamera UCameraComponent, the zone's camera viewpoint
The subsystem does not own the zone actors. Zone actors register themselves via overlap callbacks and unregister on end-overlap. The subsystem holds raw pointers wrapped in UPROPERTY for GC safety.
Zone Stack¶
All zones the player currently overlaps are stored in OverlappingZones, sorted descending by Priority. The topmost entry (OverlappingZones[0]) is the active zone and determines the camera target.
Only transitions that change the top-of-stack are meaningful. Entering a lower-priority zone while inside a higher-priority one produces no camera change. Exiting a non-top zone is similarly silent unless it was the current SourceZone.
Interpolation Camera¶
The subsystem spawns a hidden ACameraActor called InterpCamera. This is the actual view target for the player controller during zone blending. Each tick, the subsystem computes a blended position/rotation/FOV and writes it to InterpCamera. The player controller's SetViewTargetWithBlend is only called with BlendTime = 0 for instant handoff -- all interpolation is handled manually in Tick() to avoid conflicting with UE's camera manager blend.
Blending Modes¶
Position-Based (Default)¶
The blend alpha is derived from how far the player has penetrated into the zone's box volume. This makes the transition reversible -- walking backward reverses the blend.
The raw alpha is computed in ACameraZoneActor::GetBlendAlpha():
- Transform player location to zone-local space.
- Compute penetration depth as
min(DepthX, DepthY)where depth isExtent - |LocalPos|on each axis. - Normalize:
RawAlpha = Clamp(PenetrationDepth / TransitionDepth, 0, 1). - Apply
SmoothStepfor ease-in/ease-out.
The subsystem then applies temporal smoothing via FInterpTo(SmoothedAlpha, RawAlpha, DeltaTime, AlphaSmoothingSpeed) to reduce jitter from frame-to-frame position changes.
For position-based entry blends, both the top zone and the zone below it (the SourceZone) keep their tracking ticks active. This means the blend interpolates between two live, player-tracking cameras rather than between a frozen snapshot and a live camera. This eliminates drift when the player reverses direction mid-blend.
Designer-facing properties:
| Property | Default | Purpose |
|---|---|---|
| TransitionDepth | 250 | Distance in units from zone edge to full blend |
| AlphaSmoothingSpeed | 12 | FInterpTo speed for smoothing raw alpha |
Time-Based (Per-Zone Option)¶
When bUseTimeBasedBlend = true on a zone, entry transitions use elapsed time divided by BlendTime instead of penetration depth. The alpha curve is SmoothStep(0, 1, T). This is non-reversible -- once triggered, the blend runs to completion regardless of player movement.
Time-based blends always use a static captured source (no live source zone), since the blend is not tied to spatial position.
Exit Transitions¶
Exiting the top zone always uses a time-based blend back to the new top zone, regardless of the zone's blend mode setting. The source is captured from InterpCamera at the moment of exit.
There is one optimization: if the source zone is being promoted to the new top and SmoothedAlpha < 0.05 at exit time, the camera was already at the source zone's position. In this case the exit transition is skipped entirely to avoid a velocity discontinuity from freezing a static snapshot.
Player Tracking¶
Each zone can independently track the player by setting bTrackPlayer = true. When tracking is active, the zone's Tick() moves ZoneCamera via VInterpTo toward a target derived from the player's position.
Tracking target computation (ComputeTrackingTarget):
- Clamped (
bClampTracking = true): The camera offset from the zone's authored position is clamped withinTrackingClampExtenton X and Y. This keeps the camera within a bounded region. - Unclamped: The camera follows the player at a fixed offset (
AuthoredCameraOffset, captured at BeginPlay).
Designer-facing properties:
| Property | Default | Purpose |
|---|---|---|
| bTrackPlayer | false | Enable player tracking |
| TrackingInterpSpeed | 3.0 | VInterpTo speed for camera follow |
| bClampTracking | true | Constrain tracking to clamp region |
| TrackingClampExtent | (100, 100) | Half-extents of the clamp region in X/Y |
Tracking is activated and deactivated by the subsystem, not by the zone itself. When activated, ActivateTracking() snaps ZoneCamera to ComputeTrackingTarget() immediately (avoiding stale positions) and enables tick. When deactivated, tick is disabled but the camera stays at its last tracked position -- it is not reset to the authored location.
Cinematic Overrides¶
PushCinematicTarget / PopCinematicTarget provide a stack-based override mechanism. While any cinematic target is active:
- Zone transitions are suppressed.
- Zone tracking is deactivated.
- The player controller's view target is set directly to the cinematic actor.
When the cinematic stack empties, RefreshActiveCamera restores zone control by blending from the current camera manager position back to the active zone.
Lifecycle and Initialization¶
Subsystem¶
Initialize(): Zeroes all state. No world interaction.Tick(): Runs every frame. Early-outs if no zone interp is active, cinematic is active, or InterpCamera is missing.Deinitialize(): Destroys the InterpCamera.
Zone Actor¶
- Constructor: Creates
ZoneBoundsandZoneCamerasubobjects with defaults. Tick starts disabled. BeginPlay(): CapturesAuthoredCameraLocationandAuthoredCameraOffset. Binds overlap delegates. SchedulesCheckInitialOverlapsfor next tick to handle zones the player spawns inside of.CheckInitialOverlaps(): Queries for already-overlapping pawns and callsOnZoneEntered(this, 0.0f)with instant blend. This handles the case where the player starts inside a zone at level load.
State Transitions¶
Entry (OnZoneEntered)¶
- Add zone to stack, re-sort by priority.
- If top-of-stack did not change, return.
- Deactivate previous top and previous source zone tracking.
- Activate new top zone tracking.
- If position-based and there was a previous top: set
SourceZone = PreviousTop, activate its tracking (live source). - If time-based or no previous top: capture static blend source from InterpCamera or CameraManager.
- Set
SmoothedAlpha = 0(or 1 for instant entry), reset blend timers. - If InterpCamera is not the current view target, position it at the source and hand off with
BlendTime = 0.
Exit (OnZoneExited)¶
- If exiting zone is the source zone (not top): freeze its camera as static source, deactivate tracking, attempt to promote next stack entry as new live source.
- Remove zone from stack.
- If exiting zone was not the top, return.
- Capture blend source from InterpCamera.
- Determine new top zone.
- If new top was the source zone and alpha < 0.05: skip exit transition, set
SmoothedAlpha = 1. - Otherwise: start time-based exit transition to new top zone.
- If no zones remain: blend back to pawn camera.
Per-Frame Tick¶
if exit_transition or time_based:
advance BlendElapsedTime
SmoothedAlpha = SmoothStep(0, 1, T)
if exit transition complete:
clear bExitTransition
set up live source if applicable
else (position-based):
RawAlpha = Zone->GetBlendAlpha(PlayerLocation)
SmoothedAlpha = FInterpTo(SmoothedAlpha, RawAlpha, DeltaTime, Zone->AlphaSmoothingSpeed)
determine source (live SourceZone camera or static capture)
determine target (active zone's ZoneCamera)
InterpCamera.Location = Lerp(Source, Target, SmoothedAlpha)
InterpCamera.Rotation = Lerp(Source, Target, SmoothedAlpha)
InterpCamera.FOV = Lerp(Source, Target, SmoothedAlpha)
Level Setup¶
- Place
ACameraZoneActorinstances in the level. - Scale
ZoneBoundsto cover the desired region. - Position and orient
ZoneCamerafor the desired viewpoint. - Set
Priority: higher values take precedence. A "Main" zone covering the whole map should have the lowest priority (e.g. 0). Nested zones should have higher priority. - Configure blend parameters per zone.
- For zones that should follow the player, enable
bTrackPlayerand tuneTrackingInterpSpeedand clamp settings.
Zones can overlap freely. The priority system ensures deterministic ordering. The player can enter and exit zones in any order.
Debug Logging¶
All debug logs use the LogTemp category. Zone and state change events log at Log verbosity. Per-tick alpha values log at Verbose verbosity (hidden by default).
To see per-tick logs in PIE, run the console command:
All actor names in logs use GetActorNameOrLabel() and will reflect the label assigned in the World Outliner.
Known Constraints¶
- Single-player only. No replication, no RPC, no authority checks.
- Z-axis is ignored in blend alpha computation (only X/Y penetration depth matters).
CheckInitialOverlapsruns one tick after BeginPlay. There is a single-frame delay before zone cameras activate for zones the player spawns inside.- Cinematic overrides do not blend between cinematic targets -- each push/pop is a direct
SetViewTargetWithBlend. - The 0.05 alpha threshold for skipping exit transitions is hardcoded. If a zone has very different spatial scale or smoothing speed, this threshold may need adjustment.
Source files¶
| File | Purpose |
|---|---|
Source/Rephrased_Demo/CameraZoneActor.h |
Zone actor class declaration |
Source/Rephrased_Demo/CameraZoneActor.cpp |
Zone actor implementation |
Source/Rephrased_Demo/RuneCameraSubsystem.h |
Subsystem class declaration |
Source/Rephrased_Demo/RuneCameraSubsystem.cpp |
Subsystem implementation |
Source/Rephrased_Demo/EffectShowcaseCamera.h |
Showcase camera (cinematic target during rune effects) |
See also: Camera authoring for the original design doc covering integration points, and Adding a camera zone for the recipe.
Change history¶
- 2026-05-23 — Gabriel Li — Title normalized (engineering systems page, not design doc framing).