Reference · Updated 2552.07.05.16.39

Aim Down Sights (ADS)

Camera FOV zoom while aiming. First user: the pistol/sidearm — ADS engages automatically while the pistol is drawn (holding LT already brings it up, so drawing is aiming). Built da

Camera FOV zoom while aiming. First user: the pistol/sidearm — ADS engages automatically while the pistol is drawn (holding LT already brings it up, so drawing is aiming). Built data-driven so any weapon can opt in.

Status: implemented on the grenade-refinement branch. ADS narrows the world camera FOV; the FP viewmodel (arms + weapons) is rendered at a separate, fixed first-person FOV so it does not swell with the zoom — UE's native first-person rendering does the projection split (see "The viewmodel swell fix" below). An earlier manual viewmodel counter-scale was tried and abandoned first; that history is kept as a footgun record. ⚠ C++ uncommitted; needs a full rebuild (constructor + header edits).

#Data

USLWeaponDataAsset::ADSFieldOfView (degrees, `SystemLinkADS`). 0 = no ADS zoom (default for every weapon),

so nothing changes until set. Lower = more zoom. Editor step: set DA_SL_Pistol.ADSFieldOfView (~65; the base FOV is 95, not 90) and tune.

#Implementation

ASLPlayerCharacter::TickADS(DeltaTime)local only (it's the FP camera):

  • Captures the base FOV once in BeginPlay (DefaultFieldOfView = Camera->FieldOfView).
  • Aiming = WeaponsComponent->IsSidearmActive() and the sidearm defines an ADSFieldOfView > 0.
  • ADSFOVAlpha (0 = base, 1 = ADS) advances toward the target with FMath::FInterpConstantTo at rate
  • 1 / Duration, where Duration = the sidearm's SidearmDrawDuration zooming in / SidearmHolsterDuration zooming out — so the FOV tracks the pistol coming up / going away.

  • Camera->SetFieldOfView(Lerp(DefaultFieldOfView, ADSFieldOfView, ADSFOVAlpha)), and it leaves the camera alone
  • once settled back to base (so it doesn't fight anything else that drives FOV).

#The viewmodel swell fix — separate first-person FOV (shipped 2026-07-05)

Narrowing the camera FOV for ADS magnifies the FP viewmodel too, because the arms + weapons were rendered with the same camera FOV as the world. At 95→65 that's a ~1.7× blow-up of something centimetres from the camera, and because it magnifies about the screen centre it also shoves the off-centre gun down/off-frame and shears it — the "giant pistol / gripping air" look. It is not a socket bug; re-seating the socket won't touch it.

The fix uses UE 5.5+ native first-person rendering, which renders flagged primitives at their own FOV independent of the camera:

  • Meshes (ASLCharacterBase constructor): FPMesh, FPWeaponMesh, FPSidearmMesh set
  • FirstPersonPrimitiveType = EFirstPersonPrimitiveType::FirstPerson. The sidearm mesh missing this flag was the original bug — the pistol is the sidearm, so it was the one FP mesh still rendering at the world FOV and swelling.

  • Camera (ASLPlayerCharacter constructor): bEnableFirstPersonFieldOfView = true,
  • FirstPersonFieldOfView = 95 (matched to the base FOV so the arms look identical at rest).

  • Project (Config/DefaultEngine.ini): r.FirstPerson.Enabled=True. This is a read-only render cvar — it
  • only reads at startup and gates shader permutations, so it can't be toggled at runtime; changing it needs an editor restart.

Result: ADS narrows the world FOV (the world zooms) while the viewmodel holds its size and position at the fixed FP FOV. Verified clean with the project's Substrate materials + Virtual Shadow Maps (the risk with an experimental render path — no artefacts on the FP meshes). Limitation: FirstPersonFieldOfView is hardcoded to the base 95; if a runtime FOV setting is added, drive it from the same source as the base FOV.

These live in C++ (single source of truth for every character). BP_SL_MasterChief's CDO may still carry
redundant-but-identical overrides from the spike — harmless; reset them to default post-rebuild if you want the
.uasset clean.

#Viewmodel counter-scale (superseded — tried and abandoned 2026-07-03)

This was the first attempt at the swell above, before the first-person-FOV fix — cancel the magnification by counter-scaling the viewmodel. Both implementations broke; kept as a footgun record. The camera FOV magnifies everything by tan(Default/2) / tan(Current/2), and the FP weapon sits centimeters from the camera, so it swells far more than the distant world. The plan was to scale the FP viewmodel by the inverse (ADSScaleCompensation = tan(Current/2) / tan(Default/2), ~0.64 at 95→65). Two implementations, both broke — diagnosed live via the bridge on the drawn pawn:

  • Folded into the ARMS mesh scale (ApplyFPWeaponObstructionScale): shrinking the arm rig toward its pivot
  • (which sits well below the camera) dragged the socket-attached pistol ~81 cm below the camera, out the bottom of frame → pistol invisible while aiming. It was rendering fine, just at the player's feet.

  • Applied to the PISTOL mesh only (scale in place about its socket): the pistol held its size and stayed in
  • hand, but the arms — deliberately left out to avoid the translation above — ballooned with the FOV, so a giant hand wrapped a normal pistol and read as "gripping air."

Keeping arms + weapon proportional requires scaling them together, which reintroduces the translation — solvable only by scaling the whole rig about the camera. That's effectively what UE's first-person rendering does for free, which is why the counter-scale was dropped in favour of it (see "The viewmodel swell fix" above). ApplyFPWeaponObstructionScale is back to obstruction-only (BaseScale × ObstructionRatio).

The ADSScaleCompensation member that fed this has been removed from SLPlayerCharacter.h.

Separately (Option B, this session): the FP sidearm now has its own decoupled FP scale. It's a child of the arms and used to inherit the arms' scale — which is the equipped main weapon's FirstPersonMeshScale — so the pistol's size depended on which primary you held. ApplyFPWeaponObstructionScale now divides that main-weapon baseline back out, so sidearm world scale = DA_SL_<Sidearm>.FirstPersonMeshScale × obstruction. Tune the pistol size directly via DA_SL_Pistol.FirstPersonMeshScale (live data value).

#Key detail — read the sidearm's OWN data

Read GetSidearmWeapon()->WeaponData, not WeaponsComponent->GetActiveFireWeaponData(). The active-fire indirection flips back to the primary the instant the pistol holsters (SidearmActive drops), which would lose the ADS FOV + holster duration mid-zoom-out and snap the FOV. The sidearm actor persists regardless of drawn state, so its data is stable through the whole transition.

#Footgun hit during build — FOV vibrated + FP gun looked detached

First implementation advanced the alpha with a manual signed step: Dir = (Target > Alpha) ? +1 : -1; Alpha += Dir * dt/Duration. When Alpha reaches Target, Target > Alpha is false → Dir flips to −1 and pushes Alpha back → it oscillates ±step every frame. Because the FP weapon sits right against the camera, a FOV pumping ~25° per frame flung its apparent size/position around (blurry, "gun not in hand") while the distant world looked stable. Fix: FInterpConstantTo — advances at a constant rate and stops exactly at the target. (See Docs/Footguns.md.)

#Status — build & test

⚠ Uncommitted; needs a full rebuild (constructor + header edits) and an editor restart (the r.FirstPerson.Enabled cvar only reads at startup). After that: set DA_SL_Pistol.ADSFieldOfView ≈ 65 (under the Sidearm category) and draw the pistol — the world zooms smoothly while the viewmodel holds its size and stays in hand (fixed first-person FOV), zooming back out on holster. Verified 2026-07-05 with Substrate + VSM, no FP artefacts. Tune the pistol's on-screen size via DA_SL_Pistol.FirstPersonMeshScale (see the counter-scale section's Option B note).

#Not built yet / future

  • Tighter spread + reduced sensitivity while aiming — same data-driven pattern (ADSSpreadMultiplier +
  • ADSSensitivityMultiplier on the weapon data; hook GetCurrentSpread and the look input). FOV-only for now.

  • Weapon aim pose + reticle swap — needs a centered aim anim (Maya).
  • Main-weapon ADS — would need a separate aim input (LT is taken by the sidearm draw); the auto-on-draw
  • trigger is sidearm-specific. The ADSFieldOfView data + FOV mechanism already generalise.

  • If a runtime FOV setting is added, re-cache DefaultFieldOfView when it changes so ADS respects it.
  • Docs/SidearmMode.md — the sidearm draw/holster it rides on (SidearmDrawDuration/SidearmHolsterDuration).
  • Docs/WeaponsSystem.mdGetCurrentSpread (future ADS spread hook).