修改线求解器

This commit is contained in:
2026-04-13 20:55:07 +08:00
parent 6e918bb1a6
commit caa260b53b

View File

@@ -15,93 +15,18 @@ namespace NBF
public class FishingLineSolver : FGearBase
{
[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]
private struct SegmentLayout
{
public float[] GapLengths;
public int VirtualNodeCount => Mathf.Max(0, GapLengths.Length - 1);
}
[SerializeField] public LineType LineType;
public JointPinchController PinchController;
[Header("References")] [SerializeField]
private Transform anchorTransform;
[SerializeField] private FishingLineNode[] logicalNodes = Array.Empty<FishingLineNode>();
[SerializeField] private FishingLineRenderer lineRenderer;
[Header("First Segment")] [Min(0f)] [SerializeField]
private float firstSegmentLength = 1.2f;
public JointPinchController PinchController;
[Min(0.001f)] [SerializeField] private float firstSegmentStep = 0.1f;
[Header("Joint")] [Min(1)] [SerializeField]
private int jointSolverIterations = 12;
[SerializeField] private float jointProjectionDistance = 0.02f;
[SerializeField] private float jointProjectionAngle = 1f;
[Header("Limit Detection")]
[Min(0f)]
// 极限判定的长度容差,允许链路在总长或单段长度上存在少量误差。
[SerializeField]
private float lengthLimitTolerance = 0.01f;
[Min(0f)]
// 达到极限后,只有当前超长值大于该阈值时,才开始进入断线候选计时。
[SerializeField]
private float breakStretchThreshold = 0.05f;
[Min(0f)]
// 断线候选状态允许持续的最大时间;超过后会发出一次断线消息。
[SerializeField]
private float breakLimitDuration = 0.15f;
[Header("Runtime Debug")] [SerializeField]
private bool autoBuildOnStart = true;
private readonly List<ChainPoint> chainPoints = new();
private readonly List<float> restLengths = new();
private readonly List<int> pinnedIndices = new();
private readonly List<ConfigurableJoint> runtimeJoints = new();
private bool chainDirty = true;
private int runtimeVirtualPointCount;
public float FirstSegmentLength => firstSegmentLength;
public float FirstSegmentStep => firstSegmentStep;
public IReadOnlyList<ChainPoint> ChainPoints => chainPoints;
public IReadOnlyList<float> RestLengths => restLengths;
public IReadOnlyList<int> PinnedIndices => pinnedIndices;
#region LineNode
/// <summary>
/// 当前配置的逻辑节点只读列表。
@@ -109,269 +34,21 @@ namespace NBF
/// </summary>
public IReadOnlyList<FishingLineNode> LogicalNodes => logicalNodes;
/// <summary>
/// 当前整条鱼线的配置总长度,等于所有段的静止长度之和。
/// </summary>
public float TotalLineLength { get; private set; }
/// <summary>
/// 当前逻辑节点链路的实际总长度,按相邻逻辑节点的实际距离累加
/// 获取指定顺序索引的逻辑节点
/// 索引基于 logicalNodes 配置顺序;超出范围或节点为空时返回 null。
/// </summary>
public float CurrentLogicalChainLength { get; private set; }
/// <summary>
/// 当前首尾逻辑节点之间的直线距离,仅作为端点跨度观察值。
/// </summary>
public float CurrentEndpointDistance { get; private set; }
/// <summary>
/// 当前逻辑链总长度超出配置总长度的部分,小于等于零时记为 0。
/// </summary>
public float CurrentStretchLength { get; private set; }
/// <summary>
/// 当前所有逻辑段中,单段超出配置长度的最大值。
/// </summary>
public float MaxSegmentStretchLength { get; private set; }
/// <summary>
/// 当前超限最明显的逻辑段索引;为 -1 表示没有段处于超限。
/// </summary>
public int MaxOverstretchedSegmentIndex { get; private set; } = -1;
/// <summary>
/// 当前受拉比例。
/// 该值取“单段实际长度 / 单段配置长度”和“整链实际长度 / 整链配置长度”中的最大值。
/// 约等于 1 表示接近拉直,大于 1 表示已经出现超限拉伸。
/// </summary>
public float CurrentTensionRatio { get; private set; }
/// <summary>
/// 断线候选拉伸阈值。
/// 只有当前处于极限状态,且超长值大于该阈值时,才会开始累计断线计时。
/// </summary>
public float BreakStretchThreshold => breakStretchThreshold;
/// <summary>
/// 断线候选状态可持续的最大时间。
/// 当断线计时超过该值时,会发出一次断线消息。
/// </summary>
public float BreakLimitDuration => breakLimitDuration;
/// <summary>
/// 当前拉力极限百分比。
/// 当超长值小于等于 lengthLimitTolerance 时为 0
/// 当超长值大于等于 breakStretchThreshold 时为 100
/// 中间区间按线性比例映射,供 UI 显示使用。
/// </summary>
public float CurrentBreakStretchPercent => EvaluateBreakStretchPercent(CurrentStretchLength);
/// <summary>
/// 当前是否处于极限状态。
/// 只要整链超出总长度容差,或任一逻辑段超出单段容差,即认为到达极限。
/// </summary>
public bool IsAtLimit { get; private set; }
/// <summary>
/// 当前断线候选状态的累计时间。
/// 只有在处于极限状态,且 CurrentStretchLength 大于断线阈值时才会累加;否则重置为 0。
/// </summary>
public float LimitStateTime { get; private set; }
/// <summary>
/// 当前是否正在进行断线候选计时。
/// </summary>
public bool IsBreakCountdownActive => IsAtLimit && CurrentStretchLength > breakStretchThreshold;
/// <summary>
/// 当前极限断线消息是否已经发出过。
/// 在退出断线候选状态前只会发一次,避免重复通知。
/// </summary>
public bool HasBreakNotificationSent { get; private set; }
/// <summary>
/// 当鱼线达到断线条件时发出的一次性消息。
/// 外部可订阅该事件,在回调中执行切线、播放表现或状态切换。
/// </summary>
public event Action<FishingLineSolver> OnLineBreakRequested;
public int LogicalNodeCount => logicalNodes?.Length ?? 0;
public int RuntimeVirtualNodeCount => runtimeVirtualPointCount;
public int ActiveRuntimeVirtualNodeCount => runtimeVirtualPointCount;
public int OrderedNodeCount => chainPoints.Count;
public int SegmentCount => restLengths.Count;
public bool IsChainDirty => chainDirty;
private void Reset()
public FishingLineNode GetLogicalNode(int logicalIndex)
{
if (lineRenderer == null)
if (logicalNodes == null || logicalIndex < 0 || logicalIndex >= logicalNodes.Length)
{
TryGetComponent(out lineRenderer);
}
}
protected override void OnInit()
{
var tipRb = Rod.Asset.LineConnectorRigidbody;
anchorTransform = tipRb.transform;
}
private void Start()
{
if (autoBuildOnStart)
{
BuildLine();
}
}
private void FixedUpdate()
{
if (logicalNodes == null || logicalNodes.Length == 0)
{
ResetLimitState();
return;
return null;
}
UpdateAnchorNode();
if (chainDirty)
{
RebuildRuntimeChain();
}
EvaluateLimitState(Time.fixedDeltaTime);
return logicalNodes[logicalIndex];
}
private void LateUpdate()
{
if (chainDirty)
{
RebuildRuntimeChain();
}
SyncLogicalPointPositions();
if (lineRenderer != null && chainPoints.Count > 1)
{
lineRenderer.Render(this, Time.deltaTime);
}
}
private void OnValidate()
{
firstSegmentLength = Mathf.Max(0f, firstSegmentLength);
firstSegmentStep = Mathf.Max(0.001f, firstSegmentStep);
jointSolverIterations = Mathf.Max(1, jointSolverIterations);
lengthLimitTolerance = Mathf.Max(0f, lengthLimitTolerance);
breakStretchThreshold = Mathf.Max(0f, breakStretchThreshold);
breakLimitDuration = Mathf.Max(0f, breakLimitDuration);
chainDirty = true;
}
/// <summary>
/// 按当前配置重建整条鱼线的运行时链路,并立即刷新极限状态。
/// </summary>
[ContextMenu("Build Line")]
public void BuildLine()
{
ConfigureStartNode();
ConfigureLogicalJoints();
RebuildRuntimeChain();
EvaluateLimitState(0f);
}
/// <summary>
/// 设置指定逻辑段的配置长度。
/// segmentIndex 为 0 时表示第一段;大于 0 时表示对应逻辑节点到下一个逻辑节点的线长。
/// </summary>
public void SetLenght(float length, int segmentIndex = 0)
{
var clamped = Mathf.Max(0f, length);
var currentLength = GetSegmentLength(segmentIndex);
if (Mathf.Approximately(clamped, currentLength))
{
return;
}
if (segmentIndex <= 0)
{
firstSegmentLength = clamped;
}
else
{
if (logicalNodes == null || segmentIndex >= logicalNodes.Length)
{
return;
}
var sourceNode = logicalNodes[segmentIndex];
if (sourceNode == null)
{
return;
}
sourceNode.SegmentLengthToNext = clamped;
}
UpdateJointLimit(segmentIndex + 1, clamped);
chainDirty = true;
}
/// <summary>
/// 立即按当前配置重建鱼线。
/// </summary>
public void RebuildNow()
{
BuildLine();
}
/// <summary>
/// 根据类型获取逻辑节点类型
/// </summary>
/// <param name="nodeType"></param>
/// <returns></returns>
public FishingLineNode GetLogicalNode(FishingLineNode.NodeType nodeType)
{
foreach (var fishingLineNode in logicalNodes)
{
if (fishingLineNode.Type == nodeType)
{
return fishingLineNode;
}
}
return null;
}
// /// <summary>
// /// 获取指定顺序索引的逻辑节点。
// /// 索引基于 logicalNodes 配置顺序;超出范围或节点为空时返回 null。
// /// </summary>
// public FishingLineNode GetLogicalNode(int logicalIndex)
// {
// if (logicalNodes == null || logicalIndex < 0 || logicalIndex >= logicalNodes.Length)
// {
// return null;
// }
//
// return logicalNodes[logicalIndex];
// }
//
// /// <summary>
// /// 尝试获取指定顺序索引的逻辑节点。
// /// 获取失败时返回 false并将 node 置为 null。
// /// </summary>
// public bool TryGetLogicalNode(int logicalIndex, out FishingLineNode node)
// {
// node = GetLogicalNode(logicalIndex);
// return node != null;
// }
/// <summary>
/// 获取当前起点逻辑节点。
/// 会返回配置顺序中第一个非空节点。
@@ -390,530 +67,6 @@ namespace NBF
return FindLastValidLogicalNode();
}
/// <summary>
/// 获取当前运行时链路中相邻采样点的实际距离。
/// 这里的采样点包含逻辑节点和虚拟节点。
/// </summary>
public float GetActualDistance(int segmentIndex)
{
if (segmentIndex < 0 || segmentIndex >= chainPoints.Count - 1)
{
return 0f;
}
return Vector3.Distance(chainPoints[segmentIndex].Position, chainPoints[segmentIndex + 1].Position);
}
/// <summary>
/// 返回当前鱼线运行时调试摘要,包含链路结构、长度和极限状态信息。
/// </summary>
public string GetRuntimeDebugSummary()
{
var builder = new StringBuilder(512);
builder.Append("chain:")
.Append(chainDirty ? "dirty" : "ready")
.Append(" logical:")
.Append(LogicalNodeCount)
.Append(" runtimeVirtual:")
.Append(RuntimeVirtualNodeCount)
.Append(" ordered:")
.Append(OrderedNodeCount)
.Append(" total:")
.Append(TotalLineLength.ToString("F2"))
.Append("m")
.Append(" chain:")
.Append(CurrentLogicalChainLength.ToString("F2"))
.Append("m")
.Append(" tension:")
.Append(CurrentTensionRatio.ToString("F3"))
.Append(" breakPercent:")
.Append(CurrentBreakStretchPercent.ToString("F1"))
.Append('%')
.Append(" limit:")
.Append(IsAtLimit ? "yes" : "no")
.Append(" limitTime:")
.Append(LimitStateTime.ToString("F2"))
.Append("s");
for (var i = 0; i < chainPoints.Count; i++)
{
var point = chainPoints[i];
builder.AppendLine()
.Append('#')
.Append(i)
.Append(' ')
.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)
{
builder.Append(" seg rest:")
.Append(restLengths[i].ToString("F3"))
.Append(" actual:")
.Append(GetActualDistance(i).ToString("F3"));
}
}
return builder.ToString();
}
private void ConfigureStartNode()
{
if (logicalNodes == null || logicalNodes.Length == 0 || logicalNodes[0] == null)
{
return;
}
var startNode = logicalNodes[0];
startNode.Type = FishingLineNode.NodeType.Start;
if (startNode.Body != null)
{
startNode.Body.isKinematic = true;
startNode.Body.interpolation = RigidbodyInterpolation.Interpolate;
startNode.Body.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
}
UpdateAnchorNode();
}
private void ConfigureLogicalJoints()
{
runtimeJoints.Clear();
if (logicalNodes == null)
{
return;
}
for (var i = 1; i < logicalNodes.Length; i++)
{
var current = logicalNodes[i];
var previous = logicalNodes[i - 1];
if (current == null || previous == null || current.Body == null || previous.Body == null)
{
continue;
}
current.Body.solverIterations = jointSolverIterations;
current.Body.interpolation = RigidbodyInterpolation.Interpolate;
current.Body.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
var joint = current.GetComponent<ConfigurableJoint>();
if (joint == null)
{
joint = current.gameObject.AddComponent<ConfigurableJoint>();
}
joint.autoConfigureConnectedAnchor = true;
joint.connectedBody = previous.Body;
joint.xMotion = ConfigurableJointMotion.Limited;
joint.yMotion = ConfigurableJointMotion.Limited;
joint.zMotion = ConfigurableJointMotion.Limited;
joint.angularXMotion = ConfigurableJointMotion.Free;
joint.angularYMotion = ConfigurableJointMotion.Free;
joint.angularZMotion = ConfigurableJointMotion.Free;
joint.projectionMode = JointProjectionMode.PositionAndRotation;
joint.projectionDistance = jointProjectionDistance;
joint.projectionAngle = jointProjectionAngle;
var limit = joint.linearLimit;
limit.limit = GetSegmentLength(i - 1);
joint.linearLimit = limit;
runtimeJoints.Add(joint);
}
}
private void UpdateAnchorNode()
{
if (anchorTransform == null || logicalNodes == null || logicalNodes.Length == 0 || logicalNodes[0] == null)
{
return;
}
var startNode = logicalNodes[0];
startNode.transform.SetPositionAndRotation(anchorTransform.position, anchorTransform.rotation);
if (startNode.Body != null)
{
if (!startNode.Body.isKinematic)
{
startNode.Body.linearVelocity = Vector3.zero;
startNode.Body.angularVelocity = Vector3.zero;
}
}
}
private void EvaluateLimitState(float deltaTime)
{
CurrentLogicalChainLength = 0f;
CurrentEndpointDistance = 0f;
CurrentStretchLength = 0f;
MaxSegmentStretchLength = 0f;
MaxOverstretchedSegmentIndex = -1;
CurrentTensionRatio = 0f;
if (logicalNodes == null || logicalNodes.Length < 2)
{
SetLimitState(false);
UpdateBreakCountdown(deltaTime);
return;
}
FishingLineNode firstNode = null;
FishingLineNode lastNode = null;
for (var segmentIndex = 0; segmentIndex < logicalNodes.Length; segmentIndex++)
{
var node = logicalNodes[segmentIndex];
if (node == null)
{
continue;
}
firstNode ??= node;
lastNode = node;
if (segmentIndex >= logicalNodes.Length - 1)
{
continue;
}
var nextNode = logicalNodes[segmentIndex + 1];
if (nextNode == null)
{
continue;
}
var actualDistance = Vector3.Distance(node.Position, nextNode.Position);
var configuredLength = GetSegmentLength(segmentIndex);
CurrentLogicalChainLength += actualDistance;
if (configuredLength > 0.0001f)
{
CurrentTensionRatio = Mathf.Max(CurrentTensionRatio, actualDistance / configuredLength);
}
var segmentStretchLength = Mathf.Max(0f, actualDistance - configuredLength);
if (segmentStretchLength > MaxSegmentStretchLength)
{
MaxSegmentStretchLength = segmentStretchLength;
MaxOverstretchedSegmentIndex = segmentStretchLength > 0f ? segmentIndex : -1;
}
}
if (firstNode != null && lastNode != null && !ReferenceEquals(firstNode, lastNode))
{
CurrentEndpointDistance = Vector3.Distance(firstNode.Position, lastNode.Position);
}
if (TotalLineLength > 0.0001f)
{
CurrentStretchLength = Mathf.Max(0f, CurrentLogicalChainLength - TotalLineLength);
CurrentTensionRatio = Mathf.Max(CurrentTensionRatio, CurrentLogicalChainLength / TotalLineLength);
}
else if (CurrentLogicalChainLength > 0f)
{
CurrentStretchLength = CurrentLogicalChainLength;
CurrentTensionRatio = Mathf.Max(CurrentTensionRatio, 1f);
}
var exceedsTotalLength = CurrentStretchLength > lengthLimitTolerance;
var exceedsSegmentLength = MaxSegmentStretchLength > lengthLimitTolerance;
SetLimitState(exceedsTotalLength || exceedsSegmentLength);
UpdateBreakCountdown(deltaTime);
}
private void SetLimitState(bool isAtLimit)
{
IsAtLimit = isAtLimit;
}
private void UpdateBreakCountdown(float deltaTime)
{
if (!IsBreakCountdownActive)
{
LimitStateTime = 0f;
HasBreakNotificationSent = false;
return;
}
LimitStateTime += Mathf.Max(0f, deltaTime);
if (HasBreakNotificationSent || LimitStateTime < breakLimitDuration)
{
return;
}
HasBreakNotificationSent = true;
NotifyLineBreakRequested();
}
/// <summary>
/// 发出鱼线达到断线条件的消息。
/// 这里预留给外部订阅,当前不在求解器内部直接执行断线逻辑。
/// </summary>
private void NotifyLineBreakRequested()
{
OnLineBreakRequested?.Invoke(this);
}
private void ResetLimitState()
{
CurrentLogicalChainLength = 0f;
CurrentEndpointDistance = 0f;
CurrentStretchLength = 0f;
MaxSegmentStretchLength = 0f;
MaxOverstretchedSegmentIndex = -1;
CurrentTensionRatio = 0f;
IsAtLimit = false;
LimitStateTime = 0f;
HasBreakNotificationSent = false;
}
private float EvaluateBreakStretchPercent(float stretchLength)
{
if (stretchLength <= lengthLimitTolerance)
{
return 0f;
}
if (stretchLength >= breakStretchThreshold)
{
return 100f;
}
if (breakStretchThreshold <= lengthLimitTolerance)
{
return 100f;
}
return Mathf.InverseLerp(lengthLimitTolerance, breakStretchThreshold, stretchLength) * 100f;
}
private void RebuildRuntimeChain()
{
chainPoints.Clear();
restLengths.Clear();
pinnedIndices.Clear();
TotalLineLength = 0f;
runtimeVirtualPointCount = 0;
if (logicalNodes == null || logicalNodes.Length < 2)
{
chainDirty = false;
return;
}
var segmentLayouts = new SegmentLayout[logicalNodes.Length - 1];
for (var segmentIndex = 0; segmentIndex < segmentLayouts.Length; segmentIndex++)
{
segmentLayouts[segmentIndex] = BuildSegmentLayout(segmentIndex);
}
AddLogicalPoint(logicalNodes[0], 0);
pinnedIndices.Add(0);
for (var segmentIndex = 0; segmentIndex < segmentLayouts.Length; segmentIndex++)
{
var fromNode = logicalNodes[segmentIndex];
var toNode = logicalNodes[segmentIndex + 1];
if (fromNode == null || toNode == null)
{
continue;
}
var layout = segmentLayouts[segmentIndex];
AddVirtualPoints(fromNode.Position, toNode.Position, layout, segmentIndex);
for (var gapIndex = 0; gapIndex < layout.GapLengths.Length; gapIndex++)
{
restLengths.Add(layout.GapLengths[gapIndex]);
TotalLineLength += layout.GapLengths[gapIndex];
}
AddLogicalPoint(toNode, segmentIndex + 1);
pinnedIndices.Add(chainPoints.Count - 1);
}
chainDirty = false;
}
private SegmentLayout BuildSegmentLayout(int segmentIndex)
{
var totalLength = GetSegmentLength(segmentIndex);
if (segmentIndex == 0)
{
return new SegmentLayout
{
GapLengths = BuildFirstSegmentGaps(totalLength, firstSegmentStep),
};
}
var sourceNode = logicalNodes[segmentIndex];
var virtualCount = sourceNode != null ? sourceNode.FixedVirtualNodesToNext : 0;
var gapCount = Mathf.Max(1, virtualCount + 1);
var gapLength = totalLength / gapCount;
var gaps = new float[gapCount];
for (var i = 0; i < gaps.Length; i++)
{
gaps[i] = gapLength;
}
return new SegmentLayout
{
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)
{
if (totalLength <= 0f)
{
return new[] { 0f };
}
if (totalLength < step)
{
return new[] { totalLength };
}
var fullStepCount = Mathf.FloorToInt(totalLength / step);
var remainder = totalLength - (fullStepCount * step);
if (remainder > 0.0001f)
{
var gaps = new float[fullStepCount + 1];
gaps[0] = remainder;
for (var i = 1; i < gaps.Length; i++)
{
gaps[i] = step;
}
return gaps;
}
var divisibleGaps = new float[fullStepCount];
for (var i = 0; i < divisibleGaps.Length; i++)
{
divisibleGaps[i] = step;
}
return divisibleGaps;
}
private void UpdateJointLimit(int logicalNodeIndex, float limitValue)
{
if (logicalNodeIndex <= 0 || logicalNodeIndex >= logicalNodes.Length)
{
return;
}
var node = logicalNodes[logicalNodeIndex];
if (node == null)
{
return;
}
var joint = node.GetComponent<ConfigurableJoint>();
if (joint == null)
{
return;
}
var limit = joint.linearLimit;
limit.limit = limitValue;
joint.linearLimit = limit;
}
private float GetSegmentLength(int segmentIndex)
{
if (segmentIndex <= 0)
{
return firstSegmentLength;
}
if (logicalNodes == null || segmentIndex >= logicalNodes.Length)
{
return 0f;
}
var sourceNode = logicalNodes[segmentIndex];
return sourceNode != null ? sourceNode.SegmentLengthToNext : 0f;
}
private FishingLineNode FindFirstValidLogicalNode()
{
if (logicalNodes == null)
@@ -950,14 +103,12 @@ namespace NBF
return null;
}
private static long BuildLogicalPointKey(int logicalIndex)
{
return (1L << 62) | (uint)logicalIndex;
}
#endregion
private static long BuildVirtualPointKey(int segmentIndex, int stableIndex)
protected override void OnInit()
{
return ((long)(segmentIndex + 1) << 32) | (uint)stableIndex;
var tipRb = Rod.Asset.LineConnectorRigidbody;
anchorTransform = tipRb.transform;
}
}
}