A MoveMode implementation for swimming on the water surface in the Water Tool 2 package. It detects water level relative to the player, plays splash and footstep sounds, toggles a swimming loop sound, adjusts rigidbody damping/gravity while swimming, applies water current forces, and controls swim-related input like jump.
using Sandbox;
using Sandbox.Movement;
namespace RedSnail.WaterTool2;
/// <summary>
/// Swimming for the PlayerController, on the water network's surface (waves included): splashes going in and out,
/// footsteps in the shallows, and the current dragging the swimmer along.
/// </summary>
[Title("MoveMode - Swim (Water Tool 2)")]
[Category("Water Tool 2")]
[Icon("scuba_diving")]
public sealed class MoveModeSwimFixed : MoveMode
{
private bool m_WasInWater;
private bool m_HasTouchedWater;
private TimeSince m_TimeSinceStep;
[Property] public int Priority { get; set; } = 10;
/// <summary>
/// How much of the body has to be under water to swim, from the feet (0) to the head (1).
/// </summary>
[Property, Range(0, 1)] public float SwimLevel { get; set; } = 0.7f;
/// <summary>
/// How much of the body is under water, from the feet (0) to the head (1).
/// </summary>
public float WaterLevel { get; private set; }
/// <summary>
/// How hard the current grips the swimmer: the rate (per second) the body's velocity is pulled towards the flow.
/// </summary>
[Property, Group("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 = 1.0f;
}
public override int Score(PlayerController _Controller)
{
return WaterLevel > SwimLevel ? Priority : -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;
// Jumping out of the water.
if (Input.Down("Jump"))
Controller.Jump(Vector3.Up * 300);
}
protected override void OnFixedUpdate()
{
UpdateWaterLevel();
if (m_HasTouchedWater != m_WasInWater)
{
var splash = m_HasTouchedWater ? WaterSplashEnter : WaterSplashExit;
if (splash.IsValid())
Sound.Play(splash, WorldPosition);
m_WasInWater = m_HasTouchedWater;
}
float stepInterval = (Controller.WalkSpeed * 0.5f / (Controller.Velocity.Length + 0.001f)).Clamp(0.2f, 1.0f);
if (m_HasTouchedWater && Controller.IsOnGround && Controller.Velocity.Length > 10 && m_TimeSinceStep > stepInterval)
{
m_TimeSinceStep = 0;
if (WaterFootsteps.IsValid())
Sound.Play(WaterFootsteps, WorldPosition);
}
if (Controller.IsSwimming)
ApplyCurrentForce();
}
private void UpdateWaterLevel()
{
if (Controller?.Body is null)
return;
// Something carrying the player (a boat) decides for it.
if (GameObject.Parent != Scene)
return;
var transform = WorldTransform;
Vector3 head = transform.PointToWorld(new Vector3(0, 0, Controller.CurrentHeight));
Vector3 foot = transform.Position;
float surface = WaterNetwork.GetWaterHeightAt(head);
float level = 0.0f;
if (surface > float.MinValue)
{
level = Vector3.InverseLerp(new Vector3(0, 0, surface), foot.WithX(0).WithY(0), head.WithX(0).WithY(0));
level = (level * 100).CeilToInt() / 100.0f;
}
m_HasTouchedWater = level > 0.0f;
WaterLevel = level;
}
private void ApplyCurrentForce()
{
Vector3 flow = WaterNetwork.GetFlowVelocityAt(WorldPosition);
if (flow.LengthSquared < 0.01f)
return;
Vector3 relative = flow - Controller.Body.Velocity.WithZ(0.0f);
Controller.Body.ApplyForce(relative * (Controller.Body.Mass * CurrentDrag));
}
public override Vector3 UpdateMove(Rotation _Eyes, Vector3 _Input)
{
if (Input.Down("jump"))
_Input += Vector3.Up;
return base.UpdateMove(_Eyes, _Input);
}
}