GameLoop/GameManager.Water.cs
using Sandbox.Audio;

[Icon( "water" )]
public partial class WaterVolume : Component, Component.ITriggerListener
{
	[Property, Group( "Sound" )] private SoundEvent SoundEnter { get; set; }
		= ResourceLibrary.Get<SoundEvent>( "sounds/water_enter.sound" );

	[Property, Group( "Sound" )] private SoundEvent SoundExit { get; set; }
		= ResourceLibrary.Get<SoundEvent>( "sounds/water_exit.sound" );

	[RequireComponent] private BoxCollider Collider { get; set; }

	/// <summary>
	/// Roots of objects currently in the water.
	/// Handy for playing sounds only once per object, even if it has multiple colliders (like ragdolls)
	/// </summary>
	private HashSet<GameObject> Objects = new();
	private List<Rigidbody> Bodies = new();

	private BBox Bounds => BBox.FromPositionAndSize( WorldTransform.PointToWorld( Collider.Center ), WorldScale * Collider.Scale );

	protected override void OnFixedUpdate()
	{
		if ( Bodies is null || !Collider.IsValid() )
			return;

		var bbox = Bounds;
		var waterSurface = bbox.Center + Vector3.Up * bbox.Extents.z;
		var waterPlane = new Plane( waterSurface, Vector3.Up );

		for ( int i = Bodies.Count - 1; i >= 0; i-- )
		{
			var body = Bodies[i];
			if ( !body.IsValid() )
			{
				Bodies.RemoveAt( i );
				continue;
			}

			body.ApplyBuoyancy( waterPlane, Time.Delta );
		}
	}

	bool _wasCamUnderwater;
	protected override void OnUpdate()
	{
		if ( !Collider.IsValid() )
			return;

		var camera = Scene.Camera;
		bool isCamUnderwater = camera.IsValid() && Bounds.Contains( camera.WorldPosition );
		if ( isCamUnderwater != _wasCamUnderwater )
		{
			if ( isCamUnderwater ) OnCameraEnter();
			else OnCameraExit();
		}

		_wasCamUnderwater = isCamUnderwater;
	}

	DspProcessor _dsp;
	void OnCameraEnter()
	{
		var gameMixer = Mixer.FindMixerByName( "Game" );
		if ( gameMixer is null ) return;
		
		_dsp ??= new DspProcessor( "water.small" );
		gameMixer.AddProcessor( _dsp );
	}

	void OnCameraExit()
	{
		var gameMixer = Mixer.FindMixerByName( "Game" );
		if ( gameMixer is null ) return;

		gameMixer.RemoveProcessor( _dsp );
		_dsp = null;
	}

	void ITriggerListener.OnTriggerEnter( Collider other )
	{
		var body = other.Rigidbody;
		if ( !body.IsValid() || Bodies.Contains( body ) )
			return;

		Bodies.Add( body );

		var root = other.GameObject.Root;
		if ( Objects.Add( root ) )
		{
			if ( SoundEnter.IsValid() )
			{
				other.GameObject.PlaySound( SoundEnter );
			}
		}
	}

	void ITriggerListener.OnTriggerExit( Collider other )
	{
		var body = other.Rigidbody;
		if ( !body.IsValid() ) return;

		Bodies.Remove( body );

		var root = other.GameObject.Root;
		if ( Objects.Remove( root ) )
		{
			if ( SoundExit.IsValid() )
			{
				other.GameObject.PlaySound( SoundExit );
			}
		}
	}
}

public sealed partial class GameManager : ISceneLoadingEvents
{
	void ISceneLoadingEvents.AfterLoad( Scene scene )
	{
		var waterVolumes = scene.GetAll<Collider>().Where( x => x.Tags.Has( "water" ) );
		if ( waterVolumes.Count() < 1 ) return;

		foreach ( var volume in waterVolumes )
		{
			volume.Surface ??= Surface.FindByName( "water" );
			volume.GetOrAddComponent<WaterVolume>();
		}
	}
}