PlayerCameraHandler component that updates the player's camera field of view (FOV) each frame. It smooths FOV changes, applies aim/zoom and scope FOVs, and adjusts transition speeds based on weapon aim info and perk/tech modifiers.
using SWB.Shared;
namespace SWB.Base;
public class PlayerCameraHandler : Component
{
[Property] public float ThirdPersonAimZoom { get; set; } = 1.4f;
public Weapon Weapon { get; set; }
float targetPlayerFOV = -1;
float finalPlayerFOV;
float playerFOVSpeed = 1;
float cachedFOV = -1f;
float cachedVerticalFOV;
IPlayerBase player => Weapon.Owner;
protected override void OnStart()
{
if ( IsProxy )
Enabled = false;
}
protected override void OnEnabled()
{
targetPlayerFOV = -1;
playerFOVSpeed = 1;
}
protected override void OnDestroy()
{
if ( IsProxy || player is null ) return;
player.FieldOfView = Screen.CreateVerticalFieldOfView( Preferences.FieldOfView );
}
protected override void OnUpdate()
{
if ( targetPlayerFOV == -1 )
{
targetPlayerFOV = Preferences.FieldOfView;
finalPlayerFOV = Preferences.FieldOfView;
}
var decayRate = playerFOVSpeed * 10f;
if ( decayRate > 0f )
{
var halfLife = System.MathF.Log( 2f ) / decayRate;
finalPlayerFOV = MathX.ExponentialDecay( finalPlayerFOV, targetPlayerFOV, halfLife, RealTime.Delta );
}
if ( finalPlayerFOV != cachedFOV )
{
cachedFOV = finalPlayerFOV;
cachedVerticalFOV = Screen.CreateVerticalFieldOfView( finalPlayerFOV );
}
player.FieldOfView = cachedVerticalFOV;
// Initialize the target vectors for this frame
targetPlayerFOV = Preferences.FieldOfView;
HandleIronFOV();
}
void HandleIronFOV()
{
var isAiming = !Weapon.ShouldTuckVar && Weapon.IsAiming;
if ( isAiming && !Weapon.IsReloading )
{
var aimZoom = Weapon.AimInfo.PlayerFOVZoom;
if ( !player.IsFirstPerson && aimZoom <= 0 )
aimZoom = ThirdPersonAimZoom;
if ( aimZoom > 0 )
targetPlayerFOV = CalculateZoomedFOV( Preferences.FieldOfView, aimZoom );
if ( Weapon.IsScoping && Weapon.ScopeInfo.Magnification > 0 )
// ⚠️ Relative to the PLAYER's own FOV, so 2.5x is 2.5x for everyone.
targetPlayerFOV = Preferences.FieldOfView / Weapon.ScopeInfo.Magnification;
else if ( Weapon.IsScoping && Weapon.ScopeInfo.FOV > 0 )
targetPlayerFOV = Weapon.ScopeInfo.FOV;
// ⚠️ DEADSHOT AND QUICKDRAW BOTH SCALE THE AIM-IN ZOOM. There are two FOVs
// in an ADS — the viewmodel's and the player camera's — handled in two
// different files. Speeding up only the one in ViewModelHandler would leave
// the world zooming at its old rate behind a gun that had already finished
// moving.
//
// ⛔ THAT IS EXACTLY WHAT HAPPENED WITH THE TECH NODE. Quickdraw was wired in
// ViewModelHandler — covering the pose rate and the VIEWMODEL FOV — and this
// line was missed, so the camera kept its own zoom rate for one build. The
// two files are the whole reason this comment exists, and it still did not
// stop the second one being forgotten. If a third ADS rate ever appears,
// give all three one shared accessor rather than a third copy of this line.
//
// ⚠️ THE SECOND NODE ARRIVED, SO THE ACCESSOR ABOVE ASKED FOR NOW EXISTS.
// `TechEffects.AdsSpeedFactor` is Quickdraw x Emplacement, read identically
// here and in ViewModelHandler — a node added to it reaches both FOVs at
// once. Emplacement is why it could not wait: at x0.25 a one-sided miss
// desyncs the world zoom from the gun pose by FOUR times, where Quickdraw's
// 1.20 was barely visible and still shipped wrong for a build.
playerFOVSpeed = Weapon.AimInfo.AimInFOVSpeed
* NZombies.PerkEffects.AimSpeedMultiplierFor( Weapon )
* NZombies.TechEffects.AdsSpeedFactor( Weapon );
}
else
{
playerFOVSpeed = Weapon.AimInfo.AimOutFOVSpeed;
}
}
/// <summary>
/// Take the camera’s current FOV, convert it into a projected view size, divide that size by the zoom factor, then convert it back into a new FOV.
/// </summary>
static float CalculateZoomedFOV( float baseFOV, float zoom )
{
var halfFOV = MathX.DegreeToRadian( baseFOV ) * 0.5f;
return MathX.RadianToDegree( 2f * System.MathF.Atan( System.MathF.Tan( halfFOV ) / zoom ) );
}
}