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;
// Track initialization state
private bool isInitialized = false;
///
/// Public method to explicitly initialize the rope.
/// Call this after setting up endpoints if creating ropes at runtime.
///
/// True if initialization was successful, false otherwise
public bool Initialize()
{
// Skip if already initialized
if (isInitialized)
return true;
InitializeLineRenderer();
if (AreEndPointsValid())
{
currentValue = GetMidPoint();
targetValue = currentValue;
currentVelocity = Vector3.zero;
SetSplinePoint(); // Ensure initial spline point is set correctly
isInitialized = true;
return true;
}
return false;
}
private void Start()
{
// Use the same initialization method to avoid code duplication
Initialize();
}
private void OnValidate()
{
if (!Application.isPlaying)
{
InitializeLineRenderer();
if (AreEndPointsValid())
{
RecalculateRope();
SimulatePhysics();
}
else
{
lineRenderer.positionCount = 0;
}
}
}
private void InitializeLineRenderer()
{
if (!lineRenderer)
{
lineRenderer = GetComponent();
}
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;
}
///
/// Set the start point of the rope
///
public void SetStartPoint(Transform newStartPoint, bool recalculateRope = false)
{
startPoint = newStartPoint;
if (recalculateRope)
RecalculateRope();
}
///
/// Set the end point of the rope
///
public void SetEndPoint(Transform newEndPoint, bool recalculateRope = false)
{
endPoint = newEndPoint;
if (recalculateRope)
RecalculateRope();
}
///
/// Set the mid point of the rope
///
public void SetMidPoint(Transform newMidPoint, bool recalculateRope = false)
{
midPoint = newMidPoint;
if (recalculateRope)
RecalculateRope();
}
///
/// Get a point along the rope at the specified position (0-1)
///
public Vector3 GetPointAt(float position)
{
position = Mathf.Clamp01(position);
Vector3 mid = GetMidPoint();
return GetRationalBezierPoint(startPoint.position, mid, endPoint.position, position, StartPointWeight, midPointWeight, EndPointWeight);
}
///
/// Force recalculation of the rope
///
public void RecalculateRope()
{
if (!isInitialized)
{
Initialize();
}
if (AreEndPointsValid())
{
SetSplinePoint();
SimulatePhysics();
NotifyPointsChanged();
}
}
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;
}
}
}