diff --git a/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab b/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab index df96b2294..015598035 100644 --- a/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab +++ b/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab @@ -493,7 +493,7 @@ GameObject: - component: {fileID: 54298866000586118} - component: {fileID: 153691655494134957} - component: {fileID: 2717383850592950062} - - component: {fileID: 4574796762267659512} + - component: {fileID: 6678694395924494533} m_Layer: 16 m_Name: Float m_TagString: Untagged @@ -659,7 +659,7 @@ MonoBehaviour: m_EditorClassIdentifier: Assembly-CSharp::NBF.BobberController _rbody: {fileID: 54298866000586118} joint: {fileID: 153691655494134957} ---- !u!114 &4574796762267659512 +--- !u!114 &6678694395924494533 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -687,14 +687,24 @@ MonoBehaviour: lockXZAroundAnchor: 1 xzSmoothTime: 0.15 maxPlanarOffset: 0.15 - keepUpright: 1 - rotationLerpSpeed: 8 - maxTiltAngle: 18 - planarTiltFactor: 120 downForceToSink: 0.0025 maxExtraSink: 0.08 enableBottomTouchAdjust: 1 bottomTouchLift: 0.01 + lureBody: {fileID: 54679398375713381} + referenceLength: 0.3 + minSubmergeToStand: 0.16 + verticalLieThreshold: 0.18 + verticalUprightThreshold: 0.75 + planarTiltThreshold: 0.3 + planarDominanceMultiplier: 1.2 + postureHysteresis: 0.04 + tiltedAngle: 38 + lyingAngle: 88 + uprightMaxTiltAngle: 8 + planarTiltFactor: 120 + rotationLerpSpeed: 8 + debugResetKey: 1 debugTapKey: 1 debugSlowSinkKey: 1 debugLiftKey: 1 diff --git a/Assets/Scripts/Test/New/BobberPresentationController.cs b/Assets/Scripts/Test/New/BobberPresentationController.cs index 1a8be5494..66654ac52 100644 --- a/Assets/Scripts/Test/New/BobberPresentationController.cs +++ b/Assets/Scripts/Test/New/BobberPresentationController.cs @@ -26,90 +26,157 @@ public enum BobberControlMode public enum BobberBiteType { None, - Tap, // 轻点 - SlowSink, // 缓沉 - Lift, // 送漂 - BlackDrift // 黑漂/快速拖入 + Tap, // 轻点 + SlowSink, // 缓沉 + Lift, // 送漂 + BlackDrift // 黑漂/快速拖入 +} + +public enum BobberPosture +{ + Lying, + Tilted, + Upright } [DisallowMultipleComponent] [RequireComponent(typeof(Rigidbody))] public class BobberPresentationController : MonoBehaviour { - [Header("Water")] [Tooltip("没有水提供器时使用固定水位")] + [Header("Water")] + [Tooltip("没有水提供器时使用固定水位")] public float fallbackWaterLevel = 0f; [Tooltip("可选:挂实现了 IWaterSurfaceProvider 的组件")] public MonoBehaviour waterProviderBehaviour; - [Header("Enter Water")] [Tooltip("底部进入水面多少米后切换为漂像控制")] + [Header("Enter Water")] + [Tooltip("底部进入水面多少米后切换为漂像控制")] public float enterWaterDepth = 0.002f; - [Tooltip("离开水面多少米后回到空中物理。一般给负值做滞回")] public float exitWaterDepth = -0.01f; + [Tooltip("离开水面多少米后回到空中物理。一般给负值做滞回")] + public float exitWaterDepth = -0.01f; - [Header("Geometry")] [Tooltip("浮漂总高度(米)")] + [Header("Geometry")] + [Tooltip("浮漂总高度(米)")] public float floatHeight = 0.08f; [Tooltip("如果 Pivot 在浮漂底部,这里填 0;如果 Pivot 在模型中心,就填底部相对 Pivot 的本地 Y")] public float bottomOffsetLocalY = 0f; - [Header("Base Float")] [Tooltip("基础吃铅比例,决定静止时有多少在水下")] [Range(0.05f, 0.95f)] + [Header("Base Float")] + [Tooltip("基础吃铅比例,决定静止时有多少在水下")] + [Range(0.05f, 0.95f)] public float baseSubmergeRatio = 0.28f; - [Tooltip("Y 轴平滑时间,越小响应越快")] public float ySmoothTime = 0.08f; + [Tooltip("Y 轴平滑时间,越小响应越快")] + public float ySmoothTime = 0.08f; - [Tooltip("最大竖直速度限制(用于 SmoothDamp)")] public float maxYSpeed = 2f; + [Tooltip("最大竖直速度限制(用于 SmoothDamp)")] + public float maxYSpeed = 2f; - [Tooltip("静止小死区,减少微抖")] public float yDeadZone = 0.0005f; + [Tooltip("静止小死区,减少微抖")] + public float yDeadZone = 0.0005f; - [Header("Surface Motion")] [Tooltip("是否启用轻微水面起伏")] + [Header("Surface Motion")] + [Tooltip("是否启用轻微水面起伏")] public bool enableSurfaceBobbing = true; - [Tooltip("水面轻微起伏振幅(米)")] public float surfaceBobAmplitude = 0.0015f; + [Tooltip("水面轻微起伏振幅(米)")] + public float surfaceBobAmplitude = 0.0015f; - [Tooltip("水面轻微起伏频率")] public float surfaceBobFrequency = 1.2f; + [Tooltip("水面轻微起伏频率")] + public float surfaceBobFrequency = 1.2f; - [Header("XZ Motion")] [Tooltip("入水后是否锁定 XZ 到入水点附近")] + [Header("XZ Motion")] + [Tooltip("入水后是否锁定 XZ 到入水点附近")] public bool lockXZAroundAnchor = true; - [Tooltip("XZ 跟随平滑时间")] public float xzSmoothTime = 0.15f; + [Tooltip("XZ 跟随平滑时间")] + public float xzSmoothTime = 0.15f; - [Tooltip("水流/拖拽带来的额外平面偏移最大值")] public float maxPlanarOffset = 0.15f; + [Tooltip("水流/拖拽带来的额外平面偏移最大值")] + public float maxPlanarOffset = 0.15f; - [Header("Upright")] [Tooltip("是否始终保持漂大致竖直")] - public bool keepUpright = true; - - [Tooltip("姿态平滑速度")] public float rotationLerpSpeed = 8f; - - [Tooltip("允许的最大倾斜角度")] public float maxTiltAngle = 18f; - - [Tooltip("平面拖拽对倾斜的影响强度")] public float planarTiltFactor = 120f; - - [Header("Sink By Weight / Tension")] [Tooltip("外部向下拉力映射为下沉量的系数。你可以把钩/铅/线组的等效向下拉力喂进来")] + [Header("Sink By Weight / Tension")] + [Tooltip("外部向下拉力映射为下沉量的系数。你可以把钩/铅/线组的等效向下拉力喂进来")] public float downForceToSink = 0.0025f; - [Tooltip("向下拉力下沉的最大附加量")] public float maxExtraSink = 0.08f; + [Tooltip("向下拉力下沉的最大附加量")] + public float maxExtraSink = 0.08f; - [Header("Bottom Touch")] [Tooltip("触底时是否启用修正")] + [Header("Bottom Touch")] + [Tooltip("触底时是否启用修正")] public bool enableBottomTouchAdjust = true; - [Tooltip("触底后减少的下沉量(例如铅坠到底,漂会回升一点)")] public float bottomTouchLift = 0.01f; + [Tooltip("触底后减少的下沉量(例如铅坠到底,漂会回升一点)")] + public float bottomTouchLift = 0.01f; - [Header("Debug Input")] [Tooltip("调试:按 R 恢复默认")] - + [Header("Posture Source")] + [Tooltip("下方 Lure / 钩组 / 铅坠的刚体。姿态主要根据它和浮漂的相对位置判断")] + public Rigidbody lureBody; + + [Tooltip("用于归一化的参考长度。一般填:浮漂到 Lure 在“正常拉直”时的大致长度")] + public float referenceLength = 0.30f; + + [Header("Posture Threshold")] + [Tooltip("最小入水比例。不够时优先躺漂")] + public float minSubmergeToStand = 0.16f; + + [Tooltip("垂直分量比低于该值时,优先躺漂")] + public float verticalLieThreshold = 0.18f; + + [Tooltip("垂直分量比高于该值,且水平分量较小时,允许立漂")] + public float verticalUprightThreshold = 0.75f; + + [Tooltip("水平分量比高于该值时,不允许完全立漂")] + public float planarTiltThreshold = 0.30f; + + [Tooltip("水平分量明显大于垂直分量时,优先躺漂")] + public float planarDominanceMultiplier = 1.20f; + + [Tooltip("姿态切换滞回")] + public float postureHysteresis = 0.04f; + + [Header("Posture Rotation")] + [Tooltip("倾斜状态角度")] + public float tiltedAngle = 38f; + + [Tooltip("躺漂角度")] + public float lyingAngle = 88f; + + [Tooltip("立漂时允许的最大附加倾角")] + public float uprightMaxTiltAngle = 8f; + + [Tooltip("平面方向对立漂/斜漂附加倾角的影响强度")] + public float planarTiltFactor = 120f; + + [Tooltip("姿态平滑速度")] + public float rotationLerpSpeed = 8f; + + [Header("Debug Input")] + [Tooltip("调试:按 R 恢复默认")] public bool debugResetKey = true; - [Header("Debug Input")] [Tooltip("调试:按 T 触发轻点")] + + [Tooltip("调试:按 T 触发轻点")] public bool debugTapKey = true; - [Tooltip("调试:按 G 触发缓沉")] public bool debugSlowSinkKey = true; + [Tooltip("调试:按 G 触发缓沉")] + public bool debugSlowSinkKey = true; - [Tooltip("调试:按 H 触发送漂")] public bool debugLiftKey = true; + [Tooltip("调试:按 H 触发送漂")] + public bool debugLiftKey = true; - [Tooltip("调试:按 B 触发黑漂")] public bool debugBlackDriftKey = true; + [Tooltip("调试:按 B 触发黑漂")] + public bool debugBlackDriftKey = true; - [Header("Debug")] public bool drawDebug = false; + [Header("Debug")] + public bool drawDebug = false; public BobberControlMode CurrentMode => _mode; + public BobberPosture CurrentPosture => _posture; + public float CurrentVerticalRatio => _verticalRatio; + public float CurrentPlanarRatio => _planarRatio; /// 外部可写:等效向下拉力(不是必须是真实力,作为输入信号即可) public float ExternalDownForce { get; set; } @@ -123,6 +190,7 @@ public class BobberPresentationController : MonoBehaviour private Rigidbody _rb; private IWaterSurfaceProvider _waterProvider; private BobberControlMode _mode = BobberControlMode.AirPhysics; + private BobberPosture _posture = BobberPosture.Lying; private float _defaultLinearDamping; private float _defaultAngularDamping; @@ -144,6 +212,12 @@ public class BobberPresentationController : MonoBehaviour private float _biteAmplitude; private Vector3 _blackDriftDirection; + // posture runtime + private float _verticalRatio; + private float _planarRatio; + private float _verticalDistance; + private float _planarDistance; + private void Awake() { _rb = GetComponent(); @@ -256,8 +330,10 @@ public class BobberPresentationController : MonoBehaviour transform.position = pos; - // 3. 算目标旋转 - UpdateTargetRotation(); + // 3. 姿态判定 + 目标旋转 + EvaluatePostureByComponents(waterY); + UpdateTargetRotationByPosture(); + transform.rotation = Quaternion.Slerp( transform.rotation, _targetRotation, @@ -281,6 +357,12 @@ public class BobberPresentationController : MonoBehaviour _activeBiteType = BobberBiteType.None; _biteTimer = 0f; + _posture = BobberPosture.Lying; + _verticalRatio = 0f; + _planarRatio = 0f; + _verticalDistance = 0f; + _planarDistance = 0f; + _rb.useGravity = false; _rb.linearVelocity = Vector3.zero; _rb.angularVelocity = Vector3.zero; @@ -327,9 +409,6 @@ public class BobberPresentationController : MonoBehaviour surfaceBob = Mathf.Sin(Time.time * surfaceBobFrequency * Mathf.PI * 2f) * surfaceBobAmplitude; } - // Pivot 对应的目标 Y: - // target bottom = waterY - sinkDepth - // pivotY = target bottom - bottomOffsetLocalY + 动画偏移 float totalSink = baseSinkDepth + sinkByForce + bottomAdjust; float targetBottomY = waterY - totalSink; float targetPivotY = targetBottomY - bottomOffsetLocalY + surfaceBob + _biteOffsetY; @@ -343,7 +422,6 @@ public class BobberPresentationController : MonoBehaviour Vector3 basePos = lockXZAroundAnchor ? _waterAnchorPos : transform.position; - // 黑漂时额外平面位移 if (_activeBiteType == BobberBiteType.BlackDrift) { float t = Mathf.Clamp01(_biteDuration > 0f ? _biteTimer / _biteDuration : 1f); @@ -359,28 +437,170 @@ public class BobberPresentationController : MonoBehaviour ); } - private void UpdateTargetRotation() + private void EvaluatePostureByComponents(float waterY) { - if (!keepUpright) + float submergeRatio = Mathf.Clamp01( + (waterY - GetBottomWorldPosition().y) / Mathf.Max(0.0001f, floatHeight) + ); + + if (lureBody == null) { - _targetRotation = transform.rotation; + _verticalDistance = 0f; + _planarDistance = 0f; + _verticalRatio = 0f; + _planarRatio = 0f; + + if (submergeRatio < minSubmergeToStand) + _posture = BobberPosture.Lying; + else if (ExternalPlanarOffset.magnitude > 0.01f) + _posture = BobberPosture.Tilted; + else + _posture = BobberPosture.Upright; + return; } - Vector3 up = Vector3.up; - Vector3 planar = new Vector3(_xzSmoothVelocity.x, 0f, _xzSmoothVelocity.z); + Vector3 bobberPos = _rb.worldCenterOfMass; + Vector3 lurePos = lureBody.worldCenterOfMass; + Vector3 delta = lurePos - bobberPos; - float speed = planar.magnitude; - Vector3 tiltAxis = speed > 1e-5f ? Vector3.Cross(up, planar.normalized) : Vector3.zero; + _verticalDistance = Mathf.Max(0f, Vector3.Dot(delta, Vector3.down)); + _planarDistance = Vector3.ProjectOnPlane(delta, Vector3.up).magnitude; - float tiltAngle = Mathf.Clamp(speed * planarTiltFactor, 0f, maxTiltAngle); + float refLen = Mathf.Max(0.0001f, referenceLength); + _verticalRatio = _verticalDistance / refLen; + _planarRatio = _planarDistance / refLen; - Quaternion upright = Quaternion.FromToRotation(transform.up, Vector3.up) * transform.rotation; - Quaternion tilt = tiltAxis.sqrMagnitude > 1e-6f - ? Quaternion.AngleAxis(tiltAngle, tiltAxis.normalized) - : Quaternion.identity; + switch (_posture) + { + case BobberPosture.Lying: + { + bool canStandUpright = + submergeRatio >= minSubmergeToStand && + _verticalRatio > verticalUprightThreshold + postureHysteresis && + _planarRatio < planarTiltThreshold - postureHysteresis; - _targetRotation = tilt * upright; + bool canTilt = + submergeRatio >= minSubmergeToStand * 0.8f && + _verticalRatio > verticalLieThreshold + postureHysteresis; + + if (canStandUpright) + { + _posture = BobberPosture.Upright; + } + else if (canTilt) + { + _posture = BobberPosture.Tilted; + } + + break; + } + + case BobberPosture.Tilted: + { + bool shouldLie = + submergeRatio < minSubmergeToStand * 0.75f || + _verticalRatio < verticalLieThreshold - postureHysteresis || + _planarDistance > _verticalDistance * planarDominanceMultiplier; + + bool shouldStand = + submergeRatio >= minSubmergeToStand && + _verticalRatio > verticalUprightThreshold + postureHysteresis && + _planarRatio < planarTiltThreshold - postureHysteresis; + + if (shouldLie) + { + _posture = BobberPosture.Lying; + } + else if (shouldStand) + { + _posture = BobberPosture.Upright; + } + + break; + } + + case BobberPosture.Upright: + { + bool shouldLie = + submergeRatio < minSubmergeToStand * 0.75f || + _verticalRatio < verticalLieThreshold - postureHysteresis || + _planarDistance > _verticalDistance * (planarDominanceMultiplier + 0.15f); + + bool shouldTilt = + _verticalRatio < verticalUprightThreshold - postureHysteresis || + _planarRatio > planarTiltThreshold + postureHysteresis; + + if (shouldLie) + { + _posture = BobberPosture.Lying; + } + else if (shouldTilt) + { + _posture = BobberPosture.Tilted; + } + + break; + } + } + } + + private void UpdateTargetRotationByPosture() + { + Vector3 planarDir = Vector3.zero; + + if (lureBody != null) + { + Vector3 delta = lureBody.worldCenterOfMass - _rb.worldCenterOfMass; + planarDir = Vector3.ProjectOnPlane(delta, Vector3.up); + } + + if (planarDir.sqrMagnitude < 1e-6f) + { + planarDir = new Vector3(_xzSmoothVelocity.x, 0f, _xzSmoothVelocity.z); + } + + if (planarDir.sqrMagnitude < 1e-6f) + { + planarDir = new Vector3(ExternalPlanarOffset.x, 0f, ExternalPlanarOffset.y); + } + + if (planarDir.sqrMagnitude < 1e-6f) + { + planarDir = transform.forward; + } + + planarDir.Normalize(); + + Vector3 tiltAxis = Vector3.Cross(Vector3.up, planarDir); + if (tiltAxis.sqrMagnitude < 1e-6f) + { + tiltAxis = transform.right; + } + + float angle; + switch (_posture) + { + case BobberPosture.Lying: + angle = lyingAngle; + break; + + case BobberPosture.Tilted: + { + float extra = Mathf.Clamp(_planarRatio * planarTiltFactor, 0f, 18f); + angle = Mathf.Clamp(tiltedAngle + extra, tiltedAngle, lyingAngle); + break; + } + + default: + { + float extra = Mathf.Clamp(_planarRatio * planarTiltFactor, 0f, uprightMaxTiltAngle); + angle = extra; + break; + } + } + + _targetRotation = Quaternion.AngleAxis(angle, tiltAxis.normalized); } #endregion @@ -406,7 +626,7 @@ public class BobberPresentationController : MonoBehaviour /// /// 送漂:向上抬 /// - public void PlayLift(float amplitude = 0.015f, float duration = 0.6f) + public void PlayLift(float amplitude = 0.015f, float duration = 1.2f) { StartBite(BobberBiteType.Lift, amplitude, duration); } @@ -452,8 +672,14 @@ public class BobberPresentationController : MonoBehaviour { if (_activeBiteType == BobberBiteType.None) { - _biteOffsetY = Mathf.SmoothDamp(_biteOffsetY, 0f, ref _biteOffsetYVelocity, 0.08f, Mathf.Infinity, - Time.fixedDeltaTime); + _biteOffsetY = Mathf.SmoothDamp( + _biteOffsetY, + 0f, + ref _biteOffsetYVelocity, + 0.08f, + Mathf.Infinity, + Time.fixedDeltaTime + ); return; } @@ -465,7 +691,6 @@ public class BobberPresentationController : MonoBehaviour switch (_activeBiteType) { case BobberBiteType.Tap: - // 先快速下压,再回弹 if (t < 0.35f) { float k = t / 0.35f; @@ -476,7 +701,6 @@ public class BobberPresentationController : MonoBehaviour float k = (t - 0.35f) / 0.65f; targetOffset = -Mathf.Lerp(_biteAmplitude, 0f, k); } - break; case BobberBiteType.SlowSink: @@ -505,7 +729,6 @@ public class BobberPresentationController : MonoBehaviour { if (_activeBiteType == BobberBiteType.SlowSink || _activeBiteType == BobberBiteType.BlackDrift) { - // 这两种默认停留在最终状态,由外部决定何时 StopBite return; } @@ -536,7 +759,7 @@ public class BobberPresentationController : MonoBehaviour { StopBite(); } - + if (debugTapKey && Input.GetKeyDown(KeyCode.T)) PlayTap(); @@ -569,6 +792,19 @@ public class BobberPresentationController : MonoBehaviour Debug.DrawLine(a + Vector3.left * 0.03f, a + Vector3.right * 0.03f, Color.green); Debug.DrawLine(a + Vector3.forward * 0.03f, a + Vector3.back * 0.03f, Color.green); } + + if (lureBody != null) + { + Vector3 bobber = _rb.worldCenterOfMass; + Vector3 lure = lureBody.worldCenterOfMass; + Debug.DrawLine(bobber, lure, Color.magenta); + + Vector3 verticalEnd = bobber + Vector3.down * _verticalDistance; + Debug.DrawLine(bobber, verticalEnd, Color.red); + + Vector3 planar = Vector3.ProjectOnPlane(lure - bobber, Vector3.up); + Debug.DrawLine(verticalEnd, verticalEnd + planar, Color.blue); + } } #if UNITY_EDITOR @@ -579,13 +815,26 @@ public class BobberPresentationController : MonoBehaviour maxYSpeed = Mathf.Max(0.01f, maxYSpeed); xzSmoothTime = Mathf.Max(0.001f, xzSmoothTime); rotationLerpSpeed = Mathf.Max(0.01f, rotationLerpSpeed); - maxTiltAngle = Mathf.Clamp(maxTiltAngle, 0f, 89f); + maxPlanarOffset = Mathf.Max(0f, maxPlanarOffset); downForceToSink = Mathf.Max(0f, downForceToSink); maxExtraSink = Mathf.Max(0f, maxExtraSink); surfaceBobAmplitude = Mathf.Max(0f, surfaceBobAmplitude); surfaceBobFrequency = Mathf.Max(0f, surfaceBobFrequency); yDeadZone = Mathf.Max(0f, yDeadZone); + + referenceLength = Mathf.Max(0.0001f, referenceLength); + minSubmergeToStand = Mathf.Clamp01(minSubmergeToStand); + verticalLieThreshold = Mathf.Clamp(verticalLieThreshold, 0f, 2f); + verticalUprightThreshold = Mathf.Max(verticalLieThreshold, verticalUprightThreshold); + planarTiltThreshold = Mathf.Clamp(planarTiltThreshold, 0f, 2f); + planarDominanceMultiplier = Mathf.Max(0.1f, planarDominanceMultiplier); + postureHysteresis = Mathf.Clamp(postureHysteresis, 0f, 0.3f); + + tiltedAngle = Mathf.Clamp(tiltedAngle, 0f, 89f); + lyingAngle = Mathf.Clamp(lyingAngle, tiltedAngle, 89.9f); + uprightMaxTiltAngle = Mathf.Clamp(uprightMaxTiltAngle, 0f, tiltedAngle); + planarTiltFactor = Mathf.Max(0f, planarTiltFactor); } #endif