Files
Fishing2/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingBobberFeature.cs
2026-04-12 18:37:24 +08:00

665 lines
20 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;
namespace NBF
{
public enum FishingBobberControlMode
{
AirPhysics,
WaterPresentation,
}
public enum FishingBobberBiteType
{
None,
Tap,
SlowSink,
Lift,
BlackDrift,
}
public enum BobberTiltAxis
{
LocalX,
LocalZ,
}
[DisallowMultipleComponent]
[RequireComponent(typeof(Rigidbody))]
public class FishingBobberFeature : FishingLineNodeMotionFeature
{
protected override int DefaultPriority => 100;
#region
[Header("入水检测")]
[Tooltip("当前测试阶段固定水面高度。")]
public float waterLevel = 0f;
[Tooltip("浮漂底部进入水面达到该深度后,切换为水面表现控制。")]
public float enterWaterDepth = 0.002f;
[Tooltip("浮漂底部高于该深度时退出水面表现控制。通常设为负值用于滞回。")]
public float exitWaterDepth = -0.01f;
[Tooltip("浮漂总高度,单位米。")]
public float floatHeight = 0.08f;
[Tooltip("如果 Pivot 在浮漂底部填 0如果 Pivot 在模型中部,填底部相对 Pivot 的本地 Y 偏移。")]
public float bottomOffsetLocalY;
[Tooltip("Y 轴控制的平滑时间。")]
public float ySmoothTime = 0.08f;
[Tooltip("Y 轴平滑时允许的最大竖直速度。")]
public float maxYSpeed = 2f;
[Tooltip("Y 轴死区,小范围内直接贴目标值以减少微抖。")]
public float yDeadZone = 0.0005f;
#endregion
#region
[Header("吃水控制")]
[Tooltip("基准底部重量。当前重量等于该值时,使用基础吃水深度。")]
public float neutralBottomWeight = 1f;
[Tooltip("当前底部总重量。运行时可由其他脚本更新。")]
public float currentBottomWeight = 1f;
[Tooltip("基准重量下的基础吃水深度。")]
public float baseDraftDepth = 0.02f;
[Tooltip("每单位重量变化对应增加或减少的吃水深度。")]
public float draftDepthPerWeight = 0.01f;
[Tooltip("吃水深度下限。")]
public float minDraftDepth = 0.005f;
[Tooltip("吃水深度上限。")]
public float maxDraftDepth = 0.08f;
[Tooltip("吃水深度变化的平滑时间。")]
public float draftSmoothTime = 0.18f;
#endregion
#region
[Header("漂相动画")]
[Tooltip("漂相位移动画的平滑时间。")]
public float biteSmoothTime = 0.03f;
[Tooltip("点漂默认振幅。")]
public float tapAmplitude = 0.008f;
[Tooltip("点漂默认时长。")]
public float tapDuration = 0.18f;
[Tooltip("缓沉默认振幅。")]
public float slowSinkAmplitude = 0.025f;
[Tooltip("缓沉默认时长。")]
public float slowSinkDuration = 1.2f;
[Tooltip("顶漂默认振幅。")]
public float liftAmplitude = 0.015f;
[Tooltip("顶漂默认时长。")]
public float liftDuration = 1.2f;
[Tooltip("黑漂默认振幅。")]
public float blackDriftAmplitude = 0.06f;
[Tooltip("黑漂默认时长。")]
public float blackDriftDuration = 0.8f;
#endregion
#region
[Header("输入测试")]
[Tooltip("是否启用运行时按键测试漂相。")]
public bool enableDebugInput = true;
[Tooltip("停止当前漂相的按键。")]
public KeyCode stopBiteKey = KeyCode.R;
[Tooltip("触发点漂的按键。")]
public KeyCode tapKey = KeyCode.T;
[Tooltip("触发缓沉的按键。")]
public KeyCode slowSinkKey = KeyCode.G;
[Tooltip("触发顶漂的按键。")]
public KeyCode liftKey = KeyCode.H;
[Tooltip("触发黑漂的按键。")]
public KeyCode blackDriftKey = KeyCode.B;
#endregion
#region 姿
[Header("姿态控制")]
[Tooltip("重量低于该值时,姿态趋向躺漂。")]
public float lyingWeightThreshold = 0.4f;
[Tooltip("重量达到该值附近时,姿态趋向半躺。")]
public float tiltedWeightThreshold = 0.8f;
[Tooltip("重量达到该值及以上时,姿态趋向立漂。")]
public float uprightWeightThreshold = 1.2f;
[Tooltip("躺漂对应的倾角。")]
public float lyingAngle = 88f;
[Tooltip("半躺对应的倾角。")]
public float tiltedAngle = 42f;
[Tooltip("立漂对应的倾角,通常为 0。")]
public float uprightAngle = 0f;
[Tooltip("绕哪个本地轴做倾倒。")]
public BobberTiltAxis tiltAxis = BobberTiltAxis.LocalX;
[Tooltip("是否反转倾倒方向。")]
public bool invertTiltDirection;
[Tooltip("姿态旋转的平滑速度。")]
public float rotationLerpSpeed = 8f;
[Tooltip("入水后用于压制旋转抖动的角阻尼。")]
public float waterAngularDamping = 999f;
#endregion
#region
public FishingBobberControlMode CurrentMode => _mode;
public FishingBobberBiteType CurrentBiteType => _activeBiteType;
public float CurrentDraftDepth => _currentDraftDepth;
public float CurrentBottomWeight => currentBottomWeight;
private Rigidbody _rb;
private FishingBobberControlMode _mode = FishingBobberControlMode.AirPhysics;
private bool _defaultsCached;
private bool _waterStateInitialized;
private float _defaultAngularDamping;
private bool _defaultUseGravity;
private float _draftVelocity;
private float _currentDraftDepth;
private float _ySmoothVelocity;
private float _biteOffsetY;
private float _biteOffsetYVelocity;
private Quaternion _uprightReferenceRotation;
private Quaternion _targetRotation;
private FishingBobberBiteType _activeBiteType = FishingBobberBiteType.None;
private float _biteTimer;
private float _biteDuration;
private float _biteAmplitude;
#endregion
#region Unity
private void Awake()
{
EnsureRuntimeReferences();
InitializeRuntimeState();
}
private void Update()
{
HandleDebugInput();
}
#endregion
#region
public override bool IsSupportedNode(FishingLineNode node)
{
return node != null && node.Type == FishingLineNode.NodeType.Float;
}
protected override void OnBind()
{
EnsureRuntimeReferences();
InitializeRuntimeState();
}
public override bool CanControl()
{
EnsureRuntimeReferences();
if (_rb == null || !IsSupportedNode(Node))
{
return false;
}
var submergeDepth = GetSubmergeDepth();
if (_mode == FishingBobberControlMode.WaterPresentation)
{
return submergeDepth >= exitWaterDepth;
}
return submergeDepth > enterWaterDepth;
}
public override void OnMotionActivated()
{
EnsureRuntimeReferences();
EnterWaterPresentationMode();
}
public override void OnMotionDeactivated()
{
EnsureRuntimeReferences();
ExitWaterPresentationMode();
}
public override void TickMotion(float deltaTime)
{
EnsureRuntimeReferences();
if (_rb == null)
{
return;
}
var submergeDepth = GetSubmergeDepth();
if (submergeDepth < exitWaterDepth)
{
ExitWaterPresentationMode();
return;
}
if (_mode != FishingBobberControlMode.WaterPresentation)
{
EnterWaterPresentationMode();
}
UpdateBiteAnimation(deltaTime);
UpdateDraft(deltaTime);
UpdateVerticalPosition(deltaTime);
UpdateRotation(deltaTime);
}
#endregion
#region
public void SetBottomWeight(float weight)
{
currentBottomWeight = weight;
}
public void PlayTap(float amplitude = -1f, float duration = -1f)
{
StartBite(
FishingBobberBiteType.Tap,
amplitude > 0f ? amplitude : tapAmplitude,
duration > 0f ? duration : tapDuration);
}
public void PlaySlowSink(float amplitude = -1f, float duration = -1f)
{
StartBite(
FishingBobberBiteType.SlowSink,
amplitude > 0f ? amplitude : slowSinkAmplitude,
duration > 0f ? duration : slowSinkDuration);
}
public void PlayLift(float amplitude = -1f, float duration = -1f)
{
StartBite(
FishingBobberBiteType.Lift,
amplitude > 0f ? amplitude : liftAmplitude,
duration > 0f ? duration : liftDuration);
}
public void PlayBlackDrift(float amplitude = -1f, float duration = -1f)
{
StartBite(
FishingBobberBiteType.BlackDrift,
amplitude > 0f ? amplitude : blackDriftAmplitude,
duration > 0f ? duration : blackDriftDuration);
}
public void StopBite()
{
_activeBiteType = FishingBobberBiteType.None;
_biteTimer = 0f;
_biteDuration = 0f;
_biteAmplitude = 0f;
}
#endregion
#region
private void EnsureRuntimeReferences()
{
if (_rb == null)
{
_rb = Node != null && Node.Body != null ? Node.Body : GetComponent<Rigidbody>();
}
}
private void InitializeRuntimeState()
{
if (_rb == null)
{
return;
}
if (!_defaultsCached)
{
_defaultAngularDamping = _rb.angularDamping;
_defaultUseGravity = _rb.useGravity;
_defaultsCached = true;
}
_currentDraftDepth = CalculateRawDraftDepth();
_draftVelocity = 0f;
_ySmoothVelocity = 0f;
_biteOffsetY = 0f;
_biteOffsetYVelocity = 0f;
_targetRotation = transform.rotation;
if (!_waterStateInitialized)
{
_uprightReferenceRotation = transform.rotation;
}
}
private void EnterWaterPresentationMode()
{
if (_rb == null)
{
return;
}
_mode = FishingBobberControlMode.WaterPresentation;
_waterStateInitialized = true;
_uprightReferenceRotation = transform.rotation;
_targetRotation = transform.rotation;
_draftVelocity = 0f;
_ySmoothVelocity = 0f;
_biteOffsetYVelocity = 0f;
_currentDraftDepth = CalculateRawDraftDepth();
_rb.useGravity = false;
_rb.angularDamping = waterAngularDamping;
}
private void ExitWaterPresentationMode()
{
_mode = FishingBobberControlMode.AirPhysics;
RestorePhysicsState();
}
private void RestorePhysicsState()
{
if (_rb == null || !_defaultsCached)
{
return;
}
_rb.useGravity = _defaultUseGravity;
_rb.angularDamping = _defaultAngularDamping;
}
private float GetSubmergeDepth()
{
return waterLevel - GetBottomWorldPosition().y;
}
private Vector3 GetBottomWorldPosition()
{
return transform.TransformPoint(new Vector3(0f, bottomOffsetLocalY, 0f));
}
private float CalculateRawDraftDepth()
{
var weightDelta = currentBottomWeight - neutralBottomWeight;
var targetDraft = baseDraftDepth + weightDelta * draftDepthPerWeight;
return Mathf.Clamp(targetDraft, minDraftDepth, maxDraftDepth);
}
private void UpdateDraft(float deltaTime)
{
var targetDraft = CalculateRawDraftDepth();
_currentDraftDepth = Mathf.SmoothDamp(
_currentDraftDepth,
targetDraft,
ref _draftVelocity,
Mathf.Max(0.0001f, draftSmoothTime),
Mathf.Infinity,
deltaTime);
}
private void UpdateVerticalPosition(float deltaTime)
{
var position = transform.position;
var targetY = waterLevel - _currentDraftDepth - bottomOffsetLocalY + _biteOffsetY;
if (Mathf.Abs(position.y - targetY) < yDeadZone)
{
position.y = targetY;
_ySmoothVelocity = 0f;
}
else
{
position.y = Mathf.SmoothDamp(
position.y,
targetY,
ref _ySmoothVelocity,
Mathf.Max(0.0001f, ySmoothTime),
maxYSpeed,
deltaTime);
}
transform.position = position;
var velocity = _rb.linearVelocity;
if (Mathf.Abs(velocity.y) > 0f)
{
velocity.y = 0f;
_rb.linearVelocity = velocity;
}
}
private void UpdateRotation(float deltaTime)
{
var targetTiltAngle = EvaluateTargetTiltAngle();
var signedAngle = invertTiltDirection ? -targetTiltAngle : targetTiltAngle;
var localAxis = tiltAxis == BobberTiltAxis.LocalX ? Vector3.right : Vector3.forward;
_targetRotation = _uprightReferenceRotation * Quaternion.AngleAxis(signedAngle, localAxis);
_rb.angularVelocity = Vector3.zero;
transform.rotation = Quaternion.Slerp(
transform.rotation,
_targetRotation,
1f - Mathf.Exp(-Mathf.Max(0.01f, rotationLerpSpeed) * deltaTime));
}
private float EvaluateTargetTiltAngle()
{
if (currentBottomWeight <= lyingWeightThreshold)
{
return lyingAngle;
}
if (currentBottomWeight <= tiltedWeightThreshold)
{
var t = Mathf.InverseLerp(lyingWeightThreshold, tiltedWeightThreshold, currentBottomWeight);
return Mathf.Lerp(lyingAngle, tiltedAngle, t);
}
if (currentBottomWeight <= uprightWeightThreshold)
{
var t = Mathf.InverseLerp(tiltedWeightThreshold, uprightWeightThreshold, currentBottomWeight);
return Mathf.Lerp(tiltedAngle, uprightAngle, t);
}
return uprightAngle;
}
private void StartBite(FishingBobberBiteType type, float amplitude, float duration)
{
if (_mode != FishingBobberControlMode.WaterPresentation)
{
return;
}
_activeBiteType = type;
_biteTimer = 0f;
_biteDuration = Mathf.Max(0.01f, duration);
_biteAmplitude = Mathf.Max(0f, amplitude);
_biteOffsetYVelocity = 0f;
}
private void UpdateBiteAnimation(float deltaTime)
{
if (_activeBiteType == FishingBobberBiteType.None)
{
_biteOffsetY = Mathf.SmoothDamp(
_biteOffsetY,
0f,
ref _biteOffsetYVelocity,
Mathf.Max(0.0001f, biteSmoothTime),
Mathf.Infinity,
deltaTime);
return;
}
_biteTimer += deltaTime;
var t = Mathf.Clamp01(_biteTimer / _biteDuration);
var targetOffset = 0f;
switch (_activeBiteType)
{
case FishingBobberBiteType.Tap:
if (t < 0.35f)
{
var downT = t / 0.35f;
targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, downT);
}
else
{
var upT = (t - 0.35f) / 0.65f;
targetOffset = -Mathf.Lerp(_biteAmplitude, 0f, upT);
}
break;
case FishingBobberBiteType.SlowSink:
targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, t);
break;
case FishingBobberBiteType.Lift:
targetOffset = Mathf.SmoothStep(0f, _biteAmplitude, t);
break;
case FishingBobberBiteType.BlackDrift:
targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, t);
break;
}
_biteOffsetY = Mathf.SmoothDamp(
_biteOffsetY,
targetOffset,
ref _biteOffsetYVelocity,
Mathf.Max(0.0001f, biteSmoothTime),
Mathf.Infinity,
deltaTime);
if (_biteTimer >= _biteDuration &&
_activeBiteType != FishingBobberBiteType.SlowSink &&
_activeBiteType != FishingBobberBiteType.BlackDrift)
{
_activeBiteType = FishingBobberBiteType.None;
}
}
private void HandleDebugInput()
{
if (!Application.isPlaying || !enableDebugInput)
{
return;
}
if (Input.GetKeyDown(stopBiteKey))
{
StopBite();
}
if (Input.GetKeyDown(tapKey))
{
PlayTap();
}
if (Input.GetKeyDown(slowSinkKey))
{
PlaySlowSink();
}
if (Input.GetKeyDown(liftKey))
{
PlayLift();
}
if (Input.GetKeyDown(blackDriftKey))
{
PlayBlackDrift();
}
}
#endregion
#region
#if UNITY_EDITOR
private void OnValidate()
{
floatHeight = Mathf.Max(0.001f, floatHeight);
ySmoothTime = Mathf.Max(0.001f, ySmoothTime);
maxYSpeed = Mathf.Max(0.01f, maxYSpeed);
yDeadZone = Mathf.Max(0f, yDeadZone);
neutralBottomWeight = Mathf.Max(0f, neutralBottomWeight);
currentBottomWeight = Mathf.Max(0f, currentBottomWeight);
minDraftDepth = Mathf.Max(0f, minDraftDepth);
maxDraftDepth = Mathf.Max(minDraftDepth, maxDraftDepth);
baseDraftDepth = Mathf.Clamp(baseDraftDepth, minDraftDepth, maxDraftDepth);
draftDepthPerWeight = Mathf.Max(0f, draftDepthPerWeight);
draftSmoothTime = Mathf.Max(0.001f, draftSmoothTime);
biteSmoothTime = Mathf.Max(0.001f, biteSmoothTime);
tapAmplitude = Mathf.Max(0f, tapAmplitude);
tapDuration = Mathf.Max(0.01f, tapDuration);
slowSinkAmplitude = Mathf.Max(0f, slowSinkAmplitude);
slowSinkDuration = Mathf.Max(0.01f, slowSinkDuration);
liftAmplitude = Mathf.Max(0f, liftAmplitude);
liftDuration = Mathf.Max(0.01f, liftDuration);
blackDriftAmplitude = Mathf.Max(0f, blackDriftAmplitude);
blackDriftDuration = Mathf.Max(0.01f, blackDriftDuration);
lyingWeightThreshold = Mathf.Max(0f, lyingWeightThreshold);
tiltedWeightThreshold = Mathf.Max(lyingWeightThreshold, tiltedWeightThreshold);
uprightWeightThreshold = Mathf.Max(tiltedWeightThreshold, uprightWeightThreshold);
uprightAngle = Mathf.Clamp(uprightAngle, 0f, 89f);
tiltedAngle = Mathf.Clamp(tiltedAngle, uprightAngle, 89f);
lyingAngle = Mathf.Clamp(lyingAngle, tiltedAngle, 89.9f);
rotationLerpSpeed = Mathf.Max(0.01f, rotationLerpSpeed);
waterAngularDamping = Mathf.Max(0f, waterAngularDamping);
}
#endif
#endregion
}
}