接入新逻辑

# Conflicts:
#	Assets/Scenes/RopeTest.unity
#	Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs
#	Assets/Scripts/Fishing/Rope/Rope.cs
#	Assets/Scripts/Fishing/Rope/Rope.cs.meta
This commit is contained in:
Bob.Song
2026-04-26 14:42:54 +08:00
parent 05fa2d6e5e
commit d432c468b1
48 changed files with 6150 additions and 6790 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +0,0 @@
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Rope))]
public class RopeFishLineEditor : Editor
{
private Rope _target;
void OnEnable()
{
_target = target as Rope;
// lookAtPoint = serializedObject.FindProperty("lookAtPoint");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (GUILayout.Button("打印总长度"))
{
_target.DebugLength();
// Debug.Log($"总长度={_target.GetCurrentLength()} 目标长度={_target.GetTargetLength()} smoot={_target.GetLengthSmoothVel()} relLen={_target.GetLengthByPoints()} PolylineLength={_target.GetPhysicsPolylineLength()}");
}
// serializedObject.Update();
// EditorGUILayout.PropertyField(lookAtPoint);
// serializedObject.ApplyModifiedProperties();
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f24add4ba0ae4e76acd98c4a5512c366
timeCreated: 1771850618

View File

@@ -0,0 +1,138 @@
using UnityEngine;
namespace NBF
{
public enum FLineLogicNodeType
{
Start,
Bobber,
End
}
/// <summary>
/// 硬线节点组件 - 可选,用于更方便的管理
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class FLineLogicNode : MonoBehaviour
{
[Header("节点设置")] public FLineLogicNodeType NodeType = FLineLogicNodeType.Bobber;
[SerializeField] private bool isKinematic = false;
[SerializeField] private float mass = 1f;
[Header("到下一个节点的段配置")] [SerializeField, Min(0.01f)]
private float nextSegmentMaxLength = 1f;
[SerializeField, Min(0f)] private float nextSegmentMinLength = 0f;
private Rigidbody rb;
private FLine parentCable;
public Rigidbody Rigidbody
{
get
{
if (!rb)
{
rb = GetComponent<Rigidbody>();
}
return rb;
}
}
public float NextSegmentMaxLength
{
get => nextSegmentMaxLength;
set => nextSegmentMaxLength = Mathf.Max(0.01f, value);
}
public float NextSegmentMinLength
{
get => nextSegmentMinLength;
set => nextSegmentMinLength = Mathf.Max(0f, value);
}
void Awake()
{
rb = Rigidbody;
InitializeNode();
}
void OnValidate()
{
mass = Mathf.Max(0.0001f, mass);
nextSegmentMaxLength = Mathf.Max(0.01f, nextSegmentMaxLength);
nextSegmentMinLength = Mathf.Max(0f, nextSegmentMinLength);
}
private void InitializeNode()
{
if (Rigidbody)
{
Rigidbody.isKinematic = isKinematic;
Rigidbody.mass = Mathf.Max(0.0001f, mass);
Rigidbody.useGravity = !isKinematic;
}
}
public void AttachToCable(FLine cable)
{
parentCable = cable;
// // 计算到前一个节点的距离
// var bodies = parentCable.GetConnectedBodies();
// int myIndex = bodies.IndexOf(rb);
//
// if (myIndex > 0 && bodies[myIndex - 1] != null)
// {
// float distanceToPrevious = Vector3.Distance(
// transform.position,
// bodies[myIndex - 1].position
// );
// parentCable.SetSegmentLength(myIndex - 1, distanceToPrevious);
// }
}
/// <summary>
/// 固定/释放此节点
/// </summary>
public void SetFixed(bool fixed_)
{
if (Rigidbody)
{
isKinematic = fixed_;
Rigidbody.isKinematic = fixed_;
Rigidbody.useGravity = !fixed_;
}
}
/// <summary>
/// 施加力到此节点(会影响相邻节点)
/// </summary>
public void ApplyForce(Vector3 force, ForceMode mode = ForceMode.Force)
{
if (parentCable)
{
parentCable.ApplyForceAtBody(Rigidbody, force, mode);
}
else if (Rigidbody)
{
Rigidbody.AddForce(force, mode);
}
}
public void SetSegmentLengths(float maxLength, float minLength)
{
NextSegmentMaxLength = maxLength;
NextSegmentMinLength = minLength;
}
public bool TryGetAdjacentBodies(out Rigidbody previousBody, out Rigidbody nextBody)
{
previousBody = null;
nextBody = null;
return parentCable && parentCable.TryGetAdjacentBodies(this, out previousBody, out nextBody);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 324bd710e0c5460ea880c6314ae95058
timeCreated: 1776948853

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9e43f5ab12b64bb9a47c0c674a3177f2
timeCreated: 1776960000

View File

@@ -0,0 +1,349 @@
using System.Collections.Generic;
using UnityEngine;
namespace NBF
{
/// <summary>
/// 硬线系统测试脚本,直接读取 FLine 已配置的节点。
/// </summary>
public class FLineTest : MonoBehaviour
{
[Header("测试控制")] [SerializeField] private KeyCode fixMiddleKey = KeyCode.M;
[SerializeField] private KeyCode applyForceKey = KeyCode.F;
[SerializeField] private Vector3 testForce = new Vector3(0f, 10f, 0f);
[Header("动态间距控制")]
// [SerializeField] private KeyCode extendKey = KeyCode.UpArrow;
// [SerializeField] private KeyCode contractKey = KeyCode.DownArrow;
[SerializeField]
private KeyCode resetKey = KeyCode.Alpha0;
[SerializeField] private KeyCode pullFirstKey = KeyCode.UpArrow;
[SerializeField] private KeyCode relaxFirstKey = KeyCode.DownArrow;
[SerializeField, Min(0.01f)] private float extendAmount = 0.5f;
[SerializeField, Min(0.01f)] private float holdAdjustSpeed = 1f;
[SerializeField, Min(0.01f)] private float transitionSpeed = 2f;
[SerializeField] private bool smoothTransition = true;
[SerializeField] private FLine line;
private float[] initialLengths;
private void OnValidate()
{
extendAmount = Mathf.Max(0.01f, extendAmount);
holdAdjustSpeed = Mathf.Max(0.01f, holdAdjustSpeed);
transitionSpeed = Mathf.Max(0.01f, transitionSpeed);
}
private void Start()
{
RefreshInitialLengths(true);
line.OnLineBreakRequested += OnLineBreakRequested;
}
private void OnLineBreakRequested(FLine lineSolver)
{
Debug.LogError($"当前拉力达到极限,切线,极限时间={lineSolver.LimitStateTime}");
var nodes = line.GetLineNodes();
nodes[^1].Rigidbody.isKinematic = false;
// line.Body.isKinematic = false;
}
private void Update()
{
if (!EnsureCable())
return;
RefreshInitialLengths();
HandleInput();
if (line.CurrentBreakStretchPercent > 10f)
{
Debug.LogError(
$"当前极限情况CurrentBreakStretchPercent={line.CurrentBreakStretchPercent} CurrentStretchLength={line.CurrentStretchLength} TotalLength={line.TotalLength} LimitStateTime={line.LimitStateTime}");
}
}
private bool EnsureCable()
{
if (line)
return true;
line = GetComponent<FLine>();
return line != null;
}
private List<FLineLogicNode> GetNodes()
{
return line != null ? line.GetLineNodes() : null;
}
private void RefreshInitialLengths(bool force = false)
{
List<FLineLogicNode> nodes = GetNodes();
int segmentCount = nodes != null ? Mathf.Max(0, nodes.Count - 1) : 0;
if (!force && initialLengths != null && initialLengths.Length == segmentCount)
return;
initialLengths = new float[segmentCount];
for (int i = 0; i < segmentCount; i++)
{
float configuredLength = line.GetSegmentMaxLength(i);
if (configuredLength <= 0f)
configuredLength = Mathf.Max(0.01f, line.GetCurrentSegmentLength(i));
initialLengths[i] = Mathf.Max(0.01f, configuredLength);
}
}
private float GetCurrentMaxLength(int segmentIndex)
{
float length = line.GetSegmentMaxLength(segmentIndex);
return Mathf.Max(0.01f, length);
}
private float GetTargetMaxLength(int segmentIndex)
{
float length = line.GetTargetMaxLength(segmentIndex);
if (length <= 0f)
length = line.GetSegmentMaxLength(segmentIndex);
return Mathf.Max(0.01f, length);
}
private int GetSegmentCount()
{
return initialLengths != null ? initialLengths.Length : 0;
}
private void HandleInput()
{
HandleOriginalControls();
HandleDynamicDistanceControls();
}
private void HandleOriginalControls()
{
List<FLineLogicNode> nodes = GetNodes();
if (nodes == null)
return;
if (Input.GetKeyDown(fixMiddleKey) && nodes.Count >= 3)
{
int middleIndex = nodes.Count / 2;
FLineLogicNode middleNode = nodes[middleIndex];
Rigidbody middleRb = middleNode ? middleNode.Rigidbody : null;
if (middleNode && middleRb)
{
bool newState = !middleRb.isKinematic;
middleNode.SetFixed(newState);
Debug.Log($"中间节点({middleIndex}) {(newState ? "" : "")} - 观察其他节点变化");
}
}
if (Input.GetKeyDown(applyForceKey) && nodes.Count > 2)
{
int randomIndex = Random.Range(1, nodes.Count - 1);
FLineLogicNode targetNode = nodes[randomIndex];
Rigidbody targetRb = targetNode ? targetNode.Rigidbody : null;
if (targetNode && targetRb && !targetRb.isKinematic)
{
targetNode.ApplyForce(testForce, ForceMode.Impulse);
Debug.Log($"对节点{randomIndex}施加力: {testForce} - 观察传递效果");
}
}
}
private void HandleDynamicDistanceControls()
{
if (line == null || initialLengths == null || initialLengths.Length == 0)
return;
// HandleLengthAdjustInput();
if (Input.GetKeyDown(resetKey))
ResetAllSegments();
if (Input.GetKeyDown(pullFirstKey))
PullFirstSegment(extendAmount * 0.5f);
if (Input.GetKeyDown(relaxFirstKey))
RelaxFirstSegment(extendAmount * 0.5f);
if (Input.mouseScrollDelta.y != 0f && Input.GetKey(KeyCode.LeftShift))
AdjustNearestSegment(Input.mouseScrollDelta.y * 0.1f);
}
private void ApplySegmentTargetLength(int segmentIndex, float targetLength)
{
line.SetSegmentMaxLength(segmentIndex, targetLength, transitionSpeed);
}
private void ResetAllSegments()
{
for (int i = 0; i < GetSegmentCount(); i++)
{
ApplySegmentTargetLength(i, initialLengths[i]);
}
Debug.Log("重置绳索到 FLine 初始节点配置");
}
private void PullFirstSegment(float amount)
{
if (GetSegmentCount() <= 0)
return;
float targetLength = Mathf.Max(0.1f, GetTargetMaxLength(0) - amount);
ApplySegmentTargetLength(0, targetLength);
Debug.Log($"拉紧第一段到 {targetLength:F2}");
}
private void RelaxFirstSegment(float amount)
{
if (GetSegmentCount() <= 0)
return;
float targetLength = GetTargetMaxLength(0) + amount;
ApplySegmentTargetLength(0, targetLength);
Debug.Log($"放松第一段到 {targetLength:F2}");
}
private void AdjustNearestSegment(float amount)
{
int nearestSegment = FindNearestSegmentToCamera();
if (nearestSegment < 0 || nearestSegment >= GetSegmentCount())
return;
float targetLength = Mathf.Max(0.1f, GetTargetMaxLength(nearestSegment) + amount);
ApplySegmentTargetLength(nearestSegment, targetLength);
Debug.Log($"调整段{nearestSegment}到 {targetLength:F2}");
}
private int FindNearestSegmentToCamera()
{
Camera cam = Camera.main;
List<FLineLogicNode> nodes = GetNodes();
if (cam == null || nodes == null || nodes.Count < 2)
return -1;
int nearestSegment = -1;
float nearestDistance = float.MaxValue;
for (int i = 0; i < nodes.Count - 1; i++)
{
Rigidbody bodyA = nodes[i] ? nodes[i].Rigidbody : null;
Rigidbody bodyB = nodes[i + 1] ? nodes[i + 1].Rigidbody : null;
if (!bodyA || !bodyB)
continue;
Vector3 midPoint = (bodyA.position + bodyB.position) * 0.5f;
Vector3 screenPoint = cam.WorldToScreenPoint(midPoint);
Vector3 mousePos = Input.mousePosition;
float distance = Vector2.Distance(
new Vector2(screenPoint.x, screenPoint.y),
new Vector2(mousePos.x, mousePos.y)
);
if (distance < nearestDistance)
{
nearestDistance = distance;
nearestSegment = i;
}
}
return nearestSegment;
}
private void OnGUI()
{
if (!EnsureCable())
return;
RefreshInitialLengths();
List<FLineLogicNode> nodes = GetNodes();
int nodeCount = nodes != null ? nodes.Count : 0;
GUILayout.BeginArea(new Rect(10f, 10f, 360f, 260f));
GUILayout.Label("=== 硬线系统测试控制 ===");
GUILayout.Label("原始控制:");
GUILayout.Label($" {fixMiddleKey} - 固定/释放中间节点");
GUILayout.Label($" {applyForceKey} - 随机施加力");
GUILayout.Space(10f);
GUILayout.Label("动态间距控制:");
GUILayout.Label($" {resetKey} - 重置到初始节点配置");
GUILayout.Label($" {pullFirstKey} - 拉紧第一段");
GUILayout.Label($" {relaxFirstKey} - 放松第一段");
GUILayout.Label(" Shift+滚轮 - 调整最近段");
GUILayout.Space(10f);
GUILayout.Label("设置:");
GUILayout.Label($" 节点数: {nodeCount}");
GUILayout.Label(" 初始长度来源: FLine 节点配置");
GUILayout.Label($" 过渡模式: {(smoothTransition ? "" : "")}");
if (smoothTransition)
GUILayout.Label($" 过渡速度: {transitionSpeed:F1}");
GUILayout.EndArea();
GUILayout.BeginArea(new Rect(10f, 280f, 360f, 220f));
GUILayout.Label("=== 各段实际长度 ===");
for (int i = 0; i < Mathf.Min(GetSegmentCount(), 10); i++)
{
Rigidbody bodyA = nodes[i] ? nodes[i].Rigidbody : null;
Rigidbody bodyB = nodes[i + 1] ? nodes[i + 1].Rigidbody : null;
if (!bodyA || !bodyB)
continue;
float actualDistance = Vector3.Distance(bodyA.position, bodyB.position);
float currentLimit = GetCurrentMaxLength(i);
float targetLimit = GetTargetMaxLength(i);
string segmentInfo = $"段{i}: {actualDistance:F2} (限制: {currentLimit:F2}";
if (line.IsSegmentTransitioning(i))
segmentInfo += $" -> {targetLimit:F2}";
segmentInfo += ")";
if (actualDistance > targetLimit * 1.1f)
{
GUI.color = Color.red;
}
else if (line.IsSegmentTransitioning(i))
{
GUI.color = Color.yellow;
}
else
{
GUI.color = Color.green;
}
GUILayout.Label(segmentInfo);
}
GUI.color = Color.white;
bool anyTransitioning = false;
for (int i = 0; i < GetSegmentCount(); i++)
{
if (line.IsSegmentTransitioning(i))
{
anyTransitioning = true;
break;
}
}
if (anyTransitioning)
GUILayout.Label("状态: 过渡中...");
GUILayout.EndArea();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c2c33030c494448488a01acae5f2ba54

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 72431056211140f1bdbf95503df5f198
timeCreated: 1777005389

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5ff7196c0f1441d4807ea7be8f921eb6
timeCreated: 1777011145

View File

@@ -1,4 +1,5 @@
using UnityEngine;
using NBF;
using UnityEngine;
public class SimpleWaterSurfaceProvider : MonoBehaviour, IWaterSurfaceProvider
{

View File

@@ -20,7 +20,7 @@ namespace NBF
public bool isPinched { get; private set; }
private bool moveToTargetDone;
private float _speed;
@@ -44,11 +44,6 @@ namespace NBF
}
}
private void LateUpdate()
{
// SyncPosition();
}
private void Update()
{
SyncPosition();

View File

@@ -13,7 +13,7 @@ namespace NBF
public struct ThrowAnimationRequest
{
public LureController Lure;
public FLineLogicNode Lure;
public Vector3 ThrowOriginPosition;
public Vector3 StartPosition;
public Vector3 Forward;

View File

@@ -22,7 +22,7 @@ namespace NBF
private float _castElapsedTime;
private Vector3 _castStartPos;
private Vector3 _castTargetPos;
private LureController _castingLure;
private FLineLogicNode _castingLure;
public bool IsPlaying => _castingLure != null;
@@ -51,7 +51,7 @@ namespace NBF
_chargedProgress = Mathf.Clamp01(request.ChargedProgress);
_castElapsedTime = 0f;
var lureBody = request.Lure.RBody;
var lureBody = request.Lure.Rigidbody;
_castStartPos = request.StartPosition;
Vector3 forward = GetHorizontalForward(request.Forward);
@@ -81,7 +81,7 @@ namespace NBF
return;
}
var lureBody = _castingLure.RBody;
var lureBody = _castingLure.Rigidbody;
if (snapToTarget)
{
_castingLure.transform.position = _castTargetPos;
@@ -150,4 +150,4 @@ namespace NBF
return position;
}
}
}
}

View File

@@ -101,9 +101,11 @@ namespace NBF
var handItemView = Player.HandItem.GetComponent<PlayerItemView>();
if (handItemView != null && handItemView.Rod != null)
{
if (handItemView.Rod.Line.PinchController != null)
var endNode = handItemView.Rod.Line.GetNode(FLineLogicNodeType.End);
var pinch = endNode.gameObject.GetComponent<JointPinchController>();
if (pinch != null)
{
handItemView.Rod.Line.PinchController.StartPinch(view.Unity.ModelAsset.Pinch);
pinch.StartPinch(view.Unity.ModelAsset.Pinch);
}
}
}
@@ -118,9 +120,12 @@ namespace NBF
var handItemView = Player.HandItem.GetComponent<PlayerItemView>();
if (handItemView != null && handItemView.Rod != null)
{
if (handItemView.Rod.Line.PinchController != null)
var endNode = handItemView.Rod.Line.GetNode(FLineLogicNodeType.End);
var pinch = endNode.gameObject.GetComponent<JointPinchController>();
if (pinch != null)
{
handItemView.Rod.Line.PinchController.ReleasePinch();
pinch.ReleasePinch();
;
}
}
}

View File

@@ -61,18 +61,19 @@ namespace NBF
PlayerView.Unity.ModelAsset.PlayerAnimator.StartThrow = false;
var rod = GetRod();
if (rod == null || rod.Line == null || rod.Line.Lure == null)
if (rod == null || rod.Line == null)
{
return;
}
var endNode = rod.Line.GetNode(FLineLogicNodeType.End);
_throwAnimation = CreateThrowAnimation(rod);
_throwAnimation.Player = Player;
_throwAnimation?.Play(new ThrowAnimationRequest
{
Lure = rod.Line.Lure,
Lure = endNode,
ThrowOriginPosition = PlayerView.Unity.transform.position,
StartPosition = rod.Line.Lure.RBody.position,
StartPosition = endNode.Rigidbody.position,
Forward = PlayerView.Unity.transform.forward,
ChargedProgress = ChargedProgress
});
@@ -108,4 +109,4 @@ namespace NBF
}
}
}
}
}

View File

@@ -7,7 +7,8 @@ namespace NBF
protected override void OnInit()
{
// transform.position = Rod.lineHandler.LineConnector_1.transform.position;
SetParent(Rod.Line.Bobber.transform);
var endNode = Rod.Line.GetNode(FLineLogicNodeType.Bobber);
SetParent(endNode.transform);
transform.localPosition = Vector3.zero;
// var buoyancy = GetComponentInParent<CapsuleBuoyancyStable>();
// buoyancy.InitBobber();

View File

@@ -10,18 +10,17 @@ namespace NBF
{
hookAsset = GetComponent<HookAsset>();
}
protected override void OnInit()
{
// transform.position = Rod.lineHandler.LineConnector_2.transform.position;
// transform.rotation = Rod.lineHandler.LineConnector_2.transform.rotation; // 确保旋转也同步
// SetParent(Rod.lineHandler.LineConnector_2.transform);
SetParent(Rod.Line.Lure.transform);
var endNode = Rod.Line.GetNode(FLineLogicNodeType.End);
SetParent(endNode.transform);
transform.localPosition = Vector3.zero;
// var target = lineHandler.LineConnector_2.GetComponent<Rigidbody>();
// var joint = Hook.gameObject.GetComponent<ConfigurableJoint>();
// joint.connectedBody = target;

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
fileFormatVersion: 2
guid: c0403ffd74ce46fab8bd4ef057e51432
timeCreated: 1766582567
guid: c7095cf554c345839173044e4786b0ba
timeCreated: 1776948821

View File

@@ -13,8 +13,8 @@ namespace NBF
// LureHookWaterDisplacement = Lure.GetComponent<FWaterDisplacement>();
// SetParent(Rod.lineHandler.LineConnector_1.transform);
SetParent(Rod.Line.Lure.transform);
var endNode = Rod.Line.GetNode(FLineLogicNodeType.End);
SetParent(endNode.transform);
transform.localPosition = Vector3.zero;
}
}

