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

945 lines
31 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 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
}
}