586 lines
20 KiB
C#
586 lines
20 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using NBC;
|
||
// using Obi;
|
||
using UnityEngine;
|
||
|
||
namespace NBF
|
||
{
|
||
public enum LineType
|
||
{
|
||
Hand,
|
||
HandDouble,
|
||
Spinning,
|
||
SpinningFloat,
|
||
}
|
||
|
||
/// <summary>
|
||
/// 线模式
|
||
/// </summary>
|
||
public enum LineMode
|
||
{
|
||
Joint,
|
||
Constraint
|
||
}
|
||
|
||
public class FLine : FGearBase
|
||
{
|
||
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;
|
||
[SerializeField] private bool showDebugInfo = true;
|
||
[Header("动态间距设置")] [SerializeField] private float defaultTransitionSpeed = 2f; // 默认长度变化速度(单位/秒)
|
||
|
||
private LineMode _lineMode = LineMode.Joint;
|
||
// [SerializeField] private bool isLureConnect;
|
||
//
|
||
// [SerializeField] private RodLine rodLine;
|
||
//
|
||
// /// <summary>
|
||
// /// 主线
|
||
// /// </summary>
|
||
// [SerializeField] private Rope fishingRope;
|
||
//
|
||
// /// <summary>
|
||
// /// 浮漂和鱼钩线
|
||
// /// </summary>
|
||
// [SerializeField] private Rope bobberRope;
|
||
|
||
|
||
// public LureController Lure;
|
||
// public BobberController Bobber;
|
||
//
|
||
// public JointPinchController PinchController;
|
||
|
||
private readonly List<ConnectionConstraint> _constraints = new List<ConnectionConstraint>();
|
||
|
||
public FLineLogicNode StartNode { get; private set; }
|
||
public FLineLogicNode BobberNode => GetNode(FLineLogicNodeType.Bobber);
|
||
public FLineLogicNode EndNode { get; private set; }
|
||
public LineMode LineMode => _lineMode;
|
||
|
||
|
||
public float LinelenghtDiferent;
|
||
|
||
|
||
[System.Serializable]
|
||
public class ConnectionConstraint
|
||
{
|
||
public FLineLogicNodeType NodeType;
|
||
public Rigidbody bodyA;
|
||
public Rigidbody bodyB;
|
||
public float maxDistance;
|
||
public float minDistance;
|
||
public float currentDistance;
|
||
public Vector3 direction;
|
||
|
||
// 动态目标距离(用于平滑过渡)
|
||
public float targetMaxDistance;
|
||
public bool hasPendingTransition;
|
||
public bool hasPendingMaxTransition;
|
||
public float maxTransitionSpeed;
|
||
|
||
public ConnectionConstraint(Rigidbody a, Rigidbody b, float maxDist, float minDist = 0f)
|
||
{
|
||
bodyA = a;
|
||
bodyB = b;
|
||
maxDistance = maxDist;
|
||
minDistance = minDist;
|
||
targetMaxDistance = maxDist;
|
||
hasPendingTransition = false;
|
||
hasPendingMaxTransition = false;
|
||
maxTransitionSpeed = 0f;
|
||
}
|
||
|
||
public void UpdateCurrentState()
|
||
{
|
||
if (bodyA && bodyB)
|
||
{
|
||
Vector3 delta = bodyB.position - bodyA.position;
|
||
currentDistance = delta.magnitude;
|
||
direction = currentDistance > 0.0001f ? delta.normalized : Vector3.right;
|
||
}
|
||
}
|
||
}
|
||
|
||
protected override void OnInit()
|
||
{
|
||
if (anchorTransform == null)
|
||
{
|
||
var tipRb = Rod.Asset.LineConnectorRigidbody;
|
||
|
||
anchorTransform = tipRb.transform;
|
||
}
|
||
}
|
||
|
||
|
||
private void Start()
|
||
{
|
||
BuildConstraints();
|
||
StartNode = GetNode(FLineLogicNodeType.Start);
|
||
EndNode = GetNode(FLineLogicNodeType.End);
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
LinelenghtDiferent = GetLineDistance();
|
||
|
||
//非钓鱼状态
|
||
if (Rod) Rod.PlayerItem.Tension = Mathf.Clamp(LinelenghtDiferent, 0f, 0.05f);
|
||
}
|
||
|
||
private void FixedUpdate()
|
||
{
|
||
UpdateAnchorNode();
|
||
FixedUpdateConstraints();
|
||
}
|
||
|
||
public void ChangeMode(LineMode mode)
|
||
{
|
||
_lineMode = mode;
|
||
|
||
foreach (var fLineLogicNode in lineNodes)
|
||
{
|
||
fLineLogicNode.ChangeMode(mode);
|
||
}
|
||
}
|
||
|
||
public List<FLineLogicNode> GetLineNodes()
|
||
{
|
||
return new List<FLineLogicNode>(lineNodes);
|
||
}
|
||
|
||
public void Print()
|
||
{
|
||
// Log.Info($"当前线情况 TotalLength={TotalLength} CurrentStretchLength={CurrentStretchLength}");
|
||
}
|
||
|
||
#region 连接点
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <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 void SetLenght(float lenght, FLineLogicNodeType type = FLineLogicNodeType.Bobber)
|
||
{
|
||
var node = GetNode(type);
|
||
if (node != null)
|
||
{
|
||
if (_lineMode == LineMode.Joint)
|
||
{
|
||
node.SetLenght(lenght);
|
||
}
|
||
else
|
||
{
|
||
SetSegmentMaxLength(lenght, type);
|
||
}
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 脚本约束
|
||
|
||
private void FixedUpdateConstraints()
|
||
{
|
||
if (_lineMode != LineMode.Constraint) return;
|
||
if (!enabled || lineNodes.Count < 2) return;
|
||
|
||
// 更新动态过渡
|
||
UpdateTransitions();
|
||
|
||
for (int iteration = 0; iteration < constraintIterations; iteration++)
|
||
{
|
||
ApplyDistanceConstraints();
|
||
}
|
||
|
||
ApplyDamping();
|
||
}
|
||
|
||
private void BuildConstraints()
|
||
{
|
||
_constraints.Clear();
|
||
if (lineNodes.Count < 2) return;
|
||
|
||
// 创建约束
|
||
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,
|
||
nextNode.Lenght
|
||
);
|
||
constraint.NodeType = nextNode.NodeType;
|
||
_constraints.Add(constraint);
|
||
}
|
||
}
|
||
}
|
||
|
||
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 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按速度过渡某段的最大距离
|
||
/// </summary>
|
||
private void SetSegmentMaxLength(float targetLength, FLineLogicNodeType type = FLineLogicNodeType.Bobber,
|
||
float transitionSpeed = 2f)
|
||
{
|
||
var constraint = _constraints.Find(t => t.NodeType == type);
|
||
if (constraint == null) return;
|
||
|
||
targetLength = Mathf.Max(0.01f, targetLength);
|
||
float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed);
|
||
|
||
constraint.targetMaxDistance = targetLength;
|
||
constraint.maxTransitionSpeed = speed;
|
||
constraint.hasPendingMaxTransition = Mathf.Abs(constraint.maxDistance - targetLength) >= 0.0001f;
|
||
constraint.hasPendingTransition = constraint.hasPendingMaxTransition;
|
||
}
|
||
|
||
|
||
/// <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;
|
||
var node = GetNode(constraint.NodeType);
|
||
// SyncSegmentMaxLength(i, nextMaxDistance);
|
||
node.SetLenght(nextMaxDistance);
|
||
|
||
if (Mathf.Abs(nextMaxDistance - constraint.targetMaxDistance) < 0.0001f)
|
||
{
|
||
constraint.maxDistance = constraint.targetMaxDistance;
|
||
constraint.hasPendingMaxTransition = false;
|
||
// SyncSegmentMaxLength(i, constraint.targetMaxDistance);
|
||
node.SetLenght(constraint.targetMaxDistance);
|
||
}
|
||
}
|
||
|
||
constraint.hasPendingTransition = constraint.hasPendingMaxTransition;
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 获取某个段是否正在进行过渡
|
||
/// </summary>
|
||
public bool IsSegmentTransitioning(int segmentIndex)
|
||
{
|
||
if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
|
||
{
|
||
return _constraints[segmentIndex].hasPendingTransition;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
#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)
|
||
// {
|
||
// TotalLength += node.Lenght;
|
||
// }
|
||
//
|
||
// var realLen = Vector3.Distance(startNode.transform.position, endNode.transform.position);
|
||
// CurrentStretchLength = realLen - TotalLength;
|
||
// if (CurrentStretchLength < 0f)
|
||
// {
|
||
// CurrentStretchLength = 0f;
|
||
// }
|
||
//
|
||
// SetLimitState(CurrentStretchLength > lengthLimitTolerance);
|
||
// if (LineMode != LineMode.Constraint) return;
|
||
//
|
||
// 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 Tension
|
||
|
||
private float GetLineDistance()
|
||
{
|
||
//第一个节点到竿稍的位置-第一段鱼线长度
|
||
return Vector3.Distance(StartNode.transform.position, BobberNode.transform.position) -
|
||
BobberNode.Lenght;
|
||
}
|
||
|
||
public float GetTension(float weight)
|
||
{
|
||
return weight * GetLineDistance();
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
} |