View File

@@ -72,24 +72,14 @@ namespace NBF
if (Line.LineType == LineType.Spinning)
{
//没有浮漂类型
Line.Lure.SetJointDistance(PlayerItem.LineLength);
if (PlayerItem.StretchRope)
{
// Line.SetTargetLength(PlayerItem.Tension > 0f ? 0f : PlayerItem.LineLength);
Line.SetTargetLength(PlayerItem.LineLength);
}
Line.SetLenght(PlayerItem.LineLength);
}
else
{
//有浮漂
Line.Lure.SetJointDistance(PlayerItem.FloatLength);
Line.Bobber.SetJointDistance(PlayerItem.LineLength - PlayerItem.FloatLength);
if (PlayerItem.StretchRope)
{
// Line.SetTargetLength(PlayerItem.Tension > 0f ? 0f : PlayerItem.LineLength - PlayerItem.FloatLength);
Line.SetTargetLength(PlayerItem.LineLength - PlayerItem.FloatLength);
Line.SetLureLength(PlayerItem.FloatLength);
}
Line.SetSegmentMaxLength(0, PlayerItem.LineLength - PlayerItem.FloatLength);
//浮漂位置
Line.SetSegmentMaxLength(1, PlayerItem.FloatLength);
}
}
@@ -338,12 +328,12 @@ namespace NBF
var state = PlayerItem.Owner.State;
Vector3 vector = Line.Lure.transform.position;
var endNode = Line.GetNode(FLineLogicNodeType.End);
Vector3 vector = endNode.transform.position;
// 当前物体的朝向与指向 Lure 的方向之间的夹角,在 0完全对齐到 1完全相反之间的一个比例值
float headingAlignment = Vector3.Angle(base.transform.forward,
(Line.Lure.transform.position - transform.position).normalized) / 180f;
(endNode.transform.position - transform.position).normalized) / 180f;
// 经过朝向调制后的有效张力
var effectiveTension = Mathf.Clamp(CurrentTension01 * headingAlignment, 0f, 1f);

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 98ba9d435a0e49c9bb527c34cc91894d
timeCreated: 1766759607

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 2d71eb3d89064cc4aab3317e49dc3979
timeCreated: 1772097895

View File

