diff --git a/Assets/Doc/浮漂.md b/Assets/Doc/浮漂.md new file mode 100644 index 000000000..63aefa122 --- /dev/null +++ b/Assets/Doc/浮漂.md @@ -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(); + _cap = GetComponent(); + _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 级尺寸那种)。 diff --git a/Assets/Doc/浮漂.md.meta b/Assets/Doc/浮漂.md.meta new file mode 100644 index 000000000..95d1934fc --- /dev/null +++ b/Assets/Doc/浮漂.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 55d482dca23741a48523966428073ce1 +timeCreated: 1772525626 \ No newline at end of file diff --git a/Assets/Scenes/BobberTest.unity b/Assets/Scenes/BobberTest.unity index ac37e88ad..a019c98e1 100644 --- a/Assets/Scenes/BobberTest.unity +++ b/Assets/Scenes/BobberTest.unity @@ -2002,7 +2002,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 +2122,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 +2137,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 +2549,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 +2888,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} @@ -8153,7 +7951,6 @@ SceneRoots: - {fileID: 3065509872725565573} - {fileID: 8114378222086924161} - {fileID: 1518432889} - - {fileID: 1738432958} - {fileID: 909052972} - {fileID: 668361904} - {fileID: 154764977} diff --git a/Assets/Scripts/Test/BobberTest.cs b/Assets/Scripts/Test/BobberTest.cs new file mode 100644 index 000000000..179e13a1e --- /dev/null +++ b/Assets/Scripts/Test/BobberTest.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Test/BobberTest.cs.meta b/Assets/Scripts/Test/BobberTest.cs.meta new file mode 100644 index 000000000..31a11c454 --- /dev/null +++ b/Assets/Scripts/Test/BobberTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3444ef411bcf4f7fa35c03ec7d800ff8 +timeCreated: 1772525731 \ No newline at end of file