# Conflicts: # Assets/Scenes/RopeTest.unity # Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs # Assets/Scripts/Fishing/Rope/Rope.cs # Assets/Scripts/Fishing/Rope/Rope.cs.meta
1006 lines
36 KiB
C#
1006 lines
36 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
namespace NBF
|
|
{
|
|
/// <summary>
|
|
/// Used for FLine visual rendering.
|
|
/// Reads rigid body nodes from the cable and simulates extra render points between logic nodes.
|
|
/// </summary>
|
|
[RequireComponent(typeof(FLine))]
|
|
[RequireComponent(typeof(LineRenderer))]
|
|
public class FLineRenderer : MonoBehaviour
|
|
{
|
|
private sealed class VisualSegmentState
|
|
{
|
|
public int SegmentIndex;
|
|
public int Subdivisions;
|
|
public LineRenderer Renderer;
|
|
public GameObject RendererObject;
|
|
public Vector3[] Points;
|
|
public Vector3[] PrevPoints;
|
|
public float[] EdgeRestLengths;
|
|
public bool[] SupportedPoints;
|
|
public Vector3[] SmoothPoints;
|
|
public Vector3[] SegmentPoints;
|
|
public int SmoothCapacity;
|
|
public int SegmentCapacity;
|
|
|
|
public int PointCount => Points != null ? Points.Length : 0;
|
|
}
|
|
|
|
[Header("Source")] [SerializeField] private FLine cable;
|
|
[SerializeField, Min(1)] private int visualSubdivisionsPerSegment = 5;
|
|
[SerializeField] private List<int> visualSubdivisionsBySegment = new List<int>();
|
|
[SerializeField, Min(0f)] private float visualLengthTrim = 0.05f;
|
|
|
|
[Header("Visual Physics")] [SerializeField, Min(0f)]
|
|
private float gravityStrength = 9.81f;
|
|
|
|
[SerializeField, Range(0f, 1f)] private float velocityDamping = 0.985f;
|
|
[SerializeField, Range(1, 40)] private int solverIterations = 12;
|
|
[SerializeField, Range(0f, 1f)] private float stiffness = 1f;
|
|
[SerializeField] private Vector3 windVelocity = Vector3.zero;
|
|
[SerializeField, Min(0f)] private float windResponse = 0.5f;
|
|
[SerializeField, Min(0f)] private float maxWindAcceleration = 30f;
|
|
[SerializeField, Min(0f)] private float maxPointSpeed = 25f;
|
|
|
|
[Header("Ground")] [SerializeField] private bool stopOnGround = false;
|
|
[SerializeField] private LayerMask groundMask = 0;
|
|
[SerializeField, Min(0f)] private float groundRadius = 0.01f;
|
|
[SerializeField, Min(0f)] private float groundCastHeight = 1f;
|
|
[SerializeField, Min(0.01f)] private float groundCastDistance = 3f;
|
|
|
|
[Header("Water")] [SerializeField] private bool stopOnWaterSurface = false;
|
|
[SerializeField] private float waterSurfaceIgnoreNodeCount = 3;
|
|
[SerializeField] private float waterLevelY = 0f;
|
|
[SerializeField, Min(0f)] private float waterSurfaceOffset = 0.002f;
|
|
|
|
[Header("Line Renderer")] [SerializeField, Min(0.0001f)]
|
|
private float lineWidth = 0.01f;
|
|
|
|
[SerializeField] private bool smoothLine = true;
|
|
[SerializeField, Range(0, 4)] private int smoothingIterations = 2;
|
|
[SerializeField, Range(0.05f, 0.45f)] private float smoothingStrength = 0.25f;
|
|
[SerializeField, Min(0f)] private float lengthTolerance = 0.0005f;
|
|
|
|
[Header("Taut Stabilization")] [SerializeField, Min(0f)]
|
|
private float tautSlackThreshold = 0.01f;
|
|
|
|
[SerializeField, Range(0f, 1f)] private float tautPositionBlend = 0.85f;
|
|
[SerializeField, Range(0f, 1f)] private float tautVelocityRetention = 0.05f;
|
|
|
|
[Header("Debug")] [SerializeField] private bool drawVisualNodes = false;
|
|
[SerializeField] private float gizmoNodeRadius = 0.015f;
|
|
|
|
private readonly List<Rigidbody> _bodies = new List<Rigidbody>();
|
|
private readonly List<VisualSegmentState> _segments = new List<VisualSegmentState>();
|
|
|
|
private LineRenderer _lineRenderer;
|
|
private bool _needsRebuild = true;
|
|
|
|
private const float MinDistance = 0.0001f;
|
|
private const float MinDeltaTime = 0.0001f;
|
|
private const string SegmentRendererNamePrefix = "FLineRenderer Segment ";
|
|
|
|
private void Awake()
|
|
{
|
|
cable = cable ? cable : GetComponent<FLine>();
|
|
_lineRenderer = GetComponent<LineRenderer>();
|
|
ConfigureLineRenderer(_lineRenderer);
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
_needsRebuild = true;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
ClearLine();
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
DestroyExtraSegmentRenderers();
|
|
}
|
|
|
|
private void OnValidate()
|
|
{
|
|
visualSubdivisionsPerSegment = Mathf.Max(1, visualSubdivisionsPerSegment);
|
|
gravityStrength = Mathf.Max(0f, gravityStrength);
|
|
visualLengthTrim = Mathf.Max(0f, visualLengthTrim);
|
|
velocityDamping = Mathf.Clamp01(velocityDamping);
|
|
solverIterations = Mathf.Clamp(solverIterations, 1, 40);
|
|
stiffness = Mathf.Clamp01(stiffness);
|
|
windResponse = Mathf.Max(0f, windResponse);
|
|
maxWindAcceleration = Mathf.Max(0f, maxWindAcceleration);
|
|
maxPointSpeed = Mathf.Max(0f, maxPointSpeed);
|
|
groundRadius = Mathf.Max(0f, groundRadius);
|
|
groundCastHeight = Mathf.Max(0f, groundCastHeight);
|
|
groundCastDistance = Mathf.Max(0.01f, groundCastDistance);
|
|
waterSurfaceOffset = Mathf.Max(0f, waterSurfaceOffset);
|
|
lineWidth = Mathf.Max(0.0001f, lineWidth);
|
|
smoothingIterations = Mathf.Clamp(smoothingIterations, 0, 4);
|
|
smoothingStrength = Mathf.Clamp(smoothingStrength, 0.05f, 0.45f);
|
|
lengthTolerance = Mathf.Max(0f, lengthTolerance);
|
|
tautSlackThreshold = Mathf.Max(0f, tautSlackThreshold);
|
|
tautPositionBlend = Mathf.Clamp01(tautPositionBlend);
|
|
tautVelocityRetention = Mathf.Clamp01(tautVelocityRetention);
|
|
gizmoNodeRadius = Mathf.Max(0f, gizmoNodeRadius);
|
|
|
|
if (visualSubdivisionsBySegment == null)
|
|
visualSubdivisionsBySegment = new List<int>();
|
|
|
|
for (int i = 0; i < visualSubdivisionsBySegment.Count; i++)
|
|
visualSubdivisionsBySegment[i] = Mathf.Max(0, visualSubdivisionsBySegment[i]);
|
|
|
|
_needsRebuild = true;
|
|
|
|
if (_lineRenderer == null)
|
|
_lineRenderer = GetComponent<LineRenderer>();
|
|
|
|
ConfigureLineRenderer(_lineRenderer);
|
|
SyncSegmentRendererStyles();
|
|
}
|
|
|
|
private void FixedUpdate()
|
|
{
|
|
if (!PrepareSimulation())
|
|
return;
|
|
|
|
float dt = Mathf.Max(Time.fixedDeltaTime, MinDeltaTime);
|
|
if (!stopOnGround && !stopOnWaterSurface)
|
|
ClearSupportedPoints();
|
|
|
|
LockCableNodes();
|
|
SimulateFreePoints(dt);
|
|
RefreshRestLengths();
|
|
|
|
for (int i = 0; i < solverIterations; i++)
|
|
{
|
|
LockCableNodes();
|
|
SolveDistanceConstraints();
|
|
ApplySurfaceConstraints();
|
|
}
|
|
|
|
EnforceExactSegmentLengths();
|
|
StabilizeTautSegments();
|
|
LockCableNodes();
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
if (!PrepareSimulation())
|
|
{
|
|
ClearLine();
|
|
return;
|
|
}
|
|
|
|
LockCableNodes();
|
|
DrawLine();
|
|
}
|
|
|
|
public void Rebuild()
|
|
{
|
|
_needsRebuild = true;
|
|
}
|
|
|
|
public void SetWind(Vector3 velocity, float response)
|
|
{
|
|
windVelocity = velocity;
|
|
windResponse = Mathf.Max(0f, response);
|
|
}
|
|
|
|
private bool PrepareSimulation()
|
|
{
|
|
if (cable == null)
|
|
cable = GetComponent<FLine>();
|
|
|
|
if (_lineRenderer == null)
|
|
{
|
|
_lineRenderer = GetComponent<LineRenderer>();
|
|
ConfigureLineRenderer(_lineRenderer);
|
|
}
|
|
|
|
if (cable == null || !RefreshBodies())
|
|
return false;
|
|
|
|
EnsureSegmentStates();
|
|
return _segments.Count > 0;
|
|
}
|
|
|
|
private bool RefreshBodies()
|
|
{
|
|
List<Rigidbody> sourceBodies = cable.GetConnectedBodies();
|
|
if (sourceBodies == null || sourceBodies.Count < 2)
|
|
return false;
|
|
|
|
bool changed = sourceBodies.Count != _bodies.Count;
|
|
if (!changed)
|
|
{
|
|
for (int i = 0; i < sourceBodies.Count; i++)
|
|
{
|
|
if (sourceBodies[i] != _bodies[i])
|
|
{
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
_bodies.Clear();
|
|
_bodies.AddRange(sourceBodies);
|
|
_needsRebuild = true;
|
|
}
|
|
|
|
for (int i = 0; i < _bodies.Count; i++)
|
|
{
|
|
if (_bodies[i] == null)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void EnsureSegmentStates()
|
|
{
|
|
int segmentCount = _bodies.Count - 1;
|
|
EnsureSegmentRendererCount(segmentCount);
|
|
|
|
for (int segmentIndex = 0; segmentIndex < segmentCount; segmentIndex++)
|
|
{
|
|
VisualSegmentState state = _segments[segmentIndex];
|
|
state.SegmentIndex = segmentIndex;
|
|
state.Renderer = GetOrCreateSegmentRenderer(segmentIndex, state);
|
|
|
|
int subdivisions = GetVisualSubdivisionsForSegment(segmentIndex);
|
|
int pointCount = subdivisions + 1;
|
|
if (state.Points == null ||
|
|
state.PointCount != pointCount ||
|
|
state.Subdivisions != subdivisions ||
|
|
_needsRebuild)
|
|
{
|
|
state.Subdivisions = subdivisions;
|
|
state.Points = new Vector3[pointCount];
|
|
state.PrevPoints = new Vector3[pointCount];
|
|
state.EdgeRestLengths = new float[subdivisions];
|
|
state.SupportedPoints = new bool[pointCount];
|
|
state.SmoothPoints = null;
|
|
state.SegmentPoints = null;
|
|
state.SmoothCapacity = 0;
|
|
state.SegmentCapacity = 0;
|
|
InitializeSegmentPoints(state);
|
|
}
|
|
}
|
|
|
|
_needsRebuild = false;
|
|
}
|
|
|
|
private void EnsureSegmentRendererCount(int segmentCount)
|
|
{
|
|
while (_segments.Count < segmentCount)
|
|
_segments.Add(new VisualSegmentState());
|
|
|
|
for (int i = _segments.Count - 1; i >= segmentCount; i--)
|
|
{
|
|
DestroySegmentRenderer(_segments[i]);
|
|
_segments.RemoveAt(i);
|
|
}
|
|
}
|
|
|
|
private LineRenderer GetOrCreateSegmentRenderer(int segmentIndex, VisualSegmentState state)
|
|
{
|
|
if (segmentIndex == 0)
|
|
return _lineRenderer;
|
|
|
|
if (state.Renderer != null)
|
|
return state.Renderer;
|
|
|
|
string rendererName = SegmentRendererNamePrefix + segmentIndex;
|
|
GameObject rendererObject;
|
|
LineRenderer renderer;
|
|
|
|
Transform child = transform.Find(rendererName);
|
|
if (child != null)
|
|
{
|
|
rendererObject = child.gameObject;
|
|
renderer = child.GetComponent<LineRenderer>();
|
|
}
|
|
else
|
|
{
|
|
rendererObject = new GameObject(rendererName);
|
|
rendererObject.transform.SetParent(transform, false);
|
|
renderer = null;
|
|
}
|
|
|
|
if (renderer == null)
|
|
renderer = rendererObject.AddComponent<LineRenderer>();
|
|
|
|
state.RendererObject = rendererObject;
|
|
state.Renderer = renderer;
|
|
ConfigureLineRenderer(renderer);
|
|
return renderer;
|
|
}
|
|
|
|
private void DestroyExtraSegmentRenderers()
|
|
{
|
|
for (int i = 1; i < _segments.Count; i++)
|
|
DestroySegmentRenderer(_segments[i]);
|
|
}
|
|
|
|
private void DestroySegmentRenderer(VisualSegmentState state)
|
|
{
|
|
if (state == null)
|
|
return;
|
|
|
|
if (state.Renderer == _lineRenderer)
|
|
{
|
|
state.Renderer = null;
|
|
state.RendererObject = null;
|
|
return;
|
|
}
|
|
|
|
GameObject rendererObject = state.RendererObject;
|
|
if (rendererObject == null && state.Renderer != null)
|
|
rendererObject = state.Renderer.gameObject;
|
|
|
|
if (state.Renderer != null)
|
|
state.Renderer.positionCount = 0;
|
|
|
|
state.Renderer = null;
|
|
state.RendererObject = null;
|
|
|
|
if (rendererObject == null)
|
|
return;
|
|
|
|
if (Application.isPlaying)
|
|
Destroy(rendererObject);
|
|
else
|
|
DestroyImmediate(rendererObject);
|
|
}
|
|
|
|
private void InitializeSegmentPoints(VisualSegmentState state)
|
|
{
|
|
int subdivisions = Mathf.Max(1, state.Subdivisions);
|
|
Vector3 start = _bodies[state.SegmentIndex].position;
|
|
Vector3 end = _bodies[state.SegmentIndex + 1].position;
|
|
|
|
for (int i = 0; i <= subdivisions; i++)
|
|
{
|
|
float t = i / (float)subdivisions;
|
|
Vector3 point = Vector3.Lerp(start, end, t);
|
|
state.Points[i] = point;
|
|
state.PrevPoints[i] = point;
|
|
state.SupportedPoints[i] = false;
|
|
}
|
|
|
|
RefreshRestLengths(state);
|
|
}
|
|
|
|
private void RefreshRestLengths()
|
|
{
|
|
for (int i = 0; i < _segments.Count; i++)
|
|
RefreshRestLengths(_segments[i]);
|
|
}
|
|
|
|
private void RefreshRestLengths(VisualSegmentState state)
|
|
{
|
|
float currentDistance = Vector3.Distance(
|
|
_bodies[state.SegmentIndex].position,
|
|
_bodies[state.SegmentIndex + 1].position
|
|
);
|
|
float maxLength = cable.GetSegmentMaxLength(state.SegmentIndex);
|
|
if (maxLength <= 0f)
|
|
maxLength = currentDistance;
|
|
|
|
float visualRest = Mathf.Max(maxLength - visualLengthTrim, 0f);
|
|
float totalRest = Mathf.Max(visualRest, currentDistance);
|
|
float edgeRest = totalRest / Mathf.Max(1, state.Subdivisions);
|
|
|
|
for (int i = 0; i < state.EdgeRestLengths.Length; i++)
|
|
state.EdgeRestLengths[i] = edgeRest;
|
|
}
|
|
|
|
private void ConfigureLineRenderer(LineRenderer renderer)
|
|
{
|
|
if (renderer == null)
|
|
return;
|
|
|
|
if (_lineRenderer != null && renderer != _lineRenderer)
|
|
CopyLineRendererTemplate(_lineRenderer, renderer);
|
|
|
|
renderer.useWorldSpace = true;
|
|
renderer.loop = false;
|
|
renderer.startWidth = lineWidth;
|
|
renderer.endWidth = lineWidth;
|
|
}
|
|
|
|
private void CopyLineRendererTemplate(LineRenderer template, LineRenderer target)
|
|
{
|
|
if (template == null || target == null || template == target)
|
|
return;
|
|
|
|
target.alignment = template.alignment;
|
|
target.textureMode = template.textureMode;
|
|
target.shadowBias = template.shadowBias;
|
|
target.generateLightingData = template.generateLightingData;
|
|
target.numCapVertices = template.numCapVertices;
|
|
target.numCornerVertices = template.numCornerVertices;
|
|
target.startColor = template.startColor;
|
|
target.endColor = template.endColor;
|
|
target.colorGradient = template.colorGradient;
|
|
target.widthCurve = template.widthCurve;
|
|
target.widthMultiplier = template.widthMultiplier;
|
|
target.sharedMaterial = template.sharedMaterial;
|
|
target.sortingLayerID = template.sortingLayerID;
|
|
target.sortingOrder = template.sortingOrder;
|
|
target.shadowCastingMode = template.shadowCastingMode;
|
|
target.receiveShadows = template.receiveShadows;
|
|
target.enabled = template.enabled;
|
|
}
|
|
|
|
private void SyncSegmentRendererStyles()
|
|
{
|
|
for (int i = 0; i < _segments.Count; i++)
|
|
ConfigureLineRenderer(_segments[i].Renderer);
|
|
}
|
|
|
|
private void ClearLine()
|
|
{
|
|
if (_lineRenderer != null)
|
|
_lineRenderer.positionCount = 0;
|
|
|
|
for (int i = 1; i < _segments.Count; i++)
|
|
{
|
|
LineRenderer renderer = _segments[i].Renderer;
|
|
if (renderer != null)
|
|
renderer.positionCount = 0;
|
|
}
|
|
}
|
|
|
|
private void LockCableNodes()
|
|
{
|
|
for (int i = 0; i < _segments.Count; i++)
|
|
LockCableNodes(_segments[i]);
|
|
}
|
|
|
|
private void LockCableNodes(VisualSegmentState state)
|
|
{
|
|
if (state.Points == null || state.PrevPoints == null || state.PointCount < 2)
|
|
return;
|
|
|
|
int lastIndex = state.PointCount - 1;
|
|
Vector3 start = _bodies[state.SegmentIndex].position;
|
|
Vector3 end = _bodies[state.SegmentIndex + 1].position;
|
|
|
|
state.Points[0] = start;
|
|
state.PrevPoints[0] = start;
|
|
state.SupportedPoints[0] = false;
|
|
|
|
state.Points[lastIndex] = end;
|
|
state.PrevPoints[lastIndex] = end;
|
|
state.SupportedPoints[lastIndex] = false;
|
|
}
|
|
|
|
private void SimulateFreePoints(float dt)
|
|
{
|
|
for (int i = 0; i < _segments.Count; i++)
|
|
SimulateFreePoints(_segments[i], dt);
|
|
}
|
|
|
|
private void SimulateFreePoints(VisualSegmentState state, float dt)
|
|
{
|
|
if (state.PointCount <= 2)
|
|
return;
|
|
|
|
float dt2 = dt * dt;
|
|
float maxDisp = maxPointSpeed > 0f ? maxPointSpeed * dt : float.PositiveInfinity;
|
|
float maxDispSq = maxDisp * maxDisp;
|
|
Vector3 gravity = Vector3.down * gravityStrength;
|
|
bool useWind = windResponse > 0f && windVelocity.sqrMagnitude > 0.0001f;
|
|
float chordLength = Vector3.Distance(
|
|
_bodies[state.SegmentIndex].position,
|
|
_bodies[state.SegmentIndex + 1].position
|
|
);
|
|
float tautness = GetSegmentTautness(state.SegmentIndex, chordLength);
|
|
float mobility = 1f - tautness;
|
|
|
|
for (int i = 1; i < state.PointCount - 1; i++)
|
|
{
|
|
if (state.SupportedPoints[i])
|
|
{
|
|
state.PrevPoints[i] = state.Points[i];
|
|
continue;
|
|
}
|
|
|
|
Vector3 current = state.Points[i];
|
|
Vector3 displacement = current - state.PrevPoints[i];
|
|
if (maxPointSpeed > 0f && displacement.sqrMagnitude > maxDispSq)
|
|
displacement = displacement.normalized * maxDisp;
|
|
|
|
displacement *= Mathf.Lerp(tautVelocityRetention, 1f, mobility);
|
|
|
|
Vector3 acceleration = gravity;
|
|
if (useWind)
|
|
{
|
|
Vector3 velocity = displacement / dt;
|
|
Vector3 windAcceleration = (windVelocity - velocity) * windResponse;
|
|
if (maxWindAcceleration > 0f &&
|
|
windAcceleration.sqrMagnitude > maxWindAcceleration * maxWindAcceleration)
|
|
windAcceleration = windAcceleration.normalized * maxWindAcceleration;
|
|
|
|
acceleration += windAcceleration;
|
|
}
|
|
|
|
acceleration *= mobility;
|
|
|
|
state.PrevPoints[i] = current;
|
|
state.Points[i] = current + displacement * velocityDamping + acceleration * dt2;
|
|
}
|
|
}
|
|
|
|
private void SolveDistanceConstraints()
|
|
{
|
|
float solveStiffness = Mathf.Clamp01(stiffness);
|
|
for (int i = 0; i < _segments.Count; i++)
|
|
SolveDistanceConstraints(_segments[i], solveStiffness);
|
|
}
|
|
|
|
private void SolveDistanceConstraints(VisualSegmentState state, float solveStiffness)
|
|
{
|
|
if (state.EdgeRestLengths == null || state.EdgeRestLengths.Length == 0)
|
|
return;
|
|
|
|
SolveSweep(state, 0, state.EdgeRestLengths.Length, 1, solveStiffness);
|
|
SolveSweep(state, state.EdgeRestLengths.Length - 1, -1, -1, solveStiffness);
|
|
}
|
|
|
|
private void SolveSweep(VisualSegmentState state, int start, int endExclusive, int step, float solveStiffness)
|
|
{
|
|
float chordLength = Vector3.Distance(state.Points[0], state.Points[state.PointCount - 1]);
|
|
bool useMaxOnly = ShouldUseMaxOnlyConstraint(state.SegmentIndex, chordLength, state);
|
|
|
|
for (int edge = start; edge != endExclusive; edge += step)
|
|
{
|
|
int aIndex = edge;
|
|
int bIndex = edge + 1;
|
|
Vector3 a = state.Points[aIndex];
|
|
Vector3 b = state.Points[bIndex];
|
|
Vector3 delta = b - a;
|
|
float sqrMagnitude = delta.sqrMagnitude;
|
|
if (sqrMagnitude < 1e-10f)
|
|
continue;
|
|
|
|
float distance = Mathf.Sqrt(sqrMagnitude);
|
|
float rest = Mathf.Max(state.EdgeRestLengths[edge], MinDistance);
|
|
if (useMaxOnly && distance <= rest + lengthTolerance)
|
|
continue;
|
|
|
|
float error = useMaxOnly ? Mathf.Max(distance - rest, 0f) : distance - rest;
|
|
Vector3 correction = delta * (error / distance * solveStiffness);
|
|
|
|
bool aLocked = aIndex == 0;
|
|
bool bLocked = bIndex == state.PointCount - 1;
|
|
|
|
if (!aLocked && !bLocked)
|
|
{
|
|
state.Points[aIndex] = a + correction * 0.5f;
|
|
state.Points[bIndex] = b - correction * 0.5f;
|
|
}
|
|
else if (aLocked && !bLocked)
|
|
{
|
|
state.Points[bIndex] = b - correction;
|
|
}
|
|
else if (!aLocked && bLocked)
|
|
{
|
|
state.Points[aIndex] = a + correction;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplySurfaceConstraints()
|
|
{
|
|
if (!stopOnGround && !stopOnWaterSurface)
|
|
return;
|
|
|
|
for (int i = 0; i < _segments.Count; i++)
|
|
ApplySurfaceConstraints(_segments[i]);
|
|
}
|
|
|
|
private void ApplySurfaceConstraints(VisualSegmentState state)
|
|
{
|
|
if (state.PointCount <= 2)
|
|
return;
|
|
|
|
for (int i = 0; i < state.SupportedPoints.Length; i++)
|
|
state.SupportedPoints[i] = false;
|
|
|
|
for (int i = 1; i < state.PointCount - 1; i++)
|
|
{
|
|
float minY = SampleSurfaceMinY(state, i);
|
|
if (float.IsNegativeInfinity(minY))
|
|
continue;
|
|
|
|
Vector3 point = state.Points[i];
|
|
if (point.y < minY)
|
|
{
|
|
point.y = minY;
|
|
state.Points[i] = point;
|
|
|
|
Vector3 prev = state.PrevPoints[i];
|
|
if (prev.y < minY)
|
|
{
|
|
prev.y = minY;
|
|
state.PrevPoints[i] = prev;
|
|
}
|
|
}
|
|
|
|
if (state.Points[i].y <= minY + lengthTolerance)
|
|
state.SupportedPoints[i] = true;
|
|
}
|
|
}
|
|
|
|
private void EnforceExactSegmentLengths()
|
|
{
|
|
for (int i = 0; i < _segments.Count; i++)
|
|
EnforceExactSegmentLengths(_segments[i]);
|
|
|
|
ApplySurfaceConstraints();
|
|
}
|
|
|
|
private void EnforceExactSegmentLengths(VisualSegmentState state)
|
|
{
|
|
if (state.Subdivisions <= 1 || state.PointCount <= 2)
|
|
return;
|
|
|
|
Vector3 start = state.Points[0];
|
|
Vector3 end = state.Points[state.PointCount - 1];
|
|
float chordLength = Vector3.Distance(start, end);
|
|
if (ShouldUseMaxOnlyConstraint(state.SegmentIndex, chordLength, state))
|
|
return;
|
|
|
|
float targetLength = GetTargetSegmentLength(state.SegmentIndex, chordLength);
|
|
float currentLength = ComputeSegmentLength(state);
|
|
if (currentLength <= targetLength + lengthTolerance)
|
|
return;
|
|
|
|
float blend = FindSegmentCompressionBlend(state, start, end, targetLength);
|
|
ApplySegmentCompression(state, start, end, blend);
|
|
}
|
|
|
|
private void StabilizeTautSegments()
|
|
{
|
|
for (int i = 0; i < _segments.Count; i++)
|
|
StabilizeTautSegments(_segments[i]);
|
|
|
|
ApplySurfaceConstraints();
|
|
}
|
|
|
|
private void StabilizeTautSegments(VisualSegmentState state)
|
|
{
|
|
if (state.Subdivisions <= 1 || state.PointCount <= 2)
|
|
return;
|
|
|
|
Vector3 start = state.Points[0];
|
|
Vector3 end = state.Points[state.PointCount - 1];
|
|
float chordLength = Vector3.Distance(start, end);
|
|
if (ShouldUseMaxOnlyConstraint(state.SegmentIndex, chordLength, state))
|
|
return;
|
|
|
|
float tautness = GetSegmentTautness(state.SegmentIndex, chordLength);
|
|
if (tautness <= 0f)
|
|
return;
|
|
|
|
float positionBlend = tautPositionBlend * tautness;
|
|
bool fullyTaut = tautness >= 0.999f;
|
|
|
|
for (int pointIndex = 1; pointIndex < state.PointCount - 1; pointIndex++)
|
|
{
|
|
float t = pointIndex / (float)state.Subdivisions;
|
|
Vector3 linear = Vector3.Lerp(start, end, t);
|
|
|
|
if (fullyTaut)
|
|
{
|
|
state.Points[pointIndex] = linear;
|
|
state.PrevPoints[pointIndex] = linear;
|
|
continue;
|
|
}
|
|
|
|
Vector3 stabilized = Vector3.Lerp(state.Points[pointIndex], linear, positionBlend);
|
|
state.Points[pointIndex] = stabilized;
|
|
state.PrevPoints[pointIndex] = Vector3.Lerp(state.PrevPoints[pointIndex], stabilized, positionBlend);
|
|
}
|
|
}
|
|
|
|
private float ComputeSegmentLength(VisualSegmentState state)
|
|
{
|
|
float length = 0f;
|
|
for (int i = 0; i < state.PointCount - 1; i++)
|
|
length += Vector3.Distance(state.Points[i], state.Points[i + 1]);
|
|
|
|
return length;
|
|
}
|
|
|
|
private float FindSegmentCompressionBlend(VisualSegmentState state, Vector3 start, Vector3 end,
|
|
float targetLength)
|
|
{
|
|
float minBlend = 0f;
|
|
float maxBlend = 1f;
|
|
|
|
for (int iteration = 0; iteration < 10; iteration++)
|
|
{
|
|
float midBlend = (minBlend + maxBlend) * 0.5f;
|
|
float midLength = ComputeCompressedSegmentLength(state, start, end, midBlend);
|
|
if (midLength > targetLength)
|
|
maxBlend = midBlend;
|
|
else
|
|
minBlend = midBlend;
|
|
}
|
|
|
|
return minBlend;
|
|
}
|
|
|
|
private float ComputeCompressedSegmentLength(VisualSegmentState state, Vector3 start, Vector3 end, float blend)
|
|
{
|
|
Vector3 previous = start;
|
|
float length = 0f;
|
|
|
|
for (int pointIndex = 1; pointIndex < state.PointCount - 1; pointIndex++)
|
|
{
|
|
float t = pointIndex / (float)state.Subdivisions;
|
|
Vector3 linear = Vector3.Lerp(start, end, t);
|
|
Vector3 current = state.Points[pointIndex];
|
|
Vector3 blended = Vector3.Lerp(linear, current, blend);
|
|
length += Vector3.Distance(previous, blended);
|
|
previous = blended;
|
|
}
|
|
|
|
length += Vector3.Distance(previous, end);
|
|
return length;
|
|
}
|
|
|
|
private void ApplySegmentCompression(VisualSegmentState state, Vector3 start, Vector3 end, float blend)
|
|
{
|
|
for (int pointIndex = 1; pointIndex < state.PointCount - 1; pointIndex++)
|
|
{
|
|
float t = pointIndex / (float)state.Subdivisions;
|
|
Vector3 linear = Vector3.Lerp(start, end, t);
|
|
Vector3 compressed = Vector3.Lerp(linear, state.Points[pointIndex], blend);
|
|
state.Points[pointIndex] = compressed;
|
|
state.PrevPoints[pointIndex] = compressed;
|
|
}
|
|
}
|
|
|
|
private float SampleSurfaceMinY(VisualSegmentState state, int pointIndex)
|
|
{
|
|
Vector3 point = state.Points[pointIndex];
|
|
float minY = float.NegativeInfinity;
|
|
|
|
if (stopOnGround && groundMask.value != 0)
|
|
{
|
|
Vector3 origin = point + Vector3.up * groundCastHeight;
|
|
if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask,
|
|
QueryTriggerInteraction.Ignore))
|
|
{
|
|
minY = Mathf.Max(minY, hit.point.y + groundRadius);
|
|
}
|
|
}
|
|
|
|
if (stopOnWaterSurface && state.SegmentIndex == 0)
|
|
{
|
|
if (pointIndex > waterSurfaceIgnoreNodeCount &&
|
|
pointIndex < state.PointCount - waterSurfaceIgnoreNodeCount)
|
|
minY = Mathf.Max(minY, waterLevelY + waterSurfaceOffset);
|
|
}
|
|
|
|
return minY;
|
|
}
|
|
|
|
private void ClearSupportedPoints()
|
|
{
|
|
for (int i = 0; i < _segments.Count; i++)
|
|
{
|
|
bool[] supportedPoints = _segments[i].SupportedPoints;
|
|
if (supportedPoints == null)
|
|
continue;
|
|
|
|
for (int pointIndex = 0; pointIndex < supportedPoints.Length; pointIndex++)
|
|
supportedPoints[pointIndex] = false;
|
|
}
|
|
}
|
|
|
|
private float GetTargetSegmentLength(int segmentIndex, float chordLength)
|
|
{
|
|
float maxLength = cable.GetSegmentMaxLength(segmentIndex);
|
|
if (maxLength <= 0f)
|
|
maxLength = chordLength;
|
|
|
|
return Mathf.Max(maxLength - visualLengthTrim, chordLength);
|
|
}
|
|
|
|
private float GetSegmentTautness(int segmentIndex, float chordLength)
|
|
{
|
|
float targetLength = GetTargetSegmentLength(segmentIndex, chordLength);
|
|
float slack = Mathf.Max(targetLength - chordLength, 0f);
|
|
if (tautSlackThreshold <= 0f)
|
|
return slack <= lengthTolerance ? 1f : 0f;
|
|
|
|
return 1f - Mathf.Clamp01(slack / tautSlackThreshold);
|
|
}
|
|
|
|
private bool ShouldUseMaxOnlyConstraint(int segmentIndex, float chordLength, VisualSegmentState state)
|
|
{
|
|
if (IsSegmentSupported(state))
|
|
return true;
|
|
|
|
return GetSegmentTautness(segmentIndex, chordLength) < 0.999f;
|
|
}
|
|
|
|
private bool IsSegmentSupported(VisualSegmentState state)
|
|
{
|
|
if (state.SupportedPoints == null || state.PointCount <= 2)
|
|
return false;
|
|
|
|
for (int i = 1; i < state.PointCount - 1; i++)
|
|
{
|
|
if (state.SupportedPoints[i])
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private int GetVisualSubdivisionsForSegment(int segmentIndex)
|
|
{
|
|
if (segmentIndex >= 0 && segmentIndex < visualSubdivisionsBySegment.Count)
|
|
{
|
|
int overrideValue = visualSubdivisionsBySegment[segmentIndex];
|
|
if (overrideValue > 0)
|
|
return overrideValue;
|
|
}
|
|
|
|
return Mathf.Max(1, visualSubdivisionsPerSegment);
|
|
}
|
|
|
|
private void DrawLine()
|
|
{
|
|
SyncSegmentRendererStyles();
|
|
|
|
for (int i = 0; i < _segments.Count; i++)
|
|
DrawSegment(_segments[i]);
|
|
}
|
|
|
|
private void DrawSegment(VisualSegmentState state)
|
|
{
|
|
LineRenderer renderer = state.Renderer;
|
|
if (renderer == null)
|
|
return;
|
|
|
|
if (state.Points == null || state.PointCount < 2)
|
|
{
|
|
renderer.positionCount = 0;
|
|
return;
|
|
}
|
|
|
|
if (!smoothLine || smoothingIterations <= 0 || state.PointCount <= 2)
|
|
{
|
|
ApplyRendererPositions(renderer, state.Points, state.PointCount);
|
|
return;
|
|
}
|
|
|
|
int count = BuildSmoothedSegment(state);
|
|
ApplyRendererPositions(renderer, state.SegmentPoints, count);
|
|
}
|
|
|
|
private int BuildSmoothedSegment(VisualSegmentState state)
|
|
{
|
|
int sourceCount = state.PointCount;
|
|
EnsureSegmentCapacity(state, sourceCount);
|
|
for (int i = 0; i < sourceCount; i++)
|
|
state.SegmentPoints[i] = state.Points[i];
|
|
|
|
Vector3[] source = state.SegmentPoints;
|
|
int segmentPointCount = sourceCount;
|
|
|
|
for (int iteration = 0; iteration < smoothingIterations; iteration++)
|
|
{
|
|
int needed = segmentPointCount * 2;
|
|
Vector3[] target;
|
|
if (source == state.SegmentPoints)
|
|
{
|
|
EnsureSmoothCapacity(state, needed);
|
|
target = state.SmoothPoints;
|
|
}
|
|
else
|
|
{
|
|
EnsureSegmentCapacity(state, needed);
|
|
target = state.SegmentPoints;
|
|
}
|
|
|
|
int index = 0;
|
|
target[index++] = source[0];
|
|
|
|
for (int i = 0; i < segmentPointCount - 1; i++)
|
|
{
|
|
Vector3 a = source[i];
|
|
Vector3 b = source[i + 1];
|
|
target[index++] = Vector3.Lerp(a, b, smoothingStrength);
|
|
target[index++] = Vector3.Lerp(a, b, 1f - smoothingStrength);
|
|
}
|
|
|
|
target[index++] = source[segmentPointCount - 1];
|
|
source = target;
|
|
segmentPointCount = index;
|
|
}
|
|
|
|
if (source != state.SegmentPoints)
|
|
{
|
|
EnsureSegmentCapacity(state, segmentPointCount);
|
|
for (int i = 0; i < segmentPointCount; i++)
|
|
state.SegmentPoints[i] = source[i];
|
|
}
|
|
|
|
return segmentPointCount;
|
|
}
|
|
|
|
private void EnsureSmoothCapacity(VisualSegmentState state, int needed)
|
|
{
|
|
if (state.SmoothPoints != null && state.SmoothCapacity >= needed)
|
|
return;
|
|
|
|
state.SmoothCapacity = needed;
|
|
state.SmoothPoints = new Vector3[state.SmoothCapacity];
|
|
}
|
|
|
|
private void EnsureSegmentCapacity(VisualSegmentState state, int needed)
|
|
{
|
|
if (state.SegmentPoints != null && state.SegmentCapacity >= needed)
|
|
return;
|
|
|
|
state.SegmentCapacity = needed;
|
|
state.SegmentPoints = new Vector3[state.SegmentCapacity];
|
|
}
|
|
|
|
private void ApplyRendererPositions(LineRenderer renderer, Vector3[] positions, int count)
|
|
{
|
|
if (renderer == null)
|
|
return;
|
|
|
|
renderer.positionCount = count;
|
|
for (int i = 0; i < count; i++)
|
|
renderer.SetPosition(i, positions[i]);
|
|
}
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
if (!drawVisualNodes)
|
|
return;
|
|
|
|
for (int segmentIndex = 0; segmentIndex < _segments.Count; segmentIndex++)
|
|
{
|
|
VisualSegmentState state = _segments[segmentIndex];
|
|
if (state.Points == null)
|
|
continue;
|
|
|
|
for (int pointIndex = 0; pointIndex < state.PointCount; pointIndex++)
|
|
{
|
|
bool isLogicNode = pointIndex == 0 || pointIndex == state.PointCount - 1;
|
|
Gizmos.color = isLogicNode ? Color.yellow : Color.cyan;
|
|
Gizmos.DrawSphere(state.Points[pointIndex], gizmoNodeRadius);
|
|
#if UNITY_EDITOR
|
|
Handles.Label(
|
|
state.Points[pointIndex] + Vector3.up * gizmoNodeRadius,
|
|
$"{segmentIndex}:{pointIndex}"
|
|
);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |