945 lines
31 KiB
C#
945 lines
31 KiB
C#
using UnityEngine;
|
||
|
||
namespace NBF
|
||
{
|
||
public interface IWaterSurfaceProvider
|
||
{
|
||
float GetWaterHeight(Vector3 worldPos);
|
||
Vector3 GetWaterNormal(Vector3 worldPos);
|
||
}
|
||
|
||
public enum BobberControlMode
|
||
{
|
||
AirPhysics,
|
||
WaterPresentation,
|
||
}
|
||
|
||
public enum BobberBiteType
|
||
{
|
||
None,
|
||
Tap,
|
||
SlowSink,
|
||
Lift,
|
||
BlackDrift,
|
||
}
|
||
|
||
public enum BobberPosture
|
||
{
|
||
Lying,
|
||
Tilted,
|
||
Upright,
|
||
}
|
||
|
||
[DisallowMultipleComponent]
|
||
[RequireComponent(typeof(Rigidbody))]
|
||
public class FishingFloatFeature : FishingLineNodeMotionFeature
|
||
{
|
||
protected override int DefaultPriority => 100;
|
||
|
||
[Header("Water")]
|
||
[Tooltip("没有水提供器时使用固定水位")]
|
||
public float fallbackWaterLevel;
|
||
|
||
[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;
|
||
|
||
[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("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("平面方向对立漂/斜漂附加倾角的影响强度")]
|
||
public float planarTiltFactor = 120f;
|
||
|
||
[Tooltip("平面方向死区,小于该值时保持上一帧方向")]
|
||
public float planarDirectionDeadZone = 0.01f;
|
||
|
||
[Tooltip("平面方向平滑速度")]
|
||
public float planarDirectionLerpSpeed = 10f;
|
||
|
||
[Tooltip("姿态平滑速度")]
|
||
public float rotationLerpSpeed = 8f;
|
||
|
||
[Header("Debug Input")]
|
||
public bool debugResetKey = true;
|
||
public bool debugTapKey = true;
|
||
public bool debugSlowSinkKey = true;
|
||
public bool debugLiftKey = true;
|
||
public bool debugBlackDriftKey = true;
|
||
|
||
[Header("Debug")]
|
||
public bool drawDebug;
|
||
|
||
public bool UseTestPosture;
|
||
public BobberPosture TestPosture;
|
||
|
||
public BobberControlMode CurrentMode => _mode;
|
||
public BobberPosture CurrentPosture => _posture;
|
||
public float CurrentVerticalRatio => _verticalRatio;
|
||
public float CurrentPlanarRatio => _planarRatio;
|
||
|
||
public float ExternalDownForce { get; set; }
|
||
public bool IsBottomTouched { get; set; }
|
||
public Vector2 ExternalPlanarOffset { get; set; }
|
||
|
||
private Rigidbody _rb;
|
||
private IWaterSurfaceProvider _waterProvider;
|
||
private BobberControlMode _mode = BobberControlMode.AirPhysics;
|
||
private BobberPosture _posture = BobberPosture.Lying;
|
||
|
||
private float _defaultLinearDamping;
|
||
private float _defaultAngularDamping;
|
||
private bool _defaultUseGravity;
|
||
private bool _defaultsCached;
|
||
|
||
private Vector3 _waterAnchorPos;
|
||
private Vector3 _xzSmoothVelocity;
|
||
private float _ySmoothVelocity;
|
||
private float _biteOffsetY;
|
||
private float _biteOffsetYVelocity;
|
||
private Quaternion _targetRotation;
|
||
|
||
private BobberBiteType _activeBiteType = BobberBiteType.None;
|
||
private float _biteTimer;
|
||
private float _biteDuration;
|
||
private float _biteAmplitude;
|
||
private Vector3 _blackDriftDirection;
|
||
|
||
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;
|
||
|
||
private void Awake()
|
||
{
|
||
EnsureRuntimeReferences();
|
||
InitializeRuntimeState();
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
HandleDebugKeys();
|
||
}
|
||
|
||
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 waterY = GetWaterHeight(transform.position);
|
||
var submergeDepth = waterY - GetBottomWorldPosition().y;
|
||
if (_mode == BobberControlMode.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 waterY = GetWaterHeight(transform.position);
|
||
var submergeDepth = waterY - GetBottomWorldPosition().y;
|
||
UpdateWaterPresentation(waterY, submergeDepth, deltaTime);
|
||
|
||
if (drawDebug)
|
||
{
|
||
DrawDebug(waterY);
|
||
}
|
||
}
|
||
|
||
public void PlayTap(float amplitude = 0.008f, float duration = 0.18f)
|
||
{
|
||
StartBite(BobberBiteType.Tap, amplitude, duration);
|
||
}
|
||
|
||
public void PlaySlowSink(float amplitude = 0.025f, float duration = 1.2f)
|
||
{
|
||
StartBite(BobberBiteType.SlowSink, amplitude, duration);
|
||
}
|
||
|
||
public void PlayLift(float amplitude = 0.015f, float duration = 1.2f)
|
||
{
|
||
StartBite(BobberBiteType.Lift, amplitude, duration);
|
||
}
|
||
|
||
public void PlayBlackDrift(float amplitude = 0.06f, float duration = 0.8f, Vector3? driftDirection = null)
|
||
{
|
||
StartBite(BobberBiteType.BlackDrift, amplitude, duration);
|
||
_blackDriftDirection = (driftDirection ?? transform.forward).normalized;
|
||
}
|
||
|
||
public void StopBite()
|
||
{
|
||
_activeBiteType = BobberBiteType.None;
|
||
_biteTimer = 0f;
|
||
_biteDuration = 0f;
|
||
_biteAmplitude = 0f;
|
||
_biteOffsetY = 0f;
|
||
_biteOffsetYVelocity = 0f;
|
||
}
|
||
|
||
private void EnsureRuntimeReferences()
|
||
{
|
||
if (_rb == null)
|
||
{
|
||
_rb = Node != null && Node.Body != null ? Node.Body : GetComponent<Rigidbody>();
|
||
}
|
||
|
||
if (_waterProvider == null && waterProviderBehaviour != null)
|
||
{
|
||
_waterProvider = waterProviderBehaviour as IWaterSurfaceProvider;
|
||
}
|
||
|
||
}
|
||
|
||
private void InitializeRuntimeState()
|
||
{
|
||
if (_rb == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!_defaultsCached)
|
||
{
|
||
_defaultLinearDamping = _rb.linearDamping;
|
||
_defaultAngularDamping = _rb.angularDamping;
|
||
_defaultUseGravity = _rb.useGravity;
|
||
_defaultsCached = true;
|
||
}
|
||
|
||
_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;
|
||
}
|
||
|
||
private void UpdateWaterPresentation(float waterY, float submergeDepth, float deltaTime)
|
||
{
|
||
if (submergeDepth < exitWaterDepth)
|
||
{
|
||
ExitWaterPresentationMode();
|
||
return;
|
||
}
|
||
|
||
_rb.useGravity = false;
|
||
_rb.linearVelocity = Vector3.zero;
|
||
_rb.angularVelocity = Vector3.zero;
|
||
_rb.linearDamping = 999f;
|
||
_rb.angularDamping = 999f;
|
||
|
||
UpdateBiteAnimation(deltaTime);
|
||
|
||
var pos = transform.position;
|
||
var 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,
|
||
deltaTime);
|
||
}
|
||
|
||
var targetXZ = CalculateTargetXZ();
|
||
var planarPos = new Vector3(pos.x, 0f, pos.z);
|
||
var planarTarget = new Vector3(targetXZ.x, 0f, targetXZ.z);
|
||
planarPos = Vector3.SmoothDamp(
|
||
planarPos,
|
||
planarTarget,
|
||
ref _xzSmoothVelocity,
|
||
Mathf.Max(0.0001f, xzSmoothTime),
|
||
Mathf.Infinity,
|
||
deltaTime);
|
||
|
||
pos.x = planarPos.x;
|
||
pos.z = planarPos.z;
|
||
transform.position = pos;
|
||
|
||
EvaluatePostureByComponents(waterY, deltaTime);
|
||
UpdateTargetRotationByPosture(deltaTime);
|
||
|
||
transform.rotation = Quaternion.Slerp(
|
||
transform.rotation,
|
||
_targetRotation,
|
||
1f - Mathf.Exp(-rotationLerpSpeed * deltaTime));
|
||
}
|
||
|
||
private void EnterWaterPresentationMode()
|
||
{
|
||
if (_rb == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_mode = BobberControlMode.WaterPresentation;
|
||
_waterAnchorPos = transform.position;
|
||
_ySmoothVelocity = 0f;
|
||
_xzSmoothVelocity = Vector3.zero;
|
||
_biteOffsetY = 0f;
|
||
_biteOffsetYVelocity = 0f;
|
||
_activeBiteType = BobberBiteType.None;
|
||
_biteTimer = 0f;
|
||
|
||
_posture = BobberPosture.Lying;
|
||
_verticalRatio = 0f;
|
||
_planarRatio = 0f;
|
||
_verticalDistance = 0f;
|
||
_planarDistance = 0f;
|
||
_pendingPosture = _posture;
|
||
_pendingPostureTimer = 0f;
|
||
_postureCooldownTimer = 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;
|
||
RestoreAirPhysicsState();
|
||
}
|
||
|
||
private void RestoreAirPhysicsState()
|
||
{
|
||
if (_rb == null || !_defaultsCached)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_rb.useGravity = _defaultUseGravity;
|
||
_rb.linearDamping = _defaultLinearDamping;
|
||
_rb.angularDamping = _defaultAngularDamping;
|
||
}
|
||
|
||
private float CalculateTargetY(float waterY)
|
||
{
|
||
var baseSinkDepth = floatHeight * Mathf.Clamp01(baseSubmergeRatio);
|
||
var sinkByForce = Mathf.Clamp(ExternalDownForce * downForceToSink, 0f, maxExtraSink);
|
||
|
||
var bottomAdjust = 0f;
|
||
if (enableBottomTouchAdjust && IsBottomTouched)
|
||
{
|
||
bottomAdjust -= bottomTouchLift;
|
||
}
|
||
|
||
var surfaceBob = 0f;
|
||
if (enableSurfaceBobbing)
|
||
{
|
||
surfaceBob = Mathf.Sin(Time.time * surfaceBobFrequency * Mathf.PI * 2f) * surfaceBobAmplitude;
|
||
}
|
||
|
||
var totalSink = baseSinkDepth + sinkByForce + bottomAdjust;
|
||
var targetBottomY = waterY - totalSink;
|
||
return targetBottomY - bottomOffsetLocalY + surfaceBob + _biteOffsetY;
|
||
}
|
||
|
||
private Vector3 CalculateTargetXZ()
|
||
{
|
||
var planarOffset = Vector2.ClampMagnitude(ExternalPlanarOffset, maxPlanarOffset);
|
||
var basePos = lockXZAroundAnchor ? _waterAnchorPos : transform.position;
|
||
|
||
if (_activeBiteType == BobberBiteType.BlackDrift)
|
||
{
|
||
var t = Mathf.Clamp01(_biteDuration > 0f ? _biteTimer / _biteDuration : 1f);
|
||
var drift = Mathf.SmoothStep(0f, 1f, t) * 0.08f;
|
||
var 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 EvaluatePostureByComponents(float waterY, float deltaTime)
|
||
{
|
||
var submergeRatio = Mathf.Clamp01(
|
||
(waterY - GetBottomWorldPosition().y) / Mathf.Max(0.0001f, floatHeight));
|
||
|
||
var hasLure = lureBody != null;
|
||
if (!hasLure)
|
||
{
|
||
_verticalDistance = 0f;
|
||
_planarDistance = 0f;
|
||
_verticalRatio = 0f;
|
||
_planarRatio = 0f;
|
||
}
|
||
else
|
||
{
|
||
var bobberPos = _rb.worldCenterOfMass;
|
||
var lurePos = lureBody.worldCenterOfMass;
|
||
var delta = lurePos - bobberPos;
|
||
|
||
_verticalDistance = Mathf.Max(0f, Vector3.Dot(delta, Vector3.down));
|
||
_planarDistance = Vector3.ProjectOnPlane(delta, Vector3.up).magnitude;
|
||
|
||
var refLen = Mathf.Max(0.0001f, referenceLength);
|
||
_verticalRatio = _verticalDistance / refLen;
|
||
_planarRatio = _planarDistance / refLen;
|
||
}
|
||
|
||
var desiredPosture = DeterminePostureState(submergeRatio, hasLure);
|
||
ApplyPostureWithStability(desiredPosture, deltaTime);
|
||
}
|
||
|
||
private BobberPosture DeterminePostureState(float submergeRatio, bool hasLure)
|
||
{
|
||
if (UseTestPosture)
|
||
{
|
||
return TestPosture;
|
||
}
|
||
|
||
if (!hasLure)
|
||
{
|
||
if (submergeRatio < minSubmergeToStand)
|
||
{
|
||
return BobberPosture.Lying;
|
||
}
|
||
|
||
if (ExternalPlanarOffset.magnitude > 0.01f)
|
||
{
|
||
return BobberPosture.Tilted;
|
||
}
|
||
|
||
return BobberPosture.Upright;
|
||
}
|
||
|
||
switch (_posture)
|
||
{
|
||
case BobberPosture.Lying:
|
||
{
|
||
var canStandUpright =
|
||
submergeRatio >= minSubmergeToStand &&
|
||
_verticalRatio > verticalUprightThreshold + postureHysteresis &&
|
||
_planarRatio < planarTiltThreshold - postureHysteresis;
|
||
|
||
var canTilt =
|
||
submergeRatio >= minSubmergeToStand * 0.8f &&
|
||
_verticalRatio > verticalLieThreshold + postureHysteresis;
|
||
|
||
if (canStandUpright)
|
||
{
|
||
return BobberPosture.Upright;
|
||
}
|
||
|
||
if (canTilt)
|
||
{
|
||
return BobberPosture.Tilted;
|
||
}
|
||
|
||
return BobberPosture.Lying;
|
||
}
|
||
|
||
case BobberPosture.Tilted:
|
||
{
|
||
var shouldLie =
|
||
submergeRatio < minSubmergeToStand * 0.75f ||
|
||
_verticalRatio < verticalLieThreshold - postureHysteresis ||
|
||
_planarDistance > _verticalDistance * planarDominanceMultiplier;
|
||
|
||
var shouldStand =
|
||
submergeRatio >= minSubmergeToStand &&
|
||
_verticalRatio > verticalUprightThreshold + postureHysteresis &&
|
||
_planarRatio < planarTiltThreshold - postureHysteresis;
|
||
|
||
if (shouldLie)
|
||
{
|
||
return BobberPosture.Lying;
|
||
}
|
||
|
||
if (shouldStand)
|
||
{
|
||
return BobberPosture.Upright;
|
||
}
|
||
|
||
return BobberPosture.Tilted;
|
||
}
|
||
|
||
default:
|
||
{
|
||
var shouldLie =
|
||
submergeRatio < minSubmergeToStand * 0.75f ||
|
||
_verticalRatio < verticalLieThreshold - postureHysteresis ||
|
||
_planarDistance > _verticalDistance * (planarDominanceMultiplier + 0.15f);
|
||
|
||
var 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, float deltaTime)
|
||
{
|
||
_postureCooldownTimer = Mathf.Max(0f, _postureCooldownTimer - deltaTime);
|
||
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 += deltaTime;
|
||
if (_pendingPostureTimer >= Mathf.Max(0f, postureConfirmTime))
|
||
{
|
||
_posture = desiredPosture;
|
||
_pendingPosture = _posture;
|
||
_pendingPostureTimer = 0f;
|
||
_postureCooldownTimer = Mathf.Max(0f, postureSwitchCooldown);
|
||
}
|
||
}
|
||
|
||
private void UpdateTargetRotationByPosture(float deltaTime)
|
||
{
|
||
var candidateDir = Vector3.zero;
|
||
if (lureBody != null)
|
||
{
|
||
var delta = lureBody.worldCenterOfMass - _rb.worldCenterOfMass;
|
||
candidateDir = Vector3.ProjectOnPlane(delta, 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();
|
||
|
||
var dirDeadZone = Mathf.Max(0.0001f, planarDirectionDeadZone);
|
||
if (candidateDir.sqrMagnitude > dirDeadZone * dirDeadZone)
|
||
{
|
||
candidateDir.Normalize();
|
||
if (Vector3.Dot(candidateDir, _stablePlanarDir) < 0f)
|
||
{
|
||
candidateDir = -candidateDir;
|
||
}
|
||
|
||
var k = 1f - Mathf.Exp(-Mathf.Max(0.01f, planarDirectionLerpSpeed) * deltaTime);
|
||
_stablePlanarDir = Vector3.Slerp(_stablePlanarDir, candidateDir, k);
|
||
_stablePlanarDir.Normalize();
|
||
}
|
||
|
||
var planarDir = _stablePlanarDir;
|
||
var tiltAxis = Vector3.Cross(Vector3.up, planarDir);
|
||
if (tiltAxis.sqrMagnitude < 1e-6f)
|
||
{
|
||
tiltAxis = transform.right;
|
||
}
|
||
|
||
var angle = _posture switch
|
||
{
|
||
BobberPosture.Lying => lyingAngle,
|
||
BobberPosture.Tilted => tiltedAngle,
|
||
_ => 0f,
|
||
};
|
||
|
||
_targetRotation = Quaternion.AngleAxis(angle, tiltAxis.normalized);
|
||
}
|
||
|
||
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(float deltaTime)
|
||
{
|
||
if (_activeBiteType == BobberBiteType.None)
|
||
{
|
||
_biteOffsetY = Mathf.SmoothDamp(
|
||
_biteOffsetY,
|
||
0f,
|
||
ref _biteOffsetYVelocity,
|
||
0.08f,
|
||
Mathf.Infinity,
|
||
deltaTime);
|
||
return;
|
||
}
|
||
|
||
_biteTimer += deltaTime;
|
||
var t = Mathf.Clamp01(_biteTimer / _biteDuration);
|
||
var targetOffset = 0f;
|
||
|
||
switch (_activeBiteType)
|
||
{
|
||
case BobberBiteType.Tap:
|
||
if (t < 0.35f)
|
||
{
|
||
var k = t / 0.35f;
|
||
targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, k);
|
||
}
|
||
else
|
||
{
|
||
var 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,
|
||
deltaTime);
|
||
|
||
if (_biteTimer >= _biteDuration &&
|
||
_activeBiteType != BobberBiteType.SlowSink &&
|
||
_activeBiteType != BobberBiteType.BlackDrift)
|
||
{
|
||
_activeBiteType = BobberBiteType.None;
|
||
}
|
||
}
|
||
|
||
private float GetWaterHeight(Vector3 worldPos)
|
||
{
|
||
if (_waterProvider != null)
|
||
{
|
||
return _waterProvider.GetWaterHeight(worldPos);
|
||
}
|
||
|
||
return 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)
|
||
{
|
||
var p = transform.position;
|
||
var 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)
|
||
{
|
||
var 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 (lureBody != null)
|
||
{
|
||
var bobber = _rb.worldCenterOfMass;
|
||
var lure = lureBody.worldCenterOfMass;
|
||
Debug.DrawLine(bobber, lure, Color.magenta);
|
||
|
||
var verticalEnd = bobber + Vector3.down * _verticalDistance;
|
||
Debug.DrawLine(bobber, verticalEnd, Color.red);
|
||
|
||
var 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);
|
||
ySmoothTime = Mathf.Max(0.001f, ySmoothTime);
|
||
maxYSpeed = Mathf.Max(0.01f, maxYSpeed);
|
||
xzSmoothTime = Mathf.Max(0.001f, xzSmoothTime);
|
||
rotationLerpSpeed = Mathf.Max(0.01f, rotationLerpSpeed);
|
||
|
||
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);
|
||
|
||
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);
|
||
}
|
||
#endif
|
||
}
|
||
}
|