294 lines
10 KiB
C#
294 lines
10 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// 多点采样浮力:考虑形状(采样点分布)、重心(Rigidbody.centerOfMass)、扭矩(在点上施力)。
|
||
/// 适合小物体(0.01m级),避免“任何形状都一样直直往下/往上顶飞”
|
||
/// </summary>
|
||
[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<SamplePoint> samplePoints = new();
|
||
|
||
private struct SamplePoint
|
||
{
|
||
public Collider col;
|
||
public Vector3 localPos; // 相对 collider.transform 的本地
|
||
}
|
||
|
||
void Awake()
|
||
{
|
||
rb = GetComponent<Rigidbody>();
|
||
ApplyCOM();
|
||
|
||
waterProvider = waterProviderBehaviour as IWaterProvider;
|
||
BuildSamplePoints();
|
||
}
|
||
|
||
void OnValidate()
|
||
{
|
||
if (!rb) rb = GetComponent<Rigidbody>();
|
||
ApplyCOM();
|
||
|
||
// 在编辑器里改参数时重建采样点
|
||
if (Application.isPlaying == false)
|
||
{
|
||
// 避免在Prefab编辑等情况下报错
|
||
}
|
||
}
|
||
|
||
void Reset()
|
||
{
|
||
rb = GetComponent<Rigidbody>();
|
||
rb.useGravity = true;
|
||
rb.interpolation = RigidbodyInterpolation.Interpolate;
|
||
rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
|
||
|
||
if (colliders == null || colliders.Length == 0)
|
||
colliders = GetComponentsInChildren<Collider>();
|
||
}
|
||
|
||
private void ApplyCOM()
|
||
{
|
||
if (!rb) return;
|
||
rb.centerOfMass = centerOfMassOffset;
|
||
}
|
||
|
||
/// <summary>手动调用:当你运行时增减Collider或缩放后,建议调用一次</summary>
|
||
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<Collider>();
|
||
|
||
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<Rigidbody>(out var r))
|
||
{
|
||
Gizmos.color = Color.red;
|
||
Gizmos.DrawSphere(transform.TransformPoint(r.centerOfMass), 0.01f);
|
||
}
|
||
}
|
||
#endif
|
||
} |