things/player/Player.Camera.cs

Camera-related code for the Player partial class. It controls camera positioning, field of view, orthographic height, bounds clamping, camera shake management, and provides RPC/utility to trigger camera shake.

Networking
using System;

public struct CamShakeData
{
	public float strength;
	public float startTime;
	public float time;
	public EasingType easingType;
	public bool useRealTime;

	public CamShakeData( float _strength, float _startTime, float _time, EasingType _easingType, bool _useRealTime )
	{
		strength = _strength;
		startTime = _startTime;
		time = _time;
		easingType = _easingType;
		useRealTime = _useRealTime;
	}
}

public partial class Player
{
	private List<CamShakeData> _camShakeDatas = new();

	[Sync] public bool ShouldOverrideCameraPos { get; set; }
	[Sync] public Vector2 CameraTargetPosOverride { get; set; }

	[Sync] public float CameraHurtZoomAmount { get; set; }

	public void HandleCamera()
	{
		ApplyCameraView();
	}

	public void ApplyCameraView( bool allowHurtZoomDecay = true )
	{
		var camera = Manager.Instance.Camera;
		var container = Manager.Instance.CameraContainer;

		if ( GetSyncStat( PlayerStat.FpsMode ) > 0f )
		{
			if( ShouldOverrideCameraPos && !IsDead )
			{
				container.WorldPosition = (Vector3)CameraTargetPosOverride + Vector3.Up * 125f;
				container.WorldRotation = Rotation.LookAt( WorldPosition - container.WorldPosition );
			}
			else
			{
				container.WorldPosition = WorldPosition + Vector3.Up * 125f + Model.LocalRotation.Forward * -170f;
				container.WorldRotation = new Angles( 20f, Model.LocalRotation.Yaw(), 0f );
			}

			camera.FieldOfView = 60f * (GetSyncStat( PlayerStat.CameraDistance ) / 800f);

			// todo: animate camera when choosing perk or gameover

			return;
		}

		// todo: handle perspective cam changes when choosing/hurt/dying
		camera.FieldOfView = 60f;
		

		container.WorldRotation = new Angles( 50f, 90f, 0f );

		//var targetPitchOffset = Manager.Instance.IsPausedForChoosing ? Utils.FastSin( RealTime.Now * 0.4f ) * 2f : 0f;
		//var targetYawOffset = Manager.Instance.IsPausedForChoosing ? Utils.FastSin( RealTime.Now * 0.3f ) * 2f : 0f;
		//container.WorldRotation = Rotation.Lerp( container.WorldRotation, Rotation.From( new Angles( 50f + targetPitchOffset, 90f + targetYawOffset, 0f ) ), RealTime.Delta * 2f );

		var camDistScale = GetSyncStat( PlayerStat.CameraDistance ) / 800f;
		var pos2D = (ShouldOverrideCameraPos && !IsDead)
			? CameraTargetPosOverride
			: Position2D;

		var targetPos = GetCamTargetPos( pos2D, camDistScale );

		if( Manager.Instance.IsOrthoCamera && !Manager.Instance.IsPaused )
		{
			var targetOrthoHeight = 500f
				+ (Manager.Instance.IsPausedForChoosing && !Manager.Instance.IsGameOver ? -80f + Utils.FastSin( RealTime.Now * 1.5f ) * 13f : 0f)
				+ (Manager.Instance.IsGameOver ? -40f : 0f);
			var cameraHurtZoomAmount = CameraHurtZoomAmount;

			camera.OrthographicHeight = MathX.Lerp( camera.OrthographicHeight, targetOrthoHeight * camDistScale, RealTime.Delta * 3f ) - cameraHurtZoomAmount;

			if ( allowHurtZoomDecay && Math.Abs( CameraHurtZoomAmount ) > 0f )
			{
				CameraHurtZoomAmount = MathX.Lerp( CameraHurtZoomAmount, 0f, RealTime.Delta * 13f );

				if ( Math.Abs( CameraHurtZoomAmount ) < 0.01f )
					CameraHurtZoomAmount = 0f;
			}
		}

		container.WorldPosition = Vector3.Lerp( container.WorldPosition, targetPos, 9f * RealTime.Delta, true );

		if ( targetPos.y < -5000f )
			camera.ZFar = 50000f;
	}

	public void RestartCamera()
	{
		var container = Manager.Instance.CameraContainer;
		container.WorldPosition = new Vector3( -50f, -500f, 650f );
		container.WorldRotation = new Angles( 50f, 90f, 0f );
		container.Transform.ClearInterpolation();

		Manager.Instance.Camera.ZFar = 10000f;

		ShouldOverrideCameraPos = false;

		CameraHurtZoomAmount = 0f;
	}

	Vector3 GetCamTargetPos( Vector2 pos, float camDistScale )
	{
		if ( !Manager.Instance.IsOrthoCamera )
			camDistScale *= 0.75f;

		ClampToBounds( ref pos, camDistScale );

		var distance = Manager.Instance.IsOrthoCamera
			? 800f
			: GetSyncStat( PlayerStat.CameraDistance );

		return (Vector3)pos + Manager.Instance.CameraContainer.WorldRotation.Backward * distance;
	}

	void ClampToBounds( ref Vector2 pos2D, float camDistScale )
	{
		// if camera is zoomed in, allow it to go more out of bounds
		if ( camDistScale < 1f )
			camDistScale = MathX.Lerp( camDistScale, 0f, 0.3f );

		float X_BUFFER = 310f * camDistScale;
		float Y_LOWER_BUFFER = 200f * camDistScale;
		float Y_UPPER_BUFFER = 170f * camDistScale;

		pos2D = new Vector2(
			MathX.Clamp( pos2D.x, Manager.Instance.BOUNDS_MIN.x + X_BUFFER, Manager.Instance.BOUNDS_MAX.x - X_BUFFER ),
			MathX.Clamp( pos2D.y, Manager.Instance.BOUNDS_MIN.y + Y_LOWER_BUFFER, Manager.Instance.BOUNDS_MAX.y - Y_UPPER_BUFFER )
		);
	}

	void HandleCamShaking()
	{
		var shakeAmount = 0f;

		for ( int i = _camShakeDatas.Count - 1; i >= 0; i-- )
		{
			var data = _camShakeDatas[i];
			var time = data.useRealTime ? RealTime.Now : Time.Now;

			if ( time > data.startTime + data.time )
			{
				_camShakeDatas.RemoveAt( i );
			}
			else
			{
				float amount = Utils.Map( time, data.startTime, data.startTime + data.time, data.strength, 0f, data.easingType );
				shakeAmount = MathF.Max( amount, shakeAmount );
			}
		}

		Manager.Instance.Camera.LocalPosition = shakeAmount > 0f
			? Rotation.Random.Forward * shakeAmount
			: Vector3.Zero;
	}

	[Rpc.Owner]
	public void ShakeCamRpc( float strength, float time, EasingType easingType = EasingType.Linear, bool useRealTime = true )
	{
		ShakeCam( strength, time, easingType, useRealTime );
	}

	public void ShakeCam( float strength, float time, EasingType easingType = EasingType.Linear, bool useRealTime = true )
	{
		var timeNow = useRealTime ? RealTime.Now : Time.Now;
		_camShakeDatas.Add( new CamShakeData( strength, timeNow, time, easingType, useRealTime ) );
	}
}