961 lines
31 KiB
C#
961 lines
31 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Text;
|
||
using UnityEngine;
|
||
|
||
namespace NBF
|
||
{
|
||
public enum LineType
|
||
{
|
||
Hand,
|
||
HandDouble,
|
||
Spinning,
|
||
SpinningFloat,
|
||
}
|
||
|
||
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;
|
||
|
||
[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;
|
||
|
||
[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;
|
||
|
||
/// <summary>
|
||
/// 当前配置的逻辑节点只读列表。
|
||
/// 外部可读取节点顺序,但不应直接修改数组内容。
|
||
/// </summary>
|
||
public IReadOnlyList<FishingLineNode> LogicalNodes => logicalNodes;
|
||
|
||
/// <summary>
|
||
/// 当前整条鱼线的配置总长度,等于所有段的静止长度之和。
|
||
/// </summary>
|
||
public float TotalLineLength { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 当前逻辑节点链路的实际总长度,按相邻逻辑节点的实际距离累加。
|
||
/// </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()
|
||
{
|
||
if (lineRenderer == null)
|
||
{
|
||
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;
|
||
}
|
||
|
||
UpdateAnchorNode();
|
||
|
||
if (chainDirty)
|
||
{
|
||
RebuildRuntimeChain();
|
||
}
|
||
|
||
EvaluateLimitState(Time.fixedDeltaTime);
|
||
}
|
||
|
||
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>
|
||
/// 获取当前起点逻辑节点。
|
||
/// 会返回配置顺序中第一个非空节点。
|
||
/// </summary>
|
||
public FishingLineNode GetStartNode()
|
||
{
|
||
return FindFirstValidLogicalNode();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前终点逻辑节点。
|
||
/// 会返回配置顺序中最后一个非空节点。
|
||
/// </summary>
|
||
public FishingLineNode GetEndNode()
|
||
{
|
||
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)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
for (var i = 0; i < logicalNodes.Length; i++)
|
||
{
|
||
if (logicalNodes[i] != null)
|
||
{
|
||
return logicalNodes[i];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private FishingLineNode FindLastValidLogicalNode()
|
||
{
|
||
if (logicalNodes == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
for (var i = logicalNodes.Length - 1; i >= 0; i--)
|
||
{
|
||
if (logicalNodes[i] != null)
|
||
{
|
||
return logicalNodes[i];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static long BuildLogicalPointKey(int logicalIndex)
|
||
{
|
||
return (1L << 62) | (uint)logicalIndex;
|
||
}
|
||
|
||
private static long BuildVirtualPointKey(int segmentIndex, int stableIndex)
|
||
{
|
||
return ((long)(segmentIndex + 1) << 32) | (uint)stableIndex;
|
||
}
|
||
}
|
||
} |