diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs b/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs index e69de29bb..3d27cf64c 100644 --- a/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs +++ b/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs @@ -0,0 +1,1045 @@ +using System; +using UnityEngine; +using System.Collections.Generic; +using NBC; + +namespace NBF +{ + public struct FLineNodeConstraintState + { + public Vector3 TargetPosition; + public float PlanarStrength; + public float UpwardStrength; + public float DownwardStrength; + public float MaxCorrection; + } + + /// + /// 为 FLine 节点提供附加软约束。求解器只依赖这个通用接口,不依赖具体玩法组件。 + /// + public interface IFLineNodeConstraintProvider + { + bool TryGetConstraintState(out FLineNodeConstraintState state); + } + + public enum LineType + { + Spinning, + SpinningFloat, + Hand, + HandDouble, + } + + public class FLine : FGearBase + { + [Header("基本参数设置")] public LineType LineType; + [Header("连接点配置")] [SerializeField] private Transform anchorTransform; + [SerializeField] private List lineNodes = new List(); + + [Header("物理参数")] [SerializeField] private float positionCorrectionForce = 100f; + [SerializeField] private float dampingCoefficient = 10f; + [SerializeField] private int constraintIterations = 10; + [SerializeField] private bool useMassWeighting = true; + + [Header("动态间距设置")] [SerializeField] private float defaultTransitionSpeed = 2f; // 默认长度变化速度(单位/秒) + + [Header("调试")] [SerializeField] private bool showDebugInfo = true; + + private readonly List _constraints = new List(); + + private readonly List _nodeConstraintProviders = + new List(); + + [System.Serializable] + public class ConnectionConstraint + { + public Rigidbody bodyA; + public Rigidbody bodyB; + public float maxDistance; + public float minDistance; + public float currentDistance; + public Vector3 direction; + + // 动态目标距离(用于平滑过渡) + public float targetMaxDistance; + public float targetMinDistance; + public bool hasPendingTransition; + public bool hasPendingMaxTransition; + public bool hasPendingMinTransition; + public float maxTransitionSpeed; + public float minTransitionSpeed; + + public ConnectionConstraint(Rigidbody a, Rigidbody b, float maxDist, float minDist = 0f) + { + bodyA = a; + bodyB = b; + maxDistance = maxDist; + minDistance = minDist; + targetMaxDistance = maxDist; + targetMinDistance = minDist; + hasPendingTransition = false; + hasPendingMaxTransition = false; + hasPendingMinTransition = false; + maxTransitionSpeed = 0f; + minTransitionSpeed = 0f; + } + + public void UpdateCurrentState() + { + if (bodyA && bodyB) + { + Vector3 delta = bodyB.position - bodyA.position; + currentDistance = delta.magnitude; + direction = currentDistance > 0.0001f ? delta.normalized : Vector3.right; + } + } + } + + void Awake() + { + BuildConstraints(); + } + + protected override void OnInit() + { + anchorTransform = Rod.Asset.lineConnector; + } + + void Start() + { + if (_constraints.Count == 0) + { + Debug.LogWarning("FLine需要至少2个有效节点"); + enabled = false; + } + } + + void FixedUpdate() + { + if (!enabled || _constraints.Count == 0) return; + + UpdateAnchorNode(); + // 更新动态过渡 + UpdateTransitions(); + + for (int iteration = 0; iteration < constraintIterations; iteration++) + { + ApplyDistanceConstraints(); + ApplyNodeConstraints(); + } + + ApplyDamping(); + UpdateBreakCountdown(Time.fixedDeltaTime); + } + + private void BuildConstraints() + { + _constraints.Clear(); + _nodeConstraintProviders.Clear(); + if (lineNodes.Count < 2) return; + + for (int i = 0; i < lineNodes.Count; i++) + { + if (lineNodes[i]) + { + lineNodes[i].AttachToCable(this); + _nodeConstraintProviders.Add(CollectNodeConstraintProviders(lineNodes[i])); + } + else + { + _nodeConstraintProviders.Add(Array.Empty()); + } + } + + // 创建约束 + for (int i = 0; i < lineNodes.Count - 1; i++) + { + FLineLogicNode currentNode = lineNodes[i]; + FLineLogicNode nextNode = lineNodes[i + 1]; + Rigidbody bodyA = currentNode ? currentNode.Rigidbody : null; + Rigidbody bodyB = nextNode ? nextNode.Rigidbody : null; + + if (bodyA != null && bodyB != null) + { + var constraint = new ConnectionConstraint( + bodyA, + bodyB, + currentNode.NextSegmentMaxLength, + currentNode.NextSegmentMinLength + ); + _constraints.Add(constraint); + } + } + } + + private FLineLogicNode GetSegmentNode(int segmentIndex) + { + if (segmentIndex < 0 || segmentIndex >= lineNodes.Count - 1) + { + return null; + } + + return lineNodes[segmentIndex]; + } + + private Rigidbody GetBodyAt(int nodeIndex) + { + if (nodeIndex < 0 || nodeIndex >= lineNodes.Count) + { + return null; + } + + FLineLogicNode node = lineNodes[nodeIndex]; + return node ? node.Rigidbody : null; + } + + private IFLineNodeConstraintProvider[] GetNodeConstraintProvidersAt(int nodeIndex) + { + if (nodeIndex < 0 || nodeIndex >= _nodeConstraintProviders.Count) + { + return Array.Empty(); + } + + return _nodeConstraintProviders[nodeIndex]; + } + + private static IFLineNodeConstraintProvider[] CollectNodeConstraintProviders(FLineLogicNode node) + { + MonoBehaviour[] behaviours = node.GetComponents(); + List providers = new List(); + + for (int i = 0; i < behaviours.Length; i++) + { + MonoBehaviour behaviour = behaviours[i]; + if (behaviour is IFLineNodeConstraintProvider provider) + { + providers.Add(provider); + } + } + + return providers.ToArray(); + } + + private void SyncSegmentMaxLength(int segmentIndex, float maxLength) + { + FLineLogicNode node = GetSegmentNode(segmentIndex); + if (node) + { + node.NextSegmentMaxLength = maxLength; + } + } + + private void SyncSegmentMinLength(int segmentIndex, float minLength) + { + FLineLogicNode node = GetSegmentNode(segmentIndex); + if (node) + { + node.NextSegmentMinLength = minLength; + } + } + + private bool ContainsBody(Rigidbody targetBody) + { + for (int i = 0; i < lineNodes.Count; i++) + { + if (GetBodyAt(i) == targetBody) + { + return true; + } + } + + return false; + } + + /// + /// 更新所有活跃的过渡 + /// + private void UpdateTransitions() + { + float deltaTime = Time.fixedDeltaTime; + + for (int i = 0; i < _constraints.Count; i++) + { + var constraint = _constraints[i]; + + if (constraint.hasPendingMaxTransition) + { + float nextMaxDistance = Mathf.MoveTowards( + constraint.maxDistance, + constraint.targetMaxDistance, + constraint.maxTransitionSpeed * deltaTime + ); + constraint.maxDistance = nextMaxDistance; + SyncSegmentMaxLength(i, nextMaxDistance); + + if (Mathf.Abs(nextMaxDistance - constraint.targetMaxDistance) < 0.0001f) + { + constraint.maxDistance = constraint.targetMaxDistance; + constraint.hasPendingMaxTransition = false; + SyncSegmentMaxLength(i, constraint.targetMaxDistance); + } + } + + if (constraint.hasPendingMinTransition) + { + float nextMinDistance = Mathf.MoveTowards( + constraint.minDistance, + constraint.targetMinDistance, + constraint.minTransitionSpeed * deltaTime + ); + constraint.minDistance = nextMinDistance; + SyncSegmentMinLength(i, nextMinDistance); + + if (Mathf.Abs(nextMinDistance - constraint.targetMinDistance) < 0.0001f) + { + constraint.minDistance = constraint.targetMinDistance; + constraint.hasPendingMinTransition = false; + SyncSegmentMinLength(i, constraint.targetMinDistance); + } + } + + constraint.hasPendingTransition = + constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition; + } + } + + private void ApplyDistanceConstraints() + { + for (int i = 0; i < _constraints.Count; i++) + { + var constraint = _constraints[i]; + if (!constraint.bodyA || !constraint.bodyB) continue; + + constraint.UpdateCurrentState(); + + float currentDist = constraint.currentDistance; + float maxDist = constraint.maxDistance; + float minDist = constraint.minDistance; + + float error = 0f; + bool needCorrection = false; + + if (currentDist > maxDist) + { + error = currentDist - maxDist; + needCorrection = true; + } + + if (!needCorrection || Mathf.Abs(error) < 0.0001f) continue; + + float invMassA = constraint.bodyA.isKinematic ? 0f : 1f / constraint.bodyA.mass; + float invMassB = constraint.bodyB.isKinematic ? 0f : 1f / constraint.bodyB.mass; + float totalInvMass = invMassA + invMassB; + + if (totalInvMass < 0.0001f) continue; + + float weightA = useMassWeighting ? (invMassA / totalInvMass) : 0.5f; + float weightB = useMassWeighting ? (invMassB / totalInvMass) : 0.5f; + + Vector3 correction = constraint.direction * error; + Vector3 positionCorrectionA = correction * weightA; + Vector3 positionCorrectionB = -correction * weightB; + + constraint.bodyA.position += positionCorrectionA; + constraint.bodyB.position += positionCorrectionB; + + Vector3 velocityCorrectionA = positionCorrectionA / Time.fixedDeltaTime; + Vector3 velocityCorrectionB = positionCorrectionB / Time.fixedDeltaTime; + + constraint.bodyA.AddForce(velocityCorrectionA * constraint.bodyA.mass, ForceMode.Impulse); + constraint.bodyB.AddForce(velocityCorrectionB * constraint.bodyB.mass, ForceMode.Impulse); + } + } + + private void ApplyNodeConstraints() + { + for (int i = 0; i < lineNodes.Count; i++) + { + Rigidbody body = GetBodyAt(i); + IFLineNodeConstraintProvider[] providers = GetNodeConstraintProvidersAt(i); + + if (!body || body.isKinematic || providers.Length == 0) + { + continue; + } + + for (int providerIndex = 0; providerIndex < providers.Length; providerIndex++) + { + IFLineNodeConstraintProvider provider = providers[providerIndex]; + if (provider == null) + { + continue; + } + + if (provider is Behaviour behaviour && !behaviour.isActiveAndEnabled) + { + continue; + } + + if (!provider.TryGetConstraintState(out FLineNodeConstraintState constraint)) + { + continue; + } + + ApplyWorldTargetConstraint(body, constraint); + } + } + } + + private static void ApplyWorldTargetConstraint(Rigidbody body, FLineNodeConstraintState constraint) + { + Vector3 delta = constraint.TargetPosition - body.position; + Vector3 planarDelta = Vector3.ProjectOnPlane(delta, Vector3.up); + Vector3 correction = planarDelta * Mathf.Clamp01(constraint.PlanarStrength); + + float verticalStrength = delta.y >= 0f + ? Mathf.Clamp01(constraint.UpwardStrength) + : Mathf.Clamp01(constraint.DownwardStrength); + correction.y = delta.y * verticalStrength; + + float maxCorrection = Mathf.Max(0f, constraint.MaxCorrection); + if (maxCorrection > 0f && correction.magnitude > maxCorrection) + { + correction = correction.normalized * maxCorrection; + } + + if (correction.sqrMagnitude < 1e-8f) + { + return; + } + + body.position += correction; + + Vector3 velocityCorrection = correction / Mathf.Max(Time.fixedDeltaTime, 0.0001f); + body.AddForce(velocityCorrection * body.mass, ForceMode.Impulse); + } + + private void ApplyDamping() + { + for (int i = 0; i < _constraints.Count; i++) + { + var constraint = _constraints[i]; + if (!constraint.bodyA || !constraint.bodyB) continue; + + if (constraint.currentDistance <= constraint.maxDistance) continue; + + Vector3 relativeVelocity = constraint.bodyB.linearVelocity - constraint.bodyA.linearVelocity; + float velocityInConstraintDir = Vector3.Dot(relativeVelocity, constraint.direction); + + if (velocityInConstraintDir > 0) + { + float dampingForce = -velocityInConstraintDir * dampingCoefficient; + Vector3 dampingImpulse = constraint.direction * dampingForce * Time.fixedDeltaTime; + + constraint.bodyA.AddForce(-dampingImpulse * constraint.bodyA.mass, ForceMode.Impulse); + constraint.bodyB.AddForce(dampingImpulse * constraint.bodyB.mass, ForceMode.Impulse); + } + } + } + + private void UpdateAnchorNode() + { + if (anchorTransform == null || lineNodes.Count < 1) + { + return; + } + + var startNode = lineNodes[0].Rigidbody; + startNode.transform.SetPositionAndRotation(anchorTransform.position, anchorTransform.rotation); + + if (!startNode.isKinematic) + { + startNode.linearVelocity = Vector3.zero; + startNode.angularVelocity = Vector3.zero; + } + } + + #region 动态间距修改接口 + + public void SetLenght(float length) + { + SetSegmentMaxLength(0, length); + } + + /// + /// 按速度过渡某段的最大距离 + /// + public void SetSegmentMaxLength(int segmentIndex, float targetLength, float transitionSpeed = 2f) + { + if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return; + + targetLength = Mathf.Max(0.01f, targetLength); + float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed); + + var constraint = _constraints[segmentIndex]; + constraint.targetMaxDistance = targetLength; + constraint.maxTransitionSpeed = speed; + constraint.hasPendingMaxTransition = Mathf.Abs(constraint.maxDistance - targetLength) >= 0.0001f; + constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition; + } + + /// + /// 按速度过渡某段的最小距离 + /// + public void SetSegmentMinLength(int segmentIndex, float targetLength, float transitionSpeed = -1) + { + if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return; + + targetLength = Mathf.Max(0f, targetLength); + float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed); + + var constraint = _constraints[segmentIndex]; + constraint.targetMinDistance = targetLength; + constraint.minTransitionSpeed = speed; + constraint.hasPendingMinTransition = Mathf.Abs(constraint.minDistance - targetLength) >= 0.0001f; + constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition; + } + + /// + /// 同时按速度过渡某段的最大和最小距离 + /// + public void SetSegmentLengths(int segmentIndex, float targetMaxLength, float targetMinLength, + float transitionSpeed = -1) + { + if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return; + + targetMaxLength = Mathf.Max(0.01f, targetMaxLength); + targetMinLength = Mathf.Max(0f, targetMinLength); + float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed); + + var constraint = _constraints[segmentIndex]; + constraint.targetMaxDistance = targetMaxLength; + constraint.targetMinDistance = targetMinLength; + constraint.maxTransitionSpeed = speed; + constraint.minTransitionSpeed = speed; + constraint.hasPendingMaxTransition = Mathf.Abs(constraint.maxDistance - targetMaxLength) >= 0.0001f; + constraint.hasPendingMinTransition = Mathf.Abs(constraint.minDistance - targetMinLength) >= 0.0001f; + constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition; + } + + /// + /// 扩展整个绳索(所有段均匀缩放) + /// + public void ExtendAllSegments(float scaleFactor, float transitionSpeed = -1) + { + float speed = transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed; + + for (int i = 0; i < _constraints.Count; i++) + { + var constraint = _constraints[i]; + float newMaxLength = constraint.maxDistance * scaleFactor; + float newMinLength = constraint.minDistance * scaleFactor; + + SetSegmentLengths(i, newMaxLength, newMinLength, speed); + } + } + + /// + /// 收缩/拉紧绳索(所有段向目标长度过渡) + /// + public void TightenAllSegments(float targetLength, float transitionSpeed = -1) + { + float speed = transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed; + + for (int i = 0; i < _constraints.Count; i++) + { + var constraint = _constraints[i]; + SetSegmentLengths(i, targetLength, Mathf.Min(constraint.minDistance, targetLength), speed); + } + } + + /// + /// 对特定段施加拉力(减小最大距离) + /// + public void PullSegment(int segmentIndex, float amount, float transitionSpeed = -1) + { + if (segmentIndex >= 0 && segmentIndex < _constraints.Count) + { + var constraint = _constraints[segmentIndex]; + float newMaxLength = Mathf.Max(0.01f, constraint.targetMaxDistance - amount); + SetSegmentMaxLength(segmentIndex, newMaxLength, transitionSpeed); + } + } + + /// + /// 放松特定段(增加最大距离) + /// + public void RelaxSegment(int segmentIndex, float amount, float transitionSpeed = -1) + { + if (segmentIndex >= 0 && segmentIndex < _constraints.Count) + { + var constraint = _constraints[segmentIndex]; + float newMaxLength = constraint.targetMaxDistance + amount; + SetSegmentMaxLength(segmentIndex, newMaxLength, transitionSpeed); + } + } + + /// + /// 移除指定段的过渡 + /// + private void RemoveExistingTransition(int segmentIndex, bool removeMax, bool removeMin) + { + if (segmentIndex < 0 || segmentIndex >= _constraints.Count) + { + return; + } + + var constraint = _constraints[segmentIndex]; + if (removeMax) + { + constraint.hasPendingMaxTransition = false; + constraint.targetMaxDistance = constraint.maxDistance; + } + + if (removeMin) + { + constraint.hasPendingMinTransition = false; + constraint.targetMinDistance = constraint.minDistance; + } + + constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition; + } + + /// + /// 取消所有正在进行的过渡 + /// + public void CancelAllTransitions() + { + foreach (var constraint in _constraints) + { + constraint.hasPendingTransition = false; + constraint.hasPendingMaxTransition = false; + constraint.hasPendingMinTransition = false; + constraint.targetMaxDistance = constraint.maxDistance; + constraint.targetMinDistance = constraint.minDistance; + } + } + + /// + /// 取消指定段的过渡 + /// + public void CancelTransition(int segmentIndex) + { + RemoveExistingTransition(segmentIndex, true, true); + if (segmentIndex < _constraints.Count) + { + _constraints[segmentIndex].hasPendingTransition = false; + } + } + + /// + /// 获取某个段是否正在进行过渡 + /// + public bool IsSegmentTransitioning(int segmentIndex) + { + if (segmentIndex >= 0 && segmentIndex < _constraints.Count) + { + return _constraints[segmentIndex].hasPendingTransition; + } + + return false; + } + + /// + /// 获取某段当前的目标最大距离 + /// + public float GetTargetMaxLength(int segmentIndex) + { + if (segmentIndex >= 0 && segmentIndex < _constraints.Count) + { + return _constraints[segmentIndex].targetMaxDistance; + } + + return -1f; + } + + /// + /// 获取某段当前的目标最小距离 + /// + public float GetTargetMinLength(int segmentIndex) + { + if (segmentIndex >= 0 && segmentIndex < _constraints.Count) + { + return _constraints[segmentIndex].targetMinDistance; + } + + return -1f; + } + + /// + /// 设置默认长度变化速度 + /// + public void SetDefaultTransitionSpeed(float speed) + { + defaultTransitionSpeed = Mathf.Max(0.01f, speed); + } + + #endregion + + #region 极限判定 + + /// + /// 当前逻辑链总长度超出配置总长度的部分,小于等于零时记为 0。 + /// + [Header("Limit Detection")] + public float CurrentStretchLength { get; private set; } + + /// + /// 总长度 + /// + public float TotalLength { get; private set; } + + [Min(0f)] + // 极限判定的长度容差,允许链路在总长或单段长度上存在少量误差。 + [SerializeField] + private float lengthLimitTolerance = 0.01f; + + [Min(0f)] + // 达到极限后,只有当前超长值大于该阈值时,才开始进入断线候选计时。 + [SerializeField] + private float breakStretchThreshold = 0.3f; + + [Min(0f)] + // UI 百分比开始起算的最小超长值;低于或等于该值时统一按 0% 处理。 + [SerializeField] + private float breakStretchPercentMinThreshold = 0.06f; + + [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; } + + /// + /// 当前拉力极限百分比。 + /// 当超长值小于等于 breakStretchPercentMinThreshold 时为 0; + /// 当超长值大于等于 breakStretchThreshold 时为 100; + /// 中间区间按线性比例映射,供 UI 显示使用。 + /// + public float CurrentBreakStretchPercent => EvaluateBreakStretchPercent(CurrentStretchLength); + + /// + /// 当前是否正在进行断线候选计时。 + /// + public bool IsBreakCountdownActive => IsAtLimit && CurrentStretchLength > breakStretchThreshold; + + private float EvaluateBreakStretchPercent(float stretchLength) + { + var percentMinThreshold = Mathf.Max(lengthLimitTolerance, breakStretchPercentMinThreshold); + + if (stretchLength <= percentMinThreshold) + { + return 0f; + } + + if (stretchLength >= breakStretchThreshold) + { + return 100f; + } + + if (breakStretchThreshold <= percentMinThreshold) + { + return 100f; + } + + return Mathf.InverseLerp(percentMinThreshold, breakStretchThreshold, stretchLength) * 100f; + } + + private void SetLimitState(bool isAtLimit) + { + IsAtLimit = isAtLimit; + } + + private void UpdateBreakCountdown(float deltaTime) + { + if (lineNodes.Count < 2) + { + SetLimitState(false); + ResetLimitState(); + return; + } + + var startNode = lineNodes[0]; + var endNode = lineNodes[^1]; + TotalLength = 0; + foreach (var node in lineNodes) + { + if (node.NodeType == FLineLogicNodeType.End) continue; + TotalLength += node.NextSegmentMaxLength; + } + + var realLen = Vector3.Distance(startNode.transform.position, endNode.transform.position); + CurrentStretchLength = realLen - TotalLength; + if (CurrentStretchLength < 0f) + { + CurrentStretchLength = 0f; + } + + SetLimitState(CurrentStretchLength > lengthLimitTolerance); + + if (CurrentStretchLength > 1) + { + Log.Error($"水电费 realLen={realLen} TotalLength={TotalLength}"); + } + + 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 公共接口 + + /// + /// 获取一个节点 + /// + /// + /// + public FLineLogicNode GetNode(FLineLogicNodeType type) + { + foreach (var node in lineNodes) + { + if (node.NodeType == type) + { + return node; + } + } + + return null; + } + + + public float GetCurrentSegmentLength(int segmentIndex) + { + if (segmentIndex >= 0 && segmentIndex < _constraints.Count) + { + _constraints[segmentIndex].UpdateCurrentState(); + return _constraints[segmentIndex].currentDistance; + } + + return -1f; + } + + public void AddConnectedBody(Rigidbody rb, float maxLength = 1f, float minLength = 0f) + { + if (rb == null) + { + return; + } + + FLineLogicNode node = rb.GetComponent(); + if (!node) + { + Debug.LogWarning("AddConnectedBody需要目标刚体上挂有FLineLogicNode"); + return; + } + + if (!lineNodes.Contains(node)) + { + if (lineNodes.Count > 0 && lineNodes[lineNodes.Count - 1]) + { + lineNodes[lineNodes.Count - 1].SetSegmentLengths(maxLength, minLength); + } + + node.AttachToCable(this); + lineNodes.Add(node); + BuildConstraints(); + enabled = _constraints.Count > 0; + } + } + + public void RemoveConnectedBody(Rigidbody rb) + { + if (rb == null) + { + return; + } + + for (int i = 0; i < lineNodes.Count; i++) + { + if (GetBodyAt(i) == rb) + { + lineNodes.RemoveAt(i); + BuildConstraints(); + enabled = _constraints.Count > 0; + break; + } + } + } + + public List GetConnectedBodies() + { + List bodies = new List(lineNodes.Count); + for (int i = 0; i < lineNodes.Count; i++) + { + bodies.Add(GetBodyAt(i)); + } + + return bodies; + } + + public List GetLineNodes() + { + return new List(lineNodes); + } + + public bool TryGetAdjacentBodies(FLineLogicNode node, out Rigidbody previousBody, out Rigidbody nextBody) + { + previousBody = null; + nextBody = null; + + if (!node) + { + return false; + } + + int index = lineNodes.IndexOf(node); + if (index < 0) + { + return false; + } + + if (index > 0 && lineNodes[index - 1]) + { + previousBody = lineNodes[index - 1].Rigidbody; + } + + if (index < lineNodes.Count - 1 && lineNodes[index + 1]) + { + nextBody = lineNodes[index + 1].Rigidbody; + } + + return previousBody || nextBody; + } + + public void ApplyForceAtBody(Rigidbody targetBody, Vector3 force, ForceMode mode = ForceMode.Force) + { + if (targetBody && ContainsBody(targetBody)) + { + targetBody.AddForce(force, mode); + } + } + + public float GetSegmentMaxLength(int segmentIndex) + { + FLineLogicNode node = GetSegmentNode(segmentIndex); + if (node) + return node.NextSegmentMaxLength; + + return -1f; + } + + #endregion + + #region 可视化调试 + + void OnDrawGizmos() + { + if (!showDebugInfo || lineNodes.Count < 2) return; + + for (int i = 0; i < lineNodes.Count - 1; i++) + { + Rigidbody bodyA = GetBodyAt(i); + Rigidbody bodyB = GetBodyAt(i + 1); + FLineLogicNode segmentNode = GetSegmentNode(i); + + if (bodyA != null && bodyB != null) + { + float currentDist = Vector3.Distance(bodyA.position, bodyB.position); + float maxDist = segmentNode ? segmentNode.NextSegmentMaxLength : 1f; + float minDist = segmentNode ? segmentNode.NextSegmentMinLength : 0f; + + // 颜色:如果正在过渡用橙色,绿色=正常范围,红色=超出最大距离,蓝色=小于最小距离 + if (IsSegmentTransitioning(i)) + Gizmos.color = new Color(1f, 0.5f, 0f); // 橙色表示正在过渡 + else if (currentDist > maxDist) + Gizmos.color = Color.red; + else if (currentDist < minDist && minDist > 0) + Gizmos.color = Color.blue; + else + Gizmos.color = Color.green; + + Gizmos.DrawLine(bodyA.position, bodyB.position); + + Vector3 midPoint = (bodyA.position + bodyB.position) * 0.5f; + Gizmos.DrawWireSphere(midPoint, 0.1f); + +#if UNITY_EDITOR + string info = $"距离:{currentDist:F2}"; + if (IsSegmentTransitioning(i)) + info += $"\n过渡中→{GetTargetMaxLength(i):F2}"; + else + info += $" [最大:{maxDist:F2}"; + info += $"]"; + UnityEditor.Handles.Label(midPoint, info); +#endif + } + } + + Gizmos.color = Color.red; + for (int i = 0; i < lineNodes.Count; i++) + { + Rigidbody body = GetBodyAt(i); + if (body != null) + { + Gizmos.DrawSphere(body.position, 0.05f); + } + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs.meta b/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs.meta index 0eab8bdce..970afcb8d 100644 --- a/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs.meta +++ b/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs.meta @@ -1,2 +1,3 @@ fileFormatVersion: 2 -guid: 356b3a6fd883b8b4486025abfa664de9 \ No newline at end of file +guid: c7095cf554c345839173044e4786b0ba +timeCreated: 1776948821 \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs b/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs deleted file mode 100644 index e25706981..000000000 --- a/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs +++ /dev/null @@ -1,1044 +0,0 @@ -using System; -using UnityEngine; -using System.Collections.Generic; -using NBC; - -namespace NBF -{ - public struct FLineNodeConstraintState - { - public Vector3 TargetPosition; - public float PlanarStrength; - public float UpwardStrength; - public float DownwardStrength; - public float MaxCorrection; - } - - /// - /// 为 FLine 节点提供附加软约束。求解器只依赖这个通用接口,不依赖具体玩法组件。 - /// - public interface IFLineNodeConstraintProvider - { - bool TryGetConstraintState(out FLineNodeConstraintState state); - } - - public enum LineType - { - Spinning, - SpinningFloat, - Hand, - HandDouble, - } - - public class FLine : FGearBase - { - [Header("基本参数设置")] public LineType LineType; - [Header("连接点配置")] [SerializeField] private Transform anchorTransform; - [SerializeField] private List lineNodes = new List(); - - [Header("物理参数")] [SerializeField] private float positionCorrectionForce = 100f; - [SerializeField] private float dampingCoefficient = 10f; - [SerializeField] private int constraintIterations = 10; - [SerializeField] private bool useMassWeighting = true; - - [Header("动态间距设置")] [SerializeField] private float defaultTransitionSpeed = 2f; // 默认长度变化速度(单位/秒) - - [Header("调试")] [SerializeField] private bool showDebugInfo = true; - - private readonly List _constraints = new List(); - - private readonly List _nodeConstraintProviders = - new List(); - - [System.Serializable] - public class ConnectionConstraint - { - public Rigidbody bodyA; - public Rigidbody bodyB; - public float maxDistance; - public float minDistance; - public float currentDistance; - public Vector3 direction; - - // 动态目标距离(用于平滑过渡) - public float targetMaxDistance; - public float targetMinDistance; - public bool hasPendingTransition; - public bool hasPendingMaxTransition; - public bool hasPendingMinTransition; - public float maxTransitionSpeed; - public float minTransitionSpeed; - - public ConnectionConstraint(Rigidbody a, Rigidbody b, float maxDist, float minDist = 0f) - { - bodyA = a; - bodyB = b; - maxDistance = maxDist; - minDistance = minDist; - targetMaxDistance = maxDist; - targetMinDistance = minDist; - hasPendingTransition = false; - hasPendingMaxTransition = false; - hasPendingMinTransition = false; - maxTransitionSpeed = 0f; - minTransitionSpeed = 0f; - } - - public void UpdateCurrentState() - { - if (bodyA && bodyB) - { - Vector3 delta = bodyB.position - bodyA.position; - currentDistance = delta.magnitude; - direction = currentDistance > 0.0001f ? delta.normalized : Vector3.right; - } - } - } - - void Awake() - { - BuildConstraints(); - } - - protected override void OnInit() - { - } - - void Start() - { - if (_constraints.Count == 0) - { - Debug.LogWarning("FLine需要至少2个有效节点"); - enabled = false; - } - } - - void FixedUpdate() - { - if (!enabled || _constraints.Count == 0) return; - - UpdateAnchorNode(); - // 更新动态过渡 - UpdateTransitions(); - - for (int iteration = 0; iteration < constraintIterations; iteration++) - { - ApplyDistanceConstraints(); - ApplyNodeConstraints(); - } - - ApplyDamping(); - UpdateBreakCountdown(Time.fixedDeltaTime); - } - - private void BuildConstraints() - { - _constraints.Clear(); - _nodeConstraintProviders.Clear(); - if (lineNodes.Count < 2) return; - - for (int i = 0; i < lineNodes.Count; i++) - { - if (lineNodes[i]) - { - lineNodes[i].AttachToCable(this); - _nodeConstraintProviders.Add(CollectNodeConstraintProviders(lineNodes[i])); - } - else - { - _nodeConstraintProviders.Add(Array.Empty()); - } - } - - // 创建约束 - for (int i = 0; i < lineNodes.Count - 1; i++) - { - FLineLogicNode currentNode = lineNodes[i]; - FLineLogicNode nextNode = lineNodes[i + 1]; - Rigidbody bodyA = currentNode ? currentNode.Rigidbody : null; - Rigidbody bodyB = nextNode ? nextNode.Rigidbody : null; - - if (bodyA != null && bodyB != null) - { - var constraint = new ConnectionConstraint( - bodyA, - bodyB, - currentNode.NextSegmentMaxLength, - currentNode.NextSegmentMinLength - ); - _constraints.Add(constraint); - } - } - } - - private FLineLogicNode GetSegmentNode(int segmentIndex) - { - if (segmentIndex < 0 || segmentIndex >= lineNodes.Count - 1) - { - return null; - } - - return lineNodes[segmentIndex]; - } - - private Rigidbody GetBodyAt(int nodeIndex) - { - if (nodeIndex < 0 || nodeIndex >= lineNodes.Count) - { - return null; - } - - FLineLogicNode node = lineNodes[nodeIndex]; - return node ? node.Rigidbody : null; - } - - private IFLineNodeConstraintProvider[] GetNodeConstraintProvidersAt(int nodeIndex) - { - if (nodeIndex < 0 || nodeIndex >= _nodeConstraintProviders.Count) - { - return Array.Empty(); - } - - return _nodeConstraintProviders[nodeIndex]; - } - - private static IFLineNodeConstraintProvider[] CollectNodeConstraintProviders(FLineLogicNode node) - { - MonoBehaviour[] behaviours = node.GetComponents(); - List providers = new List(); - - for (int i = 0; i < behaviours.Length; i++) - { - MonoBehaviour behaviour = behaviours[i]; - if (behaviour is IFLineNodeConstraintProvider provider) - { - providers.Add(provider); - } - } - - return providers.ToArray(); - } - - private void SyncSegmentMaxLength(int segmentIndex, float maxLength) - { - FLineLogicNode node = GetSegmentNode(segmentIndex); - if (node) - { - node.NextSegmentMaxLength = maxLength; - } - } - - private void SyncSegmentMinLength(int segmentIndex, float minLength) - { - FLineLogicNode node = GetSegmentNode(segmentIndex); - if (node) - { - node.NextSegmentMinLength = minLength; - } - } - - private bool ContainsBody(Rigidbody targetBody) - { - for (int i = 0; i < lineNodes.Count; i++) - { - if (GetBodyAt(i) == targetBody) - { - return true; - } - } - - return false; - } - - /// - /// 更新所有活跃的过渡 - /// - private void UpdateTransitions() - { - float deltaTime = Time.fixedDeltaTime; - - for (int i = 0; i < _constraints.Count; i++) - { - var constraint = _constraints[i]; - - if (constraint.hasPendingMaxTransition) - { - float nextMaxDistance = Mathf.MoveTowards( - constraint.maxDistance, - constraint.targetMaxDistance, - constraint.maxTransitionSpeed * deltaTime - ); - constraint.maxDistance = nextMaxDistance; - SyncSegmentMaxLength(i, nextMaxDistance); - - if (Mathf.Abs(nextMaxDistance - constraint.targetMaxDistance) < 0.0001f) - { - constraint.maxDistance = constraint.targetMaxDistance; - constraint.hasPendingMaxTransition = false; - SyncSegmentMaxLength(i, constraint.targetMaxDistance); - } - } - - if (constraint.hasPendingMinTransition) - { - float nextMinDistance = Mathf.MoveTowards( - constraint.minDistance, - constraint.targetMinDistance, - constraint.minTransitionSpeed * deltaTime - ); - constraint.minDistance = nextMinDistance; - SyncSegmentMinLength(i, nextMinDistance); - - if (Mathf.Abs(nextMinDistance - constraint.targetMinDistance) < 0.0001f) - { - constraint.minDistance = constraint.targetMinDistance; - constraint.hasPendingMinTransition = false; - SyncSegmentMinLength(i, constraint.targetMinDistance); - } - } - - constraint.hasPendingTransition = - constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition; - } - } - - private void ApplyDistanceConstraints() - { - for (int i = 0; i < _constraints.Count; i++) - { - var constraint = _constraints[i]; - if (!constraint.bodyA || !constraint.bodyB) continue; - - constraint.UpdateCurrentState(); - - float currentDist = constraint.currentDistance; - float maxDist = constraint.maxDistance; - float minDist = constraint.minDistance; - - float error = 0f; - bool needCorrection = false; - - if (currentDist > maxDist) - { - error = currentDist - maxDist; - needCorrection = true; - } - - if (!needCorrection || Mathf.Abs(error) < 0.0001f) continue; - - float invMassA = constraint.bodyA.isKinematic ? 0f : 1f / constraint.bodyA.mass; - float invMassB = constraint.bodyB.isKinematic ? 0f : 1f / constraint.bodyB.mass; - float totalInvMass = invMassA + invMassB; - - if (totalInvMass < 0.0001f) continue; - - float weightA = useMassWeighting ? (invMassA / totalInvMass) : 0.5f; - float weightB = useMassWeighting ? (invMassB / totalInvMass) : 0.5f; - - Vector3 correction = constraint.direction * error; - Vector3 positionCorrectionA = correction * weightA; - Vector3 positionCorrectionB = -correction * weightB; - - constraint.bodyA.position += positionCorrectionA; - constraint.bodyB.position += positionCorrectionB; - - Vector3 velocityCorrectionA = positionCorrectionA / Time.fixedDeltaTime; - Vector3 velocityCorrectionB = positionCorrectionB / Time.fixedDeltaTime; - - constraint.bodyA.AddForce(velocityCorrectionA * constraint.bodyA.mass, ForceMode.Impulse); - constraint.bodyB.AddForce(velocityCorrectionB * constraint.bodyB.mass, ForceMode.Impulse); - } - } - - private void ApplyNodeConstraints() - { - for (int i = 0; i < lineNodes.Count; i++) - { - Rigidbody body = GetBodyAt(i); - IFLineNodeConstraintProvider[] providers = GetNodeConstraintProvidersAt(i); - - if (!body || body.isKinematic || providers.Length == 0) - { - continue; - } - - for (int providerIndex = 0; providerIndex < providers.Length; providerIndex++) - { - IFLineNodeConstraintProvider provider = providers[providerIndex]; - if (provider == null) - { - continue; - } - - if (provider is Behaviour behaviour && !behaviour.isActiveAndEnabled) - { - continue; - } - - if (!provider.TryGetConstraintState(out FLineNodeConstraintState constraint)) - { - continue; - } - - ApplyWorldTargetConstraint(body, constraint); - } - } - } - - private static void ApplyWorldTargetConstraint(Rigidbody body, FLineNodeConstraintState constraint) - { - Vector3 delta = constraint.TargetPosition - body.position; - Vector3 planarDelta = Vector3.ProjectOnPlane(delta, Vector3.up); - Vector3 correction = planarDelta * Mathf.Clamp01(constraint.PlanarStrength); - - float verticalStrength = delta.y >= 0f - ? Mathf.Clamp01(constraint.UpwardStrength) - : Mathf.Clamp01(constraint.DownwardStrength); - correction.y = delta.y * verticalStrength; - - float maxCorrection = Mathf.Max(0f, constraint.MaxCorrection); - if (maxCorrection > 0f && correction.magnitude > maxCorrection) - { - correction = correction.normalized * maxCorrection; - } - - if (correction.sqrMagnitude < 1e-8f) - { - return; - } - - body.position += correction; - - Vector3 velocityCorrection = correction / Mathf.Max(Time.fixedDeltaTime, 0.0001f); - body.AddForce(velocityCorrection * body.mass, ForceMode.Impulse); - } - - private void ApplyDamping() - { - for (int i = 0; i < _constraints.Count; i++) - { - var constraint = _constraints[i]; - if (!constraint.bodyA || !constraint.bodyB) continue; - - if (constraint.currentDistance <= constraint.maxDistance) continue; - - Vector3 relativeVelocity = constraint.bodyB.linearVelocity - constraint.bodyA.linearVelocity; - float velocityInConstraintDir = Vector3.Dot(relativeVelocity, constraint.direction); - - if (velocityInConstraintDir > 0) - { - float dampingForce = -velocityInConstraintDir * dampingCoefficient; - Vector3 dampingImpulse = constraint.direction * dampingForce * Time.fixedDeltaTime; - - constraint.bodyA.AddForce(-dampingImpulse * constraint.bodyA.mass, ForceMode.Impulse); - constraint.bodyB.AddForce(dampingImpulse * constraint.bodyB.mass, ForceMode.Impulse); - } - } - } - - private void UpdateAnchorNode() - { - if (anchorTransform == null || lineNodes.Count < 1) - { - return; - } - - var startNode = lineNodes[0].Rigidbody; - startNode.transform.SetPositionAndRotation(anchorTransform.position, anchorTransform.rotation); - - if (!startNode.isKinematic) - { - startNode.linearVelocity = Vector3.zero; - startNode.angularVelocity = Vector3.zero; - } - } - - #region 动态间距修改接口 - - public void SetLenght(float length) - { - SetSegmentMaxLength(0, length); - } - - /// - /// 按速度过渡某段的最大距离 - /// - public void SetSegmentMaxLength(int segmentIndex, float targetLength, float transitionSpeed = 2f) - { - if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return; - - targetLength = Mathf.Max(0.01f, targetLength); - float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed); - - var constraint = _constraints[segmentIndex]; - constraint.targetMaxDistance = targetLength; - constraint.maxTransitionSpeed = speed; - constraint.hasPendingMaxTransition = Mathf.Abs(constraint.maxDistance - targetLength) >= 0.0001f; - constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition; - } - - /// - /// 按速度过渡某段的最小距离 - /// - public void SetSegmentMinLength(int segmentIndex, float targetLength, float transitionSpeed = -1) - { - if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return; - - targetLength = Mathf.Max(0f, targetLength); - float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed); - - var constraint = _constraints[segmentIndex]; - constraint.targetMinDistance = targetLength; - constraint.minTransitionSpeed = speed; - constraint.hasPendingMinTransition = Mathf.Abs(constraint.minDistance - targetLength) >= 0.0001f; - constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition; - } - - /// - /// 同时按速度过渡某段的最大和最小距离 - /// - public void SetSegmentLengths(int segmentIndex, float targetMaxLength, float targetMinLength, - float transitionSpeed = -1) - { - if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return; - - targetMaxLength = Mathf.Max(0.01f, targetMaxLength); - targetMinLength = Mathf.Max(0f, targetMinLength); - float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed); - - var constraint = _constraints[segmentIndex]; - constraint.targetMaxDistance = targetMaxLength; - constraint.targetMinDistance = targetMinLength; - constraint.maxTransitionSpeed = speed; - constraint.minTransitionSpeed = speed; - constraint.hasPendingMaxTransition = Mathf.Abs(constraint.maxDistance - targetMaxLength) >= 0.0001f; - constraint.hasPendingMinTransition = Mathf.Abs(constraint.minDistance - targetMinLength) >= 0.0001f; - constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition; - } - - /// - /// 扩展整个绳索(所有段均匀缩放) - /// - public void ExtendAllSegments(float scaleFactor, float transitionSpeed = -1) - { - float speed = transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed; - - for (int i = 0; i < _constraints.Count; i++) - { - var constraint = _constraints[i]; - float newMaxLength = constraint.maxDistance * scaleFactor; - float newMinLength = constraint.minDistance * scaleFactor; - - SetSegmentLengths(i, newMaxLength, newMinLength, speed); - } - } - - /// - /// 收缩/拉紧绳索(所有段向目标长度过渡) - /// - public void TightenAllSegments(float targetLength, float transitionSpeed = -1) - { - float speed = transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed; - - for (int i = 0; i < _constraints.Count; i++) - { - var constraint = _constraints[i]; - SetSegmentLengths(i, targetLength, Mathf.Min(constraint.minDistance, targetLength), speed); - } - } - - /// - /// 对特定段施加拉力(减小最大距离) - /// - public void PullSegment(int segmentIndex, float amount, float transitionSpeed = -1) - { - if (segmentIndex >= 0 && segmentIndex < _constraints.Count) - { - var constraint = _constraints[segmentIndex]; - float newMaxLength = Mathf.Max(0.01f, constraint.targetMaxDistance - amount); - SetSegmentMaxLength(segmentIndex, newMaxLength, transitionSpeed); - } - } - - /// - /// 放松特定段(增加最大距离) - /// - public void RelaxSegment(int segmentIndex, float amount, float transitionSpeed = -1) - { - if (segmentIndex >= 0 && segmentIndex < _constraints.Count) - { - var constraint = _constraints[segmentIndex]; - float newMaxLength = constraint.targetMaxDistance + amount; - SetSegmentMaxLength(segmentIndex, newMaxLength, transitionSpeed); - } - } - - /// - /// 移除指定段的过渡 - /// - private void RemoveExistingTransition(int segmentIndex, bool removeMax, bool removeMin) - { - if (segmentIndex < 0 || segmentIndex >= _constraints.Count) - { - return; - } - - var constraint = _constraints[segmentIndex]; - if (removeMax) - { - constraint.hasPendingMaxTransition = false; - constraint.targetMaxDistance = constraint.maxDistance; - } - - if (removeMin) - { - constraint.hasPendingMinTransition = false; - constraint.targetMinDistance = constraint.minDistance; - } - - constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition; - } - - /// - /// 取消所有正在进行的过渡 - /// - public void CancelAllTransitions() - { - foreach (var constraint in _constraints) - { - constraint.hasPendingTransition = false; - constraint.hasPendingMaxTransition = false; - constraint.hasPendingMinTransition = false; - constraint.targetMaxDistance = constraint.maxDistance; - constraint.targetMinDistance = constraint.minDistance; - } - } - - /// - /// 取消指定段的过渡 - /// - public void CancelTransition(int segmentIndex) - { - RemoveExistingTransition(segmentIndex, true, true); - if (segmentIndex < _constraints.Count) - { - _constraints[segmentIndex].hasPendingTransition = false; - } - } - - /// - /// 获取某个段是否正在进行过渡 - /// - public bool IsSegmentTransitioning(int segmentIndex) - { - if (segmentIndex >= 0 && segmentIndex < _constraints.Count) - { - return _constraints[segmentIndex].hasPendingTransition; - } - - return false; - } - - /// - /// 获取某段当前的目标最大距离 - /// - public float GetTargetMaxLength(int segmentIndex) - { - if (segmentIndex >= 0 && segmentIndex < _constraints.Count) - { - return _constraints[segmentIndex].targetMaxDistance; - } - - return -1f; - } - - /// - /// 获取某段当前的目标最小距离 - /// - public float GetTargetMinLength(int segmentIndex) - { - if (segmentIndex >= 0 && segmentIndex < _constraints.Count) - { - return _constraints[segmentIndex].targetMinDistance; - } - - return -1f; - } - - /// - /// 设置默认长度变化速度 - /// - public void SetDefaultTransitionSpeed(float speed) - { - defaultTransitionSpeed = Mathf.Max(0.01f, speed); - } - - #endregion - - #region 极限判定 - - /// - /// 当前逻辑链总长度超出配置总长度的部分,小于等于零时记为 0。 - /// - [Header("Limit Detection")] - public float CurrentStretchLength { get; private set; } - - /// - /// 总长度 - /// - public float TotalLength { get; private set; } - - [Min(0f)] - // 极限判定的长度容差,允许链路在总长或单段长度上存在少量误差。 - [SerializeField] - private float lengthLimitTolerance = 0.01f; - - [Min(0f)] - // 达到极限后,只有当前超长值大于该阈值时,才开始进入断线候选计时。 - [SerializeField] - private float breakStretchThreshold = 0.3f; - - [Min(0f)] - // UI 百分比开始起算的最小超长值;低于或等于该值时统一按 0% 处理。 - [SerializeField] - private float breakStretchPercentMinThreshold = 0.06f; - - [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; } - - /// - /// 当前拉力极限百分比。 - /// 当超长值小于等于 breakStretchPercentMinThreshold 时为 0; - /// 当超长值大于等于 breakStretchThreshold 时为 100; - /// 中间区间按线性比例映射,供 UI 显示使用。 - /// - public float CurrentBreakStretchPercent => EvaluateBreakStretchPercent(CurrentStretchLength); - - /// - /// 当前是否正在进行断线候选计时。 - /// - public bool IsBreakCountdownActive => IsAtLimit && CurrentStretchLength > breakStretchThreshold; - - private float EvaluateBreakStretchPercent(float stretchLength) - { - var percentMinThreshold = Mathf.Max(lengthLimitTolerance, breakStretchPercentMinThreshold); - - if (stretchLength <= percentMinThreshold) - { - return 0f; - } - - if (stretchLength >= breakStretchThreshold) - { - return 100f; - } - - if (breakStretchThreshold <= percentMinThreshold) - { - return 100f; - } - - return Mathf.InverseLerp(percentMinThreshold, breakStretchThreshold, stretchLength) * 100f; - } - - private void SetLimitState(bool isAtLimit) - { - IsAtLimit = isAtLimit; - } - - private void UpdateBreakCountdown(float deltaTime) - { - if (lineNodes.Count < 2) - { - SetLimitState(false); - ResetLimitState(); - return; - } - - var startNode = lineNodes[0]; - var endNode = lineNodes[^1]; - TotalLength = 0; - foreach (var node in lineNodes) - { - if (node.NodeType == FLineLogicNodeType.End) continue; - TotalLength += node.NextSegmentMaxLength; - } - - var realLen = Vector3.Distance(startNode.transform.position, endNode.transform.position); - CurrentStretchLength = realLen - TotalLength; - if (CurrentStretchLength < 0f) - { - CurrentStretchLength = 0f; - } - - SetLimitState(CurrentStretchLength > lengthLimitTolerance); - - if (CurrentStretchLength > 1) - { - Log.Error($"水电费 realLen={realLen} TotalLength={TotalLength}"); - } - - 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 公共接口 - - /// - /// 获取一个节点 - /// - /// - /// - public FLineLogicNode GetNode(FLineLogicNodeType type) - { - foreach (var node in lineNodes) - { - if (node.NodeType == type) - { - return node; - } - } - - return null; - } - - - public float GetCurrentSegmentLength(int segmentIndex) - { - if (segmentIndex >= 0 && segmentIndex < _constraints.Count) - { - _constraints[segmentIndex].UpdateCurrentState(); - return _constraints[segmentIndex].currentDistance; - } - - return -1f; - } - - public void AddConnectedBody(Rigidbody rb, float maxLength = 1f, float minLength = 0f) - { - if (rb == null) - { - return; - } - - FLineLogicNode node = rb.GetComponent(); - if (!node) - { - Debug.LogWarning("AddConnectedBody需要目标刚体上挂有FLineLogicNode"); - return; - } - - if (!lineNodes.Contains(node)) - { - if (lineNodes.Count > 0 && lineNodes[lineNodes.Count - 1]) - { - lineNodes[lineNodes.Count - 1].SetSegmentLengths(maxLength, minLength); - } - - node.AttachToCable(this); - lineNodes.Add(node); - BuildConstraints(); - enabled = _constraints.Count > 0; - } - } - - public void RemoveConnectedBody(Rigidbody rb) - { - if (rb == null) - { - return; - } - - for (int i = 0; i < lineNodes.Count; i++) - { - if (GetBodyAt(i) == rb) - { - lineNodes.RemoveAt(i); - BuildConstraints(); - enabled = _constraints.Count > 0; - break; - } - } - } - - public List GetConnectedBodies() - { - List bodies = new List(lineNodes.Count); - for (int i = 0; i < lineNodes.Count; i++) - { - bodies.Add(GetBodyAt(i)); - } - - return bodies; - } - - public List GetLineNodes() - { - return new List(lineNodes); - } - - public bool TryGetAdjacentBodies(FLineLogicNode node, out Rigidbody previousBody, out Rigidbody nextBody) - { - previousBody = null; - nextBody = null; - - if (!node) - { - return false; - } - - int index = lineNodes.IndexOf(node); - if (index < 0) - { - return false; - } - - if (index > 0 && lineNodes[index - 1]) - { - previousBody = lineNodes[index - 1].Rigidbody; - } - - if (index < lineNodes.Count - 1 && lineNodes[index + 1]) - { - nextBody = lineNodes[index + 1].Rigidbody; - } - - return previousBody || nextBody; - } - - public void ApplyForceAtBody(Rigidbody targetBody, Vector3 force, ForceMode mode = ForceMode.Force) - { - if (targetBody && ContainsBody(targetBody)) - { - targetBody.AddForce(force, mode); - } - } - - public float GetSegmentMaxLength(int segmentIndex) - { - FLineLogicNode node = GetSegmentNode(segmentIndex); - if (node) - return node.NextSegmentMaxLength; - - return -1f; - } - - #endregion - - #region 可视化调试 - - void OnDrawGizmos() - { - if (!showDebugInfo || lineNodes.Count < 2) return; - - for (int i = 0; i < lineNodes.Count - 1; i++) - { - Rigidbody bodyA = GetBodyAt(i); - Rigidbody bodyB = GetBodyAt(i + 1); - FLineLogicNode segmentNode = GetSegmentNode(i); - - if (bodyA != null && bodyB != null) - { - float currentDist = Vector3.Distance(bodyA.position, bodyB.position); - float maxDist = segmentNode ? segmentNode.NextSegmentMaxLength : 1f; - float minDist = segmentNode ? segmentNode.NextSegmentMinLength : 0f; - - // 颜色:如果正在过渡用橙色,绿色=正常范围,红色=超出最大距离,蓝色=小于最小距离 - if (IsSegmentTransitioning(i)) - Gizmos.color = new Color(1f, 0.5f, 0f); // 橙色表示正在过渡 - else if (currentDist > maxDist) - Gizmos.color = Color.red; - else if (currentDist < minDist && minDist > 0) - Gizmos.color = Color.blue; - else - Gizmos.color = Color.green; - - Gizmos.DrawLine(bodyA.position, bodyB.position); - - Vector3 midPoint = (bodyA.position + bodyB.position) * 0.5f; - Gizmos.DrawWireSphere(midPoint, 0.1f); - -#if UNITY_EDITOR - string info = $"距离:{currentDist:F2}"; - if (IsSegmentTransitioning(i)) - info += $"\n过渡中→{GetTargetMaxLength(i):F2}"; - else - info += $" [最大:{maxDist:F2}"; - info += $"]"; - UnityEditor.Handles.Label(midPoint, info); -#endif - } - } - - Gizmos.color = Color.red; - for (int i = 0; i < lineNodes.Count; i++) - { - Rigidbody body = GetBodyAt(i); - if (body != null) - { - Gizmos.DrawSphere(body.position, 0.05f); - } - } - } - - #endregion - } -} \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs.meta b/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs.meta deleted file mode 100644 index 970afcb8d..000000000 --- a/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: c7095cf554c345839173044e4786b0ba -timeCreated: 1776948821 \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/Player/Tackle/FRod.cs b/Assets/Scripts/Fishing/New/View/Player/Tackle/FRod.cs index 1ce4e3c68..5ef0d95ef 100644 --- a/Assets/Scripts/Fishing/New/View/Player/Tackle/FRod.cs +++ b/Assets/Scripts/Fishing/New/View/Player/Tackle/FRod.cs @@ -241,7 +241,7 @@ namespace NBF var solver = Instantiate(lineSolverPrefab, GearRoot); solver.transform.position = Asset.lineConnector.position; solver.transform.rotation = Asset.lineConnector.rotation; - var indexNames = new[] { "fishing line float set", "fishing line spinning" }; + var indexNames = new[] { "LineHand", "LineLure" }; var path = $"Assets/ResRaw/Prefabs/Line/{indexNames[currentLineTypeIndex]}.prefab"; var prefab = Assets.Load(path); diff --git a/UserSettings/EditorUserSettings.asset b/UserSettings/EditorUserSettings.asset index c70116ffe..ba55fa1bf 100644 --- a/UserSettings/EditorUserSettings.asset +++ b/UserSettings/EditorUserSettings.asset @@ -15,32 +15,32 @@ EditorUserSettings: value: 2550581500 flags: 0 RecentlyUsedSceneGuid-0: - value: 55540305570d0f0e0c5e5e2115710d44174e4e2b7b7e77662f2d1c61b5b06069 - flags: 0 - RecentlyUsedSceneGuid-1: - value: 5452500303515f0a5f5b5a7445775e46401519787c717f677d784860e3b1676c - flags: 0 - RecentlyUsedSceneGuid-2: value: 050402550007590a0f565f2714200c44144e492f2f70753175711f66e0b8303c flags: 0 - RecentlyUsedSceneGuid-3: + RecentlyUsedSceneGuid-1: value: 06070c5f5c075c5e5e085476427a0a44474e1c2f7f7a73362f2d4d36b5b1633d flags: 0 - RecentlyUsedSceneGuid-4: + RecentlyUsedSceneGuid-2: value: 0005505f515750595e5f5f23412507441216497f2d7f24367e711c64b6b86c61 flags: 0 - RecentlyUsedSceneGuid-5: + RecentlyUsedSceneGuid-3: value: 54070c5452075002590c0871127b5a4443161c2f797176312c2f1e6bb1b4353d flags: 0 - RecentlyUsedSceneGuid-6: + RecentlyUsedSceneGuid-4: value: 5309035757065a0a54575f7216265c4444151d28792e72627d2f1935bbb8673a flags: 0 - RecentlyUsedSceneGuid-7: + RecentlyUsedSceneGuid-5: value: 00050c5150005f5f54560f2640270d4410161c28282b72357e7c4835e4b63760 flags: 0 - RecentlyUsedSceneGuid-8: + RecentlyUsedSceneGuid-6: value: 06090c5f54015f5a0f085b7b11765d444e4e1e287429773178704561b3b23561 flags: 0 + RecentlyUsedSceneGuid-7: + value: 0257035f51050d090f0f5d734521094414164e797e7a20667d7a4536e0e36461 + flags: 0 + RecentlyUsedSceneGuid-8: + value: 07060c5454040c0a545b547240700a441216417e7f2e7268752c4966b4b0663d + flags: 0 RecentlyUsedSceneGuid-9: value: 5505015f5c515a085f5b092149760f441716407a787d7564287b1b36e7e1366e flags: 0