2025-09-04 23:13:19 +02:00
using UnityEngine ;
using System ;
#if UNITY_EDITOR
using UnityEditor ;
#endif
namespace GogoGaga.OptimizedRopesAndCables
{
[ExecuteAlways]
[RequireComponent(typeof(LineRenderer))]
public class Rope : MonoBehaviour
{
public event Action OnPointsChanged ;
[Header("Rope Transforms")]
[Tooltip("The rope will start at this point")]
[SerializeField] private Transform startPoint ;
public Transform StartPoint = > startPoint ;
[Tooltip("This will move at the center hanging from the rope, like a necklace, for example")]
[SerializeField] private Transform midPoint ;
public Transform MidPoint = > midPoint ;
[Tooltip("The rope will end at this point")]
[SerializeField] private Transform endPoint ;
public Transform EndPoint = > endPoint ;
[Header("Rope Settings")]
[Tooltip("How many points should the rope have, 2 would be a triangle with straight lines, 100 would be a very flexible rope with many parts")]
[Range(2, 100)] public int linePoints = 10 ;
[Tooltip("Value highly dependent on use case, a metal cable would have high stiffness, a rubber rope would have a low one")]
public float stiffness = 350f ;
[Tooltip("0 is no damping, 50 is a lot")]
public float damping = 15f ;
[Tooltip("How long is the rope, it will hang more or less from starting point to end point depending on this value")]
public float ropeLength = 15 ;
[Tooltip("The Rope width set at start (changing this value during run time will produce no effect)")]
public float ropeWidth = 0.1f ;
[Header("Rational Bezier Weight Control")]
[Tooltip("Adjust the middle control point weight for the Rational Bezier curve")]
[Range(1, 15)] public float midPointWeight = 1f ;
private const float StartPointWeight = 1f ; //these need to stay at 1, could be removed but makes calling the rational bezier function easier to read and understand
private const float EndPointWeight = 1f ;
[Header("Midpoint Position")]
[Tooltip("Position of the midpoint along the line between start and end points")]
[Range(0.25f, 0.75f)] public float midPointPosition = 0.5f ;
private Vector3 currentValue ;
private Vector3 currentVelocity ;
private Vector3 targetValue ;
public Vector3 otherPhysicsFactors { get ; set ; }
private const float valueThreshold = 0.01f ;
private const float velocityThreshold = 0.01f ;
private LineRenderer lineRenderer ;
private bool isFirstFrame = true ;
private Vector3 prevStartPointPosition ;
private Vector3 prevEndPointPosition ;
private float prevMidPointPosition ;
private float prevMidPointWeight ;
private float prevLineQuality ;
private float prevRopeWidth ;
private float prevstiffness ;
private float prevDampness ;
private float prevRopeLength ;
public bool IsPrefab = > gameObject . scene . rootCount = = 0 ;
2025-09-22 12:16:32 +00:00
// Track initialization state
private bool isInitialized = false ;
/// <summary>
/// Public method to explicitly initialize the rope.
/// Call this after setting up endpoints if creating ropes at runtime.
/// </summary>
/// <returns>True if initialization was successful, false otherwise</returns>
public bool Initialize ( )
2025-09-04 23:13:19 +02:00
{
2025-09-22 12:16:32 +00:00
// Skip if already initialized
if ( isInitialized )
return true ;
2025-09-04 23:13:19 +02:00
InitializeLineRenderer ( ) ;
if ( AreEndPointsValid ( ) )
{
currentValue = GetMidPoint ( ) ;
targetValue = currentValue ;
currentVelocity = Vector3 . zero ;
SetSplinePoint ( ) ; // Ensure initial spline point is set correctly
2025-09-22 12:16:32 +00:00
isInitialized = true ;
return true ;
2025-09-04 23:13:19 +02:00
}
2025-09-22 12:16:32 +00:00
return false ;
}
private void Start ( )
{
// Use the same initialization method to avoid code duplication
Initialize ( ) ;
2025-09-04 23:13:19 +02:00
}
private void OnValidate ( )
{
if ( ! Application . isPlaying )
{
InitializeLineRenderer ( ) ;
if ( AreEndPointsValid ( ) )
{
RecalculateRope ( ) ;
SimulatePhysics ( ) ;
}
else
{
lineRenderer . positionCount = 0 ;
}
}
}
private void InitializeLineRenderer ( )
{
if ( ! lineRenderer )
{
lineRenderer = GetComponent < LineRenderer > ( ) ;
}
lineRenderer . startWidth = ropeWidth ;
lineRenderer . endWidth = ropeWidth ;
}
private void Update ( )
{
if ( IsPrefab )
{
return ;
}
if ( AreEndPointsValid ( ) )
{
SetSplinePoint ( ) ;
if ( ! Application . isPlaying & & ( IsPointsMoved ( ) | | IsRopeSettingsChanged ( ) ) )
{
SimulatePhysics ( ) ;
NotifyPointsChanged ( ) ;
}
prevStartPointPosition = startPoint . position ;
prevEndPointPosition = endPoint . position ;
prevMidPointPosition = midPointPosition ;
prevMidPointWeight = midPointWeight ;
prevLineQuality = linePoints ;
prevRopeWidth = ropeWidth ;
prevstiffness = stiffness ;
prevDampness = damping ;
prevRopeLength = ropeLength ;
}
}
private bool AreEndPointsValid ( )
{
return startPoint ! = null & & endPoint ! = null ;
}
private void SetSplinePoint ( )
{
if ( lineRenderer . positionCount ! = linePoints + 1 )
{
lineRenderer . positionCount = linePoints + 1 ;
}
Vector3 mid = GetMidPoint ( ) ;
targetValue = mid ;
mid = currentValue ;
if ( midPoint ! = null )
{
midPoint . position = GetRationalBezierPoint ( startPoint . position , mid , endPoint . position , midPointPosition , StartPointWeight , midPointWeight , EndPointWeight ) ;
}
for ( int i = 0 ; i < linePoints ; i + + )
{
Vector3 p = GetRationalBezierPoint ( startPoint . position , mid , endPoint . position , i / ( float ) linePoints , StartPointWeight , midPointWeight , EndPointWeight ) ;
lineRenderer . SetPosition ( i , p ) ;
}
lineRenderer . SetPosition ( linePoints , endPoint . position ) ;
}
private float CalculateYFactorAdjustment ( float weight )
{
//float k = 0.360f; //after testing this seemed to be a good value for most cases, more accurate k is available.
float k = Mathf . Lerp ( 0.493f , 0.323f , Mathf . InverseLerp ( 1 , 15 , weight ) ) ; //K calculation that is more accurate, interpolates between precalculated values.
float w = 1f + k * Mathf . Log ( weight ) ;
return w ;
}
private Vector3 GetMidPoint ( )
{
Vector3 startPointPosition = startPoint . position ;
Vector3 endPointPosition = endPoint . position ;
Vector3 midpos = Vector3 . Lerp ( startPointPosition , endPointPosition , midPointPosition ) ;
float yFactor = ( ropeLength - Mathf . Min ( Vector3 . Distance ( startPointPosition , endPointPosition ) , ropeLength ) ) / CalculateYFactorAdjustment ( midPointWeight ) ;
midpos . y - = yFactor ;
return midpos ;
}
private Vector3 GetRationalBezierPoint ( Vector3 p0 , Vector3 p1 , Vector3 p2 , float t , float w0 , float w1 , float w2 )
{
//scale each point by its weight (can probably remove w0 and w2 if the midpoint is the only adjustable weight)
Vector3 wp0 = w0 * p0 ;
Vector3 wp1 = w1 * p1 ;
Vector3 wp2 = w2 * p2 ;
//calculate the denominator of the rational Bézier curve
float denominator = w0 * Mathf . Pow ( 1 - t , 2 ) + 2 * w1 * ( 1 - t ) * t + w2 * Mathf . Pow ( t , 2 ) ;
//calculate the numerator and devide by the demoninator to get the point on the curve
Vector3 point = ( wp0 * Mathf . Pow ( 1 - t , 2 ) + wp1 * 2 * ( 1 - t ) * t + wp2 * Mathf . Pow ( t , 2 ) ) / denominator ;
return point ;
}
2025-09-22 12:16:32 +00:00
/// <summary>
/// Set the start point of the rope
/// </summary>
public void SetStartPoint ( Transform newStartPoint , bool recalculateRope = false )
2025-09-04 23:13:19 +02:00
{
2025-09-22 12:16:32 +00:00
startPoint = newStartPoint ;
if ( recalculateRope )
RecalculateRope ( ) ;
}
/// <summary>
/// Set the end point of the rope
/// </summary>
public void SetEndPoint ( Transform newEndPoint , bool recalculateRope = false )
{
endPoint = newEndPoint ;
if ( recalculateRope )
RecalculateRope ( ) ;
}
/// <summary>
/// Set the mid point of the rope
/// </summary>
public void SetMidPoint ( Transform newMidPoint , bool recalculateRope = false )
{
midPoint = newMidPoint ;
if ( recalculateRope )
RecalculateRope ( ) ;
}
/// <summary>
/// Get a point along the rope at the specified position (0-1)
/// </summary>
public Vector3 GetPointAt ( float position )
{
position = Mathf . Clamp01 ( position ) ;
Vector3 mid = GetMidPoint ( ) ;
return GetRationalBezierPoint ( startPoint . position , mid , endPoint . position , position , StartPointWeight , midPointWeight , EndPointWeight ) ;
}
/// <summary>
/// Force recalculation of the rope
/// </summary>
public void RecalculateRope ( )
{
if ( ! isInitialized )
2025-09-04 23:13:19 +02:00
{
2025-09-22 12:16:32 +00:00
Initialize ( ) ;
}
if ( AreEndPointsValid ( ) )
{
SetSplinePoint ( ) ;
SimulatePhysics ( ) ;
NotifyPointsChanged ( ) ;
2025-09-04 23:13:19 +02:00
}
}
private void FixedUpdate ( )
{
if ( IsPrefab )
{
return ;
}
if ( AreEndPointsValid ( ) )
{
if ( ! isFirstFrame )
{
SimulatePhysics ( ) ;
}
isFirstFrame = false ;
}
}
private void SimulatePhysics ( )
{
float dampingFactor = Mathf . Max ( 0 , 1 - damping * Time . fixedDeltaTime ) ;
Vector3 acceleration = ( targetValue - currentValue ) * stiffness * Time . fixedDeltaTime ;
currentVelocity = currentVelocity * dampingFactor + acceleration + otherPhysicsFactors ;
currentValue + = currentVelocity * Time . fixedDeltaTime ;
if ( Vector3 . Distance ( currentValue , targetValue ) < valueThreshold & & currentVelocity . magnitude < velocityThreshold )
{
currentValue = targetValue ;
currentVelocity = Vector3 . zero ;
}
}
private void OnDrawGizmos ( )
{
if ( ! AreEndPointsValid ( ) )
return ;
Vector3 midPos = GetMidPoint ( ) ;
// Uncomment if you need to visualize midpoint
// Gizmos.color = Color.red;
// Gizmos.DrawSphere(midPos, 0.2f);
}
private void NotifyPointsChanged ( )
{
OnPointsChanged ? . Invoke ( ) ;
}
private bool IsPointsMoved ( )
{
var startPointMoved = startPoint . position ! = prevStartPointPosition ;
var endPointMoved = endPoint . position ! = prevEndPointPosition ;
return startPointMoved | | endPointMoved ;
}
private bool IsRopeSettingsChanged ( )
{
var lineQualityChanged = ! Mathf . Approximately ( linePoints , prevLineQuality ) ;
var ropeWidthChanged = ! Mathf . Approximately ( ropeWidth , prevRopeWidth ) ;
var stiffnessChanged = ! Mathf . Approximately ( stiffness , prevstiffness ) ;
var dampnessChanged = ! Mathf . Approximately ( damping , prevDampness ) ;
var ropeLengthChanged = ! Mathf . Approximately ( ropeLength , prevRopeLength ) ;
var midPointPositionChanged = ! Mathf . Approximately ( midPointPosition , prevMidPointPosition ) ;
var midPointWeightChanged = ! Mathf . Approximately ( midPointWeight , prevMidPointWeight ) ;
return lineQualityChanged
| | ropeWidthChanged
| | stiffnessChanged
| | dampnessChanged
| | ropeLengthChanged
| | midPointPositionChanged
| | midPointWeightChanged ;
}
}
}