using UnityEngine; /// /// 简单水面接口。你可以替换成自己的水系统。 /// public interface IWaterSurfaceProvider { float GetWaterHeight(Vector3 worldPos); Vector3 GetWaterNormal(Vector3 worldPos); } /// /// 浮漂控制模式: /// 1. AirPhysics:空中/未入水,使用刚体物理 /// 2. WaterPresentation:入水后,关闭重力,Y 和旋转由脚本控制 /// public enum BobberControlMode { AirPhysics, WaterPresentation } /// /// 漂像事件类型 /// public enum BobberBiteType { None, Tap, // 轻点 SlowSink, // 缓沉 Lift, // 送漂 BlackDrift // 黑漂/快速拖入 } [DisallowMultipleComponent] [RequireComponent(typeof(Rigidbody))] public class BobberPresentationController : MonoBehaviour { [Header("Water")] [Tooltip("没有水提供器时使用固定水位")] public float fallbackWaterLevel = 0f; [Tooltip("可选:挂实现了 IWaterSurfaceProvider 的组件")] public MonoBehaviour waterProviderBehaviour; [Header("Enter Water")] [Tooltip("底部进入水面多少米后切换为漂像控制")] public float enterWaterDepth = 0.002f; [Tooltip("离开水面多少米后回到空中物理。一般给负值做滞回")] public float exitWaterDepth = -0.01f; [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)] public float baseSubmergeRatio = 0.28f; [Tooltip("Y 轴平滑时间,越小响应越快")] public float ySmoothTime = 0.08f; [Tooltip("最大竖直速度限制(用于 SmoothDamp)")] public float maxYSpeed = 2f; [Tooltip("静止小死区,减少微抖")] public float yDeadZone = 0.0005f; [Header("Surface Motion")] [Tooltip("是否启用轻微水面起伏")] public bool enableSurfaceBobbing = true; [Tooltip("水面轻微起伏振幅(米)")] public float surfaceBobAmplitude = 0.0015f; [Tooltip("水面轻微起伏频率")] public float surfaceBobFrequency = 1.2f; [Header("XZ Motion")] [Tooltip("入水后是否锁定 XZ 到入水点附近")] public bool lockXZAroundAnchor = true; [Tooltip("XZ 跟随平滑时间")] public float xzSmoothTime = 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("外部向下拉力映射为下沉量的系数。你可以把钩/铅/线组的等效向下拉力喂进来")] public float downForceToSink = 0.0025f; [Tooltip("向下拉力下沉的最大附加量")] public float maxExtraSink = 0.08f; [Header("Bottom Touch")] [Tooltip("触底时是否启用修正")] public bool enableBottomTouchAdjust = true; [Tooltip("触底后减少的下沉量(例如铅坠到底,漂会回升一点)")] public float bottomTouchLift = 0.01f; [Header("Debug Input")] [Tooltip("调试:按 R 恢复默认")] public bool debugResetKey = true; [Header("Debug Input")] [Tooltip("调试:按 T 触发轻点")] public bool debugTapKey = true; [Tooltip("调试:按 G 触发缓沉")] public bool debugSlowSinkKey = true; [Tooltip("调试:按 H 触发送漂")] public bool debugLiftKey = true; [Tooltip("调试:按 B 触发黑漂")] public bool debugBlackDriftKey = true; [Header("Debug")] public bool drawDebug = false; public BobberControlMode CurrentMode => _mode; /// 外部可写:等效向下拉力(不是必须是真实力,作为输入信号即可) public float ExternalDownForce { get; set; } /// 外部可写:是否触底 public bool IsBottomTouched { get; set; } /// 外部可写:额外平面偏移(例如风、水流、拖拽) public Vector2 ExternalPlanarOffset { get; set; } private Rigidbody _rb; private IWaterSurfaceProvider _waterProvider; private BobberControlMode _mode = BobberControlMode.AirPhysics; private float _defaultLinearDamping; private float _defaultAngularDamping; private bool _defaultUseGravity; private Vector3 _waterAnchorPos; private Vector3 _xzSmoothVelocity; private float _ySmoothVelocity; private float _biteOffsetY; private float _biteOffsetYVelocity; private Quaternion _targetRotation; // bite event runtime private BobberBiteType _activeBiteType = BobberBiteType.None; private float _biteTimer; private float _biteDuration; private float _biteAmplitude; private Vector3 _blackDriftDirection; private void Awake() { _rb = GetComponent(); _defaultLinearDamping = _rb.linearDamping; _defaultAngularDamping = _rb.angularDamping; _defaultUseGravity = _rb.useGravity; if (waterProviderBehaviour != null) _waterProvider = waterProviderBehaviour as IWaterSurfaceProvider; _targetRotation = transform.rotation; } private void Update() { HandleDebugKeys(); } private void FixedUpdate() { float waterY = GetWaterHeight(transform.position); Vector3 bottomWorld = GetBottomWorldPosition(); float submergeDepth = waterY - bottomWorld.y; switch (_mode) { case BobberControlMode.AirPhysics: UpdateAirPhysics(submergeDepth); break; case BobberControlMode.WaterPresentation: UpdateWaterPresentation(waterY, submergeDepth); break; } if (drawDebug) { DrawDebug(waterY); } } #region Main Update private void UpdateAirPhysics(float submergeDepth) { RestoreAirPhysicsState(); if (submergeDepth > enterWaterDepth) { EnterWaterPresentationMode(); } } private void UpdateWaterPresentation(float waterY, float submergeDepth) { if (submergeDepth < exitWaterDepth) { ExitWaterPresentationMode(); return; } // 完全关闭刚体干扰 _rb.useGravity = false; _rb.linearVelocity = Vector3.zero; _rb.angularVelocity = Vector3.zero; _rb.linearDamping = 999f; _rb.angularDamping = 999f; UpdateBiteAnimation(); Vector3 pos = transform.position; // 1. 算目标 Y float targetY = CalculateTargetY(waterY); if (Mathf.Abs(pos.y - targetY) < yDeadZone) { pos.y = targetY; _ySmoothVelocity = 0f; } else { pos.y = Mathf.SmoothDamp( current: pos.y, target: targetY, currentVelocity: ref _ySmoothVelocity, smoothTime: Mathf.Max(0.0001f, ySmoothTime), maxSpeed: maxYSpeed, deltaTime: Time.fixedDeltaTime ); } // 2. 算目标 XZ Vector3 targetXZ = CalculateTargetXZ(); Vector3 planarPos = new Vector3(pos.x, 0f, pos.z); Vector3 planarTarget = new Vector3(targetXZ.x, 0f, targetXZ.z); planarPos = Vector3.SmoothDamp( planarPos, planarTarget, ref _xzSmoothVelocity, Mathf.Max(0.0001f, xzSmoothTime), Mathf.Infinity, Time.fixedDeltaTime ); pos.x = planarPos.x; pos.z = planarPos.z; transform.position = pos; // 3. 算目标旋转 UpdateTargetRotation(); transform.rotation = Quaternion.Slerp( transform.rotation, _targetRotation, 1f - Mathf.Exp(-rotationLerpSpeed * Time.fixedDeltaTime) ); } #endregion #region Mode Switch private void EnterWaterPresentationMode() { _mode = BobberControlMode.WaterPresentation; _waterAnchorPos = transform.position; _ySmoothVelocity = 0f; _xzSmoothVelocity = Vector3.zero; _biteOffsetY = 0f; _biteOffsetYVelocity = 0f; _activeBiteType = BobberBiteType.None; _biteTimer = 0f; _rb.useGravity = false; _rb.linearVelocity = Vector3.zero; _rb.angularVelocity = Vector3.zero; _rb.linearDamping = 999f; _rb.angularDamping = 999f; } private void ExitWaterPresentationMode() { _mode = BobberControlMode.AirPhysics; RestoreAirPhysicsState(); } private void RestoreAirPhysicsState() { _rb.useGravity = _defaultUseGravity; _rb.linearDamping = _defaultLinearDamping; _rb.angularDamping = _defaultAngularDamping; } #endregion #region Target Calculation private float CalculateTargetY(float waterY) { float baseSinkDepth = floatHeight * Mathf.Clamp01(baseSubmergeRatio); float sinkByForce = Mathf.Clamp( ExternalDownForce * downForceToSink, 0f, maxExtraSink ); float bottomAdjust = 0f; if (enableBottomTouchAdjust && IsBottomTouched) { bottomAdjust -= bottomTouchLift; } float surfaceBob = 0f; if (enableSurfaceBobbing) { 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; return targetPivotY; } private Vector3 CalculateTargetXZ() { Vector2 planarOffset = Vector2.ClampMagnitude(ExternalPlanarOffset, maxPlanarOffset); Vector3 basePos = lockXZAroundAnchor ? _waterAnchorPos : transform.position; // 黑漂时额外平面位移 if (_activeBiteType == BobberBiteType.BlackDrift) { float t = Mathf.Clamp01(_biteDuration > 0f ? _biteTimer / _biteDuration : 1f); float drift = Mathf.SmoothStep(0f, 1f, t) * 0.08f; Vector3 blackDrift = _blackDriftDirection * drift; basePos += new Vector3(blackDrift.x, 0f, blackDrift.z); } return new Vector3( basePos.x + planarOffset.x, transform.position.y, basePos.z + planarOffset.y ); } private void UpdateTargetRotation() { if (!keepUpright) { _targetRotation = transform.rotation; return; } Vector3 up = Vector3.up; Vector3 planar = new Vector3(_xzSmoothVelocity.x, 0f, _xzSmoothVelocity.z); float speed = planar.magnitude; Vector3 tiltAxis = speed > 1e-5f ? Vector3.Cross(up, planar.normalized) : Vector3.zero; float tiltAngle = Mathf.Clamp(speed * planarTiltFactor, 0f, maxTiltAngle); Quaternion upright = Quaternion.FromToRotation(transform.up, Vector3.up) * transform.rotation; Quaternion tilt = tiltAxis.sqrMagnitude > 1e-6f ? Quaternion.AngleAxis(tiltAngle, tiltAxis.normalized) : Quaternion.identity; _targetRotation = tilt * upright; } #endregion #region Bite Presentation /// /// 轻点:快速下顿再回弹 /// public void PlayTap(float amplitude = 0.008f, float duration = 0.18f) { StartBite(BobberBiteType.Tap, amplitude, duration); } /// /// 缓沉:在持续时间内逐渐下沉 /// public void PlaySlowSink(float amplitude = 0.025f, float duration = 1.2f) { StartBite(BobberBiteType.SlowSink, amplitude, duration); } /// /// 送漂:向上抬 /// public void PlayLift(float amplitude = 0.015f, float duration = 0.6f) { StartBite(BobberBiteType.Lift, amplitude, duration); } /// /// 黑漂:快速下沉,并可配合平面拖拽 /// public void PlayBlackDrift(float amplitude = 0.06f, float duration = 0.8f, Vector3? driftDirection = null) { StartBite(BobberBiteType.BlackDrift, amplitude, duration); _blackDriftDirection = (driftDirection ?? transform.forward).normalized; } public void StopBite() { _activeBiteType = BobberBiteType.None; _biteTimer = 0f; _biteDuration = 0f; _biteAmplitude = 0f; _biteOffsetY = 0f; _biteOffsetYVelocity = 0f; } private void StartBite(BobberBiteType type, float amplitude, float duration) { if (_mode != BobberControlMode.WaterPresentation) return; _activeBiteType = type; _biteTimer = 0f; _biteDuration = Mathf.Max(0.01f, duration); _biteAmplitude = amplitude; _biteOffsetYVelocity = 0f; if (type == BobberBiteType.BlackDrift && _blackDriftDirection.sqrMagnitude < 1e-6f) { _blackDriftDirection = transform.forward.sqrMagnitude > 1e-6f ? transform.forward.normalized : Vector3.forward; } } private void UpdateBiteAnimation() { if (_activeBiteType == BobberBiteType.None) { _biteOffsetY = Mathf.SmoothDamp(_biteOffsetY, 0f, ref _biteOffsetYVelocity, 0.08f, Mathf.Infinity, Time.fixedDeltaTime); return; } _biteTimer += Time.fixedDeltaTime; float t = Mathf.Clamp01(_biteTimer / _biteDuration); float targetOffset = 0f; switch (_activeBiteType) { case BobberBiteType.Tap: // 先快速下压,再回弹 if (t < 0.35f) { float k = t / 0.35f; targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, k); } else { float k = (t - 0.35f) / 0.65f; targetOffset = -Mathf.Lerp(_biteAmplitude, 0f, k); } break; case BobberBiteType.SlowSink: targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, t); break; case BobberBiteType.Lift: targetOffset = Mathf.SmoothStep(0f, _biteAmplitude, t); break; case BobberBiteType.BlackDrift: targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, t); break; } _biteOffsetY = Mathf.SmoothDamp( _biteOffsetY, targetOffset, ref _biteOffsetYVelocity, 0.03f, Mathf.Infinity, Time.fixedDeltaTime ); if (_biteTimer >= _biteDuration) { if (_activeBiteType == BobberBiteType.SlowSink || _activeBiteType == BobberBiteType.BlackDrift) { // 这两种默认停留在最终状态,由外部决定何时 StopBite return; } _activeBiteType = BobberBiteType.None; } } #endregion #region Utilities private float GetWaterHeight(Vector3 worldPos) { return _waterProvider != null ? _waterProvider.GetWaterHeight(worldPos) : fallbackWaterLevel; } private Vector3 GetBottomWorldPosition() { return transform.TransformPoint(new Vector3(0f, bottomOffsetLocalY, 0f)); } private void HandleDebugKeys() { if (!Application.isPlaying) return; if (debugResetKey && Input.GetKeyDown(KeyCode.R)) { StopBite(); } if (debugTapKey && Input.GetKeyDown(KeyCode.T)) PlayTap(); if (debugSlowSinkKey && Input.GetKeyDown(KeyCode.G)) PlaySlowSink(); if (debugLiftKey && Input.GetKeyDown(KeyCode.H)) PlayLift(); if (debugBlackDriftKey && Input.GetKeyDown(KeyCode.B)) PlayBlackDrift(); } private void DrawDebug(float waterY) { Vector3 p = transform.position; Vector3 b = GetBottomWorldPosition(); Debug.DrawLine( new Vector3(p.x - 0.05f, waterY, p.z), new Vector3(p.x + 0.05f, waterY, p.z), Color.cyan ); Debug.DrawLine(b, b + Vector3.up * floatHeight, Color.yellow); if (_mode == BobberControlMode.WaterPresentation) { Vector3 a = _waterAnchorPos; 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 UNITY_EDITOR private void OnValidate() { floatHeight = Mathf.Max(0.001f, floatHeight); ySmoothTime = Mathf.Max(0.001f, ySmoothTime); 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); } #endif #endregion }