Files
Fishing2/Assets/Scripts/Test/New/BobberPresentationController.cs
2026-03-22 22:34:05 +08:00

842 lines
25 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;
/// <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, // 轻点
SlowSink, // 缓沉
Lift, // 送漂
BlackDrift // 黑漂/快速拖入
}
public enum BobberPosture
{
Lying,
Tilted,
Upright
}
[DisallowMultipleComponent]
[RequireComponent(typeof(Rigidbody))]
public class BobberPresentationController : MonoBehaviour
{
[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("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("水面轻微起伏频率")]
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 Rotation")]
[Tooltip("倾斜状态角度")]
public float tiltedAngle = 38f;
[Tooltip("躺漂角度")]
public float lyingAngle = 88f;
[Tooltip("立漂时允许的最大附加倾角")]
public float uprightMaxTiltAngle = 8f;
[Tooltip("平面方向对立漂/斜漂附加倾角的影响强度")]
public float planarTiltFactor = 120f;
[Tooltip("姿态平滑速度")]
public float rotationLerpSpeed = 8f;
[Header("Debug Input")]
[Tooltip("调试:按 R 恢复默认")]
public bool debugResetKey = true;
[Tooltip("调试:按 T 触发轻点")]
public bool debugTapKey = true;
[Tooltip("调试:按 G 触发缓沉")]
public bool debugSlowSinkKey = true;
[Tooltip("调试:按 H 触发送漂")]
public bool debugLiftKey = true;
[Tooltip("调试:按 B 触发黑漂")]
public bool debugBlackDriftKey = true;
[Header("Debug")]
public bool drawDebug = false;
public BobberControlMode CurrentMode => _mode;
public BobberPosture CurrentPosture => _posture;
public float CurrentVerticalRatio => _verticalRatio;
public float CurrentPlanarRatio => _planarRatio;
/// <summary>外部可写:等效向下拉力(不是必须是真实力,作为输入信号即可)</summary>
public float ExternalDownForce { get; set; }
/// <summary>外部可写:是否触底</summary>
public bool IsBottomTouched { get; set; }
/// <summary>外部可写:额外平面偏移(例如风、水流、拖拽)</summary>
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 Vector3 _waterAnchorPos;
private Vector3 _xzSmoothVelocity;
private float _ySmoothVelocity;
private float _biteOffsetY;
private float _biteOffsetYVelocity;
private Quaternion _targetRotation;
// bite event runtime
private BobberBiteType _activeBiteType = BobberBiteType.None;
private float _biteTimer;
private float _biteDuration;
private float _biteAmplitude;
private Vector3 _blackDriftDirection;
// posture runtime
private float _verticalRatio;
private float _planarRatio;
private float _verticalDistance;
private float _planarDistance;
private void Awake()
{
_rb = GetComponent<Rigidbody>();
_defaultLinearDamping = _rb.linearDamping;
_defaultAngularDamping = _rb.angularDamping;
_defaultUseGravity = _rb.useGravity;
if (waterProviderBehaviour != null)
_waterProvider = waterProviderBehaviour as IWaterSurfaceProvider;
_targetRotation = transform.rotation;
}
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(submergeDepth);
break;
case BobberControlMode.WaterPresentation:
UpdateWaterPresentation(waterY, submergeDepth);
break;
}
if (drawDebug)
{
DrawDebug(waterY);
}
}
#region Main Update
private void UpdateAirPhysics(float submergeDepth)
{
RestoreAirPhysicsState();
if (submergeDepth > enterWaterDepth)
{
EnterWaterPresentationMode();
}
}
private void UpdateWaterPresentation(float waterY, float submergeDepth)
{
if (submergeDepth < exitWaterDepth)
{
ExitWaterPresentationMode();
return;
}
// 完全关闭刚体干扰
_rb.useGravity = false;
_rb.linearVelocity = Vector3.zero;
_rb.angularVelocity = Vector3.zero;
_rb.linearDamping = 999f;
_rb.angularDamping = 999f;
UpdateBiteAnimation();
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(
current: pos.y,
target: targetY,
currentVelocity: ref _ySmoothVelocity,
smoothTime: Mathf.Max(0.0001f, ySmoothTime),
maxSpeed: maxYSpeed,
deltaTime: 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;
// 3. 姿态判定 + 目标旋转
EvaluatePostureByComponents(waterY);
UpdateTargetRotationByPosture();
transform.rotation = Quaternion.Slerp(
transform.rotation,
_targetRotation,
1f - Mathf.Exp(-rotationLerpSpeed * Time.fixedDeltaTime)
);
}
#endregion
#region Mode Switch
private void EnterWaterPresentationMode()
{
_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;
_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()
{
_rb.useGravity = _defaultUseGravity;
_rb.linearDamping = _defaultLinearDamping;
_rb.angularDamping = _defaultAngularDamping;
}
#endregion
#region Target Calculation
private float CalculateTargetY(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)
{
surfaceBob = Mathf.Sin(Time.time * surfaceBobFrequency * Mathf.PI * 2f) * surfaceBobAmplitude;
}
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 ? _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 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 submergeRatio = Mathf.Clamp01(
(waterY - GetBottomWorldPosition().y) / Mathf.Max(0.0001f, floatHeight)
);
if (lureBody == null)
{
_verticalDistance = 0f;
_planarDistance = 0f;
_verticalRatio = 0f;
_planarRatio = 0f;
if (submergeRatio < minSubmergeToStand)
_posture = BobberPosture.Lying;
else if (ExternalPlanarOffset.magnitude > 0.01f)
_posture = BobberPosture.Tilted;
else
_posture = BobberPosture.Upright;
return;
}
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;
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)
{
_posture = BobberPosture.Upright;
}
else if (canTilt)
{
_posture = BobberPosture.Tilted;
}
break;
}
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)
{
_posture = BobberPosture.Lying;
}
else if (shouldStand)
{
_posture = BobberPosture.Upright;
}
break;
}
case BobberPosture.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)
{
_posture = BobberPosture.Lying;
}
else if (shouldTilt)
{
_posture = BobberPosture.Tilted;
}
break;
}
}
}
private void UpdateTargetRotationByPosture()
{
Vector3 planarDir = Vector3.zero;
if (lureBody != null)
{
Vector3 delta = lureBody.worldCenterOfMass - _rb.worldCenterOfMass;
planarDir = Vector3.ProjectOnPlane(delta, Vector3.up);
}
if (planarDir.sqrMagnitude < 1e-6f)
{
planarDir = new Vector3(_xzSmoothVelocity.x, 0f, _xzSmoothVelocity.z);
}
if (planarDir.sqrMagnitude < 1e-6f)
{
planarDir = new Vector3(ExternalPlanarOffset.x, 0f, ExternalPlanarOffset.y);
}
if (planarDir.sqrMagnitude < 1e-6f)
{
planarDir = transform.forward;
}
planarDir.Normalize();
Vector3 tiltAxis = Vector3.Cross(Vector3.up, planarDir);
if (tiltAxis.sqrMagnitude < 1e-6f)
{
tiltAxis = transform.right;
}
float angle;
switch (_posture)
{
case BobberPosture.Lying:
angle = lyingAngle;
break;
case BobberPosture.Tilted:
{
float extra = Mathf.Clamp(_planarRatio * planarTiltFactor, 0f, 18f);
angle = Mathf.Clamp(tiltedAngle + extra, tiltedAngle, lyingAngle);
break;
}
default:
{
float extra = Mathf.Clamp(_planarRatio * planarTiltFactor, 0f, uprightMaxTiltAngle);
angle = extra;
break;
}
}
_targetRotation = Quaternion.AngleAxis(angle, tiltAxis.normalized);
}
#endregion
#region Bite Presentation
/// <summary>
/// 轻点:快速下顿再回弹
/// </summary>
public void PlayTap(float amplitude = 0.008f, float duration = 0.18f)
{
StartBite(BobberBiteType.Tap, amplitude, duration);
}
/// <summary>
/// 缓沉:在持续时间内逐渐下沉
/// </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;
}
public void StopBite()
{
_activeBiteType = BobberBiteType.None;
_biteTimer = 0f;
_biteDuration = 0f;
_biteAmplitude = 0f;
_biteOffsetY = 0f;
_biteOffsetYVelocity = 0f;
}
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()
{
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:
if (t < 0.35f)
{
float k = t / 0.35f;
targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, k);
}
else
{
float 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,
Time.fixedDeltaTime
);
if (_biteTimer >= _biteDuration)
{
if (_activeBiteType == BobberBiteType.SlowSink || _activeBiteType == BobberBiteType.BlackDrift)
{
return;
}
_activeBiteType = BobberBiteType.None;
}
}
#endregion
#region Utilities
private float GetWaterHeight(Vector3 worldPos)
{
return _waterProvider != null ? _waterProvider.GetWaterHeight(worldPos) : 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)
{
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.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);
}
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);
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);
tiltedAngle = Mathf.Clamp(tiltedAngle, 0f, 89f);
lyingAngle = Mathf.Clamp(lyingAngle, tiltedAngle, 89.9f);
uprightMaxTiltAngle = Mathf.Clamp(uprightMaxTiltAngle, 0f, tiltedAngle);
planarTiltFactor = Mathf.Max(0f, planarTiltFactor);
}
#endif
#endregion
}