Code/Miscellaneous/FixedSwim.cs

A MoveMode implementation for swimming in the game. It tracks water submersion level, plays splash and swim sounds, toggles swimming state, applies buoyant/current forces to the player's rigidbody, and adjusts physics while swimming.

File Access
using Sandbox;
using Sandbox.Movement;

namespace RedSnail.WaterTool;

/// <summary>
/// The character is swimming
/// </summary>
[Icon( "scuba_diving" ), Group( "Movement" ), Title( "MoveMode - Swim (Fixed)" )]
public sealed class MoveModeSwimFixed : MoveMode
{
	private bool m_WasInWater;
	private bool m_HasTouchedWater;
	
	private TimeSince m_TimeSinceStep;
	
	[Property]
	public int Priority { get; set; } = 10;

	[Property, Range( 0, 1 )]
	public float SwimLevel { get; set; } = 0.7f;

	/// <summary>
	/// We will update this based on how much you're in a "water" tagged trigger.
	/// </summary>
	public float WaterLevel { get; private set; }
	
	// How hard the current grips the hull, the rate (per second, at full submersion)
	// at which the body's velocity is pulled toward the flow velocity.
	[Property, Group("River Current"), Range(0.1f, 10.0f)] private float CurrentDrag { get; set; } = 1.5f;
	
	[Property, Group("Sounds")] public SoundEvent WaterSplashEnter { get; set; }
	[Property, Group("Sounds")] public SoundEvent WaterSplashExit { get; set; }
	[Property, Group("Sounds")] public SoundEvent WaterFootsteps { get; set; }
	[Property, Group("Sounds")] public SoundPointComponent WaterSwimmingLoop { get; set; }

	protected override void OnStart()
	{
		if (WaterSwimmingLoop.IsValid())
			WaterSwimmingLoop.Enabled = false;
	}
	
	public override void UpdateRigidBody( Rigidbody body )
	{
		body.Gravity = false;
		body.LinearDamping = 3.3f;
		body.AngularDamping = 1f;
	}

	public override int Score( PlayerController controller )
	{
		if ( WaterLevel > SwimLevel ) return Priority;
		return -100;
	}

	public override void OnModeBegin()
	{
		Controller.IsSwimming = true;
		
		if (WaterSwimmingLoop.IsValid())
			WaterSwimmingLoop.Enabled = true;
	}

	public override void OnModeEnd( MoveMode next )
	{
		Controller.IsSwimming = false;
		
		if (WaterSwimmingLoop.IsValid())
			WaterSwimmingLoop.Enabled = false;
		
		// jump when leaving the water
		if ( Input.Down( "Jump" ) )
		{
			Controller.Jump( Vector3.Up * 300 );
		}
	}

	protected override void OnFixedUpdate()
	{
		UpdateWaterLevel();

		if (m_HasTouchedWater != m_WasInWater)
		{
			if (m_HasTouchedWater)
			{
				if (WaterSplashEnter.IsValid())
					Sound.Play(WaterSplashEnter, WorldPosition);
			}
			else
			{
				if (WaterSplashExit.IsValid())
					Sound.Play(WaterSplashExit, WorldPosition);
			}
			
			m_WasInWater = m_HasTouchedWater;
		}

		const float epsilon = 0.001f;
		float speed = (Controller.WalkSpeed * 0.5f / Controller.Velocity.Length + epsilon).Clamp(0.2f, 1.0f);

		if (m_HasTouchedWater && Controller.IsOnGround && Controller.Velocity.Length > 10 && m_TimeSinceStep > speed)
		{
			m_TimeSinceStep = 0;
			
			Sound.Play(WaterFootsteps, WorldPosition);
		}

		if (Controller.IsSwimming)
		{
			ApplyCurrentForce();
		}
	}

	void UpdateWaterLevel()
	{
		if ( Controller?.Body == null )
			return;
		
		if (GameObject.Parent != Scene)
			return;

		var wt = WorldTransform;
		Vector3 head = wt.PointToWorld( new Vector3( 0, 0, Controller.CurrentHeight ) );
		Vector3 foot = wt.Position;

		float waterLevel = 0;
		
		var waterSurface = WaterManager.GetWaterHeightAt( head );
		var level = Vector3.InverseLerp( waterSurface, foot, head );
		level = (level * 100).CeilToInt() / 100.0f;

		if (level > waterLevel)
		{
			m_HasTouchedWater = true;
			waterLevel = level;
		}
		else
		{
			m_HasTouchedWater = false;
		}

		if ( WaterLevel != waterLevel )
		{
			WaterLevel = waterLevel;
		}
	}
	
	private void ApplyCurrentForce()
	{
		Vector3 flowVelocity = WaterManager.GetFlowVelocityAt(WorldPosition);

		if (flowVelocity.LengthSquared < 0.01f)
			return;
		
		Vector3 relative = flowVelocity - Controller.Body.Velocity.WithZ(0.0f);
		Vector3 force = relative * (Controller.Body.Mass * CurrentDrag);
		
		Controller.Body.ApplyForce(force);
	}

	public override Vector3 UpdateMove( Rotation eyes, Vector3 input )
	{
		if ( Input.Down( "jump" ) )
		{
			input += Vector3.Up;
		}

		return base.UpdateMove( eyes, input );
	}
}