修改线求解器
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user