using System; using System.Collections.Generic; using UnityEngine; using WaveHarmonic.Crest; namespace NBF { /// /// 小型浮漂/小物体:基于 Crest 的浮力(自动从多个 CapsuleCollider 计算尺寸与探针) /// - 自动收集自身与子物体上的 CapsuleCollider(可多个) /// - 计算整体长轴、长度、最大直径、底部高度 /// - 自动设置 Crest 查询尺度 _ObjectWidth(与直径同量级) /// - 自动生成 5 个探针:底部四周 + 底部中心(更稳) /// - 浮力:弹簧(k) + 阻尼(c),ForceMode.Force(质量参与) /// public sealed class BobberFloating : MonoBehaviour { [Header("Crest")] public WaterRenderer _water; [SerializeField] Rigidbody _RigidBody; [Tooltip("要瞄准哪一层水的碰撞层。")] [SerializeField] CollisionLayer _Layer = CollisionLayer.AfterAnimatedWaves; // ----------------------------- // 自动从 CapsuleCollider 计算 // ----------------------------- [Header("Auto from CapsuleColliders")] [Tooltip("自动扫描自己与子物体的 CapsuleCollider,并计算尺寸/探针/采样尺度。")] [SerializeField] bool _AutoFromCapsules = true; [Tooltip("仅使用 enabled 的 CapsuleCollider。")] [SerializeField] bool _OnlyEnabledCapsules = true; [Tooltip("运行时也会每隔一定时间重建(应对浮漂缩放/更换碰撞体)。0 表示仅 Start/OnValidate 时重建。")] [SerializeField] float _RuntimeRebuildInterval = 0f; [Tooltip("给估算出来的直径乘一个系数,用于更稳的 Crest 查询与阻尼。通常 1.0~2.0。")] [Range(0.5f, 3f)] [SerializeField] float _WidthMultiplier = 1.2f; [Tooltip("探针半径比例(相对估算半径)。越大越“撑开”,越抗翻滚,但也更容易被浪抬。建议 0.6~0.9")] [Range(0.1f, 1.2f)] [SerializeField] float _ProbeRadiusRatio = 0.75f; [Tooltip("探针在底部之上的抬高(米)。用于避免探针刚好在最底点导致抖动。建议:半径的 5%~20%。")] [SerializeField] float _ProbeLiftMeters = 0.0f; [Tooltip("探针权重分配:四周总权重(其余给中心)。建议 0.6~0.9")] [Range(0f, 1f)] [SerializeField] float _RingWeight = 0.75f; // ----------------------------- // 物理参数(吃水可控) // ----------------------------- [Header("Buoyancy (controllable submergence)")] [Tooltip("目标吃水比例:以“直径”为尺度。\n例:0.30 表示目标吃水深度约 = 直径*0.30。\n对小浮漂非常好调:想更浮(露更多)-> 降低;想更沉 -> 提高。")] [Range(0.05f, 1.2f)] [SerializeField] float _TargetSubmergenceRatio = 0.30f; [Tooltip("浮力阻尼比(0~2 常用)。越大越不抖,但会显得黏。推荐 0.7~1.2")] [Range(0f, 2f)] [SerializeField] float _DampingRatio = 0.95f; [Tooltip("最大总浮力(N)保护。若为 Infinity 则不限制。")] [SerializeField] float _MaximumBuoyancyForce = 50f; [Tooltip("吃水偏移(米)。用于校准 pivot/碰撞体中心误差。\n>0 更容易浮起(等效更浅),<0 更沉。")] [SerializeField] float _SubmergenceOffset = 0f; [Header("Water drag (relative to water surface velocity/flow)")] [Tooltip("相对水的线性阻力(N per m/s)。小浮漂建议从 (0.2, 0.6, 0.2) 起。")] [SerializeField] Vector3 _Drag = new(0.2f, 0.6f, 0.2f); [Tooltip("角阻力(N*m per rad/s)。小浮漂建议 0.01~0.05。")] [SerializeField] float _AngularDrag = 0.03f; [Header("Optional downhill acceleration")] [Range(0, 1)] [SerializeField] float _AccelerateDownhill = 0f; [Header("Force application height offset")] [SerializeField] float _ForceHeightOffset = 0f; // ----------------------------- // 自动生成的“几何结果” // ----------------------------- [Header("Computed (read-only)")] [SerializeField] float _ComputedLength = 0.1f; [SerializeField] float _ComputedDiameter = 0.04f; [SerializeField] Vector3 _ComputedAxisLocal = Vector3.up; // 刚体局部坐标中的长轴(单位向量) [SerializeField] float _ComputedBottomLocalY = -0.02f; // 刚体局部坐标中“整体最低点”的 y(沿长轴方向投影不是y;这里是按 rb local Y,仅用于Debug显示) [Header("Wave response / query scale")] [Tooltip("用于 Crest 查询的物体宽度(米)。会自动从 Capsule 直径推算。")] [SerializeField] float _ObjectWidth = 0.04f; // ----------------------------- // Debug // ----------------------------- [Space(10)] [SerializeField] DebugFields _Debug = new(); [Serializable] sealed class DebugFields { [SerializeField] internal bool _DrawProbes = false; [SerializeField] internal bool _DrawForces = false; } /// 是否有任意探针入水 public bool InWater { get; private set; } // 探针(刚体局部空间,相对 worldCenterOfMass) struct Probe { public Vector3 localOffsetFromCOM; public float weight; } Probe[] _Probes = Array.Empty(); readonly SampleFlowHelper _SampleFlowHelper = new(); Vector3[] _QueryPoints; Vector3[] _QueryResultDisplacements; Vector3[] _QueryResultVelocities; Vector3[] _QueryResultNormal; float _NextRebuildTime = 0f; void Reset() { if (_RigidBody == null) TryGetComponent(out _RigidBody); } void OnValidate() { _ObjectWidth = Mathf.Max(0.001f, _ObjectWidth); _RuntimeRebuildInterval = Mathf.Max(0f, _RuntimeRebuildInterval); _ProbeLiftMeters = Mathf.Max(0f, _ProbeLiftMeters); if (!Application.isPlaying) { if (_AutoFromCapsules) { TryRebuildFromCapsules(editor: true); } AllocateQueryArrays(); } } void Start() { if (_RigidBody == null) TryGetComponent(out _RigidBody); if (_AutoFromCapsules) { TryRebuildFromCapsules(editor: false); } AllocateQueryArrays(); } void FixedUpdate() { if (_water == null || _RigidBody == null) return; if (_AutoFromCapsules && _RuntimeRebuildInterval > 0f && Time.time >= _NextRebuildTime) { _NextRebuildTime = Time.time + _RuntimeRebuildInterval; TryRebuildFromCapsules(editor: false); AllocateQueryArrays(); } if (_Probes == null || _Probes.Length == 0) return; // Crest provider var collisions = _water.AnimatedWavesLod.Provider; int probeCount = _Probes.Length; int centerIndex = probeCount; // 最后一个点用于中心采样(表面速度/法线) Vector3 comWorld = _RigidBody.worldCenterOfMass; // Build query points for (int i = 0; i < probeCount; i++) { _QueryPoints[i] = comWorld + transform.TransformVector(_Probes[i].localOffsetFromCOM); } _QueryPoints[centerIndex] = comWorld; // Query waves collisions.Query( GetHashCode(), _ObjectWidth, _QueryPoints, _QueryResultDisplacements, _QueryResultNormal, _QueryResultVelocities, _Layer ); // Surface velocity + flow Vector3 surfaceVelocity = _QueryResultVelocities[centerIndex]; _SampleFlowHelper.Sample(transform.position, out var surfaceFlow, minimumLength: _ObjectWidth); surfaceVelocity += new Vector3(surfaceFlow.x, 0f, surfaceFlow.y); // Compute buoyancy coefficients float g = Mathf.Abs(Physics.gravity.y); // 目标吃水深度(米):按直径比例 float targetSubmergence = Mathf.Max(0.0005f, _ComputedDiameter * _TargetSubmergenceRatio); // 总弹簧系数:k ≈ m*g / targetSubmergence float kTotal = (_RigidBody.mass * g) / targetSubmergence; // 总阻尼:c = 2*sqrt(k*m)*dampingRatio float cTotal = 2f * Mathf.Sqrt(Mathf.Max(0.0001f, kTotal * _RigidBody.mass)) * _DampingRatio; InWater = false; float totalBuoyMag = 0f; // Apply distributed buoyancy for (int i = 0; i < probeCount; i++) { Vector3 p = _QueryPoints[i]; float waterHeight = _QueryResultDisplacements[i].y + _water.SeaLevel; // depth: positive means submerged float depth = (waterHeight - p.y) + _SubmergenceOffset; if (depth <= 0f) continue; InWater = true; float w = Mathf.Max(0.0001f, _Probes[i].weight); // spring float FiSpring = (kTotal * w) * depth; // damping (vertical relative velocity) Vector3 pointVel = _RigidBody.GetPointVelocity(p); float vRel = Vector3.Dot(pointVel - surfaceVelocity, Vector3.up); float FiDamp = -(cTotal * w) * vRel; float Fi = FiSpring + FiDamp; if (Fi < 0f) Fi = 0f; totalBuoyMag += Fi; Vector3 force = Fi * Vector3.up; _RigidBody.AddForceAtPosition(force, p, ForceMode.Force); if (_Debug._DrawForces) { Debug.DrawLine(p, p + force * 0.02f, Color.cyan); } } if (!InWater) return; // Clamp buoyancy (simple safety) if (_MaximumBuoyancyForce < Mathf.Infinity && totalBuoyMag > _MaximumBuoyancyForce) { float excess = totalBuoyMag - _MaximumBuoyancyForce; _RigidBody.AddForce(-excess * Vector3.up, ForceMode.Force); } // Optional downhill acceleration (usually 0 for bobber) if (_AccelerateDownhill > 0f) { Vector3 normal = _QueryResultNormal[centerIndex]; _RigidBody.AddForce(_AccelerateDownhill * g * new Vector3(normal.x, 0f, normal.z), ForceMode.Force); } // Angular drag if (_AngularDrag > 0f) { _RigidBody.AddTorque(-_AngularDrag * _RigidBody.angularVelocity, ForceMode.Force); } // Linear drag relative to water if (_Drag != Vector3.zero) { Vector3 velocityRelativeToWater = _RigidBody.linearVelocity - surfaceVelocity; Vector3 forcePosition = _RigidBody.worldCenterOfMass + _ForceHeightOffset * Vector3.up; _RigidBody.AddForceAtPosition( _Drag.x * Vector3.Dot(transform.right, -velocityRelativeToWater) * transform.right, forcePosition, ForceMode.Force); _RigidBody.AddForceAtPosition( _Drag.y * Vector3.Dot(Vector3.up, -velocityRelativeToWater) * Vector3.up, forcePosition, ForceMode.Force); _RigidBody.AddForceAtPosition( _Drag.z * Vector3.Dot(transform.forward, -velocityRelativeToWater) * transform.forward, forcePosition, ForceMode.Force); } if (_Debug._DrawProbes) { for (int i = 0; i < probeCount; i++) { Debug.DrawLine(_QueryPoints[i], _QueryPoints[i] + Vector3.up * 0.05f, Color.yellow); } } } void AllocateQueryArrays() { int probeCount = _Probes?.Length ?? 0; int n = probeCount + 1; // +1 center sample if (n <= 1) n = 2; if (_QueryPoints != null && _QueryPoints.Length == n) return; _QueryPoints = new Vector3[n]; _QueryResultDisplacements = new Vector3[n]; _QueryResultVelocities = new Vector3[n]; _QueryResultNormal = new Vector3[n]; } /// /// 公开按钮:手动重建(你也可以在 Inspector 右键脚本 -> Reset,然后 Play) /// [ContextMenu("Rebuild From CapsuleColliders")] public void RebuildFromCapsules() { TryRebuildFromCapsules(editor: Application.isEditor && !Application.isPlaying); AllocateQueryArrays(); } bool TryRebuildFromCapsules(bool editor) { var capsules = GetComponentsInChildren(includeInactive: true); if (capsules == null || capsules.Length == 0) { // 没胶囊就不改 return false; } // 收集点(在刚体局部空间) List ptsLocal = new List(capsules.Length * 8); // 同时估算“长轴”:取所有胶囊在 rb local 的端点云,计算 AABB 最大跨度轴 // 这比“看某个 Capsule.direction”更鲁棒(因为你说可能多个、分布复杂) foreach (var cap in capsules) { if (cap == null) continue; if (_OnlyEnabledCapsules && !cap.enabled) continue; AddCapsuleSamplePointsInRbLocal(cap, ptsLocal); } if (ptsLocal.Count < 2) return false; // AABB in rb local Vector3 min = ptsLocal[0]; Vector3 max = ptsLocal[0]; for (int i = 1; i < ptsLocal.Count; i++) { min = Vector3.Min(min, ptsLocal[i]); max = Vector3.Max(max, ptsLocal[i]); } Vector3 size = max - min; // 估算长轴:取跨度最大的轴 // 注意:这是 rb local 坐标的轴(x/y/z),足够用于“尺寸”和“探针布局” int axis = 0; float spanX = size.x; float spanY = size.y; float spanZ = size.z; if (spanY >= spanX && spanY >= spanZ) axis = 1; else if (spanZ >= spanX && spanZ >= spanY) axis = 2; else axis = 0; Vector3 axisLocal = axis == 0 ? Vector3.right : axis == 1 ? Vector3.up : Vector3.forward; // length along that axis float length = axis == 0 ? spanX : axis == 1 ? spanY : spanZ; // diameter:取另外两轴的最大跨度(更保守) float dia; if (axis == 0) dia = Mathf.Max(spanY, spanZ); else if (axis == 1) dia = Mathf.Max(spanX, spanZ); else dia = Mathf.Max(spanX, spanY); dia = Mathf.Max(0.001f, dia); _ComputedLength = length; _ComputedDiameter = dia; _ComputedAxisLocal = axisLocal; // Crest 查询尺度:直径同量级 * multiplier,且给下限 _ObjectWidth = Mathf.Max(0.002f, _ComputedDiameter * _WidthMultiplier); // 估算“底部位置”(用于探针 y) // 我们按“长轴”方向找最小投影的点作为底部 float minProj = float.PositiveInfinity; Vector3 bottom = ptsLocal[0]; for (int i = 0; i < ptsLocal.Count; i++) { float proj = Vector3.Dot(ptsLocal[i], axisLocal); if (proj < minProj) { minProj = proj; bottom = ptsLocal[i]; } } // 这个值只是给你在 Inspector 看一眼,不参与计算 _ComputedBottomLocalY = bottom.y; // 生成探针:以“底部附近”一圈 + 中心 BuildDefaultProbes(axisLocal, bottom, _ComputedDiameter); if (!editor) { // 运行时也确保 query arrays 充足 AllocateQueryArrays(); } return true; } void BuildDefaultProbes(Vector3 axisLocal, Vector3 bottomLocal, float diameter) { // 以刚体局部空间为准,探针 offset 是“相对 COM 的局部偏移” // 我们把探针放在 “底部投影点附近”,并沿“横向平面”撑开 float radius = diameter * 0.5f; float ringR = radius * _ProbeRadiusRatio; // 底部沿长轴方向略抬高,避免探针刚好在最底点导致 jitter // 这里用“长轴”方向抬高,而不是简单的 localY float lift = _ProbeLiftMeters; if (lift <= 0f) { // 默认:半径的 10% lift = radius * 0.10f; } Vector3 basePoint = bottomLocal + axisLocal * lift; // 需要在“横向平面”找两条正交方向(在 rb local 坐标中) // 任意取一个与 axisLocal 不平行的向量,构造正交基 Vector3 t1 = Vector3.Cross(axisLocal, Vector3.up); if (t1.sqrMagnitude < 1e-6f) t1 = Vector3.Cross(axisLocal, Vector3.right); t1.Normalize(); Vector3 t2 = Vector3.Cross(axisLocal, t1).normalized; // 权重分配 float ringTotal = Mathf.Clamp01(_RingWeight); float centerW = 1f - ringTotal; float eachRingW = ringTotal / 4f; // 探针点(rb local),最后转成 “相对 COM 的 local offset” // 注意:我们使用 worldCenterOfMass 作为基准,所以这里要把 basePoint(rb local)转换成 offset-from-COM Vector3 comLocal = transform.InverseTransformPoint(_RigidBody.worldCenterOfMass); var probes = new Probe[5]; // 四周 probes[0] = new Probe { localOffsetFromCOM = (basePoint + t1 * ringR) - comLocal, weight = eachRingW }; probes[1] = new Probe { localOffsetFromCOM = (basePoint - t1 * ringR) - comLocal, weight = eachRingW }; probes[2] = new Probe { localOffsetFromCOM = (basePoint + t2 * ringR) - comLocal, weight = eachRingW }; probes[3] = new Probe { localOffsetFromCOM = (basePoint - t2 * ringR) - comLocal, weight = eachRingW }; // 底部中心 probes[4] = new Probe { localOffsetFromCOM = basePoint - comLocal, weight = Mathf.Max(0.0001f, centerW) }; _Probes = probes; } void AddCapsuleSamplePointsInRbLocal(CapsuleCollider cap, List outPtsRbLocal) { // 计算胶囊端点(世界) + 若干“半径点”,再转换到 rb local // 胶囊在 cap.transform local:center, direction(0=x 1=y 2=z), height, radius Transform ct = cap.transform; Vector3 centerW = ct.TransformPoint(cap.center); // direction axis in world Vector3 axisW = cap.direction == 0 ? ct.right : cap.direction == 1 ? ct.up : ct.forward; axisW.Normalize(); // 处理缩放:radius 取垂直于轴的最大缩放分量(保守) Vector3 s = ct.lossyScale; float sx = Mathf.Abs(s.x); float sy = Mathf.Abs(s.y); float sz = Mathf.Abs(s.z); float radiusScale; float heightScale; if (cap.direction == 0) { heightScale = sx; radiusScale = Mathf.Max(sy, sz); } else if (cap.direction == 1) { heightScale = sy; radiusScale = Mathf.Max(sx, sz); } else { heightScale = sz; radiusScale = Mathf.Max(sx, sy); } float r = Mathf.Max(0.0005f, cap.radius * radiusScale); float h = Mathf.Max(0.001f, cap.height * heightScale); // 圆柱部分半长(端点到端点的距离为 h-2r) float half = Mathf.Max(0f, (h * 0.5f) - r); Vector3 p0W = centerW + axisW * half; Vector3 p1W = centerW - axisW * half; // 在世界空间构造两个与 axisW 正交的方向 Vector3 n1 = Vector3.Cross(axisW, Vector3.up); if (n1.sqrMagnitude < 1e-6f) n1 = Vector3.Cross(axisW, Vector3.right); n1.Normalize(); Vector3 n2 = Vector3.Cross(axisW, n1).normalized; // 采样点:两个端点 + 端点的四周半径点 + 中心四周半径点(足够稳定估算整体 AABB) AddWorldPoint(p0W, outPtsRbLocal); AddWorldPoint(p1W, outPtsRbLocal); AddWorldPoint(p0W + n1 * r, outPtsRbLocal); AddWorldPoint(p0W - n1 * r, outPtsRbLocal); AddWorldPoint(p0W + n2 * r, outPtsRbLocal); AddWorldPoint(p0W - n2 * r, outPtsRbLocal); AddWorldPoint(p1W + n1 * r, outPtsRbLocal); AddWorldPoint(p1W - n1 * r, outPtsRbLocal); AddWorldPoint(p1W + n2 * r, outPtsRbLocal); AddWorldPoint(p1W - n2 * r, outPtsRbLocal); AddWorldPoint(centerW + n1 * r, outPtsRbLocal); AddWorldPoint(centerW - n1 * r, outPtsRbLocal); AddWorldPoint(centerW + n2 * r, outPtsRbLocal); AddWorldPoint(centerW - n2 * r, outPtsRbLocal); } void AddWorldPoint(Vector3 world, List outPtsRbLocal) { // rb local(即本脚本 transform 的 local) // 注意:这里假设脚本挂在 Rigidbody 所在物体上(通常是这样) outPtsRbLocal.Add(transform.InverseTransformPoint(world)); } } }