# Conflicts: # Assets/Scenes/RopeTest.unity # Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs # Assets/Scripts/Fishing/Rope/Rope.cs # Assets/Scripts/Fishing/Rope/Rope.cs.meta
1044 lines
36 KiB
C#
1044 lines
36 KiB
C#
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为 FLine 节点提供附加软约束。求解器只依赖这个通用接口,不依赖具体玩法组件。
|
||
/// </summary>
|
||
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<FLineLogicNode> lineNodes = new List<FLineLogicNode>();
|
||
|
||
[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<ConnectionConstraint> _constraints = new List<ConnectionConstraint>();
|
||
|
||
private readonly List<IFLineNodeConstraintProvider[]> _nodeConstraintProviders =
|
||
new List<IFLineNodeConstraintProvider[]>();
|
||
|
||
[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<IFLineNodeConstraintProvider>());
|
||
}
|
||
}
|
||
|
||
// 创建约束
|
||
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<IFLineNodeConstraintProvider>();
|
||
}
|
||
|
||
return _nodeConstraintProviders[nodeIndex];
|
||
}
|
||
|
||
private static IFLineNodeConstraintProvider[] CollectNodeConstraintProviders(FLineLogicNode node)
|
||
{
|
||
MonoBehaviour[] behaviours = node.GetComponents<MonoBehaviour>();
|
||
List<IFLineNodeConstraintProvider> providers = new List<IFLineNodeConstraintProvider>();
|
||
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新所有活跃的过渡
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按速度过渡某段的最大距离
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按速度过渡某段的最小距离
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 同时按速度过渡某段的最大和最小距离
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 扩展整个绳索(所有段均匀缩放)
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 收缩/拉紧绳索(所有段向目标长度过渡)
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对特定段施加拉力(减小最大距离)
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 放松特定段(增加最大距离)
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 移除指定段的过渡
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取消所有正在进行的过渡
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取消指定段的过渡
|
||
/// </summary>
|
||
public void CancelTransition(int segmentIndex)
|
||
{
|
||
RemoveExistingTransition(segmentIndex, true, true);
|
||
if (segmentIndex < _constraints.Count)
|
||
{
|
||
_constraints[segmentIndex].hasPendingTransition = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取某个段是否正在进行过渡
|
||
/// </summary>
|
||
public bool IsSegmentTransitioning(int segmentIndex)
|
||
{
|
||
if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
|
||
{
|
||
return _constraints[segmentIndex].hasPendingTransition;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取某段当前的目标最大距离
|
||
/// </summary>
|
||
public float GetTargetMaxLength(int segmentIndex)
|
||
{
|
||
if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
|
||
{
|
||
return _constraints[segmentIndex].targetMaxDistance;
|
||
}
|
||
|
||
return -1f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取某段当前的目标最小距离
|
||
/// </summary>
|
||
public float GetTargetMinLength(int segmentIndex)
|
||
{
|
||
if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
|
||
{
|
||
return _constraints[segmentIndex].targetMinDistance;
|
||
}
|
||
|
||
return -1f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置默认长度变化速度
|
||
/// </summary>
|
||
public void SetDefaultTransitionSpeed(float speed)
|
||
{
|
||
defaultTransitionSpeed = Mathf.Max(0.01f, speed);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 极限判定
|
||
|
||
/// <summary>
|
||
/// 当前逻辑链总长度超出配置总长度的部分,小于等于零时记为 0。
|
||
/// </summary>
|
||
[Header("Limit Detection")]
|
||
public float CurrentStretchLength { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 总长度
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>
|
||
/// 当鱼线达到断线条件时发出的一次性消息。
|
||
/// 外部可订阅该事件,在回调中执行切线、播放表现或状态切换。
|
||
/// </summary>
|
||
public event Action<FLine> 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>
|
||
/// 当前拉力极限百分比。
|
||
/// 当超长值小于等于 breakStretchPercentMinThreshold 时为 0;
|
||
/// 当超长值大于等于 breakStretchThreshold 时为 100;
|
||
/// 中间区间按线性比例映射,供 UI 显示使用。
|
||
/// </summary>
|
||
public float CurrentBreakStretchPercent => EvaluateBreakStretchPercent(CurrentStretchLength);
|
||
|
||
/// <summary>
|
||
/// 当前是否正在进行断线候选计时。
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发出鱼线达到断线条件的消息。
|
||
/// 这里预留给外部订阅,当前不在求解器内部直接执行断线逻辑。
|
||
/// </summary>
|
||
private void NotifyLineBreakRequested()
|
||
{
|
||
OnLineBreakRequested?.Invoke(this);
|
||
}
|
||
|
||
private void ResetLimitState()
|
||
{
|
||
CurrentStretchLength = 0f;
|
||
IsAtLimit = false;
|
||
LimitStateTime = 0f;
|
||
HasBreakNotificationSent = false;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 公共接口
|
||
|
||
/// <summary>
|
||
/// 获取一个节点
|
||
/// </summary>
|
||
/// <param name="type"></param>
|
||
/// <returns></returns>
|
||
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<FLineLogicNode>();
|
||
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<Rigidbody> GetConnectedBodies()
|
||
{
|
||
List<Rigidbody> bodies = new List<Rigidbody>(lineNodes.Count);
|
||
for (int i = 0; i < lineNodes.Count; i++)
|
||
{
|
||
bodies.Add(GetBodyAt(i));
|
||
}
|
||
|
||
return bodies;
|
||
}
|
||
|
||
public List<FLineLogicNode> GetLineNodes()
|
||
{
|
||
return new List<FLineLogicNode>(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
|
||
}
|
||
} |