using System;
using System.Collections.Generic;
using UnityEngine;
public interface IWaterProvider
{
/// 返回该世界坐标下的水面高度
float GetWaterHeight(Vector3 worldPos);
/// 可选:水面法线(没有就返回 Vector3.up)
Vector3 GetWaterNormal(Vector3 worldPos);
/// 可选:水流速度(没有就返回 Vector3.zero)
Vector3 GetWaterVelocity(Vector3 worldPos);
}
///
/// 多点采样浮力:考虑形状(采样点分布)、重心(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
}