@@ -1,209 +0,0 @@
using UnityEngine;
public interface IWaterProvider
{
float GetWaterHeight(Vector3 worldPos);
Vector3 GetWaterNormal(Vector3 worldPos);
Vector3 GetWaterVelocity(Vector3 worldPos);
}
//
// /// <summary>
// /// 稳定优先的浮力(只支持 CapsuleCollider / SphereCollider
// /// - 竖直方向:目标吃水深度 + PD 控制(稳定,不抖、不弹飞)
// /// - 姿态Righting Torque 扶正(受 Rigidbody.centerOfMass 影响)
// /// - 入水比例:带平滑(避免水面附近开关抖动)
// /// </summary>
// [DisallowMultipleComponent]
// [RequireComponent(typeof(Rigidbody))]
// public class BuoyancyCapsuleSphere : MonoBehaviour
// {
// [Header("Collider (choose one)")] public CapsuleCollider capsule;
// public SphereCollider sphere;
//
// [Header("Water")] public MonoBehaviour waterProviderBehaviour; // 可选
// private IWaterProvider waterProvider;
//
// [Tooltip("没有 provider 时的水面高度")] public float waterLevel = 0f;
//
// [Header("Density -> Draft")] [Tooltip("水密度 kg/m^3淡水约1000")]
// public float waterDensity = 1000f;
//
// [Tooltip("物体等效密度 kg/m^3。越小越浮。浮漂可 80~400 之间调")]
// public float objectDensity = 250f;
//
// [Tooltip("额外浮力比例手感调整。1=按密度算")] public float buoyancyScale = 1f;
//
// [Header("Vertical PD (Stability key)")] [Tooltip("竖直弹簧强度(越大越“顶住”目标吃水)")]
// public float verticalKp = 35f;
//
// [Tooltip("竖直阻尼(越大越不弹、不抖)")] public float verticalKd = 12f;
//
// [Tooltip("最大向上加速度 m/s^2防止从高处落下入水被顶飞")]
// public float maxUpAccel = 25f;
//
// [Tooltip("最大向下加速度 m/s^2防止强行拉下去造成抖动")]
// public float maxDownAccel = 10f;
//
// [Header("Submergence smoothing")] [Tooltip("入水比例变化速度1/s。越大越快响应越小越稳")]
// public float submergenceSpeed = 8f;
//
// [Tooltip("水面外的缓冲(m),让浮力更平滑接管")] public float surfaceMargin = 0.01f;
//
// [Header("Righting (Rotation)")] [Tooltip("扶正强度(把 transform.up 拉向水面法线/世界上)")]
// public float rightingKp = 8f;
//
// [Tooltip("扶正阻尼(抑制旋转抖动)")] public float rightingKd = 3f;
//
// [Header("Water drag (optional but helpful)")] [Tooltip("入水时额外线阻尼(通过 rb.drag 混合)")]
// public float extraLinearDragInWater = 2.5f;
//
// [Tooltip("入水时额外角阻尼(通过 rb.angularDrag 混合)")]
// public float extraAngularDragInWater = 2.0f;
//
// [Header("Center of Mass")] [Tooltip("本地重心偏移:例如 (0,-0.02,0) 让底部更重、更容易站漂")]
// public Vector3 centerOfMassOffset = Vector3.zero;
//
// private Rigidbody rb;
// private float baseDrag, baseAngularDrag;
//
// // 关键:入水比例必须有“记忆”(滤波),否则水面边界必抖
// private float subFiltered = 0f;
//
// void Reset()
// {
// rb = GetComponent<Rigidbody>();
// rb.useGravity = true;
// rb.interpolation = RigidbodyInterpolation.Interpolate;
// rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
//
// capsule = GetComponent<CapsuleCollider>();
// sphere = GetComponent<SphereCollider>();
// }
//
// void Awake()
// {
// rb = GetComponent<Rigidbody>();
// rb.centerOfMass = centerOfMassOffset;
//
// baseDrag = rb.linearDamping;
// baseAngularDrag = rb.angularDamping;
//
// waterProvider = waterProviderBehaviour as IWaterProvider;
//
// // 只允许一个
// if (capsule != null && sphere != null)
// sphere = null;
// }
//
// void OnValidate()
// {
// objectDensity = Mathf.Max(1e-3f, objectDensity);
// waterDensity = Mathf.Max(1e-3f, waterDensity);
// submergenceSpeed = Mathf.Max(0.1f, submergenceSpeed);
// surfaceMargin = Mathf.Max(0f, surfaceMargin);
// maxUpAccel = Mathf.Max(0f, maxUpAccel);
// maxDownAccel = Mathf.Max(0f, maxDownAccel);
// }
//
// void FixedUpdate()
// {
// if (!capsule && !sphere) return;
//
// waterProvider = waterProviderBehaviour as IWaterProvider;
//
// GetCenterAndHeight(out var centerWorld, out var shapeHeight);
//
// // 水面信息
// float waterH = waterProvider?.GetWaterHeight(centerWorld) ?? waterLevel;
// Vector3 waterN = waterProvider?.GetWaterNormal(centerWorld) ?? Vector3.up;
// if (waterN.sqrMagnitude < 1e-6f) waterN = Vector3.up;
// waterN.Normalize();
//
// // 当前中心“浸没深度”(>0 表示中心在水下)
// float centerDepth = waterH - centerWorld.y;
//
// // 近似入水比例centerDepth = -H/2 -> 0; centerDepth = +H/2 -> 1
// float rawSub = Mathf.Clamp01((centerDepth + (shapeHeight * 0.5f) + surfaceMargin) /
// (shapeHeight + 2f * surfaceMargin));
//
// // 入水比例滤波(非常关键)
// float dt = Time.fixedDeltaTime;
// subFiltered = Mathf.MoveTowards(subFiltered, rawSub, submergenceSpeed * dt);
//
// // 混合拖拽(让水中更稳)
// rb.linearDamping = Mathf.Lerp(baseDrag, baseDrag + extraLinearDragInWater, subFiltered);
// rb.angularDamping = Mathf.Lerp(baseAngularDrag, baseAngularDrag + extraAngularDragInWater, subFiltered);
//
// if (subFiltered <= 1e-4f)
// return; // 基本没入水,不做任何浮力/扶正
//
// // 目标吃水比例:理想静态平衡 ≈ objectDensity / waterDensity<1 才会浮)
// float desiredSub = Mathf.Clamp01((objectDensity / waterDensity) * buoyancyScale);
//
// // 把 desiredSub 转成目标中心深度
// // desiredSub=0 -> centerDepthTarget = -H/2完全出水
// // desiredSub=1 -> centerDepthTarget = +H/2完全入水
// float centerDepthTarget = desiredSub * shapeHeight - shapeHeight * 0.5f;
//
// // 竖直 PD只沿“世界上/重力反方向”控制,最稳
// Vector3 up = (-Physics.gravity).sqrMagnitude > 1e-6f ? (-Physics.gravity).normalized : Vector3.up;
// float vUp = Vector3.Dot(rb.linearVelocity, up);
//
// float error = centerDepth - centerDepthTarget; // 深了为正 -> 需要向上推
// float accelUp = (-verticalKp * error) - (verticalKd * vUp);
//
// // 限制上下加速度,避免顶飞或强拉抖动
// accelUp = Mathf.Clamp(accelUp, -maxDownAccel, maxUpAccel);
//
// // 随入水比例渐入(避免水面边界突然接管)
// accelUp *= subFiltered;
//
// // 施加竖直加速度Acceleration 不受质量影响,更稳定)
// rb.AddForce(up * accelUp, ForceMode.Acceleration);
//
// // 扶正力矩:把物体 up 拉向 waterN平静水就是 Vector3.up
// // 注意:这个扶正与重心偏移一起工作,会形成“站漂/躺漂”的稳定姿态
// Vector3 currentUp = transform.up;
// Vector3 axis = Vector3.Cross(currentUp, waterN);
//
// // 小角度近似axis 的大小约等于 sin(theta)
// // 扶正加速度型扭矩(同样用 Acceleration减少质量/惯量差带来的抖动)
// Vector3 angVel = rb.angularVelocity;
// Vector3 torqueAccel = axis * rightingKp - angVel * rightingKd;
//
// torqueAccel *= subFiltered;
//
// rb.AddTorque(torqueAccel, ForceMode.Acceleration);
// }
//
// private void GetCenterAndHeight(out Vector3 centerWorld, out float heightWorld)
// {
// if (sphere)
// {
// // spherecenter + 半径*缩放(近似取最大缩放)
// Transform t = sphere.transform;
// centerWorld = t.TransformPoint(sphere.center);
//
// Vector3 s = t.lossyScale;
// float r = sphere.radius * Mathf.Max(Mathf.Abs(s.x), Mathf.Abs(s.y), Mathf.Abs(s.z));
// heightWorld = Mathf.Max(1e-6f, r * 2f);
// return;
// }
//
// // capsule
// Transform ct = capsule.transform;
// centerWorld = ct.TransformPoint(capsule.center);
//
// Vector3 ls = ct.lossyScale;
// float sx = Mathf.Abs(ls.x), sy = Mathf.Abs(ls.y), sz = Mathf.Abs(ls.z);
//
// float heightScale = capsule.direction switch
// {
// 0 => sx,
// 1 => sy,
// _ => sz,
// };
//
// heightWorld = Mathf.Max(1e-6f, capsule.height * heightScale);
// }
// }

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: bad586cd447d4271b97ce1cf1c81897a
timeCreated: 1772460028

