Files
Fishing2/Assets/Scripts/Test/New/BobberPresentationController.cs
2026-03-22 21:35:27 +08:00

593 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
/// <summary>
/// 简单水面接口。你可以替换成自己的水系统。
/// </summary>
public interface IWaterSurfaceProvider
{
float GetWaterHeight(Vector3 worldPos);
Vector3 GetWaterNormal(Vector3 worldPos);
}
/// <summary>
/// 浮漂控制模式:
/// 1. AirPhysics空中/未入水,使用刚体物理
/// 2. WaterPresentation入水后关闭重力Y 和旋转由脚本控制
/// </summary>
public enum BobberControlMode
{
AirPhysics,
WaterPresentation
}
/// <summary>
/// 漂像事件类型
/// </summary>
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;
/// <summary>外部可写:等效向下拉力(不是必须是真实力,作为输入信号即可)</summary>
public float ExternalDownForce { get; set; }
/// <summary>外部可写:是否触底</summary>
public bool IsBottomTouched { get; set; }
/// <summary>外部可写:额外平面偏移(例如风、水流、拖拽)</summary>
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<Rigidbody>();
_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
/// <summary>
/// 轻点:快速下顿再回弹
/// </summary>
public void PlayTap(float amplitude = 0.008f, float duration = 0.18f)
{
StartBite(BobberBiteType.Tap, amplitude, duration);
}
/// <summary>
/// 缓沉:在持续时间内逐渐下沉
/// </summary>
public void PlaySlowSink(float amplitude = 0.025f, float duration = 1.2f)
{
StartBite(BobberBiteType.SlowSink, amplitude, duration);
}
/// <summary>
/// 送漂:向上抬
/// </summary>
public void PlayLift(float amplitude = 0.015f, float duration = 0.6f)
{
StartBite(BobberBiteType.Lift, amplitude, duration);
}
/// <summary>
/// 黑漂:快速下沉,并可配合平面拖拽
/// </summary>
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
}