using System; using System.Collections.Generic; using UnityEngine; /// /// 多点采样浮力:考虑形状(采样点分布)、重心(Rigidbody.centerOfMass)、扭矩(在点上施力)。 /// 适合小物体(0.01m级),避免“任何形状都一样直直往下/往上顶飞” /// [DisallowMultipleComponent] [RequireComponent(typeof(Rigidbody))] public class BuoyancyBody : MonoBehaviour { [Header("Water")] [Tooltip("如果不填则使用简单水面:y = WaterLevel")] public MonoBehaviour waterProviderBehaviour; private IWaterProvider waterProvider; [Tooltip("简单水面模式:水面高度(y)")] public float waterLevel = 0f; [Header("Buoyancy (Physical)")] [Tooltip("水密度 kg/m^3。淡水约1000,海水约1025")] public float waterDensity = 1000f; [Tooltip("物体密度 kg/m^3。决定浮沉:< waterDensity 更容易浮,> waterDensity 更容易沉")] public float objectDensity = 300f; [Tooltip("如果 > 0 则强制使用该体积(m^3),否则按 mass/objectDensity 推体积")] public float overrideVolume = 0f; [Tooltip("浮力强度缩放(调手感用),1为物理值")] public float buoyancyScale = 1f; [Header("Stabilization / Damping")] [Tooltip("水中线性阻尼(越大越不弹、越“粘水”)")] public float linearDamping = 2.5f; [Tooltip("水中角阻尼(越大越稳定立漂)")] public float angularDamping = 2.0f; [Tooltip("入水/出水过渡平滑厚度(m)。越大越不弹,但边界更“软”】【0.01~0.05常用】")] public float surfaceSmoothing = 0.02f; [Tooltip("限制单个采样点最大浮力加速度(m/s^2),防止轻小物体从高处落下冲天")] public float maxBuoyantAccelPerPoint = 50f; [Header("Center of Mass")] [Tooltip("本地空间重心偏移(m)。可用来模拟胶囊体配重,让浮漂能站起来")] public Vector3 centerOfMassOffset = Vector3.zero; [Header("Sampling (Shape matters!)")] [Tooltip("每个Collider生成的采样点数量(越大越精确,越耗性能)。浮漂建议 12~40")] [Range(4, 200)] public int pointsPerCollider = 24; [Tooltip("采样点是否只保留在Collider内部(对非凸MeshCollider不可靠;浮漂建议用Capsule/Box/Sphere)")] public bool keepPointsInsideCollider = true; [Tooltip("运行时显示采样点(编辑器Gizmos)")] public bool drawGizmos = true; [Tooltip("只对这些Collider算浮力(为空则自动抓取子物体所有Collider)")] public Collider[] colliders; private Rigidbody rb; private readonly List samplePoints = new(); private struct SamplePoint { public Collider col; public Vector3 localPos; // 相对 collider.transform 的本地 } void Awake() { rb = GetComponent(); ApplyCOM(); waterProvider = waterProviderBehaviour as IWaterProvider; BuildSamplePoints(); } void OnValidate() { if (!rb) rb = GetComponent(); ApplyCOM(); // 在编辑器里改参数时重建采样点 if (Application.isPlaying == false) { // 避免在Prefab编辑等情况下报错 } } void Reset() { rb = GetComponent(); rb.useGravity = true; rb.interpolation = RigidbodyInterpolation.Interpolate; rb.collisionDetectionMode = CollisionDetectionMode.Continuous; if (colliders == null || colliders.Length == 0) colliders = GetComponentsInChildren(); } private void ApplyCOM() { if (!rb) return; rb.centerOfMass = centerOfMassOffset; } /// 手动调用:当你运行时增减Collider或缩放后,建议调用一次 public void Rebuild() { waterProvider = waterProviderBehaviour as IWaterProvider; ApplyCOM(); BuildSamplePoints(); } private void BuildSamplePoints() { samplePoints.Clear(); Collider[] cols = colliders; if (cols == null || cols.Length == 0) cols = GetComponentsInChildren(); foreach (var col in cols) { if (!col || !col.enabled) continue; // 用 bounds 做快速采样,再可选筛内部点 var b = col.bounds; int n = Mathf.Max(4, pointsPerCollider); // 使用“分层网格+抖动”的方式,保证不同形状/尺寸采样分布不同 int dim = Mathf.CeilToInt(Mathf.Pow(n, 1f / 3f)); dim = Mathf.Max(2, dim); Vector3 size = b.size; Vector3 step = new Vector3( size.x / (dim - 1), size.y / (dim - 1), size.z / (dim - 1) ); // 为了让小物体也稳定:如果某轴极小,step可能很小,没关系 int added = 0; for (int ix = 0; ix < dim && added < n; ix++) for (int iy = 0; iy < dim && added < n; iy++) for (int iz = 0; iz < dim && added < n; iz++) { // 0..1 float fx = (dim == 1) ? 0.5f : (float)ix / (dim - 1); float fy = (dim == 1) ? 0.5f : (float)iy / (dim - 1); float fz = (dim == 1) ? 0.5f : (float)iz / (dim - 1); // 抖动(避免规则网格导致“锁姿态”) Vector3 jitter = new Vector3( (UnityEngine.Random.value - 0.5f) * step.x * 0.25f, (UnityEngine.Random.value - 0.5f) * step.y * 0.25f, (UnityEngine.Random.value - 0.5f) * step.z * 0.25f ); Vector3 world = new Vector3( b.min.x + size.x * fx, b.min.y + size.y * fy, b.min.z + size.z * fz ) + jitter; if (keepPointsInsideCollider) { // 对凸Collider通常可靠:如果点在内部,ClosestPoint会返回点本身 Vector3 cp = col.ClosestPoint(world); if ((cp - world).sqrMagnitude > 1e-8f) continue; } // 转到 collider.transform 本地存储 Vector3 local = col.transform.InverseTransformPoint(world); samplePoints.Add(new SamplePoint { col = col, localPos = local }); added++; } // 如果内部点太少,至少补一点:用 collider.transform.position 周围 if (added < 4) { for (int i = added; i < 4; i++) { Vector3 world = col.bounds.center + UnityEngine.Random.insideUnitSphere * (col.bounds.extents.magnitude * 0.25f); Vector3 local = col.transform.InverseTransformPoint(world); samplePoints.Add(new SamplePoint { col = col, localPos = local }); } } } } void FixedUpdate() { if (samplePoints.Count == 0) BuildSamplePoints(); // 体积估计:V = m / rho_object float volume = overrideVolume > 0f ? overrideVolume : (rb.mass / Mathf.Max(1e-6f, objectDensity)); volume = Mathf.Max(1e-9f, volume); int count = samplePoints.Count; float volPerPoint = volume / count; Vector3 gravity = Physics.gravity; // 通常 (0,-9.81,0) float gMag = gravity.magnitude; // 如果重力为0就不算了 if (gMag < 1e-6f) return; // 逐点施加:浮力 + 阻尼 for (int i = 0; i < count; i++) { var sp = samplePoints[i]; if (!sp.col || !sp.col.enabled) continue; Vector3 worldPos = sp.col.transform.TransformPoint(sp.localPos); float waterH = waterProvider?.GetWaterHeight(worldPos) ?? waterLevel; float depth = waterH - worldPos.y; // >0 表示点在水下 if (depth <= 0f) continue; // 入水过渡:在水面附近用平滑厚度做0..1 float submergence01 = (surfaceSmoothing <= 1e-6f) ? 1f : Mathf.Clamp01(depth / surfaceSmoothing); // 该点产生的“排水质量”:m_displaced = rho_water * V_point * submergence float displacedMass = waterDensity * volPerPoint * submergence01; // 浮力 = -g * m_displaced Vector3 buoyancy = -gravity * displacedMass * buoyancyScale; // 限制单点最大向上加速度,防止小物体落水被“顶飞” // a = F/m -> Fmax = amax * m float Fmax = maxBuoyantAccelPerPoint * rb.mass; if (buoyancy.sqrMagnitude > Fmax * Fmax) buoyancy = buoyancy.normalized * Fmax; // 水中阻尼(相对水流速度) Vector3 waterVel = waterProvider?.GetWaterVelocity(worldPos) ?? Vector3.zero; Vector3 pointVel = rb.GetPointVelocity(worldPos); Vector3 relVel = pointVel - waterVel; // 阻尼随入水程度增强:越深越“粘” Vector3 dampingForce = -relVel * (linearDamping * displacedMass); // 把力施加在采样点上 -> 自然产生扭矩(形状/重心不同效果不同) rb.AddForceAtPosition(buoyancy + dampingForce, worldPos, ForceMode.Force); // 额外角阻尼(整体),只在“确实有一部分在水里”时施加更合理, // 这里用 submergence01 做比例:越入水越强 rb.AddTorque(-rb.angularVelocity * (angularDamping * displacedMass), ForceMode.Force); } } #if UNITY_EDITOR void OnDrawGizmosSelected() { if (!drawGizmos) return; Gizmos.matrix = Matrix4x4.identity; // 水面 Gizmos.color = new Color(0f, 0.6f, 1f, 0.25f); Vector3 p = transform.position; float y = (waterProvider != null) ? waterProvider.GetWaterHeight(p) : waterLevel; Gizmos.DrawCube(new Vector3(p.x, y, p.z), new Vector3(0.5f, 0.001f, 0.5f)); // 采样点 if (samplePoints == null) return; Gizmos.color = Color.yellow; foreach (var sp in samplePoints) { if (!sp.col) continue; Vector3 w = sp.col.transform.TransformPoint(sp.localPos); Gizmos.DrawSphere(w, 0.005f); } // COM if (TryGetComponent(out var r)) { Gizmos.color = Color.red; Gizmos.DrawSphere(transform.TransformPoint(r.centerOfMass), 0.01f); } } #endif }