Files
Fishing2/Assets/Scripts/Test/BobberFloating.cs
2026-02-27 17:44:21 +08:00

563 lines
22 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;
using WaveHarmonic.Crest;
namespace NBF
{
/// <summary>
/// 小型浮漂/小物体:基于 Crest 的浮力(自动从多个 CapsuleCollider 计算尺寸与探针)
/// - 自动收集自身与子物体上的 CapsuleCollider可多个
/// - 计算整体长轴、长度、最大直径、底部高度
/// - 自动设置 Crest 查询尺度 _ObjectWidth与直径同量级
/// - 自动生成 5 个探针:底部四周 + 底部中心(更稳)
/// - 浮力:弹簧(k) + 阻尼(c)ForceMode.Force质量参与
/// </summary>
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;
}
/// <summary>是否有任意探针入水</summary>
public bool InWater { get; private set; }
// 探针(刚体局部空间,相对 worldCenterOfMass
struct Probe
{
public Vector3 localOffsetFromCOM;
public float weight;
}
Probe[] _Probes = Array.Empty<Probe>();
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];
}
/// <summary>
/// 公开按钮:手动重建(你也可以在 Inspector 右键脚本 -> Reset然后 Play
/// </summary>
[ContextMenu("Rebuild From CapsuleColliders")]
public void RebuildFromCapsules()
{
TryRebuildFromCapsules(editor: Application.isEditor && !Application.isPlaying);
AllocateQueryArrays();
}
bool TryRebuildFromCapsules(bool editor)
{
var capsules = GetComponentsInChildren<CapsuleCollider>(includeInactive: true);
if (capsules == null || capsules.Length == 0)
{
// 没胶囊就不改
return false;
}
// 收集点(在刚体局部空间)
List<Vector3> ptsLocal = new List<Vector3>(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 作为基准,所以这里要把 basePointrb 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<Vector3> outPtsRbLocal)
{
// 计算胶囊端点(世界) + 若干“半径点”,再转换到 rb local
// 胶囊在 cap.transform localcenter, 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<Vector3> outPtsRbLocal)
{
// rb local即本脚本 transform 的 local
// 注意:这里假设脚本挂在 Rigidbody 所在物体上(通常是这样)
outPtsRbLocal.Add(transform.InverseTransformPoint(world));
}
}
}