代码提交

This commit is contained in:
2026-04-12 23:52:07 +08:00
parent 95700b71c1
commit fbe33f9f3a
16 changed files with 1802 additions and 0 deletions

View File

@@ -0,0 +1,685 @@
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 RigidbodyConstraints _defaultConstraints;
private float _draftVelocity;
private float _currentDraftDepth;
private float _ySmoothVelocity;
private float _biteOffsetY;
private float _biteOffsetYVelocity;
private bool _uprightPoseCached;
private Quaternion _cachedUprightRotation;
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);
var nextRotation = CalculateNextRotation(deltaTime);
UpdateVerticalPosition(deltaTime, nextRotation);
ApplyRotation(nextRotation);
}
#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;
_defaultConstraints = _rb.constraints;
_defaultsCached = true;
}
_currentDraftDepth = CalculateRawDraftDepth();
_draftVelocity = 0f;
_ySmoothVelocity = 0f;
_biteOffsetY = 0f;
_biteOffsetYVelocity = 0f;
_targetRotation = _rb.rotation;
if (!_uprightPoseCached)
{
_cachedUprightRotation = _rb.rotation;
_uprightPoseCached = true;
}
_uprightReferenceRotation = _cachedUprightRotation;
}
private void EnterWaterPresentationMode()
{
if (_rb == null)
{
return;
}
_mode = FishingBobberControlMode.WaterPresentation;
_waterStateInitialized = true;
_uprightReferenceRotation = _cachedUprightRotation;
_targetRotation = _rb.rotation;
_draftVelocity = 0f;
_ySmoothVelocity = 0f;
_biteOffsetYVelocity = 0f;
_currentDraftDepth = CalculateRawDraftDepth();
_rb.useGravity = false;
_rb.angularDamping = waterAngularDamping;
_rb.constraints = _defaultConstraints | RigidbodyConstraints.FreezeRotation;
_rb.angularVelocity = Vector3.zero;
}
private void ExitWaterPresentationMode()
{
_mode = FishingBobberControlMode.AirPhysics;
RestorePhysicsState();
}
private void RestorePhysicsState()
{
if (_rb == null || !_defaultsCached)
{
return;
}
_rb.useGravity = _defaultUseGravity;
_rb.angularDamping = _defaultAngularDamping;
_rb.constraints = _defaultConstraints;
}
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, Quaternion targetRotation)
{
var position = _rb.position;
var targetY = waterLevel - _currentDraftDepth - GetBottomOffsetWorldY(targetRotation) + _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);
}
_rb.MovePosition(position);
var velocity = _rb.linearVelocity;
if (Mathf.Abs(velocity.y) > 0f)
{
velocity.y = 0f;
_rb.linearVelocity = velocity;
}
}
private Quaternion CalculateNextRotation(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;
return Quaternion.Slerp(
_rb.rotation,
_targetRotation,
1f - Mathf.Exp(-Mathf.Max(0.01f, rotationLerpSpeed) * deltaTime));
}
private void ApplyRotation(Quaternion nextRotation)
{
_rb.rotation = nextRotation;
}
private float GetBottomOffsetWorldY(Quaternion rotation)
{
return (rotation * new Vector3(0f, bottomOffsetLocalY, 0f)).y;
}
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
}
}