# Conflicts: # Assets/Scenes/RopeTest.unity # Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs # Assets/Scripts/Fishing/Rope/Rope.cs # Assets/Scripts/Fishing/Rope/Rope.cs.meta
1539 lines
62 KiB
C#
1539 lines
62 KiB
C#
using UnityEngine;
|
||
|
||
namespace NBF
|
||
{
|
||
/// <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, // 轻点:快速下顿再回弹
|
||
MultiTap, // 连顿:连续多次下顿(鱼反复试探)
|
||
SlowSink, // 缓沉:分段下沉,中途短暂停顿
|
||
Lift, // 送漂:上抬至高位后轻微晃动稳住
|
||
BlackDrift, // 黑漂:快速深沉并水平拖拽
|
||
Tremble // 微颤:高频小幅抖动(鱼在蹭饵试探)
|
||
}
|
||
|
||
public enum BobberPosture
|
||
{
|
||
Lying, // 躺漂
|
||
Tilted, // 斜漂
|
||
Upright // 立漂
|
||
}
|
||
|
||
/// <summary>
|
||
/// 入水后的内部阶段,可供外部查询或驱动 UI/音效
|
||
/// </summary>
|
||
public enum BobberWaterPhase
|
||
{
|
||
Settling, // 刚入水,正在稳定
|
||
Stable, // 正常浮漂
|
||
Biting // 漂像动画中
|
||
}
|
||
|
||
public struct BobberWaterConstraintState
|
||
{
|
||
public Vector3 TargetPosition;
|
||
public float PlanarStrength;
|
||
public float UpwardStrength;
|
||
public float DownwardStrength;
|
||
public float MaxCorrection;
|
||
}
|
||
|
||
[DisallowMultipleComponent]
|
||
[RequireComponent(typeof(Rigidbody))]
|
||
public class FLineNodeBobberProvider : MonoBehaviour, IFLineNodeConstraintProvider
|
||
{
|
||
[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("Entry Animation")] [Tooltip("入水瞬间的初始下顿深度(米),形成自然入水感")]
|
||
public float entryDipAmplitude = 0.012f;
|
||
|
||
[Tooltip("入水稳定动画总时长(秒)")] public float entrySettleDuration = 0.8f;
|
||
|
||
[Header("Air Hang")] [Tooltip("悬空时是否让浮漂长轴自然沿重力方向下挂")]
|
||
public bool enableAirHangRotation = true;
|
||
|
||
[Tooltip("悬空下垂旋转的跟随速度")] public float airHangRotationSpeed = 10f;
|
||
|
||
[Tooltip("悬空时,上方支撑方向对平面朝向的影响权重。0=只看重力下垂,1=更多参考上方线段")] [Range(0f, 1f)]
|
||
public float airHangLineInfluence = 0.75f;
|
||
|
||
[Tooltip("悬空时优先让刚体本地重心方向朝向重力下方;重心方向不可用时才使用下面的手动轴")]
|
||
public bool preferAirHangCenterOfMass = true;
|
||
|
||
[UnityEngine.Serialization.FormerlySerializedAs("airHangUpAxisLocal")] [Tooltip("悬空时,浮漂本地哪个轴代表朝向底部/重心的长轴方向")]
|
||
public Vector3 airHangDownAxisLocal = Vector3.down;
|
||
|
||
[Tooltip("悬空时,用于确定绕长轴朝向的本地参考前向")] public Vector3 airHangForwardAxisLocal = Vector3.forward;
|
||
|
||
[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("主水面起伏频率(Hz)")] public float surfaceBobFrequency = 1.2f;
|
||
|
||
[Tooltip("次级起伏振幅比例(主振幅的倍率),使用黄金比例频率增加自然感")] [Range(0f, 1f)]
|
||
public float surfaceBobSecondaryRatio = 0.35f;
|
||
|
||
[Header("XZ Motion")] [Tooltip("入水后是否锁定 XZ 到入水点附近")]
|
||
public bool lockXZAroundAnchor = true;
|
||
|
||
[Tooltip("XZ 跟随平滑时间")] public float xzSmoothTime = 0.15f;
|
||
|
||
[Tooltip("水流/拖拽带来的额外平面偏移最大值")] public float maxPlanarOffset = 0.15f;
|
||
|
||
[Tooltip("移漂默认持续推力时间(秒)")] public float driftForceDuration = 0.45f;
|
||
|
||
[Header("Water Constraint")] [Tooltip("接入 FLine 自定义约束时,使用求解器驱动水面目标,而不是脚本直接写世界坐标")]
|
||
public bool useSolverWaterConstraint = true;
|
||
|
||
[Tooltip("水面对目标 XZ 的平面跟随强度")] [Range(0f, 1f)]
|
||
public float waterConstraintPlanarStrength = 0.12f;
|
||
|
||
[Tooltip("浮漂低于目标水位时,向上托回去的强度")] [Range(0f, 1f)]
|
||
public float waterConstraintUpwardStrength = 0.35f;
|
||
|
||
[Tooltip("浮漂高于目标水位时,向下拉回去的强度。建议明显小于向上强度,这样鱼线才能把它拉出水面")] [Range(0f, 1f)]
|
||
public float waterConstraintDownwardStrength = 0.05f;
|
||
|
||
[Tooltip("每次约束迭代允许的最大修正位移(米)")] public float waterConstraintMaxCorrection = 0.01f;
|
||
|
||
[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("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 Stability")] [Tooltip("候选姿态需持续多久才真正切换")]
|
||
public float postureConfirmTime = 0.08f;
|
||
|
||
[Tooltip("姿态切换后的最短冷却时间,避免来回闪烁")] public float postureSwitchCooldown = 0.10f;
|
||
|
||
[Header("Posture Rotation")] [Tooltip("斜漂角度")]
|
||
public float tiltedAngle = 38f;
|
||
|
||
[Tooltip("躺漂角度")] public float lyingAngle = 88f;
|
||
|
||
[Tooltip("立漂时,平面拉力可附加的最大倾角(度)")] public float uprightMaxTiltAngle = 8f;
|
||
|
||
[Tooltip("平面拉力对立漂附加倾角的灵敏度(度/ratio)。值越大,斜向拉力对立漂的影响越明显")]
|
||
public float planarTiltFactor = 120f;
|
||
|
||
[Tooltip("平面方向死区,小于该值时保持上一帧方向")] public float planarDirectionDeadZone = 0.01f;
|
||
|
||
[Tooltip("平面方向平滑速度")] public float planarDirectionLerpSpeed = 10f;
|
||
|
||
[Tooltip("最终旋转 Slerp 速度")] public float rotationLerpSpeed = 8f;
|
||
|
||
[Header("Posture Blend")] [Tooltip("躺漂 ↔ 斜漂 角度过渡平滑时间")]
|
||
public float postureBlendSmoothTime = 0.15f;
|
||
|
||
[Tooltip("斜漂 ↔ 立漂 角度过渡平滑时间(通常稍慢,立漂更庄重)")]
|
||
public float postureBlendSmoothTimeUpright = 0.22f;
|
||
|
||
[Header("Posture Wobble")] [Tooltip("姿态切换时是否启用旋转回弹抖动")]
|
||
public bool enablePostureWobble = true;
|
||
|
||
[Tooltip("切换触发的旋转抖动初始角速度(度/秒)")] public float postureWobbleStrength = 10f;
|
||
|
||
[Tooltip("旋转抖动弹簧衰减速率。越大衰减越快,值等于 sqrt(k) 的临界阻尼系数")]
|
||
public float postureWobbleDecay = 7f;
|
||
|
||
[Header("Debug Input")] [Tooltip("调试:按 R 停止漂像")]
|
||
public bool debugResetKey = true;
|
||
|
||
[Tooltip("调试:按 T 触发轻点")] public bool debugTapKey = true;
|
||
|
||
[Tooltip("调试:按 Y 触发连顿")] public bool debugMultiTapKey = true;
|
||
|
||
[Tooltip("调试:按 G 触发缓沉")] public bool debugSlowSinkKey = true;
|
||
|
||
[Tooltip("调试:按 H 触发送漂")] public bool debugLiftKey = true;
|
||
|
||
[Tooltip("调试:按 B 触发黑漂")] public bool debugBlackDriftKey = true;
|
||
|
||
[Tooltip("调试:按 V 触发微颤")] public bool debugTrembleKey = true;
|
||
|
||
[Tooltip("调试:按 N 触发移漂(向 transform.forward 移动 0.1m)")]
|
||
public bool debugDriftKey = true;
|
||
|
||
[Header("Debug")] public bool drawDebug = false;
|
||
|
||
public bool UseTestPosture;
|
||
public BobberPosture TestPosture;
|
||
|
||
// ── 只读公开状态 ──────────────────────────────────────────────
|
||
public BobberControlMode CurrentMode => _mode;
|
||
public BobberPosture CurrentPosture => _posture;
|
||
public BobberWaterPhase CurrentWaterPhase => _waterPhase;
|
||
public float CurrentVerticalRatio => _verticalRatio;
|
||
public float CurrentPlanarRatio => _planarRatio;
|
||
public bool UsesSolverWaterConstraint => useSolverWaterConstraint && _logicNode != null;
|
||
|
||
/// <summary>连续姿态混合值:0=躺漂, 1=斜漂, 2=立漂。可直接驱动外部动画混合树。</summary>
|
||
public float PostureBlendValue => _postureBlendValue;
|
||
|
||
// ── 外部可写输入 ───────────────────────────────────────────────
|
||
/// <summary>等效向下拉力(不必须是真实力,作为输入信号即可)</summary>
|
||
public float ExternalDownForce { get; set; }
|
||
|
||
/// <summary>是否触底</summary>
|
||
public bool IsBottomTouched { get; set; }
|
||
|
||
/// <summary>额外平面偏移(例如风、水流、拖拽)</summary>
|
||
public Vector2 ExternalPlanarOffset { get; set; }
|
||
|
||
// ── 事件 ──────────────────────────────────────────────────────
|
||
/// <summary>姿态切换完成时触发:(旧姿态, 新姿态)</summary>
|
||
public event System.Action<BobberPosture, BobberPosture> OnPostureChanged;
|
||
|
||
public bool TryGetWaterConstraintState(out BobberWaterConstraintState state)
|
||
{
|
||
state = default;
|
||
|
||
if (!UsesSolverWaterConstraint || _mode != BobberControlMode.WaterPresentation)
|
||
return false;
|
||
|
||
state.TargetPosition = _constraintTargetPosition;
|
||
state.PlanarStrength = Mathf.Clamp01(waterConstraintPlanarStrength);
|
||
state.UpwardStrength = Mathf.Clamp01(waterConstraintUpwardStrength);
|
||
state.DownwardStrength = Mathf.Clamp01(waterConstraintDownwardStrength);
|
||
state.MaxCorrection = Mathf.Max(0f, waterConstraintMaxCorrection);
|
||
return true;
|
||
}
|
||
|
||
public bool TryGetConstraintState(out FLineNodeConstraintState state)
|
||
{
|
||
state = default;
|
||
|
||
if (!TryGetWaterConstraintState(out BobberWaterConstraintState waterState))
|
||
return false;
|
||
|
||
state.TargetPosition = waterState.TargetPosition;
|
||
state.PlanarStrength = waterState.PlanarStrength;
|
||
state.UpwardStrength = waterState.UpwardStrength;
|
||
state.DownwardStrength = waterState.DownwardStrength;
|
||
state.MaxCorrection = waterState.MaxCorrection;
|
||
return true;
|
||
}
|
||
|
||
// ── 核心私有字段 ───────────────────────────────────────────────
|
||
private Rigidbody _rb;
|
||
private FLineLogicNode _logicNode;
|
||
private IWaterSurfaceProvider _waterProvider;
|
||
private BobberControlMode _mode = BobberControlMode.AirPhysics;
|
||
private BobberPosture _posture = BobberPosture.Lying;
|
||
private BobberWaterPhase _waterPhase = BobberWaterPhase.Stable;
|
||
|
||
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;
|
||
private Vector3 _constraintTargetPosition;
|
||
|
||
// 漂像运行时
|
||
private BobberBiteType _activeBiteType = BobberBiteType.None;
|
||
private float _biteTimer;
|
||
private float _biteDuration;
|
||
private float _biteAmplitude;
|
||
private Vector3 _blackDriftDirection;
|
||
private int _multiTapTotalCount;
|
||
private float _trembleRuntimeFreq = 10f;
|
||
|
||
// 姿态判断运行时
|
||
private float _verticalRatio;
|
||
private float _planarRatio;
|
||
private float _verticalDistance;
|
||
private float _planarDistance;
|
||
private BobberPosture _pendingPosture;
|
||
private float _pendingPostureTimer;
|
||
private float _postureCooldownTimer;
|
||
private Vector3 _stablePlanarDir = Vector3.forward;
|
||
|
||
// 姿态平滑混合(0=躺, 1=斜, 2=立)
|
||
private float _postureBlendValue;
|
||
private float _postureBlendVelocity;
|
||
|
||
// 旋转回弹弹簧(spring-damper)
|
||
private float _rotWobble;
|
||
private float _rotWobbleVel;
|
||
|
||
// 入水稳定动画
|
||
private float _entrySettleTimer;
|
||
private float _entrySettleOffsetY;
|
||
|
||
// 移漂运行时:驱动水面锚点在 XZ 平面移动到目标距离后停下
|
||
private bool _isDriftForceActive;
|
||
private Vector3 _driftStartAnchorPos;
|
||
private Vector3 _driftForceDirection;
|
||
private float _driftForceTimer;
|
||
private float _driftForceDuration;
|
||
|
||
// 水面起伏实例随机相位
|
||
private float _bobPhaseOffset;
|
||
|
||
// Crest 波面查询
|
||
private bool _hasCrestSampleThisFrame;
|
||
private readonly Vector3[] _waterQueryPoints = new Vector3[1];
|
||
private readonly Vector3[] _waterQueryResultDisplacements = new Vector3[1];
|
||
private readonly Vector3[] _waterQueryResultVelocities = new Vector3[1];
|
||
private readonly Vector3[] _waterQueryResultNormal = new Vector3[1];
|
||
|
||
private void Awake()
|
||
{
|
||
_rb = GetComponent<Rigidbody>();
|
||
_logicNode = GetComponent<FLineLogicNode>();
|
||
|
||
_defaultLinearDamping = _rb.linearDamping;
|
||
_defaultAngularDamping = _rb.angularDamping;
|
||
_defaultUseGravity = _rb.useGravity;
|
||
|
||
if (waterProviderBehaviour != null)
|
||
_waterProvider = waterProviderBehaviour as IWaterSurfaceProvider;
|
||
|
||
_pendingPosture = _posture;
|
||
_pendingPostureTimer = 0f;
|
||
_postureCooldownTimer = 0f;
|
||
|
||
_stablePlanarDir = Vector3.ProjectOnPlane(transform.forward, Vector3.up);
|
||
if (_stablePlanarDir.sqrMagnitude < 1e-6f) _stablePlanarDir = Vector3.forward;
|
||
else _stablePlanarDir.Normalize();
|
||
|
||
_targetRotation = transform.rotation;
|
||
_constraintTargetPosition = transform.position;
|
||
_postureBlendValue = PostureToBlend(_posture);
|
||
|
||
// 随机相位,避免多个浮漂同步起伏
|
||
_bobPhaseOffset = Random.value * Mathf.PI * 2f;
|
||
}
|
||
|
||
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(waterY, submergeDepth);
|
||
break;
|
||
case BobberControlMode.WaterPresentation:
|
||
UpdateWaterPresentation(waterY, submergeDepth);
|
||
break;
|
||
}
|
||
|
||
if (drawDebug)
|
||
DrawDebug(waterY);
|
||
}
|
||
|
||
#region Main Update
|
||
|
||
private void UpdateAirPhysics(float waterY, float submergeDepth)
|
||
{
|
||
RestoreAirPhysicsState();
|
||
UpdateAirHangRotation();
|
||
|
||
if (submergeDepth > enterWaterDepth)
|
||
EnterWaterPresentationMode(waterY);
|
||
}
|
||
|
||
private void UpdateWaterPresentation(float waterY, float submergeDepth)
|
||
{
|
||
if (submergeDepth < exitWaterDepth)
|
||
{
|
||
ExitWaterPresentationMode();
|
||
return;
|
||
}
|
||
|
||
if (UsesSolverWaterConstraint)
|
||
{
|
||
UpdateSolverDrivenWaterPresentation(waterY);
|
||
return;
|
||
}
|
||
|
||
// 入水后由脚本/约束统一驱动目标位姿,刚体速度压制掉避免额外干扰
|
||
_rb.useGravity = false;
|
||
_rb.linearVelocity = Vector3.zero;
|
||
_rb.angularVelocity = Vector3.zero;
|
||
_rb.linearDamping = 999f;
|
||
_rb.angularDamping = 999f;
|
||
|
||
TickWaterPresentationState();
|
||
|
||
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(
|
||
pos.y, targetY,
|
||
ref _ySmoothVelocity,
|
||
Mathf.Max(0.0001f, ySmoothTime),
|
||
maxYSpeed,
|
||
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;
|
||
_constraintTargetPosition = pos;
|
||
|
||
UpdateWaterPresentationRotation(waterY);
|
||
}
|
||
|
||
private void UpdateSolverDrivenWaterPresentation(float waterY)
|
||
{
|
||
_rb.useGravity = false;
|
||
_rb.angularVelocity = Vector3.zero;
|
||
_rb.linearDamping = _defaultLinearDamping;
|
||
_rb.angularDamping = _defaultAngularDamping;
|
||
|
||
TickWaterPresentationState();
|
||
UpdateConstraintTargetPosition(waterY);
|
||
UpdateWaterPresentationRotation(waterY);
|
||
}
|
||
|
||
private void TickWaterPresentationState()
|
||
{
|
||
if (_entrySettleTimer > 0f)
|
||
{
|
||
_entrySettleTimer -= Time.fixedDeltaTime;
|
||
if (_entrySettleTimer <= 0f)
|
||
{
|
||
_entrySettleTimer = 0f;
|
||
_waterPhase = _activeBiteType != BobberBiteType.None
|
||
? BobberWaterPhase.Biting
|
||
: BobberWaterPhase.Stable;
|
||
}
|
||
}
|
||
|
||
UpdateBiteAnimation();
|
||
UpdateDriftForce();
|
||
UpdateRotationWobble();
|
||
}
|
||
|
||
private void UpdateConstraintTargetPosition(float waterY)
|
||
{
|
||
Vector3 pos = _constraintTargetPosition;
|
||
|
||
float targetY = CalculateTargetY(waterY);
|
||
if (Mathf.Abs(pos.y - targetY) < yDeadZone)
|
||
{
|
||
pos.y = targetY;
|
||
_ySmoothVelocity = 0f;
|
||
}
|
||
else
|
||
{
|
||
pos.y = Mathf.SmoothDamp(
|
||
pos.y, targetY,
|
||
ref _ySmoothVelocity,
|
||
Mathf.Max(0.0001f, ySmoothTime),
|
||
maxYSpeed,
|
||
Time.fixedDeltaTime
|
||
);
|
||
}
|
||
|
||
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;
|
||
_constraintTargetPosition = pos;
|
||
}
|
||
|
||
private void UpdateWaterPresentationRotation(float waterY)
|
||
{
|
||
EvaluatePostureByComponents(waterY);
|
||
UpdateTargetRotationByPosture();
|
||
|
||
transform.rotation = Quaternion.Slerp(
|
||
transform.rotation,
|
||
_targetRotation,
|
||
1f - Mathf.Exp(-rotationLerpSpeed * Time.fixedDeltaTime)
|
||
);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Mode Switch
|
||
|
||
private void EnterWaterPresentationMode(float waterY)
|
||
{
|
||
_mode = BobberControlMode.WaterPresentation;
|
||
|
||
_waterAnchorPos = transform.position;
|
||
_constraintTargetPosition = transform.position;
|
||
_ySmoothVelocity = 0f;
|
||
_xzSmoothVelocity = Vector3.zero;
|
||
_biteOffsetY = 0f;
|
||
_biteOffsetYVelocity = 0f;
|
||
_activeBiteType = BobberBiteType.None;
|
||
_biteTimer = 0f;
|
||
|
||
// 启动入水稳定动画
|
||
_entrySettleTimer = Mathf.Max(0f, entrySettleDuration);
|
||
_entrySettleOffsetY = CalculateInitialEntrySettleOffset(waterY);
|
||
_waterPhase = BobberWaterPhase.Settling;
|
||
|
||
// 重置姿态至躺漂
|
||
_posture = BobberPosture.Lying;
|
||
_verticalRatio = 0f;
|
||
_planarRatio = 0f;
|
||
_verticalDistance = 0f;
|
||
_planarDistance = 0f;
|
||
_pendingPosture = _posture;
|
||
_pendingPostureTimer = 0f;
|
||
_postureCooldownTimer = 0f;
|
||
_postureBlendValue = PostureToBlend(_posture);
|
||
_postureBlendVelocity = 0f;
|
||
|
||
// 重置旋转弹簧
|
||
_rotWobble = 0f;
|
||
_rotWobbleVel = 0f;
|
||
|
||
// 重置移漂
|
||
_isDriftForceActive = false;
|
||
_driftStartAnchorPos = _waterAnchorPos;
|
||
_driftForceDirection = Vector3.zero;
|
||
_driftForceTimer = 0f;
|
||
_driftForceDuration = 0f;
|
||
|
||
_stablePlanarDir = Vector3.ProjectOnPlane(transform.forward, Vector3.up);
|
||
if (_stablePlanarDir.sqrMagnitude < 1e-6f) _stablePlanarDir = Vector3.forward;
|
||
else _stablePlanarDir.Normalize();
|
||
|
||
_rb.useGravity = false;
|
||
_rb.linearVelocity = Vector3.zero;
|
||
_rb.angularVelocity = Vector3.zero;
|
||
_rb.linearDamping = 999f;
|
||
_rb.angularDamping = 999f;
|
||
}
|
||
|
||
private void ExitWaterPresentationMode()
|
||
{
|
||
_mode = BobberControlMode.AirPhysics;
|
||
_waterPhase = BobberWaterPhase.Stable;
|
||
_constraintTargetPosition = transform.position;
|
||
_entrySettleOffsetY = 0f;
|
||
RestoreAirPhysicsState();
|
||
}
|
||
|
||
private void RestoreAirPhysicsState()
|
||
{
|
||
_rb.useGravity = _defaultUseGravity;
|
||
_rb.linearDamping = _defaultLinearDamping;
|
||
_rb.angularDamping = _defaultAngularDamping;
|
||
}
|
||
|
||
private void UpdateAirHangRotation()
|
||
{
|
||
if (!enableAirHangRotation || !_rb || _rb.isKinematic)
|
||
return;
|
||
|
||
Vector3 gravity = Physics.gravity;
|
||
if (gravity.sqrMagnitude < 1e-6f)
|
||
return;
|
||
|
||
Vector3 gravityDown = gravity.normalized;
|
||
Vector3 gravityUp = -gravityDown;
|
||
|
||
Vector3 supportUp = gravityUp;
|
||
bool hasUpperSupport = TryGetAirHangSupportDirection(gravityUp, out supportUp);
|
||
|
||
Vector3 targetDown = gravityDown;
|
||
|
||
Vector3 forwardHint = Vector3.zero;
|
||
if (hasUpperSupport)
|
||
{
|
||
Vector3 supportPlanar = Vector3.ProjectOnPlane(supportUp, targetDown);
|
||
if (supportPlanar.sqrMagnitude > 1e-6f)
|
||
{
|
||
supportPlanar.Normalize();
|
||
|
||
Vector3 stablePlanar = Vector3.ProjectOnPlane(_stablePlanarDir, targetDown);
|
||
if (stablePlanar.sqrMagnitude < 1e-6f)
|
||
stablePlanar = supportPlanar;
|
||
else
|
||
stablePlanar.Normalize();
|
||
|
||
forwardHint = Vector3.Slerp(stablePlanar, supportPlanar, Mathf.Clamp01(airHangLineInfluence));
|
||
}
|
||
}
|
||
|
||
if (forwardHint.sqrMagnitude < 1e-6f)
|
||
forwardHint = Vector3.ProjectOnPlane(_stablePlanarDir, targetDown);
|
||
|
||
if (forwardHint.sqrMagnitude < 1e-6f)
|
||
forwardHint = Vector3.ProjectOnPlane(transform.forward, targetDown);
|
||
|
||
if (forwardHint.sqrMagnitude < 1e-6f)
|
||
forwardHint = Vector3.ProjectOnPlane(Vector3.forward, targetDown);
|
||
|
||
if (forwardHint.sqrMagnitude < 1e-6f)
|
||
forwardHint = Vector3.ProjectOnPlane(Vector3.right, targetDown);
|
||
|
||
forwardHint.Normalize();
|
||
|
||
if (_stablePlanarDir.sqrMagnitude > 1e-6f && Vector3.Dot(forwardHint, _stablePlanarDir) < 0f)
|
||
forwardHint = -forwardHint;
|
||
|
||
Vector3 localDown = ResolveAirHangDownAxisLocal();
|
||
Quaternion targetRotation = BuildAxisAlignedRotation(targetDown, forwardHint, localDown);
|
||
float blend = 1f - Mathf.Exp(-Mathf.Max(0.01f, airHangRotationSpeed) * Time.fixedDeltaTime);
|
||
Quaternion nextRotation = Quaternion.Slerp(_rb.rotation, targetRotation, blend);
|
||
|
||
_rb.angularVelocity = Vector3.zero;
|
||
_rb.MoveRotation(nextRotation);
|
||
|
||
Vector3 projectedForward = Vector3.ProjectOnPlane(nextRotation * Vector3.forward, targetDown);
|
||
if (projectedForward.sqrMagnitude > 1e-6f)
|
||
_stablePlanarDir = projectedForward.normalized;
|
||
}
|
||
|
||
private bool TryGetAirHangSupportDirection(Vector3 gravityUp, out Vector3 supportUp)
|
||
{
|
||
supportUp = gravityUp;
|
||
|
||
if (_logicNode == null ||
|
||
!_logicNode.TryGetAdjacentBodies(out Rigidbody previousBody, out Rigidbody nextBody))
|
||
return false;
|
||
|
||
Vector3 currentPos = _rb.worldCenterOfMass;
|
||
Vector3 upperDirection = Vector3.zero;
|
||
float bestUpperDot = float.NegativeInfinity;
|
||
|
||
TryConsumeNeighbor(previousBody, currentPos, gravityUp, ref upperDirection, ref bestUpperDot);
|
||
TryConsumeNeighbor(nextBody, currentPos, gravityUp, ref upperDirection, ref bestUpperDot);
|
||
|
||
if (upperDirection.sqrMagnitude > 1e-6f && bestUpperDot > 0.05f)
|
||
{
|
||
supportUp = upperDirection;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static void TryConsumeNeighbor(
|
||
Rigidbody neighborBody,
|
||
Vector3 currentPos,
|
||
Vector3 gravityUp,
|
||
ref Vector3 upperDirection,
|
||
ref float bestUpperDot)
|
||
{
|
||
if (!neighborBody)
|
||
return;
|
||
|
||
Vector3 toNeighbor = neighborBody.worldCenterOfMass - currentPos;
|
||
if (toNeighbor.sqrMagnitude < 1e-6f)
|
||
return;
|
||
|
||
Vector3 dir = toNeighbor.normalized;
|
||
float upDot = Vector3.Dot(dir, gravityUp);
|
||
if (upDot > bestUpperDot)
|
||
{
|
||
bestUpperDot = upDot;
|
||
upperDirection = dir;
|
||
}
|
||
}
|
||
|
||
private Vector3 ResolveAirHangDownAxisLocal()
|
||
{
|
||
if (preferAirHangCenterOfMass && _rb)
|
||
{
|
||
Vector3 centerOfMassLocal = _rb.centerOfMass;
|
||
if (centerOfMassLocal.sqrMagnitude > 1e-6f)
|
||
return centerOfMassLocal.normalized;
|
||
}
|
||
|
||
return airHangDownAxisLocal.sqrMagnitude > 1e-6f
|
||
? airHangDownAxisLocal.normalized
|
||
: Vector3.down;
|
||
}
|
||
|
||
private Quaternion BuildAxisAlignedRotation(Vector3 targetDown, Vector3 forwardHint, Vector3 localDownAxis)
|
||
{
|
||
Vector3 safeDown = targetDown.sqrMagnitude > 1e-6f ? targetDown.normalized : Vector3.down;
|
||
Vector3 safeUp = -safeDown;
|
||
Vector3 safeForward = Vector3.ProjectOnPlane(forwardHint, safeDown);
|
||
if (safeForward.sqrMagnitude < 1e-6f)
|
||
safeForward = Vector3.ProjectOnPlane(Vector3.forward, safeDown);
|
||
if (safeForward.sqrMagnitude < 1e-6f)
|
||
safeForward = Vector3.ProjectOnPlane(Vector3.right, safeDown);
|
||
safeForward.Normalize();
|
||
|
||
Vector3 localDown = localDownAxis.sqrMagnitude > 1e-6f ? localDownAxis.normalized : Vector3.down;
|
||
Vector3 localUp = -localDown;
|
||
Vector3 localForward = Vector3.ProjectOnPlane(airHangForwardAxisLocal, localDown);
|
||
if (localForward.sqrMagnitude < 1e-6f)
|
||
localForward = Vector3.ProjectOnPlane(Vector3.forward, localDown);
|
||
if (localForward.sqrMagnitude < 1e-6f)
|
||
localForward = Vector3.ProjectOnPlane(Vector3.right, localDown);
|
||
localForward.Normalize();
|
||
|
||
Quaternion worldBasis = Quaternion.LookRotation(safeForward, safeUp);
|
||
Quaternion localBasis = Quaternion.LookRotation(localForward, localUp);
|
||
return worldBasis * Quaternion.Inverse(localBasis);
|
||
}
|
||
|
||
private float CalculateInitialEntrySettleOffset(float waterY)
|
||
{
|
||
float baseTargetY = CalculateBaseTargetY(waterY);
|
||
float currentOffset = Mathf.Min(0f, transform.position.y - baseTargetY);
|
||
float configuredDip = -Mathf.Max(0f, entryDipAmplitude);
|
||
return Mathf.Min(currentOffset, configuredDip);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Target Calculation
|
||
|
||
private float CalculateTargetY(float waterY)
|
||
{
|
||
float targetPivotY = CalculateBaseTargetY(waterY);
|
||
|
||
if (_entrySettleTimer <= 0f || entrySettleDuration <= 0f)
|
||
return targetPivotY;
|
||
|
||
float phase = 1f - Mathf.Clamp01(_entrySettleTimer / Mathf.Max(0.001f, entrySettleDuration));
|
||
float riseT = Mathf.SmoothStep(0f, 1f, phase);
|
||
return targetPivotY + Mathf.Lerp(_entrySettleOffsetY, 0f, riseT);
|
||
}
|
||
|
||
private float CalculateBaseTargetY(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 && !_hasCrestSampleThisFrame)
|
||
{
|
||
float t = Time.time;
|
||
float primary = Mathf.Sin(t * surfaceBobFrequency * Mathf.PI * 2f + _bobPhaseOffset)
|
||
* surfaceBobAmplitude;
|
||
float secondary = Mathf.Sin(t * surfaceBobFrequency * 1.618f * Mathf.PI * 2f + _bobPhaseOffset * 2.1f)
|
||
* surfaceBobAmplitude * surfaceBobSecondaryRatio;
|
||
surfaceBob = primary + secondary;
|
||
}
|
||
|
||
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 || _isDriftForceActive
|
||
? _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 bd = _blackDriftDirection * drift;
|
||
basePos += new Vector3(bd.x, 0f, bd.z);
|
||
}
|
||
|
||
return new Vector3(
|
||
basePos.x + planarOffset.x,
|
||
transform.position.y,
|
||
basePos.z + planarOffset.y
|
||
);
|
||
}
|
||
|
||
private void EvaluatePostureByComponents(float waterY)
|
||
{
|
||
float submergeRatio = Mathf.Clamp01(
|
||
(waterY - GetBottomWorldPosition().y) / Mathf.Max(0.0001f, floatHeight)
|
||
);
|
||
|
||
bool hasLure = lureBody != null;
|
||
|
||
if (!hasLure)
|
||
{
|
||
_verticalDistance = 0f;
|
||
_planarDistance = 0f;
|
||
_verticalRatio = 0f;
|
||
_planarRatio = 0f;
|
||
}
|
||
else
|
||
{
|
||
Vector3 bobberPos = _rb.worldCenterOfMass;
|
||
Vector3 lurePos = lureBody.worldCenterOfMass;
|
||
Vector3 delta = lurePos - bobberPos;
|
||
|
||
_verticalDistance = Mathf.Max(0f, Vector3.Dot(delta, Vector3.down));
|
||
_planarDistance = Vector3.ProjectOnPlane(delta, Vector3.up).magnitude;
|
||
|
||
float refLen = Mathf.Max(0.0001f, referenceLength);
|
||
_verticalRatio = _verticalDistance / refLen;
|
||
_planarRatio = _planarDistance / refLen;
|
||
}
|
||
|
||
BobberPosture desired = DeterminePostureState(submergeRatio, hasLure);
|
||
ApplyPostureWithStability(desired);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 姿态状态判断的唯一入口。改规则只改这里,外部负责数据采样和旋转平滑。
|
||
/// </summary>
|
||
private BobberPosture DeterminePostureState(float submergeRatio, bool hasLure)
|
||
{
|
||
if (UseTestPosture)
|
||
return TestPosture;
|
||
|
||
// 无 Lure 时的兜底规则
|
||
if (!hasLure)
|
||
{
|
||
if (submergeRatio < minSubmergeToStand)
|
||
return BobberPosture.Lying;
|
||
if (ExternalPlanarOffset.magnitude > 0.01f)
|
||
return BobberPosture.Tilted;
|
||
return BobberPosture.Upright;
|
||
}
|
||
|
||
// 带滞回的状态机:从当前状态出发,减少来回切换
|
||
switch (_posture)
|
||
{
|
||
case BobberPosture.Lying:
|
||
{
|
||
bool canStandUpright =
|
||
submergeRatio >= minSubmergeToStand &&
|
||
_verticalRatio > verticalUprightThreshold + postureHysteresis &&
|
||
_planarRatio < planarTiltThreshold - postureHysteresis;
|
||
|
||
bool canTilt =
|
||
submergeRatio >= minSubmergeToStand * 0.8f &&
|
||
_verticalRatio > verticalLieThreshold + postureHysteresis;
|
||
|
||
if (canStandUpright) return BobberPosture.Upright;
|
||
if (canTilt) return BobberPosture.Tilted;
|
||
return BobberPosture.Lying;
|
||
}
|
||
|
||
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) return BobberPosture.Lying;
|
||
if (shouldStand) return BobberPosture.Upright;
|
||
return BobberPosture.Tilted;
|
||
}
|
||
|
||
default: // 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) return BobberPosture.Lying;
|
||
if (shouldTilt) return BobberPosture.Tilted;
|
||
return BobberPosture.Upright;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ApplyPostureWithStability(BobberPosture desiredPosture)
|
||
{
|
||
_postureCooldownTimer = Mathf.Max(0f, _postureCooldownTimer - Time.fixedDeltaTime);
|
||
|
||
if (desiredPosture == _posture)
|
||
{
|
||
_pendingPosture = _posture;
|
||
_pendingPostureTimer = 0f;
|
||
return;
|
||
}
|
||
|
||
if (_postureCooldownTimer > 0f)
|
||
{
|
||
_pendingPosture = desiredPosture;
|
||
_pendingPostureTimer = 0f;
|
||
return;
|
||
}
|
||
|
||
if (_pendingPosture != desiredPosture)
|
||
{
|
||
_pendingPosture = desiredPosture;
|
||
_pendingPostureTimer = 0f;
|
||
return;
|
||
}
|
||
|
||
_pendingPostureTimer += Time.fixedDeltaTime;
|
||
if (_pendingPostureTimer >= Mathf.Max(0f, postureConfirmTime))
|
||
{
|
||
BobberPosture oldPosture = _posture;
|
||
_posture = desiredPosture;
|
||
_pendingPosture = _posture;
|
||
_pendingPostureTimer = 0f;
|
||
_postureCooldownTimer = Mathf.Max(0f, postureSwitchCooldown);
|
||
|
||
OnPostureTransition(oldPosture, _posture);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 姿态切换完成时:触发外部事件,并向弹簧注入回弹初速。
|
||
/// </summary>
|
||
private void OnPostureTransition(BobberPosture from, BobberPosture to)
|
||
{
|
||
OnPostureChanged?.Invoke(from, to);
|
||
|
||
if (!enablePostureWobble) return;
|
||
|
||
// (int)Lying=0 < Tilted=1 < Upright=2
|
||
// 向更立漂方向切换 → 角度减小 → 初始抖动为负(先过冲再弹回)
|
||
// 向更躺漂方向切换 → 角度增大 → 初始抖动为正
|
||
float sign = (int)to > (int)from ? -1f : 1f;
|
||
_rotWobbleVel += sign * postureWobbleStrength;
|
||
}
|
||
|
||
private void UpdateTargetRotationByPosture()
|
||
{
|
||
// ── 更新平面稳定方向 ────────────────────────────────────────
|
||
Vector3 candidateDir = Vector3.zero;
|
||
|
||
if (lureBody != null)
|
||
candidateDir = Vector3.ProjectOnPlane(
|
||
lureBody.worldCenterOfMass - _rb.worldCenterOfMass, Vector3.up);
|
||
|
||
if (candidateDir.sqrMagnitude < 1e-6f)
|
||
candidateDir = new Vector3(_xzSmoothVelocity.x, 0f, _xzSmoothVelocity.z);
|
||
|
||
if (candidateDir.sqrMagnitude < 1e-6f)
|
||
candidateDir = new Vector3(ExternalPlanarOffset.x, 0f, ExternalPlanarOffset.y);
|
||
|
||
if (_stablePlanarDir.sqrMagnitude < 1e-6f)
|
||
{
|
||
_stablePlanarDir = Vector3.ProjectOnPlane(transform.forward, Vector3.up);
|
||
if (_stablePlanarDir.sqrMagnitude < 1e-6f) _stablePlanarDir = Vector3.forward;
|
||
}
|
||
|
||
_stablePlanarDir.Normalize();
|
||
|
||
float dirDeadZone = Mathf.Max(0.0001f, planarDirectionDeadZone);
|
||
if (candidateDir.sqrMagnitude > dirDeadZone * dirDeadZone)
|
||
{
|
||
candidateDir.Normalize();
|
||
|
||
// 避免 180° 翻转闪烁
|
||
if (Vector3.Dot(candidateDir, _stablePlanarDir) < 0f)
|
||
candidateDir = -candidateDir;
|
||
|
||
float k = 1f - Mathf.Exp(-Mathf.Max(0.01f, planarDirectionLerpSpeed) * Time.fixedDeltaTime);
|
||
_stablePlanarDir = Vector3.Slerp(_stablePlanarDir, candidateDir, k);
|
||
_stablePlanarDir.Normalize();
|
||
}
|
||
|
||
// ── 姿态混合值平滑(0=躺, 1=斜, 2=立)──────────────────────
|
||
float targetBlend = PostureToBlend(_posture);
|
||
|
||
// 躺↔斜 用较短时间,斜↔立 用较长时间(立漂更稳重)
|
||
bool involveUpright = (_postureBlendValue > 1f) || (targetBlend > 1f);
|
||
float blendTime = involveUpright ? postureBlendSmoothTimeUpright : postureBlendSmoothTime;
|
||
|
||
_postureBlendValue = Mathf.SmoothDamp(
|
||
_postureBlendValue, targetBlend,
|
||
ref _postureBlendVelocity,
|
||
Mathf.Max(0.001f, blendTime),
|
||
Mathf.Infinity,
|
||
Time.fixedDeltaTime
|
||
);
|
||
|
||
// ── 从混合值计算目标倾角 ────────────────────────────────────
|
||
float baseAngle;
|
||
if (_postureBlendValue <= 1f)
|
||
{
|
||
// 躺漂(lyingAngle) ↔ 斜漂(tiltedAngle)
|
||
baseAngle = Mathf.Lerp(lyingAngle, tiltedAngle, _postureBlendValue);
|
||
}
|
||
else
|
||
{
|
||
// 斜漂(tiltedAngle) ↔ 立漂(uprightTilt)
|
||
// 立漂时根据平面拉力给出微小附加倾角,planarTiltFactor 控制灵敏度
|
||
float uprightTilt = Mathf.Clamp(_planarRatio * planarTiltFactor, 0f, uprightMaxTiltAngle);
|
||
baseAngle = Mathf.Lerp(tiltedAngle, uprightTilt, _postureBlendValue - 1f);
|
||
}
|
||
|
||
// 叠加旋转回弹抖动
|
||
float finalAngle = baseAngle + _rotWobble;
|
||
|
||
// ── 构建目标旋转 ─────────────────────────────────────────────
|
||
Vector3 tiltAxis = Vector3.Cross(Vector3.up, _stablePlanarDir);
|
||
if (tiltAxis.sqrMagnitude < 1e-6f)
|
||
tiltAxis = transform.right;
|
||
|
||
_targetRotation = Quaternion.AngleAxis(finalAngle, tiltAxis.normalized);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 临界阻尼弹簧:k = decay², damper = 2*decay,每帧调用以衰减旋转抖动。
|
||
/// </summary>
|
||
private void UpdateRotationWobble()
|
||
{
|
||
if (Mathf.Abs(_rotWobble) < 0.01f && Mathf.Abs(_rotWobbleVel) < 0.01f)
|
||
{
|
||
_rotWobble = 0f;
|
||
_rotWobbleVel = 0f;
|
||
return;
|
||
}
|
||
|
||
float d = Mathf.Max(0.01f, postureWobbleDecay);
|
||
float spring = -(d * d) * _rotWobble;
|
||
float damper = -(2f * d) * _rotWobbleVel;
|
||
|
||
_rotWobbleVel += (spring + damper) * Time.fixedDeltaTime;
|
||
_rotWobble += _rotWobbleVel * Time.fixedDeltaTime;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Bite Presentation
|
||
|
||
/// <summary>轻点:快速下顿再回弹</summary>
|
||
public void PlayTap(float amplitude = 0.008f, float duration = 0.18f)
|
||
{
|
||
StartBite(BobberBiteType.Tap, amplitude, duration);
|
||
}
|
||
|
||
/// <summary>连顿:count 次连续下顿,每顿之间轻微回弹,振幅略微衰减</summary>
|
||
public void PlayMultiTap(int count = 3, float amplitude = 0.006f, float intervalPerTap = 0.22f)
|
||
{
|
||
_multiTapTotalCount = Mathf.Max(1, count);
|
||
StartBite(BobberBiteType.MultiTap, amplitude, intervalPerTap * _multiTapTotalCount);
|
||
}
|
||
|
||
/// <summary>缓沉:分段下沉,40% 处有短暂停顿感(鱼吞饵试探)</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 = 1.2f)
|
||
{
|
||
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;
|
||
}
|
||
|
||
/// <summary>微颤:高频小幅抖动,振幅随时间衰减(鱼蹭饵试探)</summary>
|
||
public void PlayTremble(float amplitude = 0.004f, float duration = 1.5f, float frequency = 10f)
|
||
{
|
||
_trembleRuntimeFreq = Mathf.Max(1f, frequency);
|
||
StartBite(BobberBiteType.Tremble, amplitude, duration);
|
||
}
|
||
|
||
/// <summary>移漂:按给定世界方向在 XZ 平面平移指定距离。</summary>
|
||
public void PlayDrift(Vector3 direction, float distance)
|
||
{
|
||
PlayDrift(direction, distance, driftForceDuration);
|
||
}
|
||
|
||
/// <summary>移漂:沿给定方向在 XZ 平面移动指定距离,并在目标点停下。</summary>
|
||
public void PlayDrift(Vector3 direction, float distance, float duration)
|
||
{
|
||
if (_mode != BobberControlMode.WaterPresentation)
|
||
return;
|
||
|
||
Vector3 planarDirection = Vector3.ProjectOnPlane(direction, Vector3.up);
|
||
if (planarDirection.sqrMagnitude < 1e-6f || Mathf.Approximately(distance, 0f))
|
||
return;
|
||
|
||
float safeDuration = Mathf.Max(0.02f, duration);
|
||
|
||
SyncWaterAnchorToCurrentXZ();
|
||
|
||
_driftStartAnchorPos = _waterAnchorPos;
|
||
_driftForceDirection = planarDirection.normalized * distance;
|
||
_driftForceTimer = 0f;
|
||
_driftForceDuration = safeDuration;
|
||
_isDriftForceActive = true;
|
||
_xzSmoothVelocity = Vector3.zero;
|
||
}
|
||
|
||
public void StopDrift()
|
||
{
|
||
_isDriftForceActive = false;
|
||
_driftForceTimer = 0f;
|
||
_driftForceDuration = 0f;
|
||
_driftForceDirection = Vector3.zero;
|
||
_xzSmoothVelocity = Vector3.zero;
|
||
SyncWaterAnchorToCurrentXZ();
|
||
}
|
||
|
||
public void StopBite()
|
||
{
|
||
_activeBiteType = BobberBiteType.None;
|
||
_biteTimer = 0f;
|
||
_biteDuration = 0f;
|
||
_biteAmplitude = 0f;
|
||
_biteOffsetY = 0f;
|
||
_biteOffsetYVelocity = 0f;
|
||
|
||
if (_waterPhase == BobberWaterPhase.Biting)
|
||
_waterPhase = BobberWaterPhase.Stable;
|
||
}
|
||
|
||
private void UpdateDriftForce()
|
||
{
|
||
if (!_isDriftForceActive)
|
||
return;
|
||
|
||
_driftForceTimer += Time.fixedDeltaTime;
|
||
float t = Mathf.Clamp01(_driftForceTimer / Mathf.Max(0.0001f, _driftForceDuration));
|
||
float easedT = Mathf.SmoothStep(0f, 1f, t);
|
||
_waterAnchorPos = _driftStartAnchorPos + _driftForceDirection * easedT;
|
||
|
||
if (t < 1f)
|
||
return;
|
||
|
||
_waterAnchorPos = _driftStartAnchorPos + _driftForceDirection;
|
||
_isDriftForceActive = false;
|
||
_driftForceTimer = 0f;
|
||
_driftForceDuration = 0f;
|
||
_driftForceDirection = Vector3.zero;
|
||
_xzSmoothVelocity = Vector3.zero;
|
||
}
|
||
|
||
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;
|
||
|
||
if (_waterPhase != BobberWaterPhase.Settling)
|
||
_waterPhase = BobberWaterPhase.Biting;
|
||
}
|
||
|
||
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:
|
||
{
|
||
// 前 35% 快速下顿,后 65% 回弹
|
||
targetOffset = t < 0.35f
|
||
? -Mathf.SmoothStep(0f, _biteAmplitude, t / 0.35f)
|
||
: -Mathf.Lerp(_biteAmplitude, 0f, (t - 0.35f) / 0.65f);
|
||
break;
|
||
}
|
||
|
||
case BobberBiteType.MultiTap:
|
||
{
|
||
// 每顿时间均分,每顿内模拟单次 Tap 曲线,振幅随顿次轻微衰减
|
||
int totalTaps = Mathf.Max(1, _multiTapTotalCount);
|
||
float tapLength = 1f / totalTaps;
|
||
float cycleT = Mathf.Repeat(t, tapLength) / tapLength;
|
||
float tapIndex = Mathf.Floor(t / tapLength);
|
||
float decay = 1f - tapIndex / totalTaps * 0.25f; // 末顿约 75% 振幅
|
||
|
||
targetOffset = cycleT < 0.35f
|
||
? -Mathf.SmoothStep(0f, _biteAmplitude * decay, cycleT / 0.35f)
|
||
: -Mathf.Lerp(_biteAmplitude * decay, 0f, (cycleT - 0.35f) / 0.65f);
|
||
break;
|
||
}
|
||
|
||
case BobberBiteType.SlowSink:
|
||
{
|
||
// 三段式缓沉:0~40% 渐沉至一半,40~60% 短暂停顿,60~100% 继续下沉
|
||
float sinkOffset;
|
||
if (t < 0.4f)
|
||
sinkOffset = Mathf.SmoothStep(0f, _biteAmplitude * 0.5f, t / 0.4f);
|
||
else if (t < 0.6f)
|
||
sinkOffset = _biteAmplitude * 0.5f;
|
||
else
|
||
sinkOffset = Mathf.SmoothStep(_biteAmplitude * 0.5f, _biteAmplitude, (t - 0.6f) / 0.4f);
|
||
targetOffset = -sinkOffset;
|
||
break;
|
||
}
|
||
|
||
case BobberBiteType.Lift:
|
||
{
|
||
// 前 60% 平滑送起,60%~100% 顶部轻微振荡后稳住
|
||
if (t < 0.6f)
|
||
{
|
||
targetOffset = Mathf.SmoothStep(0f, _biteAmplitude, t / 0.6f);
|
||
}
|
||
else
|
||
{
|
||
float wobbleT = (t - 0.6f) / 0.4f;
|
||
float wobble = Mathf.Sin(wobbleT * Mathf.PI * 3f)
|
||
* _biteAmplitude * 0.12f * (1f - wobbleT);
|
||
targetOffset = _biteAmplitude + wobble;
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case BobberBiteType.BlackDrift:
|
||
{
|
||
// 前 60% 快速深沉,之后维持最大深度
|
||
float sinkT = Mathf.SmoothStep(0f, 1f, Mathf.Min(t / 0.6f, 1f));
|
||
targetOffset = -_biteAmplitude * sinkT;
|
||
break;
|
||
}
|
||
|
||
case BobberBiteType.Tremble:
|
||
{
|
||
// 高频振荡,振幅线性衰减至 0
|
||
float decay2 = 1f - t;
|
||
targetOffset = Mathf.Sin(Time.time * _trembleRuntimeFreq * Mathf.PI * 2f)
|
||
* _biteAmplitude * decay2;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Tremble 需要快速响应,其余用标准平滑时间
|
||
float smoothTime = _activeBiteType == BobberBiteType.Tremble ? 0.01f : 0.03f;
|
||
_biteOffsetY = Mathf.SmoothDamp(
|
||
_biteOffsetY, targetOffset,
|
||
ref _biteOffsetYVelocity,
|
||
smoothTime, Mathf.Infinity, Time.fixedDeltaTime
|
||
);
|
||
|
||
// 持续类漂像(缓沉/黑漂)到达终点后不自动结束,等外部调用 StopBite
|
||
if (_biteTimer >= _biteDuration)
|
||
{
|
||
if (_activeBiteType == BobberBiteType.SlowSink ||
|
||
_activeBiteType == BobberBiteType.BlackDrift)
|
||
return;
|
||
|
||
_activeBiteType = BobberBiteType.None;
|
||
if (_waterPhase == BobberWaterPhase.Biting)
|
||
_waterPhase = BobberWaterPhase.Stable;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Utilities
|
||
|
||
private static float PostureToBlend(BobberPosture posture) => posture switch
|
||
{
|
||
BobberPosture.Lying => 0f,
|
||
BobberPosture.Tilted => 1f,
|
||
_ => 2f
|
||
};
|
||
|
||
private float GetWaterHeight(Vector3 worldPos)
|
||
{
|
||
if (_waterProvider != null)
|
||
{
|
||
_hasCrestSampleThisFrame = false;
|
||
return _waterProvider.GetWaterHeight(worldPos);
|
||
}
|
||
|
||
_hasCrestSampleThisFrame = false;
|
||
return fallbackWaterLevel;
|
||
}
|
||
|
||
private Vector3 GetBottomWorldPosition()
|
||
{
|
||
return transform.TransformPoint(new Vector3(0f, bottomOffsetLocalY, 0f));
|
||
}
|
||
|
||
private void SyncWaterAnchorToCurrentXZ()
|
||
{
|
||
Vector2 planarOffset = Vector2.ClampMagnitude(ExternalPlanarOffset, maxPlanarOffset);
|
||
Vector3 pos = transform.position;
|
||
_waterAnchorPos = new Vector3(
|
||
pos.x - planarOffset.x,
|
||
_waterAnchorPos.y,
|
||
pos.z - planarOffset.y
|
||
);
|
||
}
|
||
|
||
private void HandleDebugKeys()
|
||
{
|
||
if (!Application.isPlaying) return;
|
||
|
||
if (debugResetKey && Input.GetKeyDown(KeyCode.R)) StopBite();
|
||
if (debugTapKey && Input.GetKeyDown(KeyCode.T)) PlayTap();
|
||
if (debugMultiTapKey && Input.GetKeyDown(KeyCode.Y)) PlayMultiTap();
|
||
if (debugSlowSinkKey && Input.GetKeyDown(KeyCode.G)) PlaySlowSink();
|
||
if (debugLiftKey && Input.GetKeyDown(KeyCode.H)) PlayLift();
|
||
if (debugBlackDriftKey && Input.GetKeyDown(KeyCode.B)) PlayBlackDrift();
|
||
if (debugTrembleKey && Input.GetKeyDown(KeyCode.V)) PlayTremble();
|
||
if (debugDriftKey && Input.GetKeyDown(KeyCode.N)) PlayDrift(transform.forward, 0.1f);
|
||
}
|
||
|
||
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.AirPhysics && enableAirHangRotation)
|
||
{
|
||
Vector3 hangDown = transform.TransformDirection(ResolveAirHangDownAxisLocal());
|
||
Debug.DrawLine(p, p + hangDown * 0.08f, Color.gray);
|
||
}
|
||
|
||
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);
|
||
|
||
// 姿态混合值可视化:白线高度代表 PostureBlendValue(0~2)
|
||
Debug.DrawLine(p, p + Vector3.up * (_postureBlendValue * 0.02f), Color.white);
|
||
|
||
if (UsesSolverWaterConstraint)
|
||
{
|
||
Debug.DrawLine(p, _constraintTargetPosition, Color.cyan);
|
||
Debug.DrawLine(
|
||
_constraintTargetPosition + Vector3.left * 0.02f,
|
||
_constraintTargetPosition + Vector3.right * 0.02f,
|
||
Color.cyan
|
||
);
|
||
}
|
||
}
|
||
|
||
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
|
||
private void OnValidate()
|
||
{
|
||
floatHeight = Mathf.Max(0.001f, floatHeight);
|
||
airHangRotationSpeed = Mathf.Max(0.01f, airHangRotationSpeed);
|
||
airHangLineInfluence = Mathf.Clamp01(airHangLineInfluence);
|
||
ySmoothTime = Mathf.Max(0.001f, ySmoothTime);
|
||
maxYSpeed = Mathf.Max(0.01f, maxYSpeed);
|
||
xzSmoothTime = Mathf.Max(0.001f, xzSmoothTime);
|
||
driftForceDuration = Mathf.Max(0.02f, driftForceDuration);
|
||
waterConstraintMaxCorrection = Mathf.Max(0f, waterConstraintMaxCorrection);
|
||
rotationLerpSpeed = Mathf.Max(0.01f, rotationLerpSpeed);
|
||
maxPlanarOffset = Mathf.Max(0f, maxPlanarOffset);
|
||
waterConstraintPlanarStrength = Mathf.Clamp01(waterConstraintPlanarStrength);
|
||
waterConstraintUpwardStrength = Mathf.Clamp01(waterConstraintUpwardStrength);
|
||
waterConstraintDownwardStrength = Mathf.Clamp01(waterConstraintDownwardStrength);
|
||
downForceToSink = Mathf.Max(0f, downForceToSink);
|
||
maxExtraSink = Mathf.Max(0f, maxExtraSink);
|
||
surfaceBobAmplitude = Mathf.Max(0f, surfaceBobAmplitude);
|
||
surfaceBobFrequency = Mathf.Max(0f, surfaceBobFrequency);
|
||
surfaceBobSecondaryRatio = Mathf.Clamp01(surfaceBobSecondaryRatio);
|
||
yDeadZone = Mathf.Max(0f, yDeadZone);
|
||
|
||
if (airHangDownAxisLocal.sqrMagnitude < 1e-6f)
|
||
airHangDownAxisLocal = Vector3.down;
|
||
|
||
if (airHangForwardAxisLocal.sqrMagnitude < 1e-6f)
|
||
airHangForwardAxisLocal = Vector3.forward;
|
||
|
||
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);
|
||
postureConfirmTime = Mathf.Max(0f, postureConfirmTime);
|
||
postureSwitchCooldown = Mathf.Max(0f, postureSwitchCooldown);
|
||
|
||
tiltedAngle = Mathf.Clamp(tiltedAngle, 0f, 89f);
|
||
lyingAngle = Mathf.Clamp(lyingAngle, tiltedAngle, 89.9f);
|
||
uprightMaxTiltAngle = Mathf.Clamp(uprightMaxTiltAngle, 0f, tiltedAngle);
|
||
planarTiltFactor = Mathf.Max(0f, planarTiltFactor);
|
||
planarDirectionDeadZone = Mathf.Max(0.0001f, planarDirectionDeadZone);
|
||
planarDirectionLerpSpeed = Mathf.Max(0.01f, planarDirectionLerpSpeed);
|
||
|
||
postureBlendSmoothTime = Mathf.Max(0.001f, postureBlendSmoothTime);
|
||
postureBlendSmoothTimeUpright = Mathf.Max(0.001f, postureBlendSmoothTimeUpright);
|
||
postureWobbleStrength = Mathf.Max(0f, postureWobbleStrength);
|
||
postureWobbleDecay = Mathf.Max(0.01f, postureWobbleDecay);
|
||
entryDipAmplitude = Mathf.Max(0f, entryDipAmplitude);
|
||
entrySettleDuration = Mathf.Max(0f, entrySettleDuration);
|
||
}
|
||
#endif
|
||
|
||
#endregion
|
||
}
|
||
} |