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, `SystemLink | ADS`). 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 anADSFieldOfView > 0.
ADSFOVAlpha(0 = base, 1 = ADS) advances toward the target withFMath::FInterpConstantToat 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 (
ASLCharacterBaseconstructor):FPMesh,FPWeaponMesh,FPSidearmMeshset
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 (
ASLPlayerCharacterconstructor):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).
TheADSScaleCompensationmember that fed this has been removed fromSLPlayerCharacter.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
DefaultFieldOfViewwhen it changes so ADS respects it.
#Related
Docs/SidearmMode.md— the sidearm draw/holster it rides on (SidearmDrawDuration/SidearmHolsterDuration).
Docs/WeaponsSystem.md—GetCurrentSpread(future ADS spread hook).