Skip to content

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():

  1. Transform player location to zone-local space.
  2. Compute penetration depth as min(DepthX, DepthY) where depth is Extent - |LocalPos| on each axis.
  3. Normalize: RawAlpha = Clamp(PenetrationDepth / TransitionDepth, 0, 1).
  4. Apply SmoothStep for 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 within TrackingClampExtent on 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 ZoneBounds and ZoneCamera subobjects with defaults. Tick starts disabled.
  • BeginPlay(): Captures AuthoredCameraLocation and AuthoredCameraOffset. Binds overlap delegates. Schedules CheckInitialOverlaps for next tick to handle zones the player spawns inside of.
  • CheckInitialOverlaps(): Queries for already-overlapping pawns and calls OnZoneEntered(this, 0.0f) with instant blend. This handles the case where the player starts inside a zone at level load.

State Transitions

Entry (OnZoneEntered)

  1. Add zone to stack, re-sort by priority.
  2. If top-of-stack did not change, return.
  3. Deactivate previous top and previous source zone tracking.
  4. Activate new top zone tracking.
  5. If position-based and there was a previous top: set SourceZone = PreviousTop, activate its tracking (live source).
  6. If time-based or no previous top: capture static blend source from InterpCamera or CameraManager.
  7. Set SmoothedAlpha = 0 (or 1 for instant entry), reset blend timers.
  8. If InterpCamera is not the current view target, position it at the source and hand off with BlendTime = 0.

Exit (OnZoneExited)

  1. 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.
  2. Remove zone from stack.
  3. If exiting zone was not the top, return.
  4. Capture blend source from InterpCamera.
  5. Determine new top zone.
  6. If new top was the source zone and alpha < 0.05: skip exit transition, set SmoothedAlpha = 1.
  7. Otherwise: start time-based exit transition to new top zone.
  8. 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

  1. Place ACameraZoneActor instances in the level.
  2. Scale ZoneBounds to cover the desired region.
  3. Position and orient ZoneCamera for the desired viewpoint.
  4. 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.
  5. Configure blend parameters per zone.
  6. For zones that should follow the player, enable bTrackPlayer and tune TrackingInterpSpeed and 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:

Log LogTemp Verbose

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).
  • CheckInitialOverlaps runs 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).