View File

@@ -1,24 +0,0 @@
using UnityEngine;
using WaveHarmonic.Crest;
public class BuoyancyWaterProvider : MonoBehaviour, IWaterProvider
{
public WaterRenderer Water;
readonly SampleFlowHelper _SampleFlowHelper = new();
public float waterLevel = 0f;
public float GetWaterHeight(Vector3 worldPos)
{
return waterLevel;
}
public Vector3 GetWaterNormal(Vector3 worldPos)
{
return Vector3.up;
}
public Vector3 GetWaterVelocity(Vector3 worldPos)
{
return Vector3.zero; // 关键!不要乱给
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 06d107cece7c4cbb9825557923be567f
timeCreated: 1772380723

View File

@@ -1,268 +0,0 @@
using System;
using UnityEngine;
using WaveHarmonic.Crest;
[DisallowMultipleComponent]
[RequireComponent(typeof(Rigidbody))]
public class CapsuleBuoyancyStable : MonoBehaviour
{
[Header("Buoyancy")] [Tooltip("完全浸没时总浮力 = mass*g*buoyancyScale。>1 更浮。")]
public float buoyancyScale = 1.6f;
[Tooltip("沿胶囊轴向采样点数量(建议 7~11。")] [Range(3, 15)]
public int samplePoints = 9;
[Tooltip("浸没比例曲线0=刚碰水, 1=充分在水下)。")] public AnimationCurve submergenceCurve = AnimationCurve.Linear(0, 0, 1, 1);
[Header("Damping")] [Tooltip("上浮方向速度阻尼越大越不弹。本版本只在“浮力中心”施加一次不再在每个采样点施加避免90°附近转不动。")]
public float verticalDamping = 0.6f;
[Tooltip("整体角速度阻尼(只施加一次,不要太大)。")] public float angularDamping = 0.6f;
[Header("Optional Upright Stabilizer (Recommended for bobber)")] [Tooltip("让胶囊轴向更倾向于对齐世界Up。0=关闭。")]
public float uprightSpring = 0.0f;
[Tooltip("upright 的角速度阻尼。")] public float uprightDamping = 0.5f;
[Tooltip("胶囊轴向0=X,1=Y,2=Z通常 CapsuleCollider.direction 也一样)。")]
public int uprightAxis = 1;
[Header("Water Drag")] public float extraDragInWater = 0.8f;
public float extraAngularDragInWater = 0.8f;
[Header("Anti-stiction near upright")]
[Tooltip("在接近竖直(例如90->80度附近)时降低vertical damping避免“粘住”。0=关闭。")]
[Range(0f, 1f)]
public float nearUprightDampingReduce = 0.6f;
[Tooltip("接近竖直的判定角度(度)。例如 12 表示在 |angle| < 12° 附近逐步降低阻尼。")] [Range(1f, 30f)]
public float nearUprightAngleDeg = 12f;
#region Crest5相关信息
public WaterRenderer _waterRenderer;
[Tooltip("要瞄准哪一层水的碰撞层。")] [SerializeField]
CollisionLayer _Layer = CollisionLayer.AfterAnimatedWaves;
[Header("波响应")] [Tooltip("用于物理计算的物体宽度。\n\n此值越大波响应的滤波效果和平滑程度就越高。如果无法对较大波长进行滤波则应增加 LOD 级别。")] [SerializeField]
float _ObjectWidth = 3f;
readonly SampleFlowHelper _SampleFlowHelper = new();
Vector3[] _QueryPoints;
Vector3[] _QueryResultDisplacements;
Vector3[] _QueryResultVelocities;
Vector3[] _QueryResultNormal;
#endregion
[Header("Debug")] public bool drawDebug = false;
Rigidbody _rb;
CapsuleCollider _cap;
float _baseDrag, _baseAngularDrag;
[SerializeField] private bool _init = false;
void Awake()
{
_rb = GetComponent<Rigidbody>();
_baseDrag = _rb.linearDamping;
_baseAngularDrag = _rb.angularDamping;
}
void Start()
{
int length = Mathf.Max(3, samplePoints);
_QueryPoints = new Vector3[length];
_QueryResultDisplacements = new Vector3[length];
_QueryResultVelocities = new Vector3[length];
_QueryResultNormal = new Vector3[length];
}
public void InitBobber()
{
if (_waterRenderer == null && SceneSettings.Instance)
{
_waterRenderer = SceneSettings.Instance.Water;
}
_cap = GetComponentInChildren<CapsuleCollider>();
_init = true;
}
void FixedUpdate()
{
if (!_init) return;
if (!_waterRenderer) return;
GetWorldCapsule(out Vector3 a, out Vector3 b, out float radius);
int n = Mathf.Max(3, samplePoints);
if (_QueryPoints == null || _QueryPoints.Length != n)
{
_QueryPoints = new Vector3[n];
_QueryResultDisplacements = new Vector3[n];
_QueryResultVelocities = new Vector3[n];
_QueryResultNormal = new Vector3[n];
}
float fullBuoyancy = _rb.mass * Physics.gravity.magnitude * buoyancyScale;
float perPointMax = fullBuoyancy / n;
// 采样点
for (int i = 0; i < n; i++)
{
float t = (float)i / (n - 1);
_QueryPoints[i] = Vector3.Lerp(a, b, t);
}
// Crest 查询
var collisions = _waterRenderer.AnimatedWavesLod.Provider;
collisions.Query(GetHashCode(), _ObjectWidth, _QueryPoints, _QueryResultDisplacements,
_QueryResultNormal, _QueryResultVelocities, _Layer);
float subSum = 0f;
int wetCount = 0;
// 用于计算“浮力中心”(Center of Buoyancy)与水流速度平均
Vector3 cobSum = Vector3.zero;
Vector3 wvSum = Vector3.zero;
float cobW = 0f;
// 1) 多点只加浮力不再在每点加vertical damping
for (int i = 0; i < n; i++)
{
Vector3 p = _QueryPoints[i];
float waterH = _QueryResultDisplacements[i].y + _waterRenderer.SeaLevel;
float depth = waterH - p.y;
float sub = Mathf.InverseLerp(-radius, radius, depth);
if (sub <= 0f) continue;
sub = Mathf.Clamp01(submergenceCurve.Evaluate(sub));
subSum += sub;
wetCount++;
cobSum += p * sub;
wvSum += _QueryResultVelocities[i] * sub;
cobW += sub;
Vector3 buoyForce = Vector3.up * (perPointMax * sub);
_rb.AddForceAtPosition(buoyForce, p, ForceMode.Force);
if (drawDebug)
{
Debug.DrawLine(p, p + buoyForce / (_rb.mass * 10f), Color.cyan, 0f, false);
}
}
float subAvg = (wetCount > 0) ? (subSum / wetCount) : 0f;
// 2) vertical damping只在“浮力中心”施加一次关键修复不再产生抑制旋转的力矩
if (subAvg > 0f && cobW > 1e-6f)
{
Vector3 cob = cobSum / cobW;
Vector3 waterVelAvg = wvSum / cobW;
// 接近竖直时降低vertical damping避免90->80度“粘住”
float vdScale = 1f;
if (nearUprightDampingReduce > 0f)
{
Vector3 axisWorld = GetAxisWorld(uprightAxis);
float angleFromUp = Vector3.Angle(axisWorld, Vector3.up); // 0=竖直
float t = Mathf.Clamp01(angleFromUp / Mathf.Max(0.001f, nearUprightAngleDeg));
// t=0(很竖直) -> 1(离开竖直)
vdScale = Mathf.Lerp(1f - nearUprightDampingReduce, 1f, t);
}
Vector3 pointVel = _rb.GetPointVelocity(cob);
Vector3 relVel = pointVel - waterVelAvg;
float vUp = Vector3.Dot(relVel, Vector3.up);
Vector3 dampForce = -Vector3.up * (vUp * verticalDamping * _rb.mass * subAvg * vdScale);
_rb.AddForceAtPosition(dampForce, cob, ForceMode.Force);
if (drawDebug)
{
Debug.DrawLine(cob, cob + dampForce / (_rb.mass * 10f), Color.yellow, 0f, false);
}
}
// 3) 角阻尼:只加一次
if (subAvg > 0f)
{
_rb.AddTorque(-_rb.angularVelocity * (angularDamping * _rb.mass * subAvg), ForceMode.Force);
}
// 4) upright保持你原逻辑
if (subAvg > 0f && uprightSpring > 0f)
{
Vector3 axisWorld = GetAxisWorld(uprightAxis);
Vector3 targetUp = Vector3.up;
Vector3 errorAxis = Vector3.Cross(axisWorld, targetUp);
float errorMag = errorAxis.magnitude;
if (errorMag > 1e-6f)
{
errorAxis /= errorMag;
Vector3 springTorque = errorAxis * (uprightSpring * errorMag * _rb.mass);
Vector3 dampTorque = -_rb.angularVelocity * (uprightDamping * _rb.mass);
_rb.AddTorque((springTorque + dampTorque) * subAvg, ForceMode.Force);
}
}
// 5) 入水 drag
if (subAvg > 0.001f)
{
_rb.linearDamping = _baseDrag + extraDragInWater * subAvg;
_rb.angularDamping = _baseAngularDrag + extraAngularDragInWater * subAvg;
}
else
{
_rb.linearDamping = _baseDrag;
_rb.angularDamping = _baseAngularDrag;
}
}
Vector3 GetAxisWorld(int axis)
{
return axis switch
{
0 => transform.right,
2 => transform.forward,
_ => transform.up,
};
}
void GetWorldCapsule(out Vector3 a, out Vector3 b, out float radius)
{
Vector3 lossy = transform.lossyScale;
int dir = _cap.direction;
float scaleAlong = (dir == 0) ? Mathf.Abs(lossy.x) : (dir == 1) ? Mathf.Abs(lossy.y) : Mathf.Abs(lossy.z);
float scaleR;
if (dir == 0) scaleR = Mathf.Max(Mathf.Abs(lossy.y), Mathf.Abs(lossy.z));
else if (dir == 1) scaleR = Mathf.Max(Mathf.Abs(lossy.x), Mathf.Abs(lossy.z));
else scaleR = Mathf.Max(Mathf.Abs(lossy.x), Mathf.Abs(lossy.y));
radius = _cap.radius * scaleR;
Vector3 center = transform.TransformPoint(_cap.center);
Vector3 axisWorld = (dir == 0) ? transform.right : (dir == 1) ? transform.up : transform.forward;
float heightWorld = Mathf.Max(0f, _cap.height * scaleAlong);
float cylinderLen = Mathf.Max(0f, heightWorld - 2f * radius);
Vector3 half = axisWorld * (cylinderLen * 0.5f);
a = center - half;
b = center + half;
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f91c9d873c83492ca6d5e3e3a67c1760
timeCreated: 1772522093

View File

@@ -1,16 +0,0 @@
using UnityEngine;
namespace Test
{
public class CollisionTest : MonoBehaviour
{
private void OnCollisionEnter(Collision other)
{
Debug.Log($"OnCollisionEnter:{other.gameObject.name}");
}
private void OnCollisionExit(Collision other)
{
Debug.Log($"OnCollisionExit:{other.gameObject.name}");
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: d5bfa3f53f894e7a87315e5cbc220e12
timeCreated: 1772767361

View File

@@ -1,337 +0,0 @@
using UnityEngine;
/// <summary>
/// 鱼线张力参考模型
///
/// 这个脚本不负责真正的鱼线渲染也不负责完整鱼AI
/// 只负责根据“竿尖 - 鱼”的关系,计算一个较合理的张力模型。
///
/// 适合先拿来验证:
/// 1. 当前张力是否合理
/// 2. 鱼竿弯曲是否合理
/// 3. 卸力出线是否自然
/// 4. 断线判定是否符合预期
/// </summary>
public class FishingLineTensionModel : MonoBehaviour
{
[Header("References")] [Tooltip("鱼竿竿尖挂点")]
public Transform rodTip;
[Tooltip("鱼对象(位置来源)")] public Transform fishTarget;
[Tooltip("鱼刚体,用来取速度并施加线的拉力")] public Rigidbody fishRb;
[Tooltip("玩家/竿尖所在刚体可选。若为空则竿尖速度按0处理")] public Rigidbody rodTipRb;
[Header("Line State")] [Tooltip("当前放线长度(米)")]
public float lineLength = 5f;
[Tooltip("最短允许线长,防止收线收到过小")] public float minLineLength = 0.5f;
[Tooltip("最长允许线长")] public float maxLineLength = 100f;
[Tooltip("手动收线速度(米/秒),这里只是参考参数")] public float reelInSpeed = 1.5f;
[Tooltip("手动放线速度(米/秒),这里只是参考参数")] public float reelOutSpeed = 3f;
[Header("Line Tension")] [Tooltip("鱼线系统等效刚度。越大,拉直后越硬")]
public float lineStiffness = 120f;
[Tooltip("沿鱼线方向的阻尼。鱼外冲越快,附加张力越大")] public float lineDamping = 20f;
[Tooltip("接近绷直时保留的一点最小张力,避免完全没手感")] public float minTensionWhenNearTight = 1f;
[Tooltip("进入预张力区域的比例。比如0.95表示距离达到线长95%就开始有一点点手感")] [Range(0.7f, 1f)]
public float nearTightRatio = 0.95f;
[Header("Rod Buffer / Flex")] [Tooltip("鱼竿最大承载参考值。张力越接近这个值,竿越弯")]
public float rodMaxLoad = 40f;
[Tooltip("鱼竿最大缓冲长度(米)。竿越弯,等效可多“吃掉”一些长度")]
public float rodFlexMax = 0.35f;
[Header("Drag / Spool")] [Tooltip("绕线轮卸力阈值。张力超过它就开始自动出线")]
public float dragThreshold = 18f;
[Tooltip("超过卸力阈值后每多1单位张力对应的自动出线速度倍率")]
public float dragSpoolFactor = 0.08f;
[Header("Break / Damage")] [Tooltip("安全张力。超过它开始累计损伤")]
public float safeTension = 22f;
[Tooltip("绝对极限张力。超过它可以直接判定断线")] public float breakTension = 35f;
[Tooltip("超安全张力时的损伤累计速度倍率")] public float lineDamageRate = 1.5f;
[Tooltip("累计损伤达到此值后断线")] public float lineDamageLimit = 10f;
[Header("Fish Force")] [Tooltip("是否把张力反向施加给鱼刚体")]
public bool applyForceToFish = true;
[Tooltip("对鱼施加拉力时的倍率,用来调手感")] public float forceToFishScale = 1f;
[Header("Runtime Debug (Read Only)")] [SerializeField]
private float currentDistance;
[SerializeField] private float currentRelativeSpeedAlongLine;
[SerializeField] private float currentRodFlexOffset;
[SerializeField] private float currentOverStretch;
[SerializeField] private float currentTension;
[SerializeField] private float currentRodBend01;
[SerializeField] private float currentLineDamage;
[SerializeField] private bool isLineBroken;
[SerializeField] private bool isAutoSpooling;
/// <summary> 当前张力,对外只读 </summary>
public float CurrentTension => currentTension;
/// <summary> 当前鱼竿弯曲比例0~1 </summary>
public float CurrentRodBend01 => currentRodBend01;
/// <summary> 当前是否断线 </summary>
public bool IsLineBroken => isLineBroken;
/// <summary> 当前累计损伤 </summary>
public float CurrentLineDamage => currentLineDamage;
private void Reset()
{
lineLength = 5f;
minLineLength = 0.5f;
maxLineLength = 100f;
lineStiffness = 120f;
lineDamping = 20f;
minTensionWhenNearTight = 1f;
nearTightRatio = 0.95f;
rodMaxLoad = 40f;
rodFlexMax = 0.35f;
dragThreshold = 18f;
dragSpoolFactor = 0.08f;
safeTension = 22f;
breakTension = 35f;
lineDamageRate = 1.5f;
lineDamageLimit = 10f;
applyForceToFish = true;
forceToFishScale = 1f;
}
private void Update()
{
// 这里只演示输入,实际项目你可能会改成输入系统控制
HandleManualLineInput();
}
private void FixedUpdate()
{
if (isLineBroken)
{
currentTension = 0f;
currentRodBend01 = 0f;
currentRodFlexOffset = 0f;
currentOverStretch = 0f;
currentRelativeSpeedAlongLine = 0f;
isAutoSpooling = false;
return;
}
if (rodTip == null || fishTarget == null)
return;
// --------------------------------------------------
// 1. 计算竿尖到鱼的几何关系
// --------------------------------------------------
Vector3 delta = fishTarget.position - rodTip.position;
float distance = delta.magnitude;
Vector3 lineDir = distance > 0.0001f ? delta / distance : Vector3.forward;
currentDistance = distance;
// --------------------------------------------------
// 2. 计算沿鱼线方向的相对速度
// > 0 代表鱼在远离竿尖,拉力应增加
// < 0 代表鱼在靠近竿尖,拉力应减小
// --------------------------------------------------
Vector3 fishVel = fishRb != null ? fishRb.linearVelocity : Vector3.zero;
Vector3 tipVel = rodTipRb != null ? rodTipRb.linearVelocity : Vector3.zero;
float relativeSpeedAlongLine = Vector3.Dot(fishVel - tipVel, lineDir);
currentRelativeSpeedAlongLine = relativeSpeedAlongLine;
// --------------------------------------------------
// 3. 根据“上一帧张力”估算当前鱼竿弯曲带来的缓冲长度
// 张力越大,竿越弯,可额外缓冲一些长度
// --------------------------------------------------
currentRodBend01 = rodMaxLoad > 0.0001f
? Mathf.Clamp01(currentTension / rodMaxLoad)
: 0f;
currentRodFlexOffset = currentRodBend01 * rodFlexMax;
// --------------------------------------------------
// 4. 计算“超限量”
// 当距离 <= 线长 + 竿缓冲时,认为线没有真正被硬拉伸
// 当距离 > 线长 + 竿缓冲时,多出来的部分转化为张力
// --------------------------------------------------
float effectiveLength = lineLength + currentRodFlexOffset;
float overStretch = Mathf.Max(0f, distance - effectiveLength);
currentOverStretch = overStretch;
// --------------------------------------------------
// 5. 计算基础张力
// T = 刚度项 + 阻尼项
// --------------------------------------------------
float tension = 0f;
// 刚度项:超过可承受长度才会产生明显张力
tension += lineStiffness * overStretch;
// 阻尼项:只有“往外冲”的速度才增加张力
if (relativeSpeedAlongLine > 0f)
{
tension += lineDamping * relativeSpeedAlongLine;
}
// --------------------------------------------------
// 6. 接近绷直时给一点最小预张力
// 避免 D 接近 lineLength 时手感突然从0跳到有力
// --------------------------------------------------
float nearTightDistance = lineLength * nearTightRatio;
if (distance >= nearTightDistance)
{
tension = Mathf.Max(tension, minTensionWhenNearTight);
}
// 线完全松很多时,可直接视为无有效张力
if (distance < nearTightDistance && overStretch <= 0f)
{
tension = 0f;
}
currentTension = Mathf.Max(0f, tension);
// --------------------------------------------------
// 7. 卸力:当张力超过绕线轮设定值时,自动出线
// 这样大鱼冲刺时不会硬顶到瞬间爆线
// --------------------------------------------------
isAutoSpooling = false;
if (currentTension > dragThreshold)
{
float extraTension = currentTension - dragThreshold;
float autoSpoolSpeed = extraTension * dragSpoolFactor;
lineLength += autoSpoolSpeed * Time.fixedDeltaTime;
lineLength = Mathf.Clamp(lineLength, minLineLength, maxLineLength);
isAutoSpooling = true;
}
// --------------------------------------------------
// 8. 断线逻辑
// 8.1 超过极限值:可直接断
// 8.2 超过安全值:持续累计损伤
// --------------------------------------------------
if (currentTension >= breakTension)
{
BreakLine();
return;
}
if (currentTension > safeTension)
{
float overload = currentTension - safeTension;
currentLineDamage += overload * lineDamageRate * Time.fixedDeltaTime;
if (currentLineDamage >= lineDamageLimit)
{
BreakLine();
return;
}
}
else
{
// 张力安全时,损伤缓慢恢复一点
currentLineDamage -= Time.fixedDeltaTime;
currentLineDamage = Mathf.Max(0f, currentLineDamage);
}
// --------------------------------------------------
// 9. 把鱼线张力反向施加给鱼
// 鱼越往外冲,线张力越大,反向拉回鱼的力也越大
// --------------------------------------------------
if (applyForceToFish && fishRb != null && currentTension > 0f)
{
Vector3 pullForce = -lineDir * currentTension * forceToFishScale;
fishRb.AddForce(pullForce, ForceMode.Force);
}
// --------------------------------------------------
// 10. 重新根据当前张力更新鱼竿弯曲值(给外部表现层用)
// --------------------------------------------------
currentRodBend01 = rodMaxLoad > 0.0001f
? Mathf.Clamp01(currentTension / rodMaxLoad)
: 0f;
}
/// <summary>
/// 示例输入:
/// R = 收线
/// F = 放线
/// 实际项目建议接你自己的输入系统
/// </summary>
private void HandleManualLineInput()
{
if (isLineBroken)
return;
if (Input.GetKey(KeyCode.R))
{
lineLength -= reelInSpeed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.F))
{
lineLength += reelOutSpeed * Time.deltaTime;
}
lineLength = Mathf.Clamp(lineLength, minLineLength, maxLineLength);
}
/// <summary>
/// 断线
/// </summary>
private void BreakLine()
{
isLineBroken = true;
currentTension = 0f;
currentRodBend01 = 0f;
currentRodFlexOffset = 0f;
currentOverStretch = 0f;
isAutoSpooling = false;
Debug.Log("鱼线断了");
}
/// <summary>
/// 外部调用:修复鱼线
/// </summary>
public void RepairLine(float repairedLength)
{
isLineBroken = false;
currentLineDamage = 0f;
lineLength = Mathf.Clamp(repairedLength, minLineLength, maxLineLength);
}
/// <summary>
/// 外部调用:直接设置线长
/// </summary>
public void SetLineLength(float length)
{
lineLength = Mathf.Clamp(length, minLineLength, maxLineLength);
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 1a6c0b4a2030426c807730fe3837b9b9
timeCreated: 1775379112

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: bc8dcefa76944b33bc63ffe5f0280067
timeCreated: 1774185300

View File

@@ -1,936 +0,0 @@
using UnityEngine;
using WaveHarmonic.Crest;
/// <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("Crest 水体。为空时会尝试从 SceneSettings 读取")]
public WaterRenderer waterRenderer;
[Tooltip("Crest 查询层级")] public CollisionLayer waterCollisionLayer = CollisionLayer.AfterAnimatedWaves;
[Tooltip("Crest 波面查询宽度(参考 BobberFloating")]
public float waterQueryObjectWidth = 0.5f;
[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 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")] [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 bool UseTestPosture;
public BobberPosture TestPosture;
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 BobberPosture _pendingPosture;
private float _pendingPostureTimer;
private float _postureCooldownTimer;
private Vector3 _stablePlanarDir = Vector3.forward;
private bool _hasCrestSampleThisFrame;
private readonly Vector3[] _waterQueryPoints = new Vector3[1];
private readonly Vector3[] _waterQueryResultDisplacements = new Vector3[1];
private readonly Vector3[] _waterQueryResultVelocities = new Vector3[1];
private readonly Vector3[] _waterQueryResultNormal = new Vector3[1];
private void Awake()
{
_rb = GetComponent<Rigidbody>();
_defaultLinearDamping = _rb.linearDamping;
_defaultAngularDamping = _rb.angularDamping;
_defaultUseGravity = _rb.useGravity;
if (waterProviderBehaviour != null)
_waterProvider = waterProviderBehaviour as IWaterSurfaceProvider;
if (waterRenderer == null && SceneSettings.Instance != null)
waterRenderer = SceneSettings.Instance.Water;
_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 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;
_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()
{
_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 && !_hasCrestSampleThisFrame)
{
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)
);
bool hasLure = lureBody != null;
if (lureBody == null)
{
_verticalDistance = 0f;
_planarDistance = 0f;
_verticalRatio = 0f;
_planarRatio = 0f;
}
else
{
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;
}
BobberPosture desiredPosture = DeterminePostureState(submergeRatio, hasLure);
ApplyPostureWithStability(desiredPosture);
}
/// <summary>
/// 只在这个函数里写姿态状态判断。
/// 你后续要改逻辑,改这里即可,外面只负责采样数据和旋转平滑。
/// </summary>
private BobberPosture DeterminePostureState(float submergeRatio, bool hasLure)
{
if (UseTestPosture)
{
return TestPosture;
}
// 没有 Lure 时,保留简单兜底规则。
if (!hasLure)
{
if (submergeRatio < minSubmergeToStand)
return BobberPosture.Lying;
if (ExternalPlanarOffset.magnitude > 0.01f)
return BobberPosture.Tilted;
return BobberPosture.Upright;
}
// 这里是完整状态判断入口(可按你的需求自由改)。
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)
return BobberPosture.Upright;
if (canTilt)
return BobberPosture.Tilted;
return BobberPosture.Lying;
}
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)
return BobberPosture.Lying;
if (shouldStand)
return BobberPosture.Upright;
return BobberPosture.Tilted;
}
default:
{
bool shouldLie =
submergeRatio < minSubmergeToStand * 0.75f ||
_verticalRatio < verticalLieThreshold - postureHysteresis ||
_planarDistance > _verticalDistance * (planarDominanceMultiplier + 0.15f);
bool 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)
{
_postureCooldownTimer = Mathf.Max(0f, _postureCooldownTimer - Time.fixedDeltaTime);
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 += Time.fixedDeltaTime;
if (_pendingPostureTimer >= Mathf.Max(0f, postureConfirmTime))
{
_posture = desiredPosture;
_pendingPosture = _posture;
_pendingPostureTimer = 0f;
_postureCooldownTimer = Mathf.Max(0f, postureSwitchCooldown);
}
}
private void UpdateTargetRotationByPosture()
{
Vector3 candidateDir = Vector3.zero;
if (lureBody != null)
{
Vector3 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();
float dirDeadZone = Mathf.Max(0.0001f, planarDirectionDeadZone);
if (candidateDir.sqrMagnitude > dirDeadZone * dirDeadZone)
{
candidateDir.Normalize();
// 保持与上一帧同向,避免 180 度翻转造成左右闪。
if (Vector3.Dot(candidateDir, _stablePlanarDir) < 0f)
candidateDir = -candidateDir;
float k = 1f - Mathf.Exp(-Mathf.Max(0.01f, planarDirectionLerpSpeed) * Time.fixedDeltaTime);
_stablePlanarDir = Vector3.Slerp(_stablePlanarDir, candidateDir, k);
_stablePlanarDir.Normalize();
}
Vector3 planarDir = _stablePlanarDir;
Vector3 tiltAxis = Vector3.Cross(Vector3.up, planarDir);
if (tiltAxis.sqrMagnitude < 1e-6f)
{
tiltAxis = transform.right;
}
float angle = _posture switch
{
BobberPosture.Lying => lyingAngle,
BobberPosture.Tilted => tiltedAngle,
_ => 0f
};
_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)
{
if (_waterProvider != null)
{
_hasCrestSampleThisFrame = false;
return _waterProvider.GetWaterHeight(worldPos);
}
if (
waterRenderer != null
&& waterRenderer.AnimatedWavesLod != null
&& waterRenderer.AnimatedWavesLod.Provider != null
)
{
_waterQueryPoints[0] = worldPos;
waterRenderer.AnimatedWavesLod.Provider.Query(
GetHashCode(),
Mathf.Max(0.001f, waterQueryObjectWidth),
_waterQueryPoints,
_waterQueryResultDisplacements,
_waterQueryResultNormal,
_waterQueryResultVelocities,
waterCollisionLayer
);
_hasCrestSampleThisFrame = true;
return _waterQueryResultDisplacements[0].y + waterRenderer.SeaLevel;
}
_hasCrestSampleThisFrame = false;
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)
{
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);
waterQueryObjectWidth = Mathf.Max(0.001f, waterQueryObjectWidth);
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
#endregion
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 2dedfafdc2d747d98c682cde3e28e513
timeCreated: 1774185233

229
Assets/TerrainDef.mat Normal file
View File

@@ -0,0 +1,229 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: TerrainDef
m_Shader: {fileID: 4800000, guid: 69c1f799e772cb6438f56c23efccb782, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 1
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses:
- MOTIONVECTORS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Control:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Control0:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Diffuse:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Mask0:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Mask1:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Mask2:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Mask3:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Normal0:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Normal1:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Normal2:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Normal3:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _NormalSAO:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _PerPixelNormal:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _PerTexProps:
m_Texture: {fileID: -6551348070273646885, guid: c661901a630dc2544a9128ea4330e178, type: 2}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _SpecGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Splat0:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Splat1:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Splat2:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _Splat3:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _TerrainHolesTexture:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_Lightmaps:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_LightmapsInd:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_ShadowMasks:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AddPrecomputedVelocity: 0
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _BlendModePreserveSpecular: 1
- _BumpScale: 1
- _ClearCoatMask: 0
- _ClearCoatSmoothness: 0
- _Contrast: 0.4
- _Cull: 2
- _Cutoff: 0.5
- _DetailAlbedoMapScale: 1
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _DstBlendAlpha: 0
- _EnableHeightBlend: 0
- _EnableInstancedPerPixelNormal: 1
- _EnvironmentReflections: 1
- _GlossMapScale: 0
- _Glossiness: 0
- _GlossyReflections: 0
- _HeightTransition: 0
- _HybridHeightBlendDistance: 300
- _Metallic: 0
- _Metallic0: 0
- _Metallic1: 0
- _Metallic2: 0
- _Metallic3: 0
- _NumLayersCount: 1
- _OcclusionStrength: 1
- _Parallax: 0.005
- _QueueOffset: 0
- _ReceiveShadows: 1
- _Shininess: 0.078125
- _Smoothness: 0.5
- _Smoothness0: 0.5
- _Smoothness1: 0.5
- _Smoothness2: 0.5
- _Smoothness3: 0.5
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _WorkflowMode: 1
- _XRMotionVectorsPass: 1
- _ZWrite: 1
m_Colors:
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
- _TriplanarUVScale: {r: 1, g: 1, b: 0, a: 0}
- _UVScale: {r: 45, g: 45, b: 0, a: 0}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &4095775999777943649
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 95965ffe54835394fbe1c387dac39df6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 007677a8d215f46f78fdc4a57e84fb09, type: 3}
m_Name: TerrainDef_keywords
m_EditorClassIdentifier: JBooth.MicroSplat.Core::JBooth.MicroSplat.MicroSplatKeywords
keywords:
- _TERRAIN_INSTANCED_PERPIXEL_NORMAL

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6f7a96b20e24fd24d8669431f1e31fc6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c661901a630dc2544a9128ea4330e178
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -13,3 +13,4 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_LastMaterialVersion: 10
m_ProjectSettingFolderPath: URPDefaultResources