From fbe33f9f3a7be2dcfa4e51d40b346cd320341802 Mon Sep 17 00:00:00 2001 From: "Bob.Song" <605277374@qq.com> Date: Sun, 12 Apr 2026 23:52:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/Fishing/New/View/FishingLine.meta | 3 + .../Fishing/New/View/FishingLine/Feature.meta | 8 + .../Feature/FishingBobberFeature.cs | 685 ++++++++++++++ .../Feature/FishingBobberFeature.cs.meta | 3 + .../Feature/FishingDefaultPhysicsFeature.cs | 44 + .../FishingDefaultPhysicsFeature.cs.meta | 3 + .../Feature/FishingFloatFeature.cs | 890 ++++++++++++++++++ .../Feature/FishingFloatFeature.cs.meta | 3 + .../Feature/FishingLineNodeFeature.cs | 63 ++ .../Feature/FishingLineNodeFeature.cs.meta | 3 + .../Feature/FishingLineNodeMotionFeature.cs | 47 + .../FishingLineNodeMotionFeature.cs.meta | 3 + .../New/View/FishingLine/FishingLineNode.cs | 33 + .../View/FishingLine/FishingLineNode.cs.meta | 3 + .../New/View/FishingLine/FishingLineSolver.cs | 8 + .../FishingLine/FishingLineSolver.cs.meta | 3 + 16 files changed, 1802 insertions(+) create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine.meta create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Feature.meta create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingBobberFeature.cs create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingBobberFeature.cs.meta create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingDefaultPhysicsFeature.cs create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingDefaultPhysicsFeature.cs.meta create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingFloatFeature.cs create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingFloatFeature.cs.meta create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeFeature.cs create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeFeature.cs.meta create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeMotionFeature.cs create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeMotionFeature.cs.meta create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/FishingLineNode.cs create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/FishingLineNode.cs.meta create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/FishingLineSolver.cs create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/FishingLineSolver.cs.meta diff --git a/Assets/Scripts/Fishing/New/View/FishingLine.meta b/Assets/Scripts/Fishing/New/View/FishingLine.meta new file mode 100644 index 000000000..2809d8ba6 --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 11c32e9f078948c880079b4903f7c8bd +timeCreated: 1776008844 \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Feature.meta b/Assets/Scripts/Fishing/New/View/FishingLine/Feature.meta new file mode 100644 index 000000000..6e4df7d6c --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Feature.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2ff164cb3132445289d22c7b3a0f4fab +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingBobberFeature.cs b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingBobberFeature.cs new file mode 100644 index 000000000..538242d30 --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingBobberFeature.cs @@ -0,0 +1,685 @@ +using UnityEngine; + +namespace NBF +{ + public enum FishingBobberControlMode + { + AirPhysics, + WaterPresentation, + } + + public enum FishingBobberBiteType + { + None, + Tap, + SlowSink, + Lift, + BlackDrift, + } + + public enum BobberTiltAxis + { + LocalX, + LocalZ, + } + + [DisallowMultipleComponent] + [RequireComponent(typeof(Rigidbody))] + public class FishingBobberFeature : FishingLineNodeMotionFeature + { + protected override int DefaultPriority => 100; + + #region 检测入水 + + [Header("入水检测")] + [Tooltip("当前测试阶段固定水面高度。")] + public float waterLevel = 0f; + + [Tooltip("浮漂底部进入水面达到该深度后,切换为水面表现控制。")] + public float enterWaterDepth = 0.002f; + + [Tooltip("浮漂底部高于该深度时退出水面表现控制。通常设为负值用于滞回。")] + public float exitWaterDepth = -0.01f; + + [Tooltip("浮漂总高度,单位米。")] + public float floatHeight = 0.08f; + + [Tooltip("如果 Pivot 在浮漂底部填 0;如果 Pivot 在模型中部,填底部相对 Pivot 的本地 Y 偏移。")] + public float bottomOffsetLocalY; + + [Tooltip("Y 轴控制的平滑时间。")] + public float ySmoothTime = 0.08f; + + [Tooltip("Y 轴平滑时允许的最大竖直速度。")] + public float maxYSpeed = 2f; + + [Tooltip("Y 轴死区,小范围内直接贴目标值以减少微抖。")] + public float yDeadZone = 0.0005f; + + #endregion + + #region 吃水控制 + + [Header("吃水控制")] + [Tooltip("基准底部重量。当前重量等于该值时,使用基础吃水深度。")] + public float neutralBottomWeight = 1f; + + [Tooltip("当前底部总重量。运行时可由其他脚本更新。")] + public float currentBottomWeight = 1f; + + [Tooltip("基准重量下的基础吃水深度。")] + public float baseDraftDepth = 0.02f; + + [Tooltip("每单位重量变化对应增加或减少的吃水深度。")] + public float draftDepthPerWeight = 0.01f; + + [Tooltip("吃水深度下限。")] + public float minDraftDepth = 0.005f; + + [Tooltip("吃水深度上限。")] + public float maxDraftDepth = 0.08f; + + [Tooltip("吃水深度变化的平滑时间。")] + public float draftSmoothTime = 0.18f; + + #endregion + + #region 漂相动画 + + [Header("漂相动画")] + [Tooltip("漂相位移动画的平滑时间。")] + public float biteSmoothTime = 0.03f; + + [Tooltip("点漂默认振幅。")] + public float tapAmplitude = 0.008f; + + [Tooltip("点漂默认时长。")] + public float tapDuration = 0.18f; + + [Tooltip("缓沉默认振幅。")] + public float slowSinkAmplitude = 0.025f; + + [Tooltip("缓沉默认时长。")] + public float slowSinkDuration = 1.2f; + + [Tooltip("顶漂默认振幅。")] + public float liftAmplitude = 0.015f; + + [Tooltip("顶漂默认时长。")] + public float liftDuration = 1.2f; + + [Tooltip("黑漂默认振幅。")] + public float blackDriftAmplitude = 0.06f; + + [Tooltip("黑漂默认时长。")] + public float blackDriftDuration = 0.8f; + + #endregion + + #region 输入测试 + + [Header("输入测试")] + [Tooltip("是否启用运行时按键测试漂相。")] + public bool enableDebugInput = true; + + [Tooltip("停止当前漂相的按键。")] + public KeyCode stopBiteKey = KeyCode.R; + + [Tooltip("触发点漂的按键。")] + public KeyCode tapKey = KeyCode.T; + + [Tooltip("触发缓沉的按键。")] + public KeyCode slowSinkKey = KeyCode.G; + + [Tooltip("触发顶漂的按键。")] + public KeyCode liftKey = KeyCode.H; + + [Tooltip("触发黑漂的按键。")] + public KeyCode blackDriftKey = KeyCode.B; + + #endregion + + #region 姿态控制 + + [Header("姿态控制")] + [Tooltip("重量低于该值时,姿态趋向躺漂。")] + public float lyingWeightThreshold = 0.4f; + + [Tooltip("重量达到该值附近时,姿态趋向半躺。")] + public float tiltedWeightThreshold = 0.8f; + + [Tooltip("重量达到该值及以上时,姿态趋向立漂。")] + public float uprightWeightThreshold = 1.2f; + + [Tooltip("躺漂对应的倾角。")] + public float lyingAngle = 88f; + + [Tooltip("半躺对应的倾角。")] + public float tiltedAngle = 42f; + + [Tooltip("立漂对应的倾角,通常为 0。")] + public float uprightAngle = 0f; + + [Tooltip("绕哪个本地轴做倾倒。")] + public BobberTiltAxis tiltAxis = BobberTiltAxis.LocalX; + + [Tooltip("是否反转倾倒方向。")] + public bool invertTiltDirection; + + [Tooltip("姿态旋转的平滑速度。")] + public float rotationLerpSpeed = 8f; + + [Tooltip("入水后用于压制旋转抖动的角阻尼。")] + public float waterAngularDamping = 999f; + + #endregion + + #region 运行时状态 + + public FishingBobberControlMode CurrentMode => _mode; + public FishingBobberBiteType CurrentBiteType => _activeBiteType; + public float CurrentDraftDepth => _currentDraftDepth; + public float CurrentBottomWeight => currentBottomWeight; + + private Rigidbody _rb; + private FishingBobberControlMode _mode = FishingBobberControlMode.AirPhysics; + + private bool _defaultsCached; + private bool _waterStateInitialized; + private float _defaultAngularDamping; + private bool _defaultUseGravity; + private RigidbodyConstraints _defaultConstraints; + + private float _draftVelocity; + private float _currentDraftDepth; + private float _ySmoothVelocity; + private float _biteOffsetY; + private float _biteOffsetYVelocity; + + private bool _uprightPoseCached; + private Quaternion _cachedUprightRotation; + private Quaternion _uprightReferenceRotation; + private Quaternion _targetRotation; + + private FishingBobberBiteType _activeBiteType = FishingBobberBiteType.None; + private float _biteTimer; + private float _biteDuration; + private float _biteAmplitude; + + #endregion + + #region Unity 生命周期 + + private void Awake() + { + EnsureRuntimeReferences(); + InitializeRuntimeState(); + } + + private void Update() + { + HandleDebugInput(); + } + + #endregion + + #region 运动控制接入 + + 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 submergeDepth = GetSubmergeDepth(); + if (_mode == FishingBobberControlMode.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 submergeDepth = GetSubmergeDepth(); + if (submergeDepth < exitWaterDepth) + { + ExitWaterPresentationMode(); + return; + } + + if (_mode != FishingBobberControlMode.WaterPresentation) + { + EnterWaterPresentationMode(); + } + + UpdateBiteAnimation(deltaTime); + UpdateDraft(deltaTime); + var nextRotation = CalculateNextRotation(deltaTime); + UpdateVerticalPosition(deltaTime, nextRotation); + ApplyRotation(nextRotation); + } + + #endregion + + #region 漂相接口 + + public void SetBottomWeight(float weight) + { + currentBottomWeight = weight; + } + + public void PlayTap(float amplitude = -1f, float duration = -1f) + { + StartBite( + FishingBobberBiteType.Tap, + amplitude > 0f ? amplitude : tapAmplitude, + duration > 0f ? duration : tapDuration); + } + + public void PlaySlowSink(float amplitude = -1f, float duration = -1f) + { + StartBite( + FishingBobberBiteType.SlowSink, + amplitude > 0f ? amplitude : slowSinkAmplitude, + duration > 0f ? duration : slowSinkDuration); + } + + public void PlayLift(float amplitude = -1f, float duration = -1f) + { + StartBite( + FishingBobberBiteType.Lift, + amplitude > 0f ? amplitude : liftAmplitude, + duration > 0f ? duration : liftDuration); + } + + public void PlayBlackDrift(float amplitude = -1f, float duration = -1f) + { + StartBite( + FishingBobberBiteType.BlackDrift, + amplitude > 0f ? amplitude : blackDriftAmplitude, + duration > 0f ? duration : blackDriftDuration); + } + + public void StopBite() + { + _activeBiteType = FishingBobberBiteType.None; + _biteTimer = 0f; + _biteDuration = 0f; + _biteAmplitude = 0f; + } + + #endregion + + #region 内部实现 + + private void EnsureRuntimeReferences() + { + if (_rb == null) + { + _rb = Node != null && Node.Body != null ? Node.Body : GetComponent(); + } + } + + private void InitializeRuntimeState() + { + if (_rb == null) + { + return; + } + + if (!_defaultsCached) + { + _defaultAngularDamping = _rb.angularDamping; + _defaultUseGravity = _rb.useGravity; + _defaultConstraints = _rb.constraints; + _defaultsCached = true; + } + + _currentDraftDepth = CalculateRawDraftDepth(); + _draftVelocity = 0f; + _ySmoothVelocity = 0f; + _biteOffsetY = 0f; + _biteOffsetYVelocity = 0f; + _targetRotation = _rb.rotation; + + if (!_uprightPoseCached) + { + _cachedUprightRotation = _rb.rotation; + _uprightPoseCached = true; + } + + _uprightReferenceRotation = _cachedUprightRotation; + } + + private void EnterWaterPresentationMode() + { + if (_rb == null) + { + return; + } + + _mode = FishingBobberControlMode.WaterPresentation; + _waterStateInitialized = true; + _uprightReferenceRotation = _cachedUprightRotation; + _targetRotation = _rb.rotation; + _draftVelocity = 0f; + _ySmoothVelocity = 0f; + _biteOffsetYVelocity = 0f; + _currentDraftDepth = CalculateRawDraftDepth(); + + _rb.useGravity = false; + _rb.angularDamping = waterAngularDamping; + _rb.constraints = _defaultConstraints | RigidbodyConstraints.FreezeRotation; + _rb.angularVelocity = Vector3.zero; + } + + private void ExitWaterPresentationMode() + { + _mode = FishingBobberControlMode.AirPhysics; + RestorePhysicsState(); + } + + private void RestorePhysicsState() + { + if (_rb == null || !_defaultsCached) + { + return; + } + + _rb.useGravity = _defaultUseGravity; + _rb.angularDamping = _defaultAngularDamping; + _rb.constraints = _defaultConstraints; + } + + private float GetSubmergeDepth() + { + return waterLevel - GetBottomWorldPosition().y; + } + + private Vector3 GetBottomWorldPosition() + { + return transform.TransformPoint(new Vector3(0f, bottomOffsetLocalY, 0f)); + } + + private float CalculateRawDraftDepth() + { + var weightDelta = currentBottomWeight - neutralBottomWeight; + var targetDraft = baseDraftDepth + weightDelta * draftDepthPerWeight; + return Mathf.Clamp(targetDraft, minDraftDepth, maxDraftDepth); + } + + private void UpdateDraft(float deltaTime) + { + var targetDraft = CalculateRawDraftDepth(); + _currentDraftDepth = Mathf.SmoothDamp( + _currentDraftDepth, + targetDraft, + ref _draftVelocity, + Mathf.Max(0.0001f, draftSmoothTime), + Mathf.Infinity, + deltaTime); + } + + private void UpdateVerticalPosition(float deltaTime, Quaternion targetRotation) + { + var position = _rb.position; + var targetY = waterLevel - _currentDraftDepth - GetBottomOffsetWorldY(targetRotation) + _biteOffsetY; + + if (Mathf.Abs(position.y - targetY) < yDeadZone) + { + position.y = targetY; + _ySmoothVelocity = 0f; + } + else + { + position.y = Mathf.SmoothDamp( + position.y, + targetY, + ref _ySmoothVelocity, + Mathf.Max(0.0001f, ySmoothTime), + maxYSpeed, + deltaTime); + } + + _rb.MovePosition(position); + + var velocity = _rb.linearVelocity; + if (Mathf.Abs(velocity.y) > 0f) + { + velocity.y = 0f; + _rb.linearVelocity = velocity; + } + } + + private Quaternion CalculateNextRotation(float deltaTime) + { + var targetTiltAngle = EvaluateTargetTiltAngle(); + var signedAngle = invertTiltDirection ? -targetTiltAngle : targetTiltAngle; + var localAxis = tiltAxis == BobberTiltAxis.LocalX ? Vector3.right : Vector3.forward; + + _targetRotation = _uprightReferenceRotation * Quaternion.AngleAxis(signedAngle, localAxis); + _rb.angularVelocity = Vector3.zero; + return Quaternion.Slerp( + _rb.rotation, + _targetRotation, + 1f - Mathf.Exp(-Mathf.Max(0.01f, rotationLerpSpeed) * deltaTime)); + } + + private void ApplyRotation(Quaternion nextRotation) + { + _rb.rotation = nextRotation; + } + + private float GetBottomOffsetWorldY(Quaternion rotation) + { + return (rotation * new Vector3(0f, bottomOffsetLocalY, 0f)).y; + } + + private float EvaluateTargetTiltAngle() + { + if (currentBottomWeight <= lyingWeightThreshold) + { + return lyingAngle; + } + + if (currentBottomWeight <= tiltedWeightThreshold) + { + var t = Mathf.InverseLerp(lyingWeightThreshold, tiltedWeightThreshold, currentBottomWeight); + return Mathf.Lerp(lyingAngle, tiltedAngle, t); + } + + if (currentBottomWeight <= uprightWeightThreshold) + { + var t = Mathf.InverseLerp(tiltedWeightThreshold, uprightWeightThreshold, currentBottomWeight); + return Mathf.Lerp(tiltedAngle, uprightAngle, t); + } + + return uprightAngle; + } + + private void StartBite(FishingBobberBiteType type, float amplitude, float duration) + { + if (_mode != FishingBobberControlMode.WaterPresentation) + { + return; + } + + _activeBiteType = type; + _biteTimer = 0f; + _biteDuration = Mathf.Max(0.01f, duration); + _biteAmplitude = Mathf.Max(0f, amplitude); + _biteOffsetYVelocity = 0f; + } + + private void UpdateBiteAnimation(float deltaTime) + { + if (_activeBiteType == FishingBobberBiteType.None) + { + _biteOffsetY = Mathf.SmoothDamp( + _biteOffsetY, + 0f, + ref _biteOffsetYVelocity, + Mathf.Max(0.0001f, biteSmoothTime), + Mathf.Infinity, + deltaTime); + return; + } + + _biteTimer += deltaTime; + var t = Mathf.Clamp01(_biteTimer / _biteDuration); + var targetOffset = 0f; + + switch (_activeBiteType) + { + case FishingBobberBiteType.Tap: + if (t < 0.35f) + { + var downT = t / 0.35f; + targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, downT); + } + else + { + var upT = (t - 0.35f) / 0.65f; + targetOffset = -Mathf.Lerp(_biteAmplitude, 0f, upT); + } + break; + + case FishingBobberBiteType.SlowSink: + targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, t); + break; + + case FishingBobberBiteType.Lift: + targetOffset = Mathf.SmoothStep(0f, _biteAmplitude, t); + break; + + case FishingBobberBiteType.BlackDrift: + targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, t); + break; + } + + _biteOffsetY = Mathf.SmoothDamp( + _biteOffsetY, + targetOffset, + ref _biteOffsetYVelocity, + Mathf.Max(0.0001f, biteSmoothTime), + Mathf.Infinity, + deltaTime); + + if (_biteTimer >= _biteDuration && + _activeBiteType != FishingBobberBiteType.SlowSink && + _activeBiteType != FishingBobberBiteType.BlackDrift) + { + _activeBiteType = FishingBobberBiteType.None; + } + } + + private void HandleDebugInput() + { + if (!Application.isPlaying || !enableDebugInput) + { + return; + } + + if (Input.GetKeyDown(stopBiteKey)) + { + StopBite(); + } + + if (Input.GetKeyDown(tapKey)) + { + PlayTap(); + } + + if (Input.GetKeyDown(slowSinkKey)) + { + PlaySlowSink(); + } + + if (Input.GetKeyDown(liftKey)) + { + PlayLift(); + } + + if (Input.GetKeyDown(blackDriftKey)) + { + PlayBlackDrift(); + } + } + + #endregion + + #region 参数校验 + +#if UNITY_EDITOR + private void OnValidate() + { + floatHeight = Mathf.Max(0.001f, floatHeight); + ySmoothTime = Mathf.Max(0.001f, ySmoothTime); + maxYSpeed = Mathf.Max(0.01f, maxYSpeed); + yDeadZone = Mathf.Max(0f, yDeadZone); + + neutralBottomWeight = Mathf.Max(0f, neutralBottomWeight); + currentBottomWeight = Mathf.Max(0f, currentBottomWeight); + minDraftDepth = Mathf.Max(0f, minDraftDepth); + maxDraftDepth = Mathf.Max(minDraftDepth, maxDraftDepth); + baseDraftDepth = Mathf.Clamp(baseDraftDepth, minDraftDepth, maxDraftDepth); + draftDepthPerWeight = Mathf.Max(0f, draftDepthPerWeight); + draftSmoothTime = Mathf.Max(0.001f, draftSmoothTime); + + biteSmoothTime = Mathf.Max(0.001f, biteSmoothTime); + tapAmplitude = Mathf.Max(0f, tapAmplitude); + tapDuration = Mathf.Max(0.01f, tapDuration); + slowSinkAmplitude = Mathf.Max(0f, slowSinkAmplitude); + slowSinkDuration = Mathf.Max(0.01f, slowSinkDuration); + liftAmplitude = Mathf.Max(0f, liftAmplitude); + liftDuration = Mathf.Max(0.01f, liftDuration); + blackDriftAmplitude = Mathf.Max(0f, blackDriftAmplitude); + blackDriftDuration = Mathf.Max(0.01f, blackDriftDuration); + + lyingWeightThreshold = Mathf.Max(0f, lyingWeightThreshold); + tiltedWeightThreshold = Mathf.Max(lyingWeightThreshold, tiltedWeightThreshold); + uprightWeightThreshold = Mathf.Max(tiltedWeightThreshold, uprightWeightThreshold); + uprightAngle = Mathf.Clamp(uprightAngle, 0f, 89f); + tiltedAngle = Mathf.Clamp(tiltedAngle, uprightAngle, 89f); + lyingAngle = Mathf.Clamp(lyingAngle, tiltedAngle, 89.9f); + rotationLerpSpeed = Mathf.Max(0.01f, rotationLerpSpeed); + waterAngularDamping = Mathf.Max(0f, waterAngularDamping); + } +#endif + + #endregion + } +} diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingBobberFeature.cs.meta b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingBobberFeature.cs.meta new file mode 100644 index 000000000..b251a45db --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingBobberFeature.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ca4d5d54d89446b0a10b7ce521fd7d9e +timeCreated: 1775958532 diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingDefaultPhysicsFeature.cs b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingDefaultPhysicsFeature.cs new file mode 100644 index 000000000..ccc0abc5a --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingDefaultPhysicsFeature.cs @@ -0,0 +1,44 @@ +using UnityEngine; + +namespace NBF +{ + /// + /// 默认物理组件 + /// + public class FishingDefaultPhysicsFeature : FishingLineNodeMotionFeature + { + [Header("Physics")] [SerializeField] private bool useGravity = true; + + protected override int DefaultPriority => 0; + + public override bool IsSupportedNode(FishingLineNode node) + { + return node != null && node.Type != FishingLineNode.NodeType.Start; + } + + public override bool CanControl() + { + return Node != null && Node.Body != null && IsSupportedNode(Node); + } + + public override void OnMotionActivated() + { + ApplyPhysicsState(); + } + + public override void TickMotion(float deltaTime) + { + ApplyPhysicsState(); + } + + private void ApplyPhysicsState() + { + if (Node == null || Node.Body == null || !IsSupportedNode(Node)) + { + return; + } + + Node.Body.useGravity = useGravity; + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingDefaultPhysicsFeature.cs.meta b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingDefaultPhysicsFeature.cs.meta new file mode 100644 index 000000000..8dfbf6598 --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingDefaultPhysicsFeature.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2ebfa4366b504ba0a3f398eded17df31 +timeCreated: 1775957743 \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingFloatFeature.cs b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingFloatFeature.cs new file mode 100644 index 000000000..5533d016b --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingFloatFeature.cs @@ -0,0 +1,890 @@ +using UnityEngine; + +namespace NBF +{ + 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 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(); + } + } + + 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) + { + 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 + } +} \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingFloatFeature.cs.meta b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingFloatFeature.cs.meta new file mode 100644 index 000000000..495e4da99 --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingFloatFeature.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 40a38940e81046e2854add979cedbef9 +timeCreated: 1775958531 \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeFeature.cs b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeFeature.cs new file mode 100644 index 000000000..5579db5ef --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeFeature.cs @@ -0,0 +1,63 @@ +using UnityEngine; + +namespace NBF +{ + public abstract class FishingLineNodeFeature : MonoBehaviour + { + /// + /// 当前功能组件所属的节点。 + /// + public FishingLineNode Node { get; private set; } + + /// + /// 当前功能组件所属的鱼线求解器。 + /// + public FishingLineSolver Solver { get; private set; } + + /// + /// 将当前功能组件绑定到指定节点和求解器。 + /// + public void Bind(FishingLineNode node, FishingLineSolver solver) + { + Node = node; + Solver = solver; + + if (!IsSupportedNode(node)) + { + Debug.LogWarning($"{GetType().Name} 不适用于节点 {node.name} 的当前配置。", this); + } + + OnBind(); + } + + /// + /// 当前功能组件是否支持挂在该节点上。 + /// 子类可按节点类型、尾节点类型或产品标识做限制。 + /// + public virtual bool IsSupportedNode(FishingLineNode node) + { + return node != null; + } + + /// + /// 节点与求解器绑定完成后的回调。 + /// + protected virtual void OnBind() + { + } + + /// + /// 鱼线链路重建完成后的回调。 + /// + public virtual void OnLineBuilt() + { + } + + /// + /// 鱼线达到断线条件后的回调。 + /// + public virtual void OnLineBreakRequested() + { + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeFeature.cs.meta b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeFeature.cs.meta new file mode 100644 index 000000000..bbf16c78e --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeFeature.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c7ea70945db841deb0ee8df85f0e15ec +timeCreated: 1775957663 \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeMotionFeature.cs b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeMotionFeature.cs new file mode 100644 index 000000000..12a12dee9 --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeMotionFeature.cs @@ -0,0 +1,47 @@ +using UnityEngine; + +namespace NBF +{ + public abstract class FishingLineNodeMotionFeature : FishingLineNodeFeature + { + [Header("Motion Control")] [SerializeField] + private int priorityOffset; + + /// + /// 当前运动控制组件的优先级。 + /// 值越大,越容易取得节点运动控制权。 + /// 最终优先级 = 默认优先级 + 调整值。 + /// + public int Priority => DefaultPriority + priorityOffset; + + /// + /// 当前运动控制组件的默认优先级。 + /// 子类可通过重写该值,决定自己相对默认物理的抢占能力。 + /// + protected virtual int DefaultPriority => 0; + + /// + /// 当前帧该运动控制组件是否希望接管节点运动。 + /// + public abstract bool CanControl(); + + /// + /// 当前运动控制组件开始接管节点时的回调。 + /// + public virtual void OnMotionActivated() + { + } + + /// + /// 当前运动控制组件失去节点控制权时的回调。 + /// + public virtual void OnMotionDeactivated() + { + } + + /// + /// 当前运动控制组件正在接管节点时,每个 FixedUpdate 执行的逻辑。 + /// + public abstract void TickMotion(float deltaTime); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeMotionFeature.cs.meta b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeMotionFeature.cs.meta new file mode 100644 index 000000000..9c76ad302 --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Feature/FishingLineNodeMotionFeature.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7e75217306a64f8f868f5f9127772de2 +timeCreated: 1775957711 \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineNode.cs b/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineNode.cs new file mode 100644 index 000000000..7906ec764 --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineNode.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace NBF +{ + public class FishingLineNode : MonoBehaviour + { + public enum NodeType + { + Start, + Float, + Weight, + Tail + } + + private FishingLineSolver _solver; + [Header("Node")] [SerializeField] private NodeType nodeType = NodeType.Tail; + [SerializeField] private Rigidbody body; + + public NodeType Type + { + get => nodeType; + set => nodeType = value; + } + + public Rigidbody Body => body; + + private void Awake() + { + _solver = GetComponentInParent(); + body = GetComponent(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineNode.cs.meta b/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineNode.cs.meta new file mode 100644 index 000000000..91ed9c8f4 --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineNode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ee9704c2e1594f4cab270bfd4ca2210b +timeCreated: 1776008915 \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineSolver.cs b/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineSolver.cs new file mode 100644 index 000000000..83d8dcee7 --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineSolver.cs @@ -0,0 +1,8 @@ +using UnityEngine; + +namespace NBF +{ + public class FishingLineSolver : MonoBehaviour + { + } +} \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineSolver.cs.meta b/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineSolver.cs.meta new file mode 100644 index 000000000..2aae6ab33 --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineSolver.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fd39a2e024a0477c9ad5698d80d9a63a +timeCreated: 1776008869 \ No newline at end of file