Code/SbTween/Extensions/PathTweenExtensions.cs

Extension methods for GameObject that create tweens moving an object along a polyline or Catmull-Rom curve over a duration. TweenPath segments movement by straight lines weighted by segment length and optionally rotates the object to look ahead; TweenPathCurve samples a Catmull-Rom spline and optionally rotates while moving.

NetworkingFile Access
using Sandbox;
using System;
using System.Collections.Generic;
namespace SbTween;

public static class PathTweenExtensions
{

    public static BaseTween TweenPath( this GameObject obj, IList<Vector3> points, float duration, bool lookAhead = false )
    {
       if ( points == null || points.Count < 2 ) return null;

       var tween = new BaseTween( duration );
       tween.Target = obj;

       int totalSegments = points.Count - 1;
       float[] segmentLengths = new float[totalSegments];
       float totalLength = 0f;

       for ( int i = 0; i < totalSegments; i++ )
       {
          segmentLengths[i] = Vector3.DistanceBetween( points[i], points[i + 1] );
          totalLength += segmentLengths[i];
       }

       return TweenManager.Instance.AddTween( tween
          .OnUpdate( p =>
          {
             if ( !obj.IsValid() ) return;
             if ( totalLength <= 0 ) return;

             float currentLengthPos = p * totalLength;
             float accumulatedLength = 0f;
             
             int targetSegment = 0;
             for ( int i = 0; i < totalSegments; i++ )
             {
                if ( currentLengthPos <= accumulatedLength + segmentLengths[i] )
                {
                   targetSegment = i;
                   break;
                }
                accumulatedLength += segmentLengths[i];
                targetSegment = i;
             }

             float segmentProgress = 0f;
             if ( segmentLengths[targetSegment] > 0 )
             {
                segmentProgress = (currentLengthPos - accumulatedLength) / segmentLengths[targetSegment];
             }

             Vector3 startPos = points[targetSegment];
             Vector3 endPos = points[targetSegment + 1];
             Vector3 nextPos = Vector3.Lerp( startPos, endPos, segmentProgress );

             if ( lookAhead && nextPos != obj.WorldPosition )
             {
                obj.WorldRotation = Rotation.LookAt( nextPos - obj.WorldPosition, Vector3.Up );
             }

             obj.WorldPosition = nextPos;
          } )
       );
    }

    public static BaseTween TweenPathCurve( this GameObject obj, IList<Vector3> points, float duration, bool lookAhead = false )
    {
       if ( points == null || points.Count < 2 ) return null;

       var tween = new BaseTween( duration );
       tween.Target = obj;

       return TweenManager.Instance.AddTween( tween
          .OnUpdate( p =>
          {
             if ( !obj.IsValid() ) return;

             Vector3 nextPos = GetCatmullRomPosition( points, p );

             if ( lookAhead && nextPos != obj.WorldPosition )
             {
                obj.WorldRotation = Rotation.LookAt( nextPos - obj.WorldPosition, Vector3.Up );
             }

             obj.WorldPosition = nextPos;
          } )
       );
    }

    private static Vector3 GetCatmullRomPosition( IList<Vector3> points, float pct )
    {
       int numSections = points.Count - 1;
       
       int currPt = (int)MathF.Floor( pct * numSections );
       currPt = Math.Clamp( currPt, 0, numSections - 1 );
       
       float t = (pct * numSections) - currPt;

       Vector3 p0 = points[Math.Clamp( currPt - 1, 0, points.Count - 1 )];
       Vector3 p1 = points[currPt];
       Vector3 p2 = points[Math.Clamp( currPt + 1, 0, points.Count - 1 )];
       Vector3 p3 = points[Math.Clamp( currPt + 2, 0, points.Count - 1 )];

       
       // https://en.wikipedia.org/wiki/Catmull–Rom_spline 
       return 0.5f * (
          (2f * p1) +
          (-p0 + p2) * t +
          (2f * p0 - 5f * p1 + 4f * p2 - p3) * t * t +
          (-p0 + 3f * p1 - 3f * p2 + p3) * t * t * t
       );
    }
}