using UnityEngine; /// /// 鱼线张力参考模型 /// /// 这个脚本不负责真正的鱼线渲染,也不负责完整鱼AI, /// 只负责根据“竿尖 - 鱼”的关系,计算一个较合理的张力模型。 /// /// 适合先拿来验证: /// 1. 当前张力是否合理 /// 2. 鱼竿弯曲是否合理 /// 3. 卸力出线是否自然 /// 4. 断线判定是否符合预期 /// public class FishingLineTensionModel : MonoBehaviour { [Header("References")] [Tooltip("鱼竿竿尖挂点")] public Transform rodTip; [Tooltip("鱼对象(位置来源)")] public Transform fishTarget; [Tooltip("鱼刚体,用来取速度并施加线的拉力")] public Rigidbody fishRb; [Tooltip("玩家/竿尖所在刚体,可选。若为空则竿尖速度按0处理")] public Rigidbody rodTipRb; [Header("Line State")] [Tooltip("当前放线长度(米)")] public float lineLength = 5f; [Tooltip("最短允许线长,防止收线收到过小")] public float minLineLength = 0.5f; [Tooltip("最长允许线长")] public float maxLineLength = 100f; [Tooltip("手动收线速度(米/秒),这里只是参考参数")] public float reelInSpeed = 1.5f; [Tooltip("手动放线速度(米/秒),这里只是参考参数")] public float reelOutSpeed = 3f; [Header("Line Tension")] [Tooltip("鱼线系统等效刚度。越大,拉直后越硬")] public float lineStiffness = 120f; [Tooltip("沿鱼线方向的阻尼。鱼外冲越快,附加张力越大")] public float lineDamping = 20f; [Tooltip("接近绷直时保留的一点最小张力,避免完全没手感")] public float minTensionWhenNearTight = 1f; [Tooltip("进入预张力区域的比例。比如0.95表示距离达到线长95%就开始有一点点手感")] [Range(0.7f, 1f)] public float nearTightRatio = 0.95f; [Header("Rod Buffer / Flex")] [Tooltip("鱼竿最大承载参考值。张力越接近这个值,竿越弯")] public float rodMaxLoad = 40f; [Tooltip("鱼竿最大缓冲长度(米)。竿越弯,等效可多“吃掉”一些长度")] public float rodFlexMax = 0.35f; [Header("Drag / Spool")] [Tooltip("绕线轮卸力阈值。张力超过它就开始自动出线")] public float dragThreshold = 18f; [Tooltip("超过卸力阈值后,每多1单位张力,对应的自动出线速度倍率")] public float dragSpoolFactor = 0.08f; [Header("Break / Damage")] [Tooltip("安全张力。超过它开始累计损伤")] public float safeTension = 22f; [Tooltip("绝对极限张力。超过它可以直接判定断线")] public float breakTension = 35f; [Tooltip("超安全张力时的损伤累计速度倍率")] public float lineDamageRate = 1.5f; [Tooltip("累计损伤达到此值后断线")] public float lineDamageLimit = 10f; [Header("Fish Force")] [Tooltip("是否把张力反向施加给鱼刚体")] public bool applyForceToFish = true; [Tooltip("对鱼施加拉力时的倍率,用来调手感")] public float forceToFishScale = 1f; [Header("Runtime Debug (Read Only)")] [SerializeField] private float currentDistance; [SerializeField] private float currentRelativeSpeedAlongLine; [SerializeField] private float currentRodFlexOffset; [SerializeField] private float currentOverStretch; [SerializeField] private float currentTension; [SerializeField] private float currentRodBend01; [SerializeField] private float currentLineDamage; [SerializeField] private bool isLineBroken; [SerializeField] private bool isAutoSpooling; /// 当前张力,对外只读 public float CurrentTension => currentTension; /// 当前鱼竿弯曲比例(0~1) public float CurrentRodBend01 => currentRodBend01; /// 当前是否断线 public bool IsLineBroken => isLineBroken; /// 当前累计损伤 public float CurrentLineDamage => currentLineDamage; private void Reset() { lineLength = 5f; minLineLength = 0.5f; maxLineLength = 100f; lineStiffness = 120f; lineDamping = 20f; minTensionWhenNearTight = 1f; nearTightRatio = 0.95f; rodMaxLoad = 40f; rodFlexMax = 0.35f; dragThreshold = 18f; dragSpoolFactor = 0.08f; safeTension = 22f; breakTension = 35f; lineDamageRate = 1.5f; lineDamageLimit = 10f; applyForceToFish = true; forceToFishScale = 1f; } private void Update() { // 这里只演示输入,实际项目你可能会改成输入系统控制 HandleManualLineInput(); } private void FixedUpdate() { if (isLineBroken) { currentTension = 0f; currentRodBend01 = 0f; currentRodFlexOffset = 0f; currentOverStretch = 0f; currentRelativeSpeedAlongLine = 0f; isAutoSpooling = false; return; } if (rodTip == null || fishTarget == null) return; // -------------------------------------------------- // 1. 计算竿尖到鱼的几何关系 // -------------------------------------------------- Vector3 delta = fishTarget.position - rodTip.position; float distance = delta.magnitude; Vector3 lineDir = distance > 0.0001f ? delta / distance : Vector3.forward; currentDistance = distance; // -------------------------------------------------- // 2. 计算沿鱼线方向的相对速度 // > 0 代表鱼在远离竿尖,拉力应增加 // < 0 代表鱼在靠近竿尖,拉力应减小 // -------------------------------------------------- Vector3 fishVel = fishRb != null ? fishRb.linearVelocity : Vector3.zero; Vector3 tipVel = rodTipRb != null ? rodTipRb.linearVelocity : Vector3.zero; float relativeSpeedAlongLine = Vector3.Dot(fishVel - tipVel, lineDir); currentRelativeSpeedAlongLine = relativeSpeedAlongLine; // -------------------------------------------------- // 3. 根据“上一帧张力”估算当前鱼竿弯曲带来的缓冲长度 // 张力越大,竿越弯,可额外缓冲一些长度 // -------------------------------------------------- currentRodBend01 = rodMaxLoad > 0.0001f ? Mathf.Clamp01(currentTension / rodMaxLoad) : 0f; currentRodFlexOffset = currentRodBend01 * rodFlexMax; // -------------------------------------------------- // 4. 计算“超限量” // 当距离 <= 线长 + 竿缓冲时,认为线没有真正被硬拉伸 // 当距离 > 线长 + 竿缓冲时,多出来的部分转化为张力 // -------------------------------------------------- float effectiveLength = lineLength + currentRodFlexOffset; float overStretch = Mathf.Max(0f, distance - effectiveLength); currentOverStretch = overStretch; // -------------------------------------------------- // 5. 计算基础张力 // T = 刚度项 + 阻尼项 // -------------------------------------------------- float tension = 0f; // 刚度项:超过可承受长度才会产生明显张力 tension += lineStiffness * overStretch; // 阻尼项:只有“往外冲”的速度才增加张力 if (relativeSpeedAlongLine > 0f) { tension += lineDamping * relativeSpeedAlongLine; } // -------------------------------------------------- // 6. 接近绷直时给一点最小预张力 // 避免 D 接近 lineLength 时,手感突然从0跳到有力 // -------------------------------------------------- float nearTightDistance = lineLength * nearTightRatio; if (distance >= nearTightDistance) { tension = Mathf.Max(tension, minTensionWhenNearTight); } // 线完全松很多时,可直接视为无有效张力 if (distance < nearTightDistance && overStretch <= 0f) { tension = 0f; } currentTension = Mathf.Max(0f, tension); // -------------------------------------------------- // 7. 卸力:当张力超过绕线轮设定值时,自动出线 // 这样大鱼冲刺时不会硬顶到瞬间爆线 // -------------------------------------------------- isAutoSpooling = false; if (currentTension > dragThreshold) { float extraTension = currentTension - dragThreshold; float autoSpoolSpeed = extraTension * dragSpoolFactor; lineLength += autoSpoolSpeed * Time.fixedDeltaTime; lineLength = Mathf.Clamp(lineLength, minLineLength, maxLineLength); isAutoSpooling = true; } // -------------------------------------------------- // 8. 断线逻辑 // 8.1 超过极限值:可直接断 // 8.2 超过安全值:持续累计损伤 // -------------------------------------------------- if (currentTension >= breakTension) { BreakLine(); return; } if (currentTension > safeTension) { float overload = currentTension - safeTension; currentLineDamage += overload * lineDamageRate * Time.fixedDeltaTime; if (currentLineDamage >= lineDamageLimit) { BreakLine(); return; } } else { // 张力安全时,损伤缓慢恢复一点 currentLineDamage -= Time.fixedDeltaTime; currentLineDamage = Mathf.Max(0f, currentLineDamage); } // -------------------------------------------------- // 9. 把鱼线张力反向施加给鱼 // 鱼越往外冲,线张力越大,反向拉回鱼的力也越大 // -------------------------------------------------- if (applyForceToFish && fishRb != null && currentTension > 0f) { Vector3 pullForce = -lineDir * currentTension * forceToFishScale; fishRb.AddForce(pullForce, ForceMode.Force); } // -------------------------------------------------- // 10. 重新根据当前张力更新鱼竿弯曲值(给外部表现层用) // -------------------------------------------------- currentRodBend01 = rodMaxLoad > 0.0001f ? Mathf.Clamp01(currentTension / rodMaxLoad) : 0f; } /// /// 示例输入: /// R = 收线 /// F = 放线 /// 实际项目建议接你自己的输入系统 /// private void HandleManualLineInput() { if (isLineBroken) return; if (Input.GetKey(KeyCode.R)) { lineLength -= reelInSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.F)) { lineLength += reelOutSpeed * Time.deltaTime; } lineLength = Mathf.Clamp(lineLength, minLineLength, maxLineLength); } /// /// 断线 /// private void BreakLine() { isLineBroken = true; currentTension = 0f; currentRodBend01 = 0f; currentRodFlexOffset = 0f; currentOverStretch = 0f; isAutoSpooling = false; Debug.Log("鱼线断了"); } /// /// 外部调用:修复鱼线 /// public void RepairLine(float repairedLength) { isLineBroken = false; currentLineDamage = 0f; lineLength = Mathf.Clamp(repairedLength, minLineLength, maxLineLength); } /// /// 外部调用:直接设置线长 /// public void SetLineLength(float length) { lineLength = Mathf.Clamp(length, minLineLength, maxLineLength); } }