Compare commits

...

2 Commits

Author SHA1 Message Date
Bob.Song
209049354c 提交测试代码 2026-03-03 18:02:59 +08:00
Bob.Song
cbfe9581b1 Test 2026-03-03 16:23:56 +08:00
8 changed files with 573 additions and 469 deletions

237
Assets/Doc/浮漂.md Normal file
View File

@@ -0,0 +1,237 @@
你这个现象(**两个角度来回切换/来回摆**)基本就是“**力矩过冲 + 阻尼施加方式不对**”导致的。
我上一版里最明显的问题是:
* **角阻尼 `AddTorque(-angularVelocity * …)` 被我放在每个采样点里执行了 n 次**
→ 等于角阻尼被放大了 n 倍,而且随着浸没点数/深度变化,阻尼强度也在跳,容易出现“卡在两边来回抽”的感觉。
* 浮力点离散 + 过强阻尼/过强浮力,很容易出现**过冲**,然后再被反向力矩拉回 → 看起来像两个角度之间来回切。
下面我给你一个更稳的版本:
**浮力仍然按多点 AddForceAtPosition**(保证重心/姿态能自然翻正)
**角阻尼只加一次**(按总体浸没程度加)
✅ 线性阻尼也更合理(仍然只阻尼“上浮方向分量”,但强度不乱跳)
✅ 增加一个可选的 **“姿态稳定器 Upright Spring”**(很适合浮漂:会更快从躺漂回到竖漂,但不会锁死角度)
---
## CapsuleBuoyancyStable.cs更稳定版
```csharp
using UnityEngine;
public interface IWaterProvider
{
float GetWaterHeight(Vector3 worldPos);
Vector3 GetWaterNormal(Vector3 worldPos);
Vector3 GetWaterVelocity(Vector3 worldPos);
}
[DisallowMultipleComponent]
[RequireComponent(typeof(Rigidbody), typeof(CapsuleCollider))]
public class CapsuleBuoyancyStable : MonoBehaviour
{
[Header("References")]
public MonoBehaviour WaterBehaviour; // 实现 IWaterProvider
private IWaterProvider Water => WaterBehaviour as IWaterProvider;
[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("上浮方向速度阻尼(越大越不弹)。")]
public float verticalDamping = 3.0f;
[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("Debug")]
public bool drawDebug = false;
Rigidbody _rb;
CapsuleCollider _cap;
float _baseDrag, _baseAngularDrag;
void Awake()
{
_rb = GetComponent<Rigidbody>();
_cap = GetComponent<CapsuleCollider>();
_baseDrag = _rb.drag;
_baseAngularDrag = _rb.angularDrag;
if (WaterBehaviour != null && Water == null)
Debug.LogError($"{name}: WaterBehaviour 没有实现 IWaterProvider。", this);
}
void FixedUpdate()
{
if (Water == null) return;
GetWorldCapsule(out Vector3 a, out Vector3 b, out float radius);
int n = Mathf.Max(3, samplePoints);
float fullBuoyancy = _rb.mass * Physics.gravity.magnitude * buoyancyScale;
float perPointMax = fullBuoyancy / n;
float subSum = 0f;
int wetCount = 0;
for (int i = 0; i < n; i++)
{
float t = (float)i / (n - 1);
Vector3 p = Vector3.Lerp(a, b, t);
float waterH = Water.GetWaterHeight(p);
float depth = waterH - p.y; // >0 在水下
float sub = Mathf.InverseLerp(-radius, radius, depth); // 0..1
if (sub <= 0f) continue;
sub = Mathf.Clamp01(submergenceCurve.Evaluate(sub));
subSum += sub;
wetCount++;
Vector3 buoyDir = Vector3.up;
Vector3 waterVel = Water.GetWaterVelocity(p);
Vector3 pointVel = _rb.GetPointVelocity(p);
Vector3 relVel = pointVel - waterVel;
// 浮力
Vector3 buoyForce = buoyDir * (perPointMax * sub);
// 只阻尼上浮方向速度分量(防弹跳)
float vUp = Vector3.Dot(relVel, buoyDir);
Vector3 dampForce = -buoyDir * (vUp * verticalDamping * _rb.mass * sub);
_rb.AddForceAtPosition(buoyForce + dampForce, p, ForceMode.Force);
if (drawDebug)
{
Debug.DrawLine(p, p + buoyForce / (_rb.mass * 10f), Color.cyan, 0f, false);
Debug.DrawLine(p, p + dampForce / (_rb.mass * 10f), Color.yellow, 0f, false);
}
}
float subAvg = (wetCount > 0) ? (subSum / wetCount) : 0f;
// 角阻尼:只加一次(关键修复点)
if (subAvg > 0f)
{
_rb.AddTorque(-_rb.angularVelocity * (angularDamping * _rb.mass * subAvg), ForceMode.Force);
}
// 可选upright 稳定器(更像“浮漂自动立起来”)
if (subAvg > 0f && uprightSpring > 0f)
{
Vector3 axisWorld = GetAxisWorld(uprightAxis);
Vector3 targetUp = Vector3.up;
// 误差轴axisWorld 需要对齐 targetUp也可反过来按你浮漂模型选
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);
}
}
// 入水整体 drag
if (subAvg > 0.001f)
{
_rb.drag = _baseDrag + extraDragInWater * subAvg;
_rb.angularDrag = _baseAngularDrag + extraAngularDragInWater * subAvg;
}
else
{
_rb.drag = _baseDrag;
_rb.angularDrag = _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; // 0=X,1=Y,2=Z
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;
}
}
```
---
## 你该怎么调(针对“两个角度来回变”)
先用这组“稳的起步值”:
* `samplePoints = 9`
* `buoyancyScale = 1.4 ~ 2.2`
* `verticalDamping = 2.0 ~ 6.0`(弹跳就加)
* `angularDamping = 0.3 ~ 1.0`(摆动就加,但别太大)
* 如果你是浮漂想要“自动立漂”:
* `uprightSpring = 0.5 ~ 3.0`
* `uprightDamping = 0.3 ~ 1.5`
---
## 额外一句:为什么 upright 会明显改善“两个角度抽动”
纯“浮力点分布”产生的扶正力矩在水面附近会很敏感(浸没量一点点变化就翻转力矩方向),尤其你做的是**超小物体**数值抖动更明显。upright 相当于给了一个“低频、连续”的姿态回正控制,配合阻尼,就不会在两个角度之间来回抽。
如果你把你当前浮漂的 **CapsuleCollider 参数height/radius/center/direction+ Rigidbody mass + drag/angularDrag + 模型 pivot 在哪** 发我,我可以直接按你的尺度给一套“几乎不用调”的默认参数(针对 0.01 级尺寸那种)。

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 55d482dca23741a48523966428073ce1
timeCreated: 1772525626

