Files
Fishing2/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineSolver.cs
2026-04-17 00:21:55 +08:00

385 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}