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 } }