From 7ea3a7868cfc8496d95190c23919e0996386c804 Mon Sep 17 00:00:00 2001 From: "Bob.Song" Date: Sat, 28 Feb 2026 17:45:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=85=E5=AE=B9=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Gaia User Data/Sessions.meta | 8 + .../Sessions/GS-20260228 - 171840.asset | 35 + .../Sessions/GS-20260228 - 171840.asset.meta | 8 + .../Sessions/GS-20260228 - 171840.meta | 8 + .../Gaia Lighting Settings.asset | 63 + .../Gaia Lighting Settings.asset.meta | 8 + .../GS-20260228 - 171840/Terrain Scenes.meta | 8 + .../GS-20260228 - 171840/TerrainScenes.asset | 38 + .../TerrainScenes.asset.meta | 8 + Assets/Gaia User Data/Stamps.meta | 8 + .../Gaia/Settings/GaiaSettings.asset | 2 +- .../Scenes/Main Demo/DWP2 Main Demo.unity | Bin 8501720 -> 8504424 bytes Assets/Scenes/BobberTest.unity | 1124 ++++++++++++++++- Assets/Scenes/GaiaTest.unity | 391 ++++++ Assets/Scenes/GaiaTest.unity.meta | 7 + Assets/Scripts/Test/BobberBuoyancyStable.cs | 130 ++ .../Scripts/Test/BobberBuoyancyStable.cs.meta | 3 + Assets/Scripts/Test/BobberFloating.cs | 628 +++------ Assets/Scripts/Test/BobberFloatingTest.cs | 9 + .../Scripts/Test/BobberFloatingTest.cs.meta | 3 + Assets/Scripts/Test/CentimeterBuoyancy.cs | 317 +++++ .../Scripts/Test/CentimeterBuoyancy.cs.meta | 3 + Assets/Scripts/Test/FloatBobberController.cs | 105 ++ .../Test/FloatBobberController.cs.meta | 3 + Fishing2.sln.DotSettings.user | 1 + UserSettings/EditorUserSettings.asset | 30 +- 26 files changed, 2395 insertions(+), 553 deletions(-) create mode 100644 Assets/Gaia User Data/Sessions.meta create mode 100644 Assets/Gaia User Data/Sessions/GS-20260228 - 171840.asset create mode 100644 Assets/Gaia User Data/Sessions/GS-20260228 - 171840.asset.meta create mode 100644 Assets/Gaia User Data/Sessions/GS-20260228 - 171840.meta create mode 100644 Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Gaia Lighting Settings.asset create mode 100644 Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Gaia Lighting Settings.asset.meta create mode 100644 Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Terrain Scenes.meta create mode 100644 Assets/Gaia User Data/Sessions/GS-20260228 - 171840/TerrainScenes.asset create mode 100644 Assets/Gaia User Data/Sessions/GS-20260228 - 171840/TerrainScenes.asset.meta create mode 100644 Assets/Gaia User Data/Stamps.meta create mode 100644 Assets/Scenes/GaiaTest.unity create mode 100644 Assets/Scenes/GaiaTest.unity.meta create mode 100644 Assets/Scripts/Test/BobberBuoyancyStable.cs create mode 100644 Assets/Scripts/Test/BobberBuoyancyStable.cs.meta create mode 100644 Assets/Scripts/Test/BobberFloatingTest.cs create mode 100644 Assets/Scripts/Test/BobberFloatingTest.cs.meta create mode 100644 Assets/Scripts/Test/CentimeterBuoyancy.cs create mode 100644 Assets/Scripts/Test/CentimeterBuoyancy.cs.meta create mode 100644 Assets/Scripts/Test/FloatBobberController.cs create mode 100644 Assets/Scripts/Test/FloatBobberController.cs.meta diff --git a/Assets/Gaia User Data/Sessions.meta b/Assets/Gaia User Data/Sessions.meta new file mode 100644 index 000000000..d913253db --- /dev/null +++ b/Assets/Gaia User Data/Sessions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9d3a120cd69656f468963a7b399a2f3b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Gaia User Data/Sessions/GS-20260228 - 171840.asset b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840.asset new file mode 100644 index 000000000..db9ca707e --- /dev/null +++ b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e251860fcd970734c9f14ebd98a146c3, type: 3} + m_Name: GS-20260228 - 171840 + m_EditorClassIdentifier: GaiaCore::Gaia.GaiaSession + m_name: Session 20260228-171840 + m_description: 'Rocking out at Creativity Central! + + + If you like Gaia please + consider rating it :)' + m_previewImage: {fileID: 0} + m_dateCreated: 2026/2/28 17:18:40 + m_terrainWidth: 1024 + m_terrainDepth: 1024 + m_terrainHeight: 1024 + m_seaLevel: 25 + m_spawnDensity: 0.8 + m_isLocked: 0 + m_previewImageBytes: + m_previewImageWidth: 0 + m_previewImageHeight: 0 + m_operations: [] + m_terrainMinMaxCache: [] + m_bakedMaskCacheEntries: [] + m_worldBiomeMaskSettings: {fileID: 0} diff --git a/Assets/Gaia User Data/Sessions/GS-20260228 - 171840.asset.meta b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840.asset.meta new file mode 100644 index 000000000..a3bac4677 --- /dev/null +++ b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a8eda7ecde0aa9d4994993b532ba21ae +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Gaia User Data/Sessions/GS-20260228 - 171840.meta b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840.meta new file mode 100644 index 000000000..1f5d90192 --- /dev/null +++ b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 743e8db08f1a57f49b2057d04785e337 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Gaia Lighting Settings.asset b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Gaia Lighting Settings.asset new file mode 100644 index 000000000..eae91f77f --- /dev/null +++ b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Gaia Lighting Settings.asset @@ -0,0 +1,63 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!850595691 &4890085278179872738 +LightingSettings: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Gaia Lighting Settings + serializedVersion: 9 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_RealtimeEnvironmentLighting: 1 + m_BounceScale: 1 + m_AlbedoBoost: 1 + m_IndirectOutputScale: 1 + m_UsingShadowmask: 1 + m_BakeBackend: 1 + m_LightmapMaxSize: 1024 + m_LightmapSizeFixed: 0 + m_UseMipmapLimits: 1 + m_BakeResolution: 40 + m_Padding: 2 + m_LightmapCompression: 3 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAO: 0 + m_MixedBakeMode: 2 + m_LightmapsBakeMode: 1 + m_FilterMode: 1 + m_LightmapParameters: {fileID: 15204, guid: 0000000000000000f000000000000000, type: 0} + m_ExportTrainingData: 0 + m_EnableWorkerProcessBaking: 1 + m_TrainingDataDestination: TrainingData + m_RealtimeResolution: 2 + m_ForceWhiteAlbedo: 0 + m_ForceUpdates: 0 + m_PVRCulling: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_LightProbeSampleCountMultiplier: 4 + m_PVRBounces: 2 + m_PVRMinBounces: 2 + m_PVREnvironmentImportanceSampling: 1 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_RespectSceneVisibilityWhenBakingGI: 0 diff --git a/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Gaia Lighting Settings.asset.meta b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Gaia Lighting Settings.asset.meta new file mode 100644 index 000000000..f0dbdc325 --- /dev/null +++ b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Gaia Lighting Settings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6f2a358d6ae67644f8c199d940dadd27 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 4890085278179872738 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Terrain Scenes.meta b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Terrain Scenes.meta new file mode 100644 index 000000000..ef8a5aec8 --- /dev/null +++ b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/Terrain Scenes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7f56f90b57af5a9448a3fb8ca3f1f2a6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/TerrainScenes.asset b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/TerrainScenes.asset new file mode 100644 index 000000000..c45120edd --- /dev/null +++ b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/TerrainScenes.asset @@ -0,0 +1,38 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: da50733ca13d24f45a2980dcd5a56ed5, type: 3} + m_Name: TerrainScenes + m_EditorClassIdentifier: GaiaCore::Gaia.TerrainSceneStorage + m_terrainLoadingEnabled: 1 + m_showTerrainLoadingDisabledWarning: 1 + m_useAddressables: 0 + m_preloadAddressablesWithImpostors: 1 + m_colliderOnlyLoading: 0 + m_terrainTilesX: 1 + m_terrainTilesZ: 1 + m_terrainTilesSize: 0 + m_useFloatingPointFix: 0 + m_hasWorldMap: 0 + m_worldMaprelativeSize: 0.5 + m_worldMapRelativeHeightmapPixels: 1 + m_worldMapPreviewHeightmapResolution: 2049 + m_worldMapPreviewRange: 1024 + m_worldMapPreviewTerrainHeight: 1024 + m_terrainScenes: [] + m_deactivateRuntimePlayer: 0 + m_deactivateRuntimeLighting: 0 + m_deactivateRuntimeAudio: 0 + m_deactivateRuntimeWeather: 0 + m_deactivateRuntimeWater: 0 + m_deactivateRuntimeScreenShotter: 0 + m_pos00X: -1.7976931348623157e+308 + m_pos00Z: -1.7976931348623157e+308 diff --git a/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/TerrainScenes.asset.meta b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/TerrainScenes.asset.meta new file mode 100644 index 000000000..3ea694b6c --- /dev/null +++ b/Assets/Gaia User Data/Sessions/GS-20260228 - 171840/TerrainScenes.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2e2428c1e05a106448aeb4c7e59def5d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Gaia User Data/Stamps.meta b/Assets/Gaia User Data/Stamps.meta new file mode 100644 index 000000000..4149897d8 --- /dev/null +++ b/Assets/Gaia User Data/Stamps.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ed3d8d94bfd55054cbdd0c355641f6ee +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Procedural Worlds/Packages - Install/Gaia/Settings/GaiaSettings.asset b/Assets/Procedural Worlds/Packages - Install/Gaia/Settings/GaiaSettings.asset index 312f41fc8..ae6ad62bb 100644 --- a/Assets/Procedural Worlds/Packages - Install/Gaia/Settings/GaiaSettings.asset +++ b/Assets/Procedural Worlds/Packages - Install/Gaia/Settings/GaiaSettings.asset @@ -26,7 +26,7 @@ MonoBehaviour: m_unloadTerrainScenes: 0 m_floatingPointFix: 0 m_targeSizePreset: 3 - m_creationWorkflow: 2 + m_creationWorkflow: 1 m_defaultStampSpawnSettings: {fileID: 11400000, guid: 0e1586b8863570048942e9493106d5e8, type: 2} m_defaultBiomeMaskSettings: {fileID: 0} m_spawnSimulateComputeShader: {fileID: 7200000, guid: ab31bdb5aecea3744951fb814c0d268b, type: 3} diff --git a/Assets/Samples/NWH Dynamic Water Physics 2/2.3.0/Base Sample/Scenes/Main Demo/DWP2 Main Demo.unity b/Assets/Samples/NWH Dynamic Water Physics 2/2.3.0/Base Sample/Scenes/Main Demo/DWP2 Main Demo.unity index 7d4cd2b178e235e81c65bd8d3e77ba3df4124947..4947c55a8de0a673ac19980eeaa26979e512364e 100644 GIT binary patch delta 13683 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 mlIAPW>UTBj|Dg*6^dScomh$@rbc^c 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 + cap = GetComponent(); + rb.useGravity = true; + + airLinearDamping = rb.linearDamping; + airAngularDamping = rb.angularDamping; + + ApplyCenterOfMass(); + rb.maxAngularVelocity = 50f; + } + + void FixedUpdate() + { + ApplyCenterOfMass(); + + Bounds b = cap.bounds; + float bottomY = b.min.y; + float topY = b.max.y; + + // 用“底部点”判定是否真正入水(必须超过阈值) + float bottomSubmersion = waterLevelY - bottomY; // >0 表示底部在水下 + if (bottomSubmersion <= enterWaterDepth) + { + // 认为未入水:不施加浮力,恢复空气阻尼 + rb.linearDamping = airLinearDamping; + rb.angularDamping = airAngularDamping; + return; + } + + // 进入水中:阻尼随浸入增强 + // 这里用一个0~1的平滑权重,避免刚入水就“猛顶” + float w = Smooth01((bottomSubmersion - enterWaterDepth) / Mathf.Max(1e-4f, smoothDepth)); + + rb.linearDamping = airLinearDamping + extraLinearDampingInWater * w; + rb.angularDamping = airAngularDamping + extraAngularDampingInWater * w; + + // 垂直速度(用刚体自身速度就够稳定) + float vY = rb.linearVelocity.y; + + // 弹簧+阻尼浮力(仅向上) + float forceY = buoyancySpring * bottomSubmersion - buoyancyDamping * vY; + if (forceY < 0f) forceY = 0f; + + // 平滑权重:刚入水时逐渐接管 + forceY *= w; + + // 限制最大上浮加速度(可选) + if (maxUpAcceleration > 0f) + { + float maxForce = rb.mass * maxUpAcceleration; + if (forceY > maxForce) forceY = maxForce; + } + + // 浮力作用点:必须放在水面下(否则会出现奇怪力矩) + float buoyY = Mathf.Min(waterLevelY - 0.001f, topY); // 强制在水面下1mm + buoyY = Mathf.Max(buoyY, bottomY); // 不低于底部 + Vector3 buoyPoint = new Vector3(b.center.x, buoyY, b.center.z); + + rb.AddForceAtPosition(Vector3.up * forceY, buoyPoint, ForceMode.Force); + + // 归正扭矩(只在水里生效) + Vector3 up = transform.up; + Vector3 axis = Vector3.Cross(up, Vector3.up); + float mag = axis.magnitude; + if (mag > 1e-4f) + { + axis /= mag; + float angle = Mathf.Asin(Mathf.Clamp(mag, -1f, 1f)); + float angVelOnAxis = Vector3.Dot(rb.angularVelocity, axis); + float torque = (rightingTorque * angle - rightingDamping * angVelOnAxis) * w; + rb.AddTorque(axis * torque, ForceMode.Acceleration); + } + } + + void ApplyCenterOfMass() + { + if (!driveCenterOfMassFromCapsule) return; + rb.centerOfMass = cap.center + extraCenterOfMassOffset; + } + + static float Smooth01(float t) + { + t = Mathf.Clamp01(t); + // smoothstep + return t * t * (3f - 2f * t); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Test/BobberBuoyancyStable.cs.meta b/Assets/Scripts/Test/BobberBuoyancyStable.cs.meta new file mode 100644 index 000000000..dadfb928d --- /dev/null +++ b/Assets/Scripts/Test/BobberBuoyancyStable.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 976f4f103cc04f34a8a81bd4abba2244 +timeCreated: 1772269304 \ No newline at end of file diff --git a/Assets/Scripts/Test/BobberFloating.cs b/Assets/Scripts/Test/BobberFloating.cs index f4d64d6fe..bf8477192 100644 --- a/Assets/Scripts/Test/BobberFloating.cs +++ b/Assets/Scripts/Test/BobberFloating.cs @@ -1,126 +1,68 @@ -using System; -using System.Collections.Generic; -using UnityEngine; +using UnityEngine; using WaveHarmonic.Crest; namespace NBF { - /// - /// 小型浮漂/小物体:基于 Crest 的浮力(自动从多个 CapsuleCollider 计算尺寸与探针) - /// - 自动收集自身与子物体上的 CapsuleCollider(可多个) - /// - 计算整体长轴、长度、最大直径、底部高度 - /// - 自动设置 Crest 查询尺度 _ObjectWidth(与直径同量级) - /// - 自动生成 5 个探针:底部四周 + 底部中心(更稳) - /// - 浮力:弹簧(k) + 阻尼(c),ForceMode.Force(质量参与) - /// public sealed class BobberFloating : MonoBehaviour { - [Header("Crest")] public WaterRenderer _water; [SerializeField] Rigidbody _RigidBody; - [Tooltip("要瞄准哪一层水的碰撞层。")] - [SerializeField] CollisionLayer _Layer = CollisionLayer.AfterAnimatedWaves; + [Tooltip("要瞄准哪一层水的碰撞层。")] [SerializeField] + CollisionLayer _Layer = CollisionLayer.AfterAnimatedWaves; - // ----------------------------- - // 自动从 CapsuleCollider 计算 - // ----------------------------- - [Header("Auto from CapsuleColliders")] - [Tooltip("自动扫描自己与子物体的 CapsuleCollider,并计算尺寸/探针/采样尺度。")] - [SerializeField] bool _AutoFromCapsules = true; + [Header("浮力")] + [Header("力强度")] + [Tooltip("对于探测器而言,大致为 100 比 1 的质量与力的比例,以使质心保持在表面附近。对于“对齐法线”,默认值适用于具有默认刚体的默认球体。")] + [SerializeField] + float _BuoyancyForceStrength = 10f; - [Tooltip("仅使用 enabled 的 CapsuleCollider。")] - [SerializeField] bool _OnlyEnabledCapsules = true; + [Header("扭矩强度")] [Tooltip("使船体方向与水的法线方向一致时所施加扭矩的大小。")] [SerializeField] + float _BuoyancyTorqueStrength = 8f; - [Tooltip("运行时也会每隔一定时间重建(应对浮漂缩放/更换碰撞体)。0 表示仅 Start/OnValidate 时重建。")] - [SerializeField] float _RuntimeRebuildInterval = 0f; + [Header("最大力矩")] [Tooltip("将浮力值固定在此数值上。\n\n适用于处理完全浸没的物体。")] [SerializeField] + float _MaximumBuoyancyForce = 100f; - [Tooltip("给估算出来的直径乘一个系数,用于更稳的 Crest 查询与阻尼。通常 1.0~2.0。")] - [Range(0.5f, 3f)] - [SerializeField] float _WidthMultiplier = 1.2f; + [Header("高度偏移")] [Tooltip("从变换中心到船体底部的高度偏移(如果存在)。\n\n默认值适用于默认球体。该值无需精确测量从中心到底部的距离。")] [SerializeField] + float _CenterToBottomOffset = -1f; - [Tooltip("探针半径比例(相对估算半径)。越大越“撑开”,越抗翻滚,但也更容易被浪抬。建议 0.6~0.9")] - [Range(0.1f, 1.2f)] - [SerializeField] float _ProbeRadiusRatio = 0.75f; + [Tooltip("顺着波浪 “冲浪” 的近似流体动力学效果。")] [Range(0, 1)] [SerializeField] + float _AccelerateDownhill; - [Tooltip("探针在底部之上的抬高(米)。用于避免探针刚好在最底点导致抖动。建议:半径的 5%~20%。")] - [SerializeField] float _ProbeLiftMeters = 0.0f; - [Tooltip("探针权重分配:四周总权重(其余给中心)。建议 0.6~0.9")] - [Range(0f, 1f)] - [SerializeField] float _RingWeight = 0.75f; + [Header("拖拽")] [Tooltip("在水中时使用拖拽功能。\n将此属性添加到刚体所声明的拖拽力上。")] [SerializeField] + Vector3 _Drag = new(2f, 3f, 1f); - // ----------------------------- - // 物理参数(吃水可控) - // ----------------------------- - [Header("Buoyancy (controllable submergence)")] - [Tooltip("目标吃水比例:以“直径”为尺度。\n例:0.30 表示目标吃水深度约 = 直径*0.30。\n对小浮漂非常好调:想更浮(露更多)-> 降低;想更沉 -> 提高。")] - [Range(0.05f, 1.2f)] - [SerializeField] float _TargetSubmergenceRatio = 0.30f; + [Tooltip("在水中会产生旋转阻力。\n\n将此阻力添加到刚体上已声明的旋转阻力值之上。")] [SerializeField] + float _AngularDrag = 0.2f; - [Tooltip("浮力阻尼比(0~2 常用)。越大越不抖,但会显得黏。推荐 0.7~1.2")] - [Range(0f, 2f)] - [SerializeField] float _DampingRatio = 0.95f; + [Tooltip("施加拉力的位置的垂直偏移量。")] [SerializeField] + float _ForceHeightOffset; - [Tooltip("最大总浮力(N)保护。若为 Infinity 则不限制。")] - [SerializeField] float _MaximumBuoyancyForce = 50f; - [Tooltip("吃水偏移(米)。用于校准 pivot/碰撞体中心误差。\n>0 更容易浮起(等效更浅),<0 更沉。")] - [SerializeField] float _SubmergenceOffset = 0f; + [Header("波响应")] [Tooltip("用于物理计算的物体宽度。\n\n此值越大,波响应的滤波效果和平滑程度就越高。如果无法对较大波长进行滤波,则应增加 LOD 级别。")] [SerializeField] + float _ObjectWidth = 3f; - [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(); + [Space(10)] [SerializeField] DebugFields _Debug = new(); - [Serializable] + [System.Serializable] sealed class DebugFields { - [SerializeField] internal bool _DrawProbes = false; - [SerializeField] internal bool _DrawForces = false; + [SerializeField] internal bool _DrawQueries = false; } - /// 是否有任意探针入水 + internal const string k_FixedUpdateMarker = "Crest.FloatingObject.FixedUpdate"; + + static Unity.Profiling.ProfilerMarker s_FixedUpdateMarker = new(k_FixedUpdateMarker); + + /// + /// 这个物体的任何部分是否浸泡在水中? + /// public bool InWater { get; private set; } - // 探针(刚体局部空间,相对 worldCenterOfMass) - struct Probe - { - public Vector3 localOffsetFromCOM; - public float weight; - } - - Probe[] _Probes = Array.Empty(); - readonly SampleFlowHelper _SampleFlowHelper = new(); Vector3[] _QueryPoints; @@ -128,436 +70,152 @@ namespace NBF Vector3[] _QueryResultVelocities; Vector3[] _QueryResultNormal; - float _NextRebuildTime = 0f; + // internal FloatingObjectProbe[] _Probe = new FloatingObjectProbe[] { new() { _Weight = 1f } }; - void Reset() - { - if (_RigidBody == null) TryGetComponent(out _RigidBody); - } + public FloatingObjectProbe[] Probe = new FloatingObjectProbe[] { new() { _Weight = 1f } }; - 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() + private void Start() { if (_RigidBody == null) TryGetComponent(out _RigidBody); - if (_AutoFromCapsules) - { - TryRebuildFromCapsules(editor: false); - } - - AllocateQueryArrays(); + var points = Probe; + // Advanced 还需要为中心增设一个位置。 + var length = points.Length; + _QueryPoints = new Vector3[length]; + _QueryResultDisplacements = new Vector3[length]; + _QueryResultVelocities = new Vector3[length]; + _QueryResultNormal = new Vector3[length]; } - void FixedUpdate() + private void FixedUpdate() { - if (_water == null || _RigidBody == null) return; + s_FixedUpdateMarker.Begin(this); - 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 points = Probe; + + // 查询 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++) + + // 更新查询点。 + for (var i = 0; i < points.Length; i++) { - _QueryPoints[i] = comWorld + transform.TransformVector(_Probes[i].localOffsetFromCOM); + var point = points[i]; + _QueryPoints[i] = + transform.TransformPoint(point._Position + new Vector3(0, _RigidBody.centerOfMass.y, 0)); } - _QueryPoints[centerIndex] = comWorld; - - // Query waves - collisions.Query( - GetHashCode(), - _ObjectWidth, - _QueryPoints, - _QueryResultDisplacements, - _QueryResultNormal, - _QueryResultVelocities, - _Layer - ); - - // Surface velocity + flow - Vector3 surfaceVelocity = _QueryResultVelocities[centerIndex]; + + _QueryPoints[^1] = transform.position + new Vector3(0, _RigidBody.centerOfMass.y, 0); + + collisions.Query(GetHashCode(), _ObjectWidth, _QueryPoints, _QueryResultDisplacements, + _QueryResultNormal, _QueryResultVelocities, _Layer); + + + //我们可以将表面速度过滤为最近两帧中的较小值。 + //存在一种极端情况:当波长被开启 / 关闭时,会产生单帧速度尖峰—— + //因为此时水面确实会发生极快的运动。 + var surfaceVelocity = _QueryResultVelocities[^1]; _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++) + surfaceVelocity += new Vector3(surfaceFlow.x, 0, surfaceFlow.y); + + if (_Debug._DrawQueries) { - 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); - } + Debug.DrawLine(transform.position + 5f * Vector3.up, + transform.position + 5f * Vector3.up + surfaceVelocity, new(1, 1, 1, 0.6f)); } - if (!InWater) return; - // Clamp buoyancy (simple safety) - if (_MaximumBuoyancyForce < Mathf.Infinity && totalBuoyMag > _MaximumBuoyancyForce) + var height = _QueryResultDisplacements[0].y + _water.SeaLevel; + var bottomDepth = height - transform.position.y - _CenterToBottomOffset; + var normal = _QueryResultNormal[0]; + + if (_Debug._DrawQueries) { - float excess = totalBuoyMag - _MaximumBuoyancyForce; - _RigidBody.AddForce(-excess * Vector3.up, ForceMode.Force); + var surfPos = transform.position; + surfPos.y = height; + DebugUtility.DrawCross(Debug.DrawLine, surfPos, normal, 1f, Color.red); } - // Optional downhill acceleration (usually 0 for bobber) - if (_AccelerateDownhill > 0f) + InWater = bottomDepth > 0f; + if (!InWater) { - Vector3 normal = _QueryResultNormal[centerIndex]; - _RigidBody.AddForce(_AccelerateDownhill * g * new Vector3(normal.x, 0f, normal.z), ForceMode.Force); + s_FixedUpdateMarker.End(); + return; } - // Angular drag - if (_AngularDrag > 0f) + var buoyancy = _BuoyancyForceStrength * bottomDepth * bottomDepth * bottomDepth * + -Physics.gravity.normalized; + if (_MaximumBuoyancyForce < Mathf.Infinity) { - _RigidBody.AddTorque(-_AngularDrag * _RigidBody.angularVelocity, ForceMode.Force); + buoyancy = Vector3.ClampMagnitude(buoyancy, _MaximumBuoyancyForce); } - // Linear drag relative to water + _RigidBody.AddForce(buoyancy, ForceMode.Acceleration); + + // // 在水面上滑行的近似流体动力学 + // if (_AccelerateDownhill > 0f) + // { + // _RigidBody.AddForce(_AccelerateDownhill * -Physics.gravity.y * new Vector3(normal.x, 0f, normal.z), + // ForceMode.Acceleration); + // } + // + // + // // 朝向 + // // 与水面垂直。默认使用一个垂直方向,但也可以使用单独的垂直方向。 + // // 根据船的长度与宽度的比例。这会根据船只的不同而产生不同的旋转效果。 + // // dimensions. + // { + // var normalLatitudinal = normal; + // + // if (_Debug._DrawQueries) + // Debug.DrawLine(transform.position, transform.position + 5f * normalLatitudinal, Color.green); + // + // var torqueWidth = Vector3.Cross(transform.up, normalLatitudinal); + // _RigidBody.AddTorque(torqueWidth * _BuoyancyTorqueStrength, ForceMode.Acceleration); + // _RigidBody.AddTorque(-_AngularDrag * _RigidBody.angularVelocity); + // } + // + // + // 相对于水进行拖拽操作 if (_Drag != Vector3.zero) { - Vector3 velocityRelativeToWater = _RigidBody.linearVelocity - surfaceVelocity; - Vector3 forcePosition = _RigidBody.worldCenterOfMass + _ForceHeightOffset * Vector3.up; - + var velocityRelativeToWater = _RigidBody.linearVelocity - surfaceVelocity; + + var 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); - + _Drag.x * Vector3.Dot(transform.right, -velocityRelativeToWater) * transform.right, forcePosition, + ForceMode.Acceleration); + _RigidBody.AddForceAtPosition(_Drag.y * Vector3.Dot(Vector3.up, -velocityRelativeToWater) * Vector3.up, + forcePosition, ForceMode.Acceleration); _RigidBody.AddForceAtPosition( _Drag.z * Vector3.Dot(transform.forward, -velocityRelativeToWater) * transform.forward, - forcePosition, - ForceMode.Force); + forcePosition, ForceMode.Acceleration); } - if (_Debug._DrawProbes) - { - for (int i = 0; i < probeCount; i++) - { - Debug.DrawLine(_QueryPoints[i], _QueryPoints[i] + Vector3.up * 0.05f, Color.yellow); - } - } + s_FixedUpdateMarker.End(); + } + } + + static class DebugUtility + { + public delegate void DrawLine(Vector3 position, Vector3 up, Color color, float duration); + + public static void DrawCross(DrawLine draw, Vector3 position, float r, Color color, float duration = 0f) + { + draw(position - Vector3.up * r, position + Vector3.up * r, color, duration); + draw(position - Vector3.right * r, position + Vector3.right * r, color, duration); + draw(position - Vector3.forward * r, position + Vector3.forward * r, color, duration); } - void AllocateQueryArrays() + public static void DrawCross(DrawLine draw, Vector3 position, Vector3 up, float r, Color color, + float duration = 0f) { - 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]; - } - - /// - /// 公开按钮:手动重建(你也可以在 Inspector 右键脚本 -> Reset,然后 Play) - /// - [ContextMenu("Rebuild From CapsuleColliders")] - public void RebuildFromCapsules() - { - TryRebuildFromCapsules(editor: Application.isEditor && !Application.isPlaying); - AllocateQueryArrays(); - } - - bool TryRebuildFromCapsules(bool editor) - { - var capsules = GetComponentsInChildren(includeInactive: true); - if (capsules == null || capsules.Length == 0) - { - // 没胶囊就不改 - return false; - } - - // 收集点(在刚体局部空间) - List ptsLocal = new List(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 作为基准,所以这里要把 basePoint(rb 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 outPtsRbLocal) - { - // 计算胶囊端点(世界) + 若干“半径点”,再转换到 rb local - // 胶囊在 cap.transform local:center, 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 outPtsRbLocal) - { - // rb local(即本脚本 transform 的 local) - // 注意:这里假设脚本挂在 Rigidbody 所在物体上(通常是这样) - outPtsRbLocal.Add(transform.InverseTransformPoint(world)); + up.Normalize(); + var right = Vector3.Normalize(Vector3.Cross(up, Vector3.forward)); + var forward = Vector3.Cross(up, right); + draw(position - up * r, position + up * r, color, duration); + draw(position - right * r, position + right * r, color, duration); + draw(position - forward * r, position + forward * r, color, duration); } } } \ No newline at end of file diff --git a/Assets/Scripts/Test/BobberFloatingTest.cs b/Assets/Scripts/Test/BobberFloatingTest.cs new file mode 100644 index 000000000..3c4375949 --- /dev/null +++ b/Assets/Scripts/Test/BobberFloatingTest.cs @@ -0,0 +1,9 @@ +using UnityEngine; + +namespace NBF +{ + public class BobberFloatingTest : MonoBehaviour + { + + } +} \ No newline at end of file diff --git a/Assets/Scripts/Test/BobberFloatingTest.cs.meta b/Assets/Scripts/Test/BobberFloatingTest.cs.meta new file mode 100644 index 000000000..154f8301d --- /dev/null +++ b/Assets/Scripts/Test/BobberFloatingTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 75d9ef57b5894116b65988fda7760fc6 +timeCreated: 1772260103 \ No newline at end of file diff --git a/Assets/Scripts/Test/CentimeterBuoyancy.cs b/Assets/Scripts/Test/CentimeterBuoyancy.cs new file mode 100644 index 000000000..aef9a69a4 --- /dev/null +++ b/Assets/Scripts/Test/CentimeterBuoyancy.cs @@ -0,0 +1,317 @@ +using UnityEngine; + +[RequireComponent(typeof(Rigidbody))] +[RequireComponent(typeof(Collider))] +public class CentimeterBuoyancy : MonoBehaviour +{ + [Header("浮力设置")] + [SerializeField] private float waterDensity = 1000f; // 水的密度 kg/m³ + [SerializeField] private float buoyancyScale = 1f; // 浮力缩放系数 + [SerializeField] private float dragScale = 1f; // 水中阻力系数 + + [Header("水面设置")] + [SerializeField] private float waterLevel = 0f; // 水面高度 + [SerializeField] private LayerMask waterLayer; // 水体层级 + + [Header("调试")] + [SerializeField] private bool showDebugInfo = true; + [SerializeField] private Color debugColor = Color.cyan; + + private Rigidbody rb; + private Collider objCollider; + private float volumeInCm; + private Vector3[] samplePoints; + private Bounds localBounds; + + void Start() + { + rb = GetComponent(); + objCollider = GetComponent(); + + // 计算物体体积(立方厘米) + volumeInCm = CalculateVolumeInCm(); + + // 计算局部空间的包围盒 + CalculateLocalBounds(); + + if (showDebugInfo) + { + Debug.Log($"物体体积: {volumeInCm:F2} cm³,质量: {rb.mass * 1000:F2} g"); + Debug.Log($"局部包围盒: 中心 {localBounds.center}, 大小 {localBounds.size}"); + } + + // 初始化采样点 + InitializeSamplePoints(); + } + + void FixedUpdate() + { + ApplyBuoyancy(); + ApplyWaterDrag(); + } + + // 计算局部空间的包围盒 + void CalculateLocalBounds() + { + // 获取碰撞器的局部空间边界 + if (objCollider is BoxCollider box) + { + localBounds = new Bounds(box.center, box.size); + } + else if (objCollider is SphereCollider sphere) + { + Vector3 size = Vector3.one * sphere.radius * 2; + localBounds = new Bounds(sphere.center, size); + } + else if (objCollider is CapsuleCollider capsule) + { + float radius = capsule.radius; + float height = capsule.height; + Vector3 size; + + // 根据胶囊方向确定尺寸 + switch (capsule.direction) + { + case 0: // X轴 + size = new Vector3(height, radius * 2, radius * 2); + break; + case 1: // Y轴 + size = new Vector3(radius * 2, height, radius * 2); + break; + case 2: // Z轴 + size = new Vector3(radius * 2, radius * 2, height); + break; + default: + size = new Vector3(radius * 2, height, radius * 2); + break; + } + localBounds = new Bounds(capsule.center, size); + } + else + { + // 对于复杂碰撞器,使用世界边界转换到局部空间 + Bounds worldBounds = objCollider.bounds; + localBounds = new Bounds( + transform.InverseTransformPoint(worldBounds.center), + transform.InverseTransformVector(worldBounds.size) + ); + } + } + + // 计算体积(立方厘米) + float CalculateVolumeInCm() + { + Bounds bounds = objCollider.bounds; + Vector3 sizeInCm = bounds.size * 100f; // 转换为厘米 + + // 根据不同碰撞器类型估算体积 + if (objCollider is BoxCollider) + { + return sizeInCm.x * sizeInCm.y * sizeInCm.z; + } + else if (objCollider is SphereCollider) + { + float radiusInCm = bounds.extents.x * 100f; + return (4f / 3f) * Mathf.PI * Mathf.Pow(radiusInCm, 3); + } + else if (objCollider is CapsuleCollider) + { + CapsuleCollider capsule = objCollider as CapsuleCollider; + float radiusInCm = capsule.radius * 100f; + float heightInCm = capsule.height * 100f; + return Mathf.PI * radiusInCm * radiusInCm * heightInCm; + } + else + { + // 对于复杂碰撞器,使用边界框估算 + return sizeInCm.x * sizeInCm.y * sizeInCm.z * 0.5f; + } + } + + // 初始化采样点 + void InitializeSamplePoints() + { + int sampleCount = 3; // 每个维度采样点数(减少采样点以提高性能) + + // 创建网格采样点用于检测浸入深度 + samplePoints = new Vector3[sampleCount * sampleCount * sampleCount]; + + int index = 0; + for (int x = 0; x < sampleCount; x++) + { + for (int y = 0; y < sampleCount; y++) + { + for (int z = 0; z < sampleCount; z++) + { + // 生成 -0.5 到 0.5 范围内的归一化坐标 + float nx = (float)x / (sampleCount - 1) - 0.5f; + float ny = (float)y / (sampleCount - 1) - 0.5f; + float nz = (float)z / (sampleCount - 1) - 0.5f; + + samplePoints[index] = new Vector3(nx, ny, nz); + index++; + } + } + } + } + + void ApplyBuoyancy() + { + float immersedVolume = 0f; + float totalImmersedDepth = 0f; + int immersedPoints = 0; + Vector3 buoyancyCenter = Vector3.zero; + + // 计算浸入体积 + foreach (Vector3 normalizedPoint in samplePoints) + { + // 将归一化坐标转换为局部空间坐标 + Vector3 localPoint = new Vector3( + localBounds.center.x + normalizedPoint.x * localBounds.size.x, + localBounds.center.y + normalizedPoint.y * localBounds.size.y, + localBounds.center.z + normalizedPoint.z * localBounds.size.z + ); + + // 转换到世界空间 + Vector3 worldPoint = transform.TransformPoint(localPoint); + + // 调试:打印第一个点的坐标 + if (showDebugInfo && immersedPoints == 0 && samplePoints.Length > 0) + { + Debug.DrawLine(worldPoint, worldPoint + Vector3.up * 0.01f, Color.red, 0.1f); + } + + if (worldPoint.y < waterLevel) + { + immersedVolume += 1f; + totalImmersedDepth += (waterLevel - worldPoint.y); + buoyancyCenter += worldPoint; + immersedPoints++; + } + } + + // 计算浸入比例 + float immersedRatio = (float)immersedPoints / samplePoints.Length; + + if (immersedRatio < 0.01f) + return; // 几乎没有浸入,不施加浮力 + + // 计算平均浸入深度 + float averageImmersedDepth = immersedPoints > 0 ? totalImmersedDepth / immersedPoints : 0; + + // 计算浮力中心 + if (immersedPoints > 0) + { + buoyancyCenter /= immersedPoints; + } + + // 计算实际浸入体积(立方厘米) + float actualImmersedVolumeCm = volumeInCm * immersedRatio; + + // 转换为立方米(1 m³ = 1,000,000 cm³) + float actualImmersedVolumeM = actualImmersedVolumeCm / 1000000f; + + // 计算浮力 = 水的密度 * 重力加速度 * 浸入体积 + // 水的密度默认 1000 kg/m³,重力加速度 9.8 m/s² + float buoyancyForceMagnitude = waterDensity * Physics.gravity.magnitude * actualImmersedVolumeM * buoyancyScale; + + // 根据浸入深度调整浮力(更深的地方浮力更大) + float depthFactor = Mathf.Clamp01(averageImmersedDepth * 10f); // 每10cm深度增加一倍 + buoyancyForceMagnitude *= (1f + depthFactor); + + // 施加浮力 + Vector3 buoyancyForce = Vector3.up * buoyancyForceMagnitude; + rb.AddForceAtPosition(buoyancyForce, buoyancyCenter, ForceMode.Force); + + // 施加浮力产生的扭矩(使物体自然上浮) + Vector3 torque = Vector3.Cross(buoyancyCenter - rb.worldCenterOfMass, buoyancyForce); + rb.AddTorque(torque * 0.1f); + + if (showDebugInfo) + { + // 绘制浮力线 + Debug.DrawLine(buoyancyCenter, buoyancyCenter + buoyancyForce * 0.001f, debugColor, 0.1f); + + // 绘制浸没点 + foreach (Vector3 normalizedPoint in samplePoints) + { + Vector3 localPoint = new Vector3( + localBounds.center.x + normalizedPoint.x * localBounds.size.x, + localBounds.center.y + normalizedPoint.y * localBounds.size.y, + localBounds.center.z + normalizedPoint.z * localBounds.size.z + ); + Vector3 worldPoint = transform.TransformPoint(localPoint); + + if (worldPoint.y < waterLevel) + { + Debug.DrawLine(worldPoint, worldPoint + Vector3.up * 0.005f, debugColor, 0.1f); + } + } + } + } + + void ApplyWaterDrag() + { + float immersedRatio = CalculateImmersedRatio(); + + if (immersedRatio > 0.01f) + { + // 线性阻力 + Vector3 dragForce = -rb.linearVelocity * dragScale * immersedRatio * 10f; + rb.AddForce(dragForce, ForceMode.Force); + + // 角阻力 + Vector3 angularDrag = -rb.angularVelocity * dragScale * immersedRatio * 5f; + rb.AddTorque(angularDrag, ForceMode.Force); + } + } + + float CalculateImmersedRatio() + { + int immersedPoints = 0; + + foreach (Vector3 normalizedPoint in samplePoints) + { + Vector3 localPoint = new Vector3( + localBounds.center.x + normalizedPoint.x * localBounds.size.x, + localBounds.center.y + normalizedPoint.y * localBounds.size.y, + localBounds.center.z + normalizedPoint.z * localBounds.size.z + ); + Vector3 worldPoint = transform.TransformPoint(localPoint); + + if (worldPoint.y < waterLevel) + { + immersedPoints++; + } + } + + return (float)immersedPoints / samplePoints.Length; + } + + void OnDrawGizmosSelected() + { + if (!showDebugInfo || !Application.isPlaying) return; + + // 绘制局部包围盒 + Gizmos.color = Color.yellow; + Vector3[] corners = new Vector3[8]; + + // 计算局部包围盒的8个角在世界空间的位置 + for (int i = 0; i < 8; i++) + { + Vector3 localCorner = localBounds.center; + localCorner.x += (i & 1) == 0 ? -localBounds.extents.x : localBounds.extents.x; + localCorner.y += (i & 2) == 0 ? -localBounds.extents.y : localBounds.extents.y; + localCorner.z += (i & 4) == 0 ? -localBounds.extents.z : localBounds.extents.z; + + corners[i] = transform.TransformPoint(localCorner); + } + + // 绘制包围盒的边 + int[] edges = new int[] { 0,1, 1,3, 3,2, 2,0, 4,5, 5,7, 7,6, 6,4, 0,4, 1,5, 2,6, 3,7 }; + for (int i = 0; i < edges.Length; i += 2) + { + Gizmos.DrawLine(corners[edges[i]], corners[edges[i + 1]]); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Test/CentimeterBuoyancy.cs.meta b/Assets/Scripts/Test/CentimeterBuoyancy.cs.meta new file mode 100644 index 000000000..bc89d66f7 --- /dev/null +++ b/Assets/Scripts/Test/CentimeterBuoyancy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3481fc2889d14a00a9de205cc39d9c33 +timeCreated: 1772262032 \ No newline at end of file diff --git a/Assets/Scripts/Test/FloatBobberController.cs b/Assets/Scripts/Test/FloatBobberController.cs new file mode 100644 index 000000000..3c93e240b --- /dev/null +++ b/Assets/Scripts/Test/FloatBobberController.cs @@ -0,0 +1,105 @@ +using UnityEngine; + +public class FloatBobberController : MonoBehaviour +{ + [SerializeField] private Rigidbody _rigidbody; + [Header("水属性")] public float waterLevel = 0f; + + [Header("浮漂最大浮力")] public float bobberVolume = 30f; // 浮漂最大浮力 (cm³) + + public float bobberHeight = 0.25f; // 浮漂长度,用来决定躺漂角度 + + [Header("配件重量")] public float sinkerWeight = 2f; + public float baitWeight = 0.5f; + public float hookWeight = 0.2f; + + [Header("Behaviour")] public float fallSpeed = 8f; + public float riseSpeed = 3f; + // public float smoothDamping = 8f; // 插值平滑 + + [Header("Noise")] public float noiseAmp = 0.015f; + public float noiseFreq = 1.5f; + + float impulseForce = 0f; + float impulseDecay = 4f; + + void FixedUpdate() + { + SimulateBobber(); + } + + void SimulateBobber() + { + if (!_rigidbody.isKinematic) return; + float totalDownwardWeight = sinkerWeight + baitWeight + hookWeight; + + float maxBuoyancy = bobberVolume; // 最大浮力 = 体积 + float netBuoyancy = maxBuoyancy - totalDownwardWeight; + + float targetY; + + // ------------------------- + // 1. 判断浮漂应该沉多少(吃水深度) + // ------------------------- + if (netBuoyancy > 0) + { + float buoyPercent = Mathf.Clamp01(netBuoyancy / maxBuoyancy); + float rise = buoyPercent * 0.1f; // 浮漂露出水面的高度 + + targetY = waterLevel + rise; + } + else + { + // 净浮力为负 → 说明浮漂整体被拉下,沉入水中 + float sinkDistance = Mathf.Abs(netBuoyancy) * 0.03f; + targetY = waterLevel - sinkDistance; + } + + targetY += Mathf.Sin(Time.time * noiseFreq) * noiseAmp; // 微扰模拟波浪 + + // 顿口/顶漂力 + if (impulseForce != 0f) + { + targetY += impulseForce * Time.deltaTime; + impulseForce = Mathf.Lerp(impulseForce, 0, Time.deltaTime * impulseDecay); + } + + // ----------------------------- + // ③ 上浮 / 下沉差速 + // ----------------------------- + float y = transform.position.y; + float diff = targetY - y; + + if (diff > 0) // 上浮 + y += diff * Time.deltaTime * riseSpeed; + else + y += diff * Time.deltaTime * fallSpeed; + + transform.position = new Vector3(transform.position.x, y, transform.position.z); + } + + + // ---------------------------------------- + // 外部控制接口 + // ---------------------------------------- + + public void TriggerDownPulse(float s = 0.8f) + { + impulseForce -= Mathf.Abs(s); + } + + public void TriggerUpPulse(float s = 0.8f) + { + impulseForce += Mathf.Abs(s); + } + + public void AddFishPull(float v) + { + sinkerWeight += v; + } + + public void ReleaseFishPull(float v) + { + sinkerWeight -= v; + } +} \ No newline at end of file diff --git a/Assets/Scripts/Test/FloatBobberController.cs.meta b/Assets/Scripts/Test/FloatBobberController.cs.meta new file mode 100644 index 000000000..727132752 --- /dev/null +++ b/Assets/Scripts/Test/FloatBobberController.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f32660b4549141fbb754dfef9a523eda +timeCreated: 1772264277 \ No newline at end of file diff --git a/Fishing2.sln.DotSettings.user b/Fishing2.sln.DotSettings.user index d062a4622..443ef6fe2 100644 --- a/Fishing2.sln.DotSettings.user +++ b/Fishing2.sln.DotSettings.user @@ -52,6 +52,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/UserSettings/EditorUserSettings.asset b/UserSettings/EditorUserSettings.asset index 87ebe81d6..2e1ad56f1 100644 --- a/UserSettings/EditorUserSettings.asset +++ b/UserSettings/EditorUserSettings.asset @@ -9,35 +9,35 @@ EditorUserSettings: value: 18134705175a055722080a3115371d4a0d55006876786860616b0471b8b07a68ffab74f9ee2a3a30300cea1a11320d0beb1a0c25f7060f494b4cc80018eb09361fc211cb1f862d19c51d19dcc413d6ade0d8ddfcddf9f4d9d29195fcfde6ebeae6f0a9c9afa6f8c5b89ff7a1aacececac4eba4d7c9d28bda flags: 0 RecentlyUsedSceneGuid-0: - value: 5a09065056055c0e595d0a20447b0b4415164e2b297c75682f7c1866b0b3656c - flags: 0 - RecentlyUsedSceneGuid-1: - value: 0750065f5d57580c5c0b5d7741275e44464e1d297b7e77342e784a64b3e13539 - flags: 0 - RecentlyUsedSceneGuid-2: value: 01085257040c5f0e0c0d5f27457b0f444e4e192c7b2d7e6428794d67e3b36668 flags: 0 - RecentlyUsedSceneGuid-3: + RecentlyUsedSceneGuid-1: value: 52530c5601535f020f565a2043770d1612154d2f747975692c7b4e66b5e3303b flags: 0 - RecentlyUsedSceneGuid-4: + RecentlyUsedSceneGuid-2: value: 5302035e5c530f0b5c0c557416270d44134e4d28787c76332f7e1f6bb1b76169 flags: 0 - RecentlyUsedSceneGuid-5: - value: 0508070250545c58585e0924437b5d444f4e4b7f7d7a71627f794c64b2e5633a - flags: 0 - RecentlyUsedSceneGuid-6: + RecentlyUsedSceneGuid-3: value: 5050570401015d0a545d087047710e44154e1c2e7f787368782c4e60e1e1636b flags: 0 - RecentlyUsedSceneGuid-7: + RecentlyUsedSceneGuid-4: value: 520004535d5751085c595a7047730e4440161e7d787022342f2d486bb4b6626a flags: 0 - RecentlyUsedSceneGuid-8: + RecentlyUsedSceneGuid-5: value: 5a035755520650595b5b5f2345740e4447154a73742d70632b7d4b65e4e66d69 flags: 0 - RecentlyUsedSceneGuid-9: + RecentlyUsedSceneGuid-6: + value: 0508070250545c58585e0924437b5d444f4e4b7f7d7a71627f794c64b2e5633a + flags: 0 + RecentlyUsedSceneGuid-7: value: 0054045155060d5a5c575f7045270d44474f4e7c7f7924637e2a1832b1b5636d flags: 0 + RecentlyUsedSceneGuid-8: + value: 5409030052070d0d095a5c7412745e444216417c2e7a23642b7e1832bab9363e + flags: 0 + RecentlyUsedSceneGuid-9: + value: 5606515f5605500b0e5c5c2615760a444615487c2a2a2467297d1932b7e4673a + flags: 0 UnityEditor.ShaderGraph.Blackboard: value: 18135939215a0a5004000b0e15254b524c030a3f2964643d120d1230e9e93a3fd6e826abbd2e2d293c4ead313b08042de6030a0afa240c0d020be94c4ba75e435d8715fa32c70d15d11612dacc11fee5d3c5d1fe9ab1bf968e93e2ffcbc3e7e2f0b3ffe0e8b0be9afeffa9ffff8e85dd8390e2969e8899daa7 flags: 0