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(); } 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 } }