View File

@@ -863,6 +863,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 06d107cece7c4cbb9825557923be567f, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::BuoyancyWaterProvider
Water: {fileID: 0}
waterLevel: 0
--- !u!4 &820800910
Transform:
@@ -2002,7 +2003,6 @@ GameObject:
- component: {fileID: 1518432886}
- component: {fileID: 1518432885}
- component: {fileID: 1518432890}
- component: {fileID: 1518432891}
m_Layer: 0
m_Name: Tip
m_TagString: Untagged
@@ -2123,7 +2123,7 @@ Transform:
m_GameObject: {fileID: 1518432884}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 2.3, z: 0}
m_LocalPosition: {x: 0, y: 1.5, z: 0}
m_LocalScale: {x: 0.01, y: 0.01, z: 0.01}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -2138,25 +2138,13 @@ MonoBehaviour:
m_GameObject: {fileID: 1518432884}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5e21f6b9b3c2483e92fdf2f3dfdcce62, type: 3}
m_Script: {fileID: 11500000, guid: 3444ef411bcf4f7fa35c03ec7d800ff8, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Test.BobberTest
rb: {fileID: 1518432885}
line: {fileID: 8144283643417267672}
lineLength: 2
floatLength: 1
--- !u!114 &1518432891
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1518432884}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a1300e9b5a5c347408708087176324c0, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::FlatWaterHeightProvider
--- !u!1 &1529912227
GameObject:
m_ObjectHideFlags: 0
@@ -2562,205 +2550,6 @@ Transform:
m_Children: []
m_Father: {fileID: 2058458420}
m_LocalEulerAnglesHint: {x: 0, y: -0, z: -0}
--- !u!1 &1738432953
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1738432958}
- component: {fileID: 1738432957}
- component: {fileID: 1738432956}
- component: {fileID: 1738432955}
- component: {fileID: 1738432954}
m_Layer: 7
m_Name: Lure (1)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!65 &1738432954
BoxCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1738432953}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Size: {x: 0.1, y: 0.1, z: 0.1}
m_Center: {x: 0, y: 0, z: 0}
--- !u!114 &1738432955
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1738432953}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ed5bbbc032ec4ca1bb56991d9141e311, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::NBF.LureController
rBody: {fileID: 1738432957}
joint: {fileID: 1738432956}
--- !u!153 &1738432956
ConfigurableJoint:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1738432953}
serializedVersion: 4
m_ConnectedBody: {fileID: 8094040829892155629}
m_ConnectedArticulationBody: {fileID: 0}
m_Anchor: {x: 0, y: 0, z: 0}
m_Axis: {x: 0, y: 0, z: 0}
m_AutoConfigureConnectedAnchor: 0
m_ConnectedAnchor: {x: 0, y: 0, z: 0}
m_SecondaryAxis: {x: 0, y: 0, z: 0}
m_XMotion: 1
m_YMotion: 1
m_ZMotion: 1
m_AngularXMotion: 2
m_AngularYMotion: 2
m_AngularZMotion: 2
m_LinearLimitSpring:
spring: 0
damper: 0
m_LinearLimit:
limit: 0.5
bounciness: 0
contactDistance: 0
m_AngularXLimitSpring:
spring: 0
damper: 0
m_LowAngularXLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_HighAngularXLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_AngularYZLimitSpring:
spring: 0
damper: 0
m_AngularYLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_AngularZLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_TargetPosition: {x: 0, y: 0, z: 0}
m_TargetVelocity: {x: 0, y: 0, z: 0}
m_XDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_YDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_ZDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_TargetRotation: {x: 0, y: 0, z: 0, w: 1}
m_TargetAngularVelocity: {x: 0, y: 0, z: 0}
m_RotationDriveMode: 0
m_AngularXDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_AngularYZDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_SlerpDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_ProjectionMode: 1
m_ProjectionDistance: 0
m_ProjectionAngle: 0
m_ConfiguredInWorldSpace: 0
m_SwapBodies: 0
m_BreakForce: Infinity
m_BreakTorque: Infinity
m_EnableCollision: 0
m_EnablePreprocessing: 0
m_MassScale: 1
m_ConnectedMassScale: 1
--- !u!54 &1738432957
Rigidbody:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1738432953}
serializedVersion: 5
m_Mass: 0.01
m_LinearDamping: 1
m_AngularDamping: 0.1
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 0.001, y: 0.001, z: 0.001}
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_ImplicitCom: 1
m_ImplicitTensor: 0
m_UseGravity: 0
m_IsKinematic: 0
m_Interpolate: 0
m_Constraints: 0
m_CollisionDetection: 0
--- !u!4 &1738432958
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1738432953}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!4 &1801125034 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 2452750316707852748, guid: c26fe2b4fef6c484089497b549dd6b04, type: 3}
@@ -3100,10 +2889,20 @@ MonoBehaviour:
dragScale: 1
waterLevel: 0
waterLayer:
serializedVersion: 2
m_Bits: 16
showDebugInfo: 1
debugColor: {r: 0, g: 1, b: 1, a: 1}
debugColor:
r: 0
g: 1
b: 1
a: 1
rb: {fileID: 0}
objCollider: {fileID: 0}
volumeInCm: 0
samplePoints: []
localBounds:
m_Center: {x: 0, y: 0, z: 0}
m_Extent: {x: 0, y: 0, z: 0}
--- !u!4 &1948332548 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 8356280719142672529, guid: 84f17dfc7c7a7485296643a4e64d6200, type: 3}
@@ -3950,7 +3749,7 @@ RectTransform:
m_Father: {fileID: 2256579932936496278}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0.2, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -4754,7 +4553,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!1 &2575899344989503316
GameObject:
m_ObjectHideFlags: 0
@@ -5510,8 +5309,8 @@ RectTransform:
m_Children: []
m_Father: {fileID: 4262266657851529727}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.2, y: 0}
m_AnchorMax: {x: 0.2, y: 1}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -6694,7 +6493,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: f91c9d873c83492ca6d5e3e3a67c1760, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::CapsuleBuoyancyStable
WaterBehaviour: {fileID: 820800909}
buoyancyScale: 1.6
samplePoints: 9
submergenceCurve:
@@ -6728,6 +6526,9 @@ MonoBehaviour:
uprightAxis: 1
extraDragInWater: 0.8
extraAngularDragInWater: 0.8
Water: {fileID: 0}
_Layer: 1
_ObjectWidth: 3
drawDebug: 0
--- !u!114 &7647511515837834160
MonoBehaviour:
@@ -7505,7 +7306,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: f91c9d873c83492ca6d5e3e3a67c1760, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::CapsuleBuoyancyStable
WaterBehaviour: {fileID: 820800909}
buoyancyScale: 1.6
samplePoints: 9
submergenceCurve:
@@ -7539,6 +7339,9 @@ MonoBehaviour:
uprightAxis: 1
extraDragInWater: 0.8
extraAngularDragInWater: 0.8
Water: {fileID: 2531380344179187550}
_Layer: 1
_ObjectWidth: 0.2
drawDebug: 0
--- !u!153 &8264839114692736908
ConfigurableJoint:
@@ -8153,7 +7956,6 @@ SceneRoots:
- {fileID: 3065509872725565573}
- {fileID: 8114378222086924161}
- {fileID: 1518432889}
- {fileID: 1738432958}
- {fileID: 909052972}
- {fileID: 668361904}
- {fileID: 154764977}

