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(); public JointPinchController PinchController; protected override void OnInit() { // var tipRb = Rod.Asset.LineConnectorRigidbody; // anchorTransform = tipRb.transform; // // GetComponentsInChildren(includeInactive: true).ToList().ForEach(delegate(Transform i) // { // i.gameObject.SetActive(true); // }); } private void Awake() { } private void Start() { InitRenderer(); GetComponentsInChildren(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 /// /// 设置指定逻辑段的配置长度。 /// segmentIndex 为 0 时表示第一段;大于 0 时表示对应逻辑节点到下一个逻辑节点的线长。 /// public void SetLenght(float length, int index = 0) { ConfigureStartNode(); var node = logicalNodes[index + 1]; if (node != null) { node.SetLenght(length); } } #endregion #region LineNode /// /// 当前配置的逻辑节点只读列表。 /// 外部可读取节点顺序,但不应直接修改数组内容。 /// public IReadOnlyList LogicalNodes => logicalNodes; /// /// 根据类型获取逻辑节点类型 /// /// /// public FishingLineNode GetLogicalNode(FishingLineNode.NodeType nodeType) { foreach (var fishingLineNode in logicalNodes) { if (fishingLineNode.Type == nodeType) { return fishingLineNode; } } return null; } /// /// 获取指定顺序索引的逻辑节点。 /// 索引基于 logicalNodes 配置顺序;超出范围或节点为空时返回 null。 /// public FishingLineNode GetLogicalNode(int logicalIndex) { if (logicalNodes == null || logicalIndex < 0 || logicalIndex >= logicalNodes.Length) { return null; } return logicalNodes[logicalIndex]; } /// /// 获取当前起点逻辑节点。 /// 会返回配置顺序中第一个非空节点。 /// public FishingLineNode GetStartNode() { return FindFirstValidLogicalNode(); } /// /// 获取当前终点逻辑节点。 /// 会返回配置顺序中最后一个非空节点。 /// 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 极限判定 /// /// 当前逻辑链总长度超出配置总长度的部分,小于等于零时记为 0。 /// [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; /// /// 当鱼线达到断线条件时发出的一次性消息。 /// 外部可订阅该事件,在回调中执行切线、播放表现或状态切换。 /// public event Action OnLineBreakRequested; /// /// 当前是否处于极限状态。 /// 只要整链超出总长度容差,或任一逻辑段超出单段容差,即认为到达极限。 /// public bool IsAtLimit { get; private set; } /// /// 当前断线候选状态的累计时间。 /// 只有在处于极限状态,且 CurrentStretchLength 大于断线阈值时才会累加;否则重置为 0。 /// public float LimitStateTime { get; private set; } /// /// 当前极限断线消息是否已经发出过。 /// 在退出断线候选状态前只会发一次,避免重复通知。 /// public bool HasBreakNotificationSent { get; private set; } /// /// 当前拉力极限百分比。 /// 当超长值小于等于 lengthLimitTolerance 时为 0; /// 当超长值大于等于 breakStretchThreshold 时为 100; /// 中间区间按线性比例映射,供 UI 显示使用。 /// public float CurrentBreakStretchPercent => EvaluateBreakStretchPercent(CurrentStretchLength); /// /// 当前是否正在进行断线候选计时。 /// 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(); } /// /// 发出鱼线达到断线条件的消息。 /// 这里预留给外部订阅,当前不在求解器内部直接执行断线逻辑。 /// 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(); node.Rope = rope; // rope.groundMask = LayerMask.GetMask("Terrain"); rope.startAnchor = node.Joint.connectedBody; rope.endAnchor = node.body; } } #endregion } }