提交测试代码

This commit is contained in:
2026-04-06 23:57:28 +08:00
parent 961f9cf9df
commit 9d23b378a6
2 changed files with 269 additions and 404 deletions

View File

@@ -22,10 +22,16 @@ namespace F2RopeLine2.FishingLine
[Min(0.001f)] [Min(0.001f)]
[SerializeField] private float maxDeltaTime = 0.0333333f; [SerializeField] private float maxDeltaTime = 0.0333333f;
[Header("Pin Follow")] [Header("Water Surface")]
[SerializeField] private bool constrainToWaterSurface = true;
[SerializeField] private Transform waterSurfaceTransform;
[SerializeField] private float waterSurfaceHeight;
[Min(0)]
[SerializeField] private int ignoreHeadNodeCount = 1;
[Min(0)]
[SerializeField] private int ignoreTailNodeCount = 1;
[Min(0f)] [Min(0f)]
[SerializeField] private float pinFollowStrength = 50f; [SerializeField] private float waterSurfaceFollowSpeed = 12f;
[SerializeField] private bool writeBackVirtualNodeTransforms = true;
[Header("Stability")] [Header("Stability")]
[Min(1)] [Min(1)]
@@ -40,15 +46,15 @@ namespace F2RopeLine2.FishingLine
[SerializeField] private float wakeDistanceThreshold = 0.001f; [SerializeField] private float wakeDistanceThreshold = 0.001f;
[Header("Debug")] [Header("Debug")]
[SerializeField] private bool drawDebugSamples = false; [SerializeField] private bool drawDebugSamples;
[SerializeField] private Color debugSampleColor = new(1f, 0.2f, 0.2f, 1f); [SerializeField] private Color debugSampleColor = new(1f, 0.2f, 0.2f, 1f);
[Min(0.001f)] [Min(0.001f)]
[SerializeField] private float debugSampleRadius = 0.015f; [SerializeField] private float debugSampleRadius = 0.015f;
private readonly List<Vector3> positions = new(); private readonly List<Vector3> positions = new();
private readonly List<Vector3> previousPositions = new(); private readonly List<Vector3> previousPositions = new();
private readonly List<FishingLineNode> sampledNodes = new(); private readonly List<long> sampledPointKeys = new();
private readonly List<Vector3> lastPinnedNodePositions = new(); private readonly List<Vector3> lastPinnedPointPositions = new();
private readonly List<float> lastRestLengths = new(); private readonly List<float> lastRestLengths = new();
private bool[] pinnedFlags = System.Array.Empty<bool>(); private bool[] pinnedFlags = System.Array.Empty<bool>();
private float accumulatedTime; private float accumulatedTime;
@@ -96,70 +102,72 @@ namespace F2RopeLine2.FishingLine
} }
solver = sourceSolver; solver = sourceSolver;
var nodes = solver.OrderedNodes; var points = solver.ChainPoints;
var restLengths = solver.RestLengths; var restLengths = solver.RestLengths;
var pinnedIndices = solver.PinnedIndices; var pinnedIndices = solver.PinnedIndices;
if (nodes.Count == 0) if (points.Count == 0)
{ {
lineRenderer.positionCount = 0; lineRenderer.positionCount = 0;
return; return;
} }
var topologyChanged = EnsureBuffers(nodes, pinnedIndices); var topologyChanged = EnsureBuffers(points, pinnedIndices);
if (topologyChanged) if (topologyChanged || ShouldWake(points, restLengths))
{ {
WakeUp(); WakeUp();
} }
if (ShouldWake(nodes, restLengths)) Simulate(points, restLengths, deltaTime);
{ ApplyToRenderer();
WakeUp(); CacheFrameState(points, restLengths);
}
Simulate(nodes, restLengths, deltaTime);
ApplyToRenderer(nodes);
CacheFrameState(nodes, restLengths);
} }
private bool EnsureBuffers( private bool EnsureBuffers(
IReadOnlyList<FishingLineNode> nodes, IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<int> pinnedIndices) IReadOnlyList<int> pinnedIndices)
{ {
var topologyChanged = positions.Count != nodes.Count; var topologyChanged = sampledPointKeys.Count != points.Count;
var previousPositionMap = new Dictionary<FishingLineNode, Vector3>(sampledNodes.Count); if (!topologyChanged)
var previousHistoryMap = new Dictionary<FishingLineNode, Vector3>(sampledNodes.Count);
for (var i = 0; i < sampledNodes.Count; i++)
{ {
var sampledNode = sampledNodes[i]; for (var i = 0; i < points.Count; i++)
if (sampledNode == null) {
if (sampledPointKeys[i] == points[i].Key)
{ {
continue; continue;
} }
previousPositionMap[sampledNode] = positions[i]; topologyChanged = true;
previousHistoryMap[sampledNode] = previousPositions[i]; break;
}
}
var previousPositionMap = new Dictionary<long, Vector3>(sampledPointKeys.Count);
var previousHistoryMap = new Dictionary<long, Vector3>(sampledPointKeys.Count);
for (var i = 0; i < sampledPointKeys.Count; i++)
{
previousPositionMap[sampledPointKeys[i]] = positions[i];
previousHistoryMap[sampledPointKeys[i]] = previousPositions[i];
} }
positions.Clear(); positions.Clear();
previousPositions.Clear(); previousPositions.Clear();
sampledNodes.Clear(); sampledPointKeys.Clear();
pinnedFlags = new bool[nodes.Count]; pinnedFlags = new bool[points.Count];
for (var i = 0; i < nodes.Count; i++) for (var i = 0; i < points.Count; i++)
{ {
var node = nodes[i]; var point = points[i];
sampledNodes.Add(node); sampledPointKeys.Add(point.Key);
if (node != null && previousPositionMap.TryGetValue(node, out var preservedPosition)) if (previousPositionMap.TryGetValue(point.Key, out var preservedPosition))
{ {
positions.Add(preservedPosition); positions.Add(preservedPosition);
previousPositions.Add(previousHistoryMap[node]); previousPositions.Add(previousHistoryMap[point.Key]);
continue; continue;
} }
var position = node != null ? node.Position : Vector3.zero; positions.Add(point.Position);
positions.Add(position); previousPositions.Add(point.Position);
previousPositions.Add(position);
} }
for (var i = 0; i < pinnedIndices.Count; i++) for (var i = 0; i < pinnedIndices.Count; i++)
@@ -174,11 +182,14 @@ namespace F2RopeLine2.FishingLine
return topologyChanged; return topologyChanged;
} }
private void Simulate(IReadOnlyList<FishingLineNode> nodes, IReadOnlyList<float> restLengths, float deltaTime) private void Simulate(
IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<float> restLengths,
float deltaTime)
{ {
if (isSleeping) if (isSleeping)
{ {
PinLogicalNodes(nodes, simulationStep); PinLogicalPoints(points);
return; return;
} }
@@ -188,31 +199,33 @@ namespace F2RopeLine2.FishingLine
var subStepCount = 0; var subStepCount = 0;
while (accumulatedTime >= simulationStep && subStepCount < maxSubStepsPerFrame) while (accumulatedTime >= simulationStep && subStepCount < maxSubStepsPerFrame)
{ {
SimulateStep(nodes, restLengths, simulationStep); SimulateStep(points, restLengths, simulationStep);
accumulatedTime -= simulationStep; accumulatedTime -= simulationStep;
subStepCount++; subStepCount++;
} }
if (subStepCount == 0) if (subStepCount == 0)
{ {
PinLogicalNodes(nodes, simulationStep); PinLogicalPoints(points);
ApplySleep(nodes); ApplySleep();
} }
EvaluateSleepState(nodes, restLengths); EvaluateSleepState(restLengths);
} }
private void SimulateStep(IReadOnlyList<FishingLineNode> nodes, IReadOnlyList<float> restLengths, float stepDelta) private void SimulateStep(
IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<float> restLengths,
float stepDelta)
{ {
var gravity = Physics.gravity * gravityScale * stepDelta * stepDelta; var gravity = Physics.gravity * gravityScale * stepDelta * stepDelta;
for (var i = 0; i < nodes.Count; i++) for (var i = 0; i < points.Count; i++)
{ {
if (pinnedFlags[i]) if (pinnedFlags[i])
{ {
var pinnedPosition = nodes[i].Position; positions[i] = points[i].Position;
positions[i] = pinnedPosition; previousPositions[i] = points[i].Position;
previousPositions[i] = pinnedPosition;
continue; continue;
} }
@@ -224,7 +237,7 @@ namespace F2RopeLine2.FishingLine
for (var iteration = 0; iteration < solverIterations; iteration++) for (var iteration = 0; iteration < solverIterations; iteration++)
{ {
PinLogicalNodes(nodes, stepDelta); PinLogicalPoints(points);
for (var segmentIndex = 0; segmentIndex < restLengths.Count; segmentIndex++) for (var segmentIndex = 0; segmentIndex < restLengths.Count; segmentIndex++)
{ {
@@ -232,24 +245,22 @@ namespace F2RopeLine2.FishingLine
} }
} }
PinLogicalNodes(nodes, stepDelta); ApplyWaterSurfaceConstraint(stepDelta);
ApplySleep(nodes); PinLogicalPoints(points);
ApplySleep();
} }
private void PinLogicalNodes(IReadOnlyList<FishingLineNode> nodes, float deltaTime) private void PinLogicalPoints(IReadOnlyList<FishingLineSolver.ChainPoint> points)
{ {
var followWeight = Mathf.Clamp01(pinFollowStrength * deltaTime); for (var i = 0; i < points.Count; i++)
for (var i = 0; i < nodes.Count; i++)
{ {
if (!pinnedFlags[i]) if (!pinnedFlags[i])
{ {
continue; continue;
} }
var targetPosition = nodes[i].Position; positions[i] = points[i].Position;
positions[i] = Vector3.Lerp(positions[i], targetPosition, followWeight); previousPositions[i] = points[i].Position;
previousPositions[i] = targetPosition;
positions[i] = targetPosition;
} }
} }
@@ -295,9 +306,19 @@ namespace F2RopeLine2.FishingLine
positions[segmentIndex + 1] -= correction; positions[segmentIndex + 1] -= correction;
} }
private void ApplySleep(IReadOnlyList<FishingLineNode> nodes) private void ApplyWaterSurfaceConstraint(float stepDelta)
{ {
for (var i = 0; i < nodes.Count; i++) if (!constrainToWaterSurface || positions.Count == 0)
{
return;
}
var surfaceHeight = waterSurfaceTransform != null ? waterSurfaceTransform.position.y : waterSurfaceHeight;
var startIndex = Mathf.Clamp(ignoreHeadNodeCount, 0, positions.Count);
var endExclusive = Mathf.Clamp(positions.Count - ignoreTailNodeCount, startIndex, positions.Count);
var followFactor = Mathf.Clamp01(waterSurfaceFollowSpeed * stepDelta);
for (var i = startIndex; i < endExclusive; i++)
{ {
if (pinnedFlags[i]) if (pinnedFlags[i])
{ {
@@ -305,21 +326,25 @@ namespace F2RopeLine2.FishingLine
} }
var current = positions[i]; var current = positions[i];
if (current.y >= surfaceHeight)
{
continue;
}
var nextY = Mathf.Lerp(current.y, surfaceHeight, followFactor);
positions[i] = new Vector3(current.x, nextY, current.z);
var previous = previousPositions[i]; var previous = previousPositions[i];
var velocityMagnitude = (current - previous).magnitude; previousPositions[i] = new Vector3(
var authoredDistance = Vector3.Distance(current, nodes[i].Position); previous.x,
Mathf.Lerp(previous.y, nextY, followFactor),
if (velocityMagnitude <= sleepVelocityThreshold && authoredDistance <= sleepDistanceThreshold) previous.z);
{
previousPositions[i] = current;
}
} }
} }
private void EvaluateSleepState(IReadOnlyList<FishingLineNode> nodes, IReadOnlyList<float> restLengths) private void ApplySleep()
{ {
var isStable = true; for (var i = 0; i < positions.Count; i++)
for (var i = 0; i < nodes.Count; i++)
{ {
if (pinnedFlags[i]) if (pinnedFlags[i])
{ {
@@ -327,7 +352,24 @@ namespace F2RopeLine2.FishingLine
} }
var velocityMagnitude = (positions[i] - previousPositions[i]).magnitude; var velocityMagnitude = (positions[i] - previousPositions[i]).magnitude;
if (velocityMagnitude > sleepVelocityThreshold) if (velocityMagnitude <= sleepVelocityThreshold)
{
previousPositions[i] = positions[i];
}
}
}
private void EvaluateSleepState(IReadOnlyList<float> restLengths)
{
var isStable = true;
for (var i = 0; i < positions.Count; i++)
{
if (pinnedFlags[i])
{
continue;
}
if ((positions[i] - previousPositions[i]).magnitude > sleepVelocityThreshold)
{ {
isStable = false; isStable = false;
break; break;
@@ -367,26 +409,28 @@ namespace F2RopeLine2.FishingLine
} }
} }
private bool ShouldWake(IReadOnlyList<FishingLineNode> nodes, IReadOnlyList<float> restLengths) private bool ShouldWake(
IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<float> restLengths)
{ {
if (!isSleeping) if (!isSleeping)
{ {
return false; return false;
} }
if (lastPinnedNodePositions.Count != nodes.Count || lastRestLengths.Count != restLengths.Count) if (lastPinnedPointPositions.Count != points.Count || lastRestLengths.Count != restLengths.Count)
{ {
return true; return true;
} }
for (var i = 0; i < nodes.Count; i++) for (var i = 0; i < points.Count; i++)
{ {
if (!pinnedFlags[i]) if (!pinnedFlags[i])
{ {
continue; continue;
} }
if (Vector3.Distance(nodes[i].Position, lastPinnedNodePositions[i]) > wakeDistanceThreshold) if (Vector3.Distance(points[i].Position, lastPinnedPointPositions[i]) > wakeDistanceThreshold)
{ {
return true; return true;
} }
@@ -403,12 +447,14 @@ namespace F2RopeLine2.FishingLine
return false; return false;
} }
private void CacheFrameState(IReadOnlyList<FishingLineNode> nodes, IReadOnlyList<float> restLengths) private void CacheFrameState(
IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<float> restLengths)
{ {
lastPinnedNodePositions.Clear(); lastPinnedPointPositions.Clear();
for (var i = 0; i < nodes.Count; i++) for (var i = 0; i < points.Count; i++)
{ {
lastPinnedNodePositions.Add(nodes[i] != null ? nodes[i].Position : Vector3.zero); lastPinnedPointPositions.Add(points[i].Position);
} }
lastRestLengths.Clear(); lastRestLengths.Clear();
@@ -425,22 +471,12 @@ namespace F2RopeLine2.FishingLine
accumulatedTime = 0f; accumulatedTime = 0f;
} }
private void ApplyToRenderer(IReadOnlyList<FishingLineNode> nodes) private void ApplyToRenderer()
{ {
lineRenderer.positionCount = positions.Count; lineRenderer.positionCount = positions.Count;
for (var i = 0; i < positions.Count; i++) for (var i = 0; i < positions.Count; i++)
{ {
lineRenderer.SetPosition(i, positions[i]); lineRenderer.SetPosition(i, positions[i]);
if (!writeBackVirtualNodeTransforms)
{
continue;
}
if (nodes[i].IsRuntimeVirtualNode)
{
nodes[i].SetVisualPosition(positions[i]);
}
} }
} }

View File

@@ -7,10 +7,30 @@ namespace F2RopeLine2.FishingLine
{ {
public class FishingLineSolver : MonoBehaviour public class FishingLineSolver : MonoBehaviour
{ {
[Serializable]
public sealed class ChainPoint
{
public long Key;
public Vector3 Position;
public bool IsLogical;
public FishingLineNode LogicalNode;
public int SegmentIndex;
public int StableIndex;
public string GetDebugName()
{
if (IsLogical)
{
return LogicalNode != null ? LogicalNode.GetDebugName() : $"L[{StableIndex}]";
}
return $"V[S{SegmentIndex}:{StableIndex}]";
}
}
[Serializable] [Serializable]
private struct SegmentLayout private struct SegmentLayout
{ {
public float TotalLength;
public float[] GapLengths; public float[] GapLengths;
public int VirtualNodeCount => Mathf.Max(0, GapLengths.Length - 1); public int VirtualNodeCount => Mathf.Max(0, GapLengths.Length - 1);
@@ -43,23 +63,19 @@ namespace F2RopeLine2.FishingLine
[SerializeField] private bool showNodeLabels = true; [SerializeField] private bool showNodeLabels = true;
[SerializeField] private bool showSegmentLabels = true; [SerializeField] private bool showSegmentLabels = true;
private readonly List<FishingLineNode> orderedNodes = new(); private readonly List<ChainPoint> chainPoints = new();
private readonly List<float> restLengths = new(); private readonly List<float> restLengths = new();
private readonly List<int> pinnedIndices = new(); private readonly List<int> pinnedIndices = new();
private readonly List<FishingLineNode> firstSegmentVirtualPool = new();
private readonly List<FishingLineNode> otherSegmentVirtualPool = new();
private readonly List<ConfigurableJoint> runtimeJoints = new(); private readonly List<ConfigurableJoint> runtimeJoints = new();
private Transform virtualNodeRoot;
private bool chainDirty = true; private bool chainDirty = true;
private int runtimeVirtualPointCount;
public Transform AnchorTransform => anchorTransform;
public float FirstSegmentLength => firstSegmentLength; public float FirstSegmentLength => firstSegmentLength;
public float FirstSegmentStep => firstSegmentStep; public float FirstSegmentStep => firstSegmentStep;
public IReadOnlyList<FishingLineNode> OrderedNodes => orderedNodes; public IReadOnlyList<ChainPoint> ChainPoints => chainPoints;
public IReadOnlyList<float> RestLengths => restLengths; public IReadOnlyList<float> RestLengths => restLengths;
@@ -69,34 +85,11 @@ namespace F2RopeLine2.FishingLine
public int LogicalNodeCount => logicalNodes?.Length ?? 0; public int LogicalNodeCount => logicalNodes?.Length ?? 0;
public int RuntimeVirtualNodeCount => firstSegmentVirtualPool.Count + otherSegmentVirtualPool.Count; public int RuntimeVirtualNodeCount => runtimeVirtualPointCount;
public int ActiveRuntimeVirtualNodeCount public int ActiveRuntimeVirtualNodeCount => runtimeVirtualPointCount;
{
get
{
var activeCount = 0;
for (var i = 0; i < firstSegmentVirtualPool.Count; i++)
{
if (firstSegmentVirtualPool[i] != null && firstSegmentVirtualPool[i].gameObject.activeSelf)
{
activeCount++;
}
}
for (var i = 0; i < otherSegmentVirtualPool.Count; i++) public int OrderedNodeCount => chainPoints.Count;
{
if (otherSegmentVirtualPool[i] != null && otherSegmentVirtualPool[i].gameObject.activeSelf)
{
activeCount++;
}
}
return activeCount;
}
}
public int OrderedNodeCount => orderedNodes.Count;
public int SegmentCount => restLengths.Count; public int SegmentCount => restLengths.Count;
@@ -110,11 +103,6 @@ namespace F2RopeLine2.FishingLine
} }
} }
private void Awake()
{
EnsureVirtualNodeRoot();
}
private void Start() private void Start()
{ {
if (autoBuildOnStart) if (autoBuildOnStart)
@@ -145,7 +133,9 @@ namespace F2RopeLine2.FishingLine
RebuildRuntimeChain(); RebuildRuntimeChain();
} }
if (lineRenderer != null && orderedNodes.Count > 1) SyncLogicalPointPositions();
if (lineRenderer != null && chainPoints.Count > 1)
{ {
lineRenderer.Render(this, Time.deltaTime); lineRenderer.Render(this, Time.deltaTime);
} }
@@ -162,8 +152,6 @@ namespace F2RopeLine2.FishingLine
[ContextMenu("Build Line")] [ContextMenu("Build Line")]
public void BuildLine() public void BuildLine()
{ {
EnsureVirtualNodeRoot();
CleanupDeadReferences();
ConfigureStartNode(); ConfigureStartNode();
ConfigureLogicalJoints(); ConfigureLogicalJoints();
RebuildRuntimeChain(); RebuildRuntimeChain();
@@ -187,33 +175,14 @@ namespace F2RopeLine2.FishingLine
BuildLine(); BuildLine();
} }
public bool TryGetOrderedNode(int index, out FishingLineNode node)
{
if (index < 0 || index >= orderedNodes.Count)
{
node = null;
return false;
}
node = orderedNodes[index];
return true;
}
public float GetActualDistance(int segmentIndex) public float GetActualDistance(int segmentIndex)
{ {
if (segmentIndex < 0 || segmentIndex >= orderedNodes.Count - 1) if (segmentIndex < 0 || segmentIndex >= chainPoints.Count - 1)
{ {
return 0f; return 0f;
} }
var fromNode = orderedNodes[segmentIndex]; return Vector3.Distance(chainPoints[segmentIndex].Position, chainPoints[segmentIndex + 1].Position);
var toNode = orderedNodes[segmentIndex + 1];
if (fromNode == null || toNode == null)
{
return 0f;
}
return Vector3.Distance(fromNode.Position, toNode.Position);
} }
public string GetRuntimeDebugSummary() public string GetRuntimeDebugSummary()
@@ -224,8 +193,6 @@ namespace F2RopeLine2.FishingLine
.Append(" logical:") .Append(" logical:")
.Append(LogicalNodeCount) .Append(LogicalNodeCount)
.Append(" runtimeVirtual:") .Append(" runtimeVirtual:")
.Append(ActiveRuntimeVirtualNodeCount)
.Append("/")
.Append(RuntimeVirtualNodeCount) .Append(RuntimeVirtualNodeCount)
.Append(" ordered:") .Append(" ordered:")
.Append(OrderedNodeCount) .Append(OrderedNodeCount)
@@ -233,19 +200,22 @@ namespace F2RopeLine2.FishingLine
.Append(TotalLineLength.ToString("F2")) .Append(TotalLineLength.ToString("F2"))
.Append("m"); .Append("m");
for (var i = 0; i < orderedNodes.Count; i++) for (var i = 0; i < chainPoints.Count; i++)
{ {
var node = orderedNodes[i]; var point = chainPoints[i];
if (node == null)
{
continue;
}
builder.AppendLine() builder.AppendLine()
.Append('#') .Append('#')
.Append(i) .Append(i)
.Append(' ') .Append(' ')
.Append(node.GetDebugSummary()); .Append(point.GetDebugName())
.Append(" pos:")
.Append(point.Position);
if (point.IsLogical && point.LogicalNode != null)
{
builder.Append(" body:")
.Append(point.LogicalNode.Body != null ? "yes" : "no");
}
if (i < restLengths.Count) if (i < restLengths.Count)
{ {
@@ -346,74 +316,49 @@ namespace F2RopeLine2.FishingLine
private void RebuildRuntimeChain() private void RebuildRuntimeChain()
{ {
CleanupDeadReferences(); chainPoints.Clear();
orderedNodes.Clear();
restLengths.Clear(); restLengths.Clear();
pinnedIndices.Clear(); pinnedIndices.Clear();
TotalLineLength = 0f; TotalLineLength = 0f;
runtimeVirtualPointCount = 0;
if (logicalNodes == null || logicalNodes.Length < 2) if (logicalNodes == null || logicalNodes.Length < 2)
{ {
DestroyAllRuntimeVirtualNodes();
chainDirty = false; chainDirty = false;
return; return;
} }
var segmentLayouts = new SegmentLayout[logicalNodes.Length - 1]; var segmentLayouts = new SegmentLayout[logicalNodes.Length - 1];
var firstSegmentVirtualCount = 0;
var otherSegmentVirtualCount = 0;
for (var segmentIndex = 0; segmentIndex < segmentLayouts.Length; segmentIndex++) for (var segmentIndex = 0; segmentIndex < segmentLayouts.Length; segmentIndex++)
{ {
segmentLayouts[segmentIndex] = BuildSegmentLayout(segmentIndex); segmentLayouts[segmentIndex] = BuildSegmentLayout(segmentIndex);
if (segmentIndex == 0)
{
firstSegmentVirtualCount = segmentLayouts[segmentIndex].VirtualNodeCount;
}
else
{
otherSegmentVirtualCount += segmentLayouts[segmentIndex].VirtualNodeCount;
}
} }
EnsureFirstSegmentVirtualPool(firstSegmentVirtualCount); AddLogicalPoint(logicalNodes[0], 0);
EnsureOtherSegmentVirtualPool(otherSegmentVirtualCount);
orderedNodes.Add(logicalNodes[0]);
pinnedIndices.Add(0); pinnedIndices.Add(0);
var otherVirtualCursor = 0;
for (var segmentIndex = 0; segmentIndex < segmentLayouts.Length; segmentIndex++) for (var segmentIndex = 0; segmentIndex < segmentLayouts.Length; segmentIndex++)
{ {
var fromNode = logicalNodes[segmentIndex]; var fromNode = logicalNodes[segmentIndex];
var toNode = logicalNodes[segmentIndex + 1]; var toNode = logicalNodes[segmentIndex + 1];
var layout = segmentLayouts[segmentIndex]; if (fromNode == null || toNode == null)
var segmentVirtualNodes = GetSegmentVirtualNodes(segmentIndex, layout.VirtualNodeCount, ref otherVirtualCursor);
for (var virtualIndex = 0; virtualIndex < segmentVirtualNodes.Count; virtualIndex++)
{ {
var virtualNode = segmentVirtualNodes[virtualIndex]; continue;
virtualNode.SetRuntimeVirtual(true, orderedNodes.Count);
virtualNode.name = $"VirtualNode_{segmentIndex}_{virtualIndex}";
virtualNode.transform.SetParent(virtualNodeRoot, false);
orderedNodes.Add(virtualNode);
} }
var layout = segmentLayouts[segmentIndex];
AddVirtualPoints(fromNode.Position, toNode.Position, layout, segmentIndex);
for (var gapIndex = 0; gapIndex < layout.GapLengths.Length; gapIndex++) for (var gapIndex = 0; gapIndex < layout.GapLengths.Length; gapIndex++)
{ {
var gapLength = layout.GapLengths[gapIndex]; restLengths.Add(layout.GapLengths[gapIndex]);
restLengths.Add(gapLength); TotalLineLength += layout.GapLengths[gapIndex];
TotalLineLength += gapLength;
} }
orderedNodes.Add(toNode); AddLogicalPoint(toNode, segmentIndex + 1);
pinnedIndices.Add(orderedNodes.Count - 1); pinnedIndices.Add(chainPoints.Count - 1);
PlaceVirtualNodesBetween(fromNode, toNode, layout, segmentVirtualNodes);
} }
DisableUnusedFirstSegmentVirtualNodes(firstSegmentVirtualCount);
DisableUnusedOtherSegmentVirtualNodes(otherSegmentVirtualCount);
UpdateJointLimitsFromConfig();
chainDirty = false; chainDirty = false;
} }
@@ -424,14 +369,14 @@ namespace F2RopeLine2.FishingLine
{ {
return new SegmentLayout return new SegmentLayout
{ {
TotalLength = totalLength,
GapLengths = BuildFirstSegmentGaps(totalLength, firstSegmentStep), GapLengths = BuildFirstSegmentGaps(totalLength, firstSegmentStep),
}; };
} }
var virtualCount = logicalNodes[segmentIndex].FixedVirtualNodesToNext; var sourceNode = logicalNodes[segmentIndex];
var virtualCount = sourceNode != null ? sourceNode.FixedVirtualNodesToNext : 0;
var gapCount = Mathf.Max(1, virtualCount + 1); var gapCount = Mathf.Max(1, virtualCount + 1);
var gapLength = gapCount > 0 ? totalLength / gapCount : totalLength; var gapLength = totalLength / gapCount;
var gaps = new float[gapCount]; var gaps = new float[gapCount];
for (var i = 0; i < gaps.Length; i++) for (var i = 0; i < gaps.Length; i++)
{ {
@@ -440,11 +385,74 @@ namespace F2RopeLine2.FishingLine
return new SegmentLayout return new SegmentLayout
{ {
TotalLength = totalLength,
GapLengths = gaps, GapLengths = gaps,
}; };
} }
private void AddLogicalPoint(FishingLineNode logicalNode, int logicalIndex)
{
if (logicalNode == null)
{
return;
}
chainPoints.Add(new ChainPoint
{
Key = BuildLogicalPointKey(logicalIndex),
Position = logicalNode.Position,
IsLogical = true,
LogicalNode = logicalNode,
SegmentIndex = logicalIndex,
StableIndex = logicalIndex,
});
}
private void AddVirtualPoints(Vector3 fromPosition, Vector3 toPosition, SegmentLayout layout, int segmentIndex)
{
if (layout.VirtualNodeCount == 0)
{
return;
}
var direction = toPosition - fromPosition;
var distance = direction.magnitude;
var normalizedDirection = distance > 0.0001f ? direction / distance : Vector3.down;
var accumulatedDistance = 0f;
for (var virtualIndex = 0; virtualIndex < layout.VirtualNodeCount; virtualIndex++)
{
accumulatedDistance += layout.GapLengths[virtualIndex];
var stableIndex = segmentIndex == 0
? layout.VirtualNodeCount - 1 - virtualIndex
: virtualIndex;
chainPoints.Add(new ChainPoint
{
Key = BuildVirtualPointKey(segmentIndex, stableIndex),
Position = fromPosition + normalizedDirection * accumulatedDistance,
IsLogical = false,
LogicalNode = null,
SegmentIndex = segmentIndex,
StableIndex = stableIndex,
});
runtimeVirtualPointCount++;
}
}
private void SyncLogicalPointPositions()
{
for (var i = 0; i < chainPoints.Count; i++)
{
var point = chainPoints[i];
if (!point.IsLogical || point.LogicalNode == null)
{
continue;
}
point.Position = point.LogicalNode.Position;
}
}
private static float[] BuildFirstSegmentGaps(float totalLength, float step) private static float[] BuildFirstSegmentGaps(float totalLength, float step)
{ {
if (totalLength <= 0f) if (totalLength <= 0f)
@@ -480,167 +488,6 @@ namespace F2RopeLine2.FishingLine
return divisibleGaps; return divisibleGaps;
} }
private void PlaceVirtualNodesBetween(
FishingLineNode fromNode,
FishingLineNode toNode,
SegmentLayout layout,
IReadOnlyList<FishingLineNode> segmentVirtualNodes)
{
if (layout.VirtualNodeCount == 0)
{
return;
}
var direction = toNode.Position - fromNode.Position;
var distance = direction.magnitude;
var normalizedDirection = distance > 0.0001f ? direction / distance : Vector3.down;
var accumulatedDistance = 0f;
for (var i = 0; i < layout.VirtualNodeCount; i++)
{
accumulatedDistance += layout.GapLengths[i];
var position = fromNode.Position + normalizedDirection * accumulatedDistance;
var virtualNode = segmentVirtualNodes[i];
var wasActive = virtualNode.gameObject.activeSelf;
if (!Application.isPlaying || !wasActive)
{
virtualNode.SetVisualPosition(position);
}
virtualNode.gameObject.SetActive(true);
}
}
private List<FishingLineNode> GetSegmentVirtualNodes(int segmentIndex, int virtualCount, ref int otherVirtualCursor)
{
var result = new List<FishingLineNode>(virtualCount);
if (virtualCount <= 0)
{
return result;
}
if (segmentIndex == 0)
{
for (var i = 0; i < virtualCount; i++)
{
result.Add(firstSegmentVirtualPool[virtualCount - 1 - i]);
}
return result;
}
for (var i = 0; i < virtualCount; i++)
{
result.Add(otherSegmentVirtualPool[otherVirtualCursor + i]);
}
otherVirtualCursor += virtualCount;
return result;
}
private void EnsureFirstSegmentVirtualPool(int targetCount)
{
EnsureVirtualNodeRoot();
while (firstSegmentVirtualPool.Count < targetCount)
{
var node = CreateRuntimeVirtualNode($"FirstSegmentVirtualNode_{firstSegmentVirtualPool.Count}");
firstSegmentVirtualPool.Add(node);
}
}
private void EnsureOtherSegmentVirtualPool(int targetCount)
{
EnsureVirtualNodeRoot();
while (otherSegmentVirtualPool.Count < targetCount)
{
var node = CreateRuntimeVirtualNode($"OtherSegmentVirtualNode_{otherSegmentVirtualPool.Count}");
otherSegmentVirtualPool.Add(node);
}
}
private FishingLineNode CreateRuntimeVirtualNode(string nodeName)
{
var runtimeObject = new GameObject(nodeName);
runtimeObject.transform.SetParent(virtualNodeRoot, false);
var node = runtimeObject.AddComponent<FishingLineNode>();
node.SetRuntimeVirtual(true, -1);
runtimeObject.hideFlags = HideFlags.DontSave;
runtimeObject.SetActive(false);
return node;
}
private void DisableUnusedFirstSegmentVirtualNodes(int usedCount)
{
for (var i = 0; i < firstSegmentVirtualPool.Count; i++)
{
var node = firstSegmentVirtualPool[i];
var active = i < usedCount;
if (node != null)
{
node.gameObject.SetActive(active);
}
}
}
private void DisableUnusedOtherSegmentVirtualNodes(int usedCount)
{
for (var i = 0; i < otherSegmentVirtualPool.Count; i++)
{
var node = otherSegmentVirtualPool[i];
var active = i < usedCount;
if (node != null)
{
node.gameObject.SetActive(active);
}
}
}
private void DestroyAllRuntimeVirtualNodes()
{
for (var i = firstSegmentVirtualPool.Count - 1; i >= 0; i--)
{
DestroyNodeObject(firstSegmentVirtualPool[i]);
}
for (var i = otherSegmentVirtualPool.Count - 1; i >= 0; i--)
{
DestroyNodeObject(otherSegmentVirtualPool[i]);
}
firstSegmentVirtualPool.Clear();
otherSegmentVirtualPool.Clear();
}
private void CleanupDeadReferences()
{
firstSegmentVirtualPool.RemoveAll(node => node == null);
otherSegmentVirtualPool.RemoveAll(node => node == null);
runtimeJoints.RemoveAll(joint => joint == null);
}
private void EnsureVirtualNodeRoot()
{
if (virtualNodeRoot != null)
{
return;
}
var existing = transform.Find("_RuntimeVirtualNodes");
if (existing != null)
{
virtualNodeRoot = existing;
return;
}
var root = new GameObject("_RuntimeVirtualNodes");
root.transform.SetParent(transform, false);
root.hideFlags = HideFlags.DontSave;
virtualNodeRoot = root.transform;
}
private void UpdateJointLimitsFromConfig() private void UpdateJointLimitsFromConfig()
{ {
for (var i = 1; i < logicalNodes.Length; i++) for (var i = 1; i < logicalNodes.Length; i++)
@@ -689,43 +536,33 @@ namespace F2RopeLine2.FishingLine
return sourceNode != null ? sourceNode.SegmentLengthToNext : 0f; return sourceNode != null ? sourceNode.SegmentLengthToNext : 0f;
} }
private void DestroyNodeObject(FishingLineNode node) private static long BuildLogicalPointKey(int logicalIndex)
{ {
if (node == null) return (1L << 62) | (uint)logicalIndex;
{
return;
} }
if (Application.isPlaying) private static long BuildVirtualPointKey(int segmentIndex, int stableIndex)
{ {
Destroy(node.gameObject); return ((long)(segmentIndex + 1) << 32) | (uint)stableIndex;
}
else
{
DestroyImmediate(node.gameObject);
}
} }
private void OnDrawGizmosSelected() private void OnDrawGizmosSelected()
{ {
if (!drawDebugChain || orderedNodes.Count < 2 || restLengths.Count == 0) if (!drawDebugChain || chainPoints.Count < 2 || restLengths.Count == 0)
{ {
return; return;
} }
Gizmos.color = debugColor; Gizmos.color = debugColor;
for (var i = 0; i < orderedNodes.Count - 1; i++) for (var i = 0; i < chainPoints.Count - 1; i++)
{ {
if (orderedNodes[i] == null || orderedNodes[i + 1] == null) var from = chainPoints[i].Position;
{ var to = chainPoints[i + 1].Position;
continue; Gizmos.DrawLine(from, to);
} Gizmos.DrawSphere(from, debugNodeRadius);
Gizmos.DrawLine(orderedNodes[i].Position, orderedNodes[i + 1].Position); var midPoint = (from + to) * 0.5f;
Gizmos.DrawSphere(orderedNodes[i].Position, debugNodeRadius); var actualLength = Vector3.Distance(from, to);
var midPoint = (orderedNodes[i].Position + orderedNodes[i + 1].Position) * 0.5f;
var actualLength = Vector3.Distance(orderedNodes[i].Position, orderedNodes[i + 1].Position);
var restLength = i < restLengths.Count ? restLengths[i] : 0f; var restLength = i < restLengths.Count ? restLengths[i] : 0f;
#if UNITY_EDITOR #if UNITY_EDITOR
@@ -739,10 +576,7 @@ namespace F2RopeLine2.FishingLine
#endif #endif
} }
if (orderedNodes.Count > 0 && orderedNodes[^1] != null) Gizmos.DrawSphere(chainPoints[^1].Position, debugNodeRadius);
{
Gizmos.DrawSphere(orderedNodes[^1].Position, debugNodeRadius);
}
#if UNITY_EDITOR #if UNITY_EDITOR
if (!showNodeLabels) if (!showNodeLabels)
@@ -750,19 +584,14 @@ namespace F2RopeLine2.FishingLine
return; return;
} }
for (var i = 0; i < orderedNodes.Count; i++) for (var i = 0; i < chainPoints.Count; i++)
{ {
var node = orderedNodes[i]; var point = chainPoints[i];
if (node == null)
{
continue;
}
var pinned = pinnedIndices.Contains(i) ? " Pinned" : string.Empty; var pinned = pinnedIndices.Contains(i) ? " Pinned" : string.Empty;
UnityEditor.Handles.color = debugColor; UnityEditor.Handles.color = debugColor;
UnityEditor.Handles.Label( UnityEditor.Handles.Label(
node.Position + Vector3.up * 0.04f, point.Position + Vector3.up * 0.04f,
$"#{i} {node.GetDebugName()}{pinned}"); $"#{i} {point.GetDebugName()}{pinned}");
} }
#endif #endif
} }