385 lines
11 KiB
C#
385 lines
11 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using UnityEngine;
|
||
|
||
namespace NBF
|
||
{
|
||
public enum LineType
|
||
{
|
||
Hand,
|
||
HandDouble,
|
||
Spinning,
|
||
SpinningFloat,
|
||
}
|
||
|
||
public class FishingLineSolver : FGearBase
|
||
{
|
||
[SerializeField] public LineType LineType;
|
||
|
||
|
||
[Header("References")] [SerializeField]
|
||
private Transform anchorTransform;
|
||
|
||
[SerializeField] private FishingLineNode[] logicalNodes = Array.Empty<FishingLineNode>();
|
||
|
||
public JointPinchController PinchController;
|
||
|
||
protected override void OnInit()
|
||
{
|
||
// var tipRb = Rod.Asset.LineConnectorRigidbody;
|
||
// anchorTransform = tipRb.transform;
|
||
//
|
||
// GetComponentsInChildren<Transform>(includeInactive: true).ToList().ForEach(delegate(Transform i)
|
||
// {
|
||
// i.gameObject.SetActive(true);
|
||
// });
|
||
}
|
||
|
||
private void Awake()
|
||
{
|
||
}
|
||
|
||
private void Start()
|
||
{
|
||
InitRenderer();
|
||
GetComponentsInChildren<Transform>(includeInactive: true).ToList().ForEach(delegate(Transform i)
|
||
{
|
||
i.gameObject.SetActive(true);
|
||
});
|
||
}
|
||
|
||
private void FixedUpdate()
|
||
{
|
||
UpdateAnchorNode();
|
||
UpdateBreakCountdown(Time.fixedDeltaTime);
|
||
}
|
||
|
||
#region Start Node
|
||
|
||
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 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;
|
||
}
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Line
|
||
|
||
/// <summary>
|
||
/// 设置指定逻辑段的配置长度。
|
||
/// segmentIndex 为 0 时表示第一段;大于 0 时表示对应逻辑节点到下一个逻辑节点的线长。
|
||
/// </summary>
|
||
public void SetLenght(float length, int index = 0)
|
||
{
|
||
ConfigureStartNode();
|
||
var node = logicalNodes[index + 1];
|
||
if (node != null)
|
||
{
|
||
node.SetLenght(length);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region LineNode
|
||
|
||
/// <summary>
|
||
/// 当前配置的逻辑节点只读列表。
|
||
/// 外部可读取节点顺序,但不应直接修改数组内容。
|
||
/// </summary>
|
||
public IReadOnlyList<FishingLineNode> LogicalNodes => logicalNodes;
|
||
|
||
/// <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>
|
||
/// 获取当前起点逻辑节点。
|
||
/// 会返回配置顺序中第一个非空节点。
|
||
/// </summary>
|
||
public FishingLineNode GetStartNode()
|
||
{
|
||
return FindFirstValidLogicalNode();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前终点逻辑节点。
|
||
/// 会返回配置顺序中最后一个非空节点。
|
||
/// </summary>
|
||
public FishingLineNode GetEndNode()
|
||
{
|
||
return FindLastValidLogicalNode();
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 极限判定
|
||
|
||
/// <summary>
|
||
/// 当前逻辑链总长度超出配置总长度的部分,小于等于零时记为 0。
|
||
/// </summary>
|
||
[Header("Limit Detection")]
|
||
public float CurrentStretchLength { get; private set; }
|
||
|
||
[Min(0f)]
|
||
// 极限判定的长度容差,允许链路在总长或单段长度上存在少量误差。
|
||
[SerializeField]
|
||
private float lengthLimitTolerance = 0.01f;
|
||
|
||
[Min(0f)]
|
||
// 达到极限后,只有当前超长值大于该阈值时,才开始进入断线候选计时。
|
||
[SerializeField]
|
||
private float breakStretchThreshold = 0.08f;
|
||
|
||
[Min(0f)]
|
||
// 断线候选状态允许持续的最大时间;超过后会发出一次断线消息。
|
||
[SerializeField]
|
||
private float breakLimitDuration = 3f;
|
||
|
||
/// <summary>
|
||
/// 当鱼线达到断线条件时发出的一次性消息。
|
||
/// 外部可订阅该事件,在回调中执行切线、播放表现或状态切换。
|
||
/// </summary>
|
||
public event Action<FishingLineSolver> OnLineBreakRequested;
|
||
|
||
/// <summary>
|
||
/// 当前是否处于极限状态。
|
||
/// 只要整链超出总长度容差,或任一逻辑段超出单段容差,即认为到达极限。
|
||
/// </summary>
|
||
public bool IsAtLimit { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 当前断线候选状态的累计时间。
|
||
/// 只有在处于极限状态,且 CurrentStretchLength 大于断线阈值时才会累加;否则重置为 0。
|
||
/// </summary>
|
||
public float LimitStateTime { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 当前极限断线消息是否已经发出过。
|
||
/// 在退出断线候选状态前只会发一次,避免重复通知。
|
||
/// </summary>
|
||
public bool HasBreakNotificationSent { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 当前拉力极限百分比。
|
||
/// 当超长值小于等于 lengthLimitTolerance 时为 0;
|
||
/// 当超长值大于等于 breakStretchThreshold 时为 100;
|
||
/// 中间区间按线性比例映射,供 UI 显示使用。
|
||
/// </summary>
|
||
public float CurrentBreakStretchPercent => EvaluateBreakStretchPercent(CurrentStretchLength);
|
||
|
||
/// <summary>
|
||
/// 当前是否正在进行断线候选计时。
|
||
/// </summary>
|
||
public bool IsBreakCountdownActive => IsAtLimit && CurrentStretchLength > breakStretchThreshold;
|
||
|
||
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 SetLimitState(bool isAtLimit)
|
||
{
|
||
IsAtLimit = isAtLimit;
|
||
}
|
||
|
||
private void UpdateBreakCountdown(float deltaTime)
|
||
{
|
||
if (logicalNodes == null || logicalNodes.Length == 0)
|
||
{
|
||
SetLimitState(false);
|
||
ResetLimitState();
|
||
return;
|
||
}
|
||
|
||
CurrentStretchLength = 0;
|
||
//计算长度
|
||
foreach (var node in logicalNodes)
|
||
{
|
||
CurrentStretchLength += node.StretchLength;
|
||
}
|
||
|
||
SetLimitState(CurrentStretchLength > lengthLimitTolerance);
|
||
|
||
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()
|
||
{
|
||
CurrentStretchLength = 0f;
|
||
IsAtLimit = false;
|
||
LimitStateTime = 0f;
|
||
HasBreakNotificationSent = false;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Renderer
|
||
|
||
private Transform _ropeRoot;
|
||
|
||
private void InitRenderer()
|
||
{
|
||
var root = new GameObject("RopeRoot");
|
||
_ropeRoot = root.transform;
|
||
_ropeRoot.SetParent(transform);
|
||
CreateRopes();
|
||
}
|
||
|
||
private void CreateRopes()
|
||
{
|
||
foreach (var node in LogicalNodes)
|
||
{
|
||
if (node.Type == FishingLineNode.NodeType.Start) continue;
|
||
var ropeObject = new GameObject($"rope_{node.Type}");
|
||
ropeObject.transform.SetParent(_ropeRoot);
|
||
var rope = ropeObject.AddComponent<Rope>();
|
||
node.Rope = rope;
|
||
// rope.groundMask = LayerMask.GetMask("Terrain");
|
||
rope.startAnchor = node.Joint.connectedBody;
|
||
rope.endAnchor = node.body;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
} |