View File

@@ -0,0 +1,24 @@
using System;
using NBF;
using UnityEngine;
namespace Test
{
public class BobberTest : MonoBehaviour
{
public Rigidbody rb;
public FLine line;
public float lineLength = 1f;
public float floatLength = 0.5f;
public void Start()
{
line.InitTest(rb);
//有浮漂
line.Lure.SetJointDistance(floatLength);
line.Bobber.SetJointDistance(lineLength - floatLength);
line.SetObiRopeStretch(lineLength - floatLength);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3444ef411bcf4f7fa35c03ec7d800ff8
timeCreated: 1772525731

View File

@@ -6,219 +6,204 @@ public interface IWaterProvider
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;
// 取“浮体中心点”作为控制点(稳定,不戳点)
Vector3 centerWorld;
float shapeHeight; // 近似“高度”sphere=直径capsule=高度
GetCenterAndHeight(out centerWorld, out shapeHeight);
// 水面信息
float waterH = (waterProvider != null) ? waterProvider.GetWaterHeight(centerWorld) : waterLevel;
Vector3 waterN = (waterProvider != null) ? 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);
}
}
//
// /// <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,7 +1,10 @@
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)

View File

@@ -1,46 +1,62 @@
using UnityEngine;
using System;
using Gaia;
using UnityEngine;
using WaveHarmonic.Crest;
[DisallowMultipleComponent]
[RequireComponent(typeof(Rigidbody), typeof(CapsuleCollider))]
public class CapsuleBuoyancyStable : MonoBehaviour
{
[Header("References")]
public MonoBehaviour WaterBehaviour; // 实现 IWaterProvider
private IWaterProvider Water => WaterBehaviour as IWaterProvider;
[Header("Buoyancy")]
[Tooltip("完全浸没时总浮力 = mass*g*buoyancyScale。>1 更浮。")]
[Header("Buoyancy")] [Tooltip("完全浸没时总浮力 = mass*g*buoyancyScale。>1 更浮。")]
public float buoyancyScale = 1.6f;
[Tooltip("沿胶囊轴向采样点数量(建议 7~11。")]
[Range(3, 15)] public int samplePoints = 9;
[Tooltip("沿胶囊轴向采样点数量(建议 7~11。")] [Range(3, 15)]
public int samplePoints = 9;
[Tooltip("浸没比例曲线0=刚碰水, 1=充分在水下)。")]
public AnimationCurve submergenceCurve = AnimationCurve.Linear(0, 0, 1, 1);
[Tooltip("浸没比例曲线0=刚碰水, 1=充分在水下)。")] public AnimationCurve submergenceCurve = AnimationCurve.Linear(0, 0, 1, 1);
[Header("Damping")]
[Tooltip("上浮方向速度阻尼(越大越不弹)。")]
[Header("Damping")] [Tooltip("上浮方向速度阻尼(越大越不弹)。")]
public float verticalDamping = 3.0f;
[Tooltip("整体角速度阻尼(只施加一次,不要太大)。")]
public float angularDamping = 0.6f;
[Tooltip("整体角速度阻尼(只施加一次,不要太大)。")] public float angularDamping = 0.6f;
[Header("Optional Upright Stabilizer (Recommended for bobber)")]
[Tooltip("让胶囊轴向更倾向于对齐世界Up。0=关闭。")]
[Header("Optional Upright Stabilizer (Recommended for bobber)")] [Tooltip("让胶囊轴向更倾向于对齐世界Up。0=关闭。")]
public float uprightSpring = 0.0f;
[Tooltip("upright 的角速度阻尼。")]
public float uprightDamping = 0.5f;
[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;
[Header("Water Drag")] public float extraDragInWater = 0.8f;
public float extraAngularDragInWater = 0.8f;
[Header("Debug")]
public bool drawDebug = false;
#region Crest5相关信息
public Transform Water;
private WaterRenderer _waterRenderer;
[Tooltip("要瞄准哪一层水的碰撞层。")] [SerializeField]
CollisionLayer _Layer = CollisionLayer.AfterAnimatedWaves;
[Header("波响应")] [Tooltip("用于物理计算的物体宽度。\n\n此值越大波响应的滤波效果和平滑程度就越高。如果无法对较大波长进行滤波则应增加 LOD 级别。")] [SerializeField]
float _ObjectWidth = 3f;
readonly SampleFlowHelper _SampleFlowHelper = new();
/// <summary>
/// 查询水面信息点位
/// </summary>
Vector3[] _QueryPoints;
Vector3[] _QueryResultDisplacements;
Vector3[] _QueryResultVelocities;
Vector3[] _QueryResultNormal;
#endregion
[Header("Debug")] public bool drawDebug = false;
Rigidbody _rb;
CapsuleCollider _cap;
@@ -52,15 +68,30 @@ public class CapsuleBuoyancyStable : MonoBehaviour
_cap = GetComponent<CapsuleCollider>();
_baseDrag = _rb.linearDamping;
_baseAngularDrag = _rb.angularDamping;
}
if (WaterBehaviour != null && Water == null)
Debug.LogError($"{name}: WaterBehaviour 没有实现 IWaterProvider。", this);
private void Start()
{
int length = Mathf.Max(3, samplePoints);
_QueryPoints = new Vector3[length];
_QueryResultDisplacements = new Vector3[length];
_QueryResultVelocities = new Vector3[length];
_QueryResultNormal = new Vector3[length];
if(Water)
_waterRenderer = Water.GetComponent<WaterRenderer>();
}
void FixedUpdate()
{
if (Water == null) return;
if (Water)
{
}
if (!_waterRenderer)
{
return;
}
GetWorldCapsule(out Vector3 a, out Vector3 b, out float radius);
int n = Mathf.Max(3, samplePoints);
@@ -70,12 +101,27 @@ public class CapsuleBuoyancyStable : MonoBehaviour
float subSum = 0f;
int wetCount = 0;
for (int i = 0; i < _QueryPoints.Length; i++)
{
float t = (float)i / (n - 1);
Vector3 p = Vector3.Lerp(a, b, t);
_QueryPoints[i] = p;
}
// 查询
var collisions = _waterRenderer.AnimatedWavesLod.Provider;
collisions.Query(GetHashCode(), _ObjectWidth, _QueryPoints, _QueryResultDisplacements,
_QueryResultNormal, _QueryResultVelocities, _Layer);
for (int i = 0; i < n; i++)
{
float t = (float)i / (n - 1);
Vector3 p = Vector3.Lerp(a, b, t);
float waterH = Water.GetWaterHeight(p);
float waterH =
_QueryResultDisplacements[i].y + _waterRenderer.SeaLevel; //GaiaConstants.Water.GetWaterHeight(p);
float depth = waterH - p.y; // >0 在水下
float sub = Mathf.InverseLerp(-radius, radius, depth); // 0..1
@@ -87,7 +133,8 @@ public class CapsuleBuoyancyStable : MonoBehaviour
Vector3 buoyDir = Vector3.up;
Vector3 waterVel = Water.GetWaterVelocity(p);
// Vector3 waterVel = GaiaConstants.Water.GetWaterVelocity(p);
Vector3 waterVel = _QueryResultVelocities[i];
Vector3 pointVel = _rb.GetPointVelocity(p);
Vector3 relVel = pointVel - waterVel;
@@ -184,8 +231,8 @@ public class CapsuleBuoyancyStable : MonoBehaviour
a = center - half;
b = center + half;
}
private void OnDrawGizmosSelected()
{
if (drawDebug)