Code/Example/Complex/PlayerWalkControllerComplex.Movement.Swimming.cs
using Sandbox;
using System;
namespace XMovement;
public partial class PlayerWalkControllerComplex : Component
{
[Property, FeatureEnabled( "Swimming" )] public bool EnableSwimming { get; set; } = true;
[Property, Feature( "Swimming" )] public string WaterTag { get; set; } = "water";
[Property, Feature( "Swimming" )] public float SwimmingSpeedScale { get; set; } = 0.8f;
[Property, Feature( "Swimming" )] public float SwimmingFriction { get; set; } = 4;
[Property, InputAction, Feature( "Swimming" )] public string SwimUpAction { get; set; } = "Jump";
[Property, InputAction, Feature( "Swimming" )] public string SwimDownAction { get; set; } = "";
private bool IsSwimming => WaterLevel > 0.5f;
float WaterLevel = 0;
public virtual void CheckWater()
{
if ( !EnableSwimming ) { WaterLevel = 0; return; }
// Use a point trace from foot to head — same as Source.
// A box trace at corners of a water trigger gives wrong results because
// the box clips outside the trigger, shrinking the fraction prematurely.
var foot = WorldPosition;
var head = WorldPosition + Vector3.Up * Controller.Height;
var pm = Scene.Trace.Ray( head, foot )
.WithTag( WaterTag )
.HitTriggers()
.IgnoreGameObjectHierarchy( GameObject )
.Run();
WaterLevel = 1 - pm.Fraction;
if ( WaterLevel > 0.1f )
{
if ( WaterLevel > 0.4f )
{
CheckWaterJump();
}
// If we are falling again, then we must not trying to jump out of water any more.
if ( (Controller.Velocity.z < 0.0f) && IsJumpingFromWater )
{
WaterJumpTime = 0.0f;
}
}
}
public virtual void SwimMove()
{
Controller.IsOnGround = false;
// Build wish velocity from full 3D view angles (pitch included), like Source WaterMove.
// This means looking down swims you down, looking up swims you up.
// Normalize before scaling so diagonal input (forward+strafe) doesn't exceed 1x speed.
// HL2 does VectorNormalize(wishdir) before capping - without this you get ~1.41x speed diagonally.
var wishvel = WishMove * EyeAngles.ToRotation();
var wishspeed = wishvel.Length;
if ( wishspeed > 0 ) wishvel = wishvel.Normal;
wishspeed = MathF.Min( wishspeed, 1f ) * GetWishSpeed() * SwimmingSpeedScale;
wishvel *= wishspeed;
// Jump key moves up; no input sinks at 60 u/s (Source behaviour)
if ( Input.Down( SwimUpAction ) )
{
wishvel = wishvel.WithZ( wishvel.z + GetWishSpeed() * SwimmingSpeedScale );
}
else if ( WishMove.IsNearlyZero() )
{
wishvel = wishvel.WithZ( wishvel.z - 60f );
}
if ( !string.IsNullOrEmpty( SwimDownAction ) && Input.Down( SwimDownAction ) )
wishvel = wishvel.WithZ( -GetWishSpeed() * SwimmingSpeedScale );
Controller.WishVelocity = wishvel;
// Water friction: proportional speed bleed before acceleration (Source WaterMove).
// Unlike ground friction this doesn't use stop-speed — it's a straight percentage.
var speed = Controller.Velocity.Length;
if ( speed > 0 )
{
var newspeed = speed - Time.Delta * speed * SwimmingFriction * Controller.SurfaceFriction;
if ( newspeed < 0.1f ) newspeed = 0;
Controller.Velocity *= newspeed / speed;
}
// Accelerate up to wish speed (standard Accelerate, no special air cap needed in water)
Controller.Acceleration = Controller.BaseAcceleration;
Controller.Accelerate( wishvel );
// Move without applying wish velocity a second time or gravity
Controller.Move( withWishVelocity: false, withGravity: false );
}
protected float WaterJumpTime { get; set; }
protected Vector3 WaterJumpVelocity { get; set; }
protected bool IsJumpingFromWater => WaterJumpTime > 0;
public virtual float WaterJumpHeight => 8;
protected void CheckWaterJump()
{
// Already water jumping.
if ( IsJumpingFromWater )
return;
// Don't hop out if we just jumped in
// only hop out if we are moving up
if ( Controller.Velocity.z < -180 )
return;
// See if we are backing up
var flatvelocity = Controller.Velocity.WithZ( 0 );
// Must be moving
var curspeed = flatvelocity.Length;
flatvelocity = flatvelocity.Normal;
// see if near an edge
var flatforward = Head.WorldRotation.Forward.WithZ( 0 ).Normal;
// Are we backing into water from steps or something? If so, don't pop forward
if ( curspeed != 0 && Vector3.Dot( flatvelocity, flatforward ) < 0 )
return;
var vecStart = WorldPosition + (Controller.BoundingBox.Mins + Controller.BoundingBox.Maxs) * .5f;
var vecEnd = vecStart + flatforward * 24;
var tr = Controller.BuildTrace( vecStart, vecEnd ).Run();
if ( tr.Fraction == 1 )
return;
vecStart.z = WorldPosition.z + HeadHeight + WaterJumpHeight;
vecEnd = vecStart + flatforward * 24;
WaterJumpVelocity = tr.Normal * -50;
tr = Controller.BuildTrace( vecStart, vecEnd ).Run();
if ( tr.Fraction < 1.0 )
return;
// Now trace down to see if we would actually land on a standable surface.
vecStart = vecEnd;
vecEnd.z -= 1024;
tr = Controller.BuildTrace( vecStart, vecEnd ).Run();
if ( tr.Fraction < 1 && tr.Normal.z >= 0.7f )
{
Controller.Velocity += new Vector3( 0, 0, 256 ) * WorldScale;
WaterJumpTime = 2000;
}
}
}