Files
Fishing2/Assets/Scripts/Test/BuoyancyBody.cs
2026-03-02 00:03:29 +08:00

304 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using UnityEngine;
public interface IWaterProvider
{
/// <summary>返回该世界坐标下的水面高度</summary>
float GetWaterHeight(Vector3 worldPos);
/// <summary>可选:水面法线(没有就返回 Vector3.up</summary>
Vector3 GetWaterNormal(Vector3 worldPos);
/// <summary>可选:水流速度(没有就返回 Vector3.zero</summary>
Vector3 GetWaterVelocity(Vector3 worldPos);
}
/// <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
}