你这个现象(**两个角度来回切换/来回摆**)基本就是“**力矩过冲 + 阻尼施加方式不对**”导致的。 我上一版里最明显的问题是: * **角阻尼 `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 级尺寸那种)。