using System.Buffers; namespace Sandbox; [Expose] public struct CharacterControllerHelper { // // Inputs and Outputs // public Vector3 Position; public Vector3 Velocity; // // Config // public float Bounce; public float MaxStandableAngle; public SceneTrace Trace; public CharacterControllerHelper( SceneTrace trace, Vector3 position, Vector3 velocity ) : this() { Velocity = velocity; Position = position; Bounce = 0.0f; MaxStandableAngle = 10.0f; Trace = trace; } /// /// Trace this from one position to another /// public SceneTraceResult TraceFromTo( Vector3 start, Vector3 end ) { return Trace.FromTo( start, end ).Run(); } /// /// Try to move to the position. Will return the fraction of the desired velocity that we traveled. /// Position and Velocity will be what we recommend using. /// public float TryMove( float timestep ) { var timeLeft = timestep; float travelFraction = 0; using var moveplanes = new VelocityClipPlanes( Velocity, 3 ); for ( int bump = 0; bump < moveplanes.Max; bump++ ) { if ( Velocity.Length.AlmostEqual( 0.0f ) ) break; var pm = TraceFromTo( Position, Position + Velocity * timeLeft ); travelFraction += pm.Fraction; timeLeft -= timeLeft * pm.Fraction; if ( !pm.Hit ) { Position = pm.EndPosition; break; } if ( pm.Fraction > 0 ) { Position = pm.EndPosition;// + pm.Normal * 0.001f; moveplanes.StartBump( Velocity ); } bool standable = pm.Normal.Angle( Vector3.Up ) <= MaxStandableAngle; //Gizmo.Transform = Transform.Zero; //var dot = Velocity.Normal.Dot( pm.Normal ); //Gizmo.Draw.Color = Color.White; //Gizmo.Draw.Text( $"{bump}\n{pm.Normal}\n{dot}", new Transform( pm.StartPosition ) ); //Gizmo.Draw.Line( pm.StartPosition + Vector3.Up * 5, pm.EndPosition + Vector3.Up * 5 ); //Gizmo.Draw.Color = Color.Green; //Gizmo.Draw.Line( pm.StartPosition + Vector3.Up * 5, pm.StartPosition + Vector3.Up * 5 + pm.Normal * 3 ); if ( !moveplanes.TryAdd( pm.Normal, ref Velocity, standable ? 0 : Bounce ) ) break; } return travelFraction; } /// /// Move our position by this delta using trace. If we hit something we'll stop, /// we won't slide across it nicely like TryMove does. /// public SceneTraceResult TraceMove( Vector3 delta ) { var tr = TraceFromTo( Position, Position + delta ); Position = tr.EndPosition; return tr; } /// /// Like TryMove but will also try to step up if it hits a wall /// public float TryMoveWithStep( float timeDelta, float stepsize ) { var startPosition = Position; // Make a copy of us to stepMove var stepMove = this; // Do a regular move var fraction = TryMove( timeDelta ); // If it got almost all the way then that's cool, use it if ( fraction <= 0.01f ) return fraction; // Move up (as much as we can) stepMove.TraceMove( Vector3.Up * stepsize ); // if the move delta is too low, we probably won't get up a step Vector3 moveBack = 0; var moveDelta = stepMove.Velocity.WithZ( 0 ) * timeDelta; var deltaLen = moveDelta.Length; // if it's really low, then we're probably moving straight up or down // so lets just early out now if ( deltaLen < 0.001f ) return fraction; if ( deltaLen < 0.5f ) { var newDelta = moveDelta.Normal * 0.5f; moveBack = moveDelta - newDelta; moveDelta = newDelta; } // Move across (using existing velocity) var stepFraction = stepMove.TraceMove( moveDelta ); // Move back down var tr = stepMove.TraceMove( Vector3.Down * stepsize ); // if we didn't land on something, return if ( !tr.Hit ) return fraction; // If we landed on a wall then this is no good if ( tr.Normal.Angle( Vector3.Up ) > MaxStandableAngle ) return fraction; // if the original non stepped attempt moved further use that if ( startPosition.Distance( Position.WithZ( startPosition.z ) ) > startPosition.Distance( stepMove.Position.WithZ( startPosition.z ) ) ) return fraction; if ( !moveBack.IsNearZeroLength ) { stepMove.TraceMove( moveBack ); } // step move moved further, copy its data to us Position = stepMove.Position; Velocity = stepMove.Velocity; return stepFraction.Fraction; } } /// /// Used to store a list of planes that an object is going to hit, and then /// remove velocity from them so the object can slide over the surface without /// going through any of the planes. /// file struct VelocityClipPlanes : IDisposable { Vector3 OrginalVelocity; Vector3 BumpVelocity; Vector3[] Planes; /// /// Maximum number of planes that can be hit /// public int Max { get; private set; } /// /// Number of planes we're currently holding /// public int Count { get; private set; } public VelocityClipPlanes( Vector3 originalVelocity, int max ) { Max = max; OrginalVelocity = originalVelocity; BumpVelocity = originalVelocity; Planes = ArrayPool.Shared.Rent( max ); Count = 0; } /// /// Try to add this plane and restrain velocity to it (and its brothers) /// /// False if we ran out of room and should stop adding planes public bool TryAdd( Vector3 normal, ref Vector3 velocity, float bounce ) { if ( Count == Max ) { return false; } Planes[Count++] = normal; // // if we only hit one plane then apply the bounce // if ( Count == 1 ) { BumpVelocity = ClipVelocity( OrginalVelocity, normal, 1.0f + bounce ); OrginalVelocity = BumpVelocity; velocity = BumpVelocity; return true; } if ( TryClip( ref velocity ) ) { // Hit the floor and the wall, go along the join if ( Count == 2 ) { var dir = Vector3.Cross( Planes[0], Planes[1] ).Normal; var d = dir.Dot( velocity ); velocity = dir * d; } else { velocity = Vector3.Zero; return false; } } // // We're moving in the opposite direction to our // original intention so just stop right there. // if ( velocity.Dot( OrginalVelocity ) <= 0 ) { velocity = 0; return false; } return true; } /// /// Try to clip our velocity to all the planes, so we're not travelling into them /// Returns true if we clipped properly /// bool TryClip( ref Vector3 velocity ) { for ( int i = 0; i < Count; i++ ) { velocity = ClipVelocity( OrginalVelocity, Planes[i] ); if ( MovingTowardsAnyPlane( velocity, i ) ) return false; } return true; } /// /// Returns true if we're moving towards any of our planes (except for skip) /// bool MovingTowardsAnyPlane( Vector3 velocity, int iSkip ) { for ( int j = 0; j < Count; j++ ) { if ( j == iSkip ) continue; if ( velocity.Dot( Planes[j] ) < 0 ) return false; } return true; } /// /// Start a new bump. Clears planes and resets BumpVelocity /// public void StartBump( Vector3 velocity ) { BumpVelocity = velocity; Count = 0; } /// /// Clip the velocity to the normal /// Vector3 ClipVelocity( Vector3 vel, Vector3 norm, float overbounce = 1.0f ) { var backoff = Vector3.Dot( vel, norm ) * overbounce; var o = vel - norm * backoff; // garry: I don't totally understand how we could still // be travelling towards the norm, but the hl2 code // does another check here, so we're going to too. var adjust = Vector3.Dot( o, norm ); if ( adjust < 0.0f ) { o -= norm * adjust; } return o; } public void Dispose() { ArrayPool.Shared.Return( Planes ); } }