From 1a72c3f52ab4400ca813a360e96973d84c786768 Mon Sep 17 00:00:00 2001 From: "Bob.Song" Date: Tue, 14 Apr 2026 18:07:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ResRaw/Prefabs/Line/FishingLine1.prefab | 14 + Assets/Scenes/RopeTest.unity | 575 +++----- .../Renderer/FishingLineRenderer.cs | 26 +- .../FishingLine/Renderer/FishingNodeRope.cs | 1313 +++++------------ .../Renderer/FishingNodeRope.cs.meta | 6 +- .../New/View/FishingLine/Renderer/Rope.cs | 1043 +++++++++++++ .../View/FishingLine/Renderer/Rope.cs.meta | 3 + Fishing2.sln.DotSettings.user | 1 + 8 files changed, 1636 insertions(+), 1345 deletions(-) create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Renderer/Rope.cs create mode 100644 Assets/Scripts/Fishing/New/View/FishingLine/Renderer/Rope.cs.meta diff --git a/Assets/ResRaw/Prefabs/Line/FishingLine1.prefab b/Assets/ResRaw/Prefabs/Line/FishingLine1.prefab index c39072b2e..186167606 100644 --- a/Assets/ResRaw/Prefabs/Line/FishingLine1.prefab +++ b/Assets/ResRaw/Prefabs/Line/FishingLine1.prefab @@ -230,6 +230,7 @@ GameObject: m_Component: - component: {fileID: 4283454774123242} - component: {fileID: 7305019728002912084} + - component: {fileID: 5599783878866626034} m_Layer: 0 m_Name: FishingLine1 m_TagString: Untagged @@ -277,6 +278,19 @@ MonoBehaviour: PinchController: {fileID: 0} lengthLimitTolerance: 0.01 breakStretchThreshold: 0.05 +--- !u!114 &5599783878866626034 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1387836627839849} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 827786ffede4e7b4781c522e8a4ba9d0, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineRenderer + solver: {fileID: 7305019728002912084} --- !u!1 &1858052053854210 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scenes/RopeTest.unity b/Assets/Scenes/RopeTest.unity index 4f1631cfa..d98350d7a 100644 --- a/Assets/Scenes/RopeTest.unity +++ b/Assets/Scenes/RopeTest.unity @@ -119,83 +119,118 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} ---- !u!1001 &96516534 -PrefabInstance: +--- !u!1 &13221083 +GameObject: m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 13221084} + - component: {fileID: 13221087} + - component: {fileID: 13221086} + - component: {fileID: 13221085} + m_Layer: 15 + m_Name: Sphere + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &13221084 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 13221083} serializedVersion: 2 - m_Modification: - serializedVersion: 3 - m_TransformParent: {fileID: 0} - m_Modifications: - - target: {fileID: 1387836627839849, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_Name - value: FishingLine1 - objectReference: {fileID: 0} - - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_LocalPosition.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_LocalPosition.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_LocalPosition.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_LocalRotation.w - value: 1 - objectReference: {fileID: 0} - - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_LocalRotation.x - value: -0 - objectReference: {fileID: 0} - - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_LocalRotation.y - value: -0 - objectReference: {fileID: 0} - - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_LocalRotation.z - value: -0 - objectReference: {fileID: 0} - - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_LocalEulerAnglesHint.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_LocalEulerAnglesHint.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_LocalEulerAnglesHint.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 54679398375713381, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: m_UseGravity - value: 1 - objectReference: {fileID: 0} - - target: {fileID: 7305019728002912084, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - propertyPath: anchorTransform - value: - objectReference: {fileID: 2055159199} - m_RemovedComponents: [] - m_RemovedGameObjects: [] - m_AddedGameObjects: - - targetCorrespondingSourceObject: {fileID: 4530253318796540, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - insertIndex: -1 - addedObject: {fileID: 1725817384} - - targetCorrespondingSourceObject: {fileID: 4026445325167852, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - insertIndex: -1 - addedObject: {fileID: 924578507} - m_AddedComponents: - - targetCorrespondingSourceObject: {fileID: 1387836627839849, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - insertIndex: -1 - addedObject: {fileID: 1656874446} - - targetCorrespondingSourceObject: {fileID: 1387836627839849, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - insertIndex: -1 - addedObject: {fileID: 1656874445} - m_SourcePrefab: {fileID: 100100000, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0.02, y: 0.02, z: 0.02} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 351243123} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!135 &13221085 +SphereCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 13221083} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Radius: 0.5 + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &13221086 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 13221083} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &13221087 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 13221083} + m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} --- !u!1 &203844586 GameObject: m_ObjectHideFlags: 0 @@ -323,7 +358,7 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} ---- !u!1 &924578506 +--- !u!1 &222460757 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -331,39 +366,39 @@ GameObject: m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - - component: {fileID: 924578507} - - component: {fileID: 924578510} - - component: {fileID: 924578509} - - component: {fileID: 924578508} + - component: {fileID: 222460758} + - component: {fileID: 222460761} + - component: {fileID: 222460760} + - component: {fileID: 222460759} m_Layer: 15 - m_Name: Sphere (1) + m_Name: Sphere m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!4 &924578507 +--- !u!4 &222460758 Transform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 924578506} + m_GameObject: {fileID: 222460757} serializedVersion: 2 - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 0.02, y: 0.02, z: 0.02} m_ConstrainProportionsScale: 0 m_Children: [] - m_Father: {fileID: 1179596065} + m_Father: {fileID: 254030182} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!135 &924578508 +--- !u!135 &222460759 SphereCollider: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 924578506} + m_GameObject: {fileID: 222460757} m_Material: {fileID: 0} m_IncludeLayers: serializedVersion: 2 @@ -378,13 +413,13 @@ SphereCollider: serializedVersion: 3 m_Radius: 0.5 m_Center: {x: 0, y: 0, z: 0} ---- !u!23 &924578509 +--- !u!23 &222460760 MeshRenderer: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 924578506} + m_GameObject: {fileID: 222460757} m_Enabled: 1 m_CastShadows: 1 m_ReceiveShadows: 1 @@ -427,19 +462,103 @@ MeshRenderer: m_SortingOrder: 0 m_MaskInteraction: 0 m_AdditionalVertexStreams: {fileID: 0} ---- !u!33 &924578510 +--- !u!33 &222460761 MeshFilter: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 924578506} + m_GameObject: {fileID: 222460757} m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} ---- !u!4 &936701947 stripped +--- !u!4 &254030182 stripped Transform: m_CorrespondingSourceObject: {fileID: 4530253318796540, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - m_PrefabInstance: {fileID: 96516534} + m_PrefabInstance: {fileID: 921851545} m_PrefabAsset: {fileID: 0} +--- !u!4 &351243123 stripped +Transform: + m_CorrespondingSourceObject: {fileID: 4026445325167852, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + m_PrefabInstance: {fileID: 921851545} + m_PrefabAsset: {fileID: 0} +--- !u!1001 &921851545 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 1387836627839849, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_Name + value: FishingLine1 + objectReference: {fileID: 0} + - target: {fileID: 4026445325167852, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4283454774123242, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4530253318796540, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 54679398375713381, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: m_UseGravity + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7305019728002912084, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + propertyPath: anchorTransform + value: + objectReference: {fileID: 2055159199} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: + - targetCorrespondingSourceObject: {fileID: 4530253318796540, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + insertIndex: -1 + addedObject: {fileID: 222460758} + - targetCorrespondingSourceObject: {fileID: 4026445325167852, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} + insertIndex: -1 + addedObject: {fileID: 13221084} + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} --- !u!1 &961739749 GameObject: m_ObjectHideFlags: 0 @@ -577,11 +696,6 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!4 &1179596065 stripped -Transform: - m_CorrespondingSourceObject: {fileID: 4026445325167852, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - m_PrefabInstance: {fileID: 96516534} - m_PrefabAsset: {fileID: 0} --- !u!1 &1482833884 GameObject: m_ObjectHideFlags: 0 @@ -694,153 +808,6 @@ MeshFilter: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1482833884} m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} ---- !u!1 &1656874440 stripped -GameObject: - m_CorrespondingSourceObject: {fileID: 1387836627839849, guid: 3f94195a9c8f8c747b6ebcfd7fae6ee6, type: 3} - m_PrefabInstance: {fileID: 96516534} - m_PrefabAsset: {fileID: 0} ---- !u!114 &1656874445 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1656874440} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 827786ffede4e7b4781c522e8a4ba9d0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineRenderer - maxRenderSegmentLength: 0.2 - constraintIterations: 12 - useGravity: 1 - gravityStrength: 9.81 - windAcceleration: {x: 0, y: 0, z: 0} - airDrag: 2 - enableGroundCollision: 1 - groundMask: - serializedVersion: 2 - m_Bits: 4294967295 - groundSampleStep: 3 - groundCastHeight: 0.5 - groundCastDistance: 2 - groundOffset: 0.01 - enableWaterSurfaceClamp: 1 - waterSurfaceProviderBehaviour: {fileID: 0} - fallbackWaterLevel: 0 - waterSurfaceOffset: 0.002 - waterClampStrength: 1 - waterEdgeExcludeCount: 2 ---- !u!120 &1656874446 -LineRenderer: - serializedVersion: 3 - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1656874440} - m_Enabled: 1 - m_CastShadows: 1 - m_ReceiveShadows: 1 - m_DynamicOccludee: 1 - m_StaticShadowCaster: 0 - m_MotionVectors: 0 - m_LightProbeUsage: 0 - m_ReflectionProbeUsage: 0 - m_RayTracingMode: 0 - m_RayTraceProcedural: 0 - m_RayTracingAccelStructBuildFlagsOverride: 0 - m_RayTracingAccelStructBuildFlags: 1 - m_SmallMeshCulling: 1 - m_ForceMeshLod: -1 - m_MeshLodSelectionBias: 0 - m_RenderingLayerMask: 1 - m_RendererPriority: 0 - m_Materials: - - {fileID: 0} - m_StaticBatchInfo: - firstSubMesh: 0 - subMeshCount: 0 - m_StaticBatchRoot: {fileID: 0} - m_ProbeAnchor: {fileID: 0} - m_LightProbeVolumeOverride: {fileID: 0} - m_ScaleInLightmap: 1 - m_ReceiveGI: 1 - m_PreserveUVs: 0 - m_IgnoreNormalsForChartDetection: 0 - m_ImportantGI: 0 - m_StitchLightmapSeams: 1 - m_SelectedEditorRenderState: 3 - m_MinimumChartSize: 4 - m_AutoUVMaxDistance: 0.5 - m_AutoUVMaxAngle: 89 - m_LightmapParameters: {fileID: 0} - m_GlobalIlluminationMeshLod: 0 - m_SortingLayerID: 0 - m_SortingLayer: 0 - m_SortingOrder: 0 - m_MaskInteraction: 0 - m_Positions: - - {x: 0, y: 0, z: 0} - - {x: 0, y: 0, z: 0} - m_Parameters: - serializedVersion: 3 - widthMultiplier: 1 - widthCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0.001 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - colorGradient: - serializedVersion: 2 - key0: {r: 1, g: 1, b: 1, a: 1} - key1: {r: 1, g: 1, b: 1, a: 1} - key2: {r: 0, g: 0, b: 0, a: 0} - key3: {r: 0, g: 0, b: 0, a: 0} - key4: {r: 0, g: 0, b: 0, a: 0} - key5: {r: 0, g: 0, b: 0, a: 0} - key6: {r: 0, g: 0, b: 0, a: 0} - key7: {r: 0, g: 0, b: 0, a: 0} - ctime0: 0 - ctime1: 65535 - ctime2: 0 - ctime3: 0 - ctime4: 0 - ctime5: 0 - ctime6: 0 - ctime7: 0 - atime0: 0 - atime1: 65535 - atime2: 0 - atime3: 0 - atime4: 0 - atime5: 0 - atime6: 0 - atime7: 0 - m_Mode: 0 - m_ColorSpace: -1 - m_NumColorKeys: 2 - m_NumAlphaKeys: 2 - numCornerVertices: 0 - numCapVertices: 0 - alignment: 0 - textureMode: 0 - textureScale: {x: 1, y: 1} - shadowBias: 0.5 - generateLightingData: 0 - m_UseWorldSpace: 1 - m_Loop: 0 - m_ApplyActiveColorSpace: 1 --- !u!1 &1694612696 GameObject: m_ObjectHideFlags: 0 @@ -953,118 +920,6 @@ MeshFilter: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1694612696} m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} ---- !u!1 &1725817383 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1725817384} - - component: {fileID: 1725817387} - - component: {fileID: 1725817386} - - component: {fileID: 1725817385} - m_Layer: 15 - m_Name: Sphere - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &1725817384 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1725817383} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 0.02, y: 0.02, z: 0.02} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 936701947} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!135 &1725817385 -SphereCollider: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1725817383} - m_Material: {fileID: 0} - m_IncludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_ExcludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_LayerOverridePriority: 0 - m_IsTrigger: 0 - m_ProvidesContacts: 0 - m_Enabled: 1 - serializedVersion: 3 - m_Radius: 0.5 - m_Center: {x: 0, y: 0, z: 0} ---- !u!23 &1725817386 -MeshRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1725817383} - m_Enabled: 1 - m_CastShadows: 1 - m_ReceiveShadows: 1 - m_DynamicOccludee: 1 - m_StaticShadowCaster: 0 - m_MotionVectors: 1 - m_LightProbeUsage: 1 - m_ReflectionProbeUsage: 1 - m_RayTracingMode: 2 - m_RayTraceProcedural: 0 - m_RayTracingAccelStructBuildFlagsOverride: 0 - m_RayTracingAccelStructBuildFlags: 1 - m_SmallMeshCulling: 1 - m_ForceMeshLod: -1 - m_MeshLodSelectionBias: 0 - m_RenderingLayerMask: 1 - m_RendererPriority: 0 - m_Materials: - - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} - m_StaticBatchInfo: - firstSubMesh: 0 - subMeshCount: 0 - m_StaticBatchRoot: {fileID: 0} - m_ProbeAnchor: {fileID: 0} - m_LightProbeVolumeOverride: {fileID: 0} - m_ScaleInLightmap: 1 - m_ReceiveGI: 1 - m_PreserveUVs: 0 - m_IgnoreNormalsForChartDetection: 0 - m_ImportantGI: 0 - m_StitchLightmapSeams: 1 - m_SelectedEditorRenderState: 3 - m_MinimumChartSize: 4 - m_AutoUVMaxDistance: 0.5 - m_AutoUVMaxAngle: 89 - m_LightmapParameters: {fileID: 0} - m_GlobalIlluminationMeshLod: 0 - m_SortingLayerID: 0 - m_SortingLayer: 0 - m_SortingOrder: 0 - m_MaskInteraction: 0 - m_AdditionalVertexStreams: {fileID: 0} ---- !u!33 &1725817387 -MeshFilter: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1725817383} - m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} --- !u!1 &2055159198 GameObject: m_ObjectHideFlags: 0 @@ -1217,8 +1072,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 5625b86b9e4b4482b82d83b962d0c873, type: 3} m_Name: m_EditorClassIdentifier: Assembly-CSharp::RodLine - lineRenderer: {fileID: 0} - points: [] --- !u!114 &2634872453375388399 MonoBehaviour: m_ObjectHideFlags: 0 @@ -2116,7 +1969,6 @@ MonoBehaviour: initialLength: 0 lengthSmoothTime: 0.15 lengthChangeVelocityKill: 0.6 - minSlack: 0.002 headMinLen: 0.01 nodeHysteresis: 0.05 constrainToGround: 1 @@ -2200,7 +2052,6 @@ MonoBehaviour: initialLength: 0 lengthSmoothTime: 0.15 lengthChangeVelocityKill: 0.6 - minSlack: 0.002 headMinLen: 0.01 nodeHysteresis: 0.05 constrainToGround: 1 @@ -2548,4 +2399,4 @@ SceneRoots: - {fileID: 203844589} - {fileID: 2055159199} - {fileID: 5634959157749674791} - - {fileID: 96516534} + - {fileID: 921851545} diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingLineRenderer.cs b/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingLineRenderer.cs index 9b71cd97a..968c807b9 100644 --- a/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingLineRenderer.cs +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingLineRenderer.cs @@ -1,19 +1,39 @@ +using System; +using System.Collections.Generic; using UnityEngine; namespace NBF { - [RequireComponent(typeof(LineRenderer))] public class FishingLineRenderer : MonoBehaviour { [Header("References")] [SerializeField] private FishingLineSolver solver; - [SerializeField] private LineRenderer lineRenderer; + private List _ropes = new List(); + + private Transform _ropeRoot; private void Awake() { - lineRenderer = GetComponent(); solver = GetComponent(); + var ropeRoot = new GameObject("RopeRenderer"); + ropeRoot.transform.SetParent(transform); + _ropeRoot = ropeRoot.transform; + } + + private void Start() + { + foreach (var node in solver.LogicalNodes) + { + if (node.Type == FishingLineNode.NodeType.Start) continue; + var ropeObj = new GameObject($"Rope_{node.Type}"); + ropeObj.transform.SetParent(_ropeRoot); + var rope = ropeObj.AddComponent(); + rope.startAnchor = node.Joint.connectedBody; + rope.endAnchor = node.body; + rope.SetLength(0.5f); + _ropes.Add(rope); + } } } } \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingNodeRope.cs b/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingNodeRope.cs index 4577e6689..714efb6c0 100644 --- a/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingNodeRope.cs +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingNodeRope.cs @@ -1,1043 +1,402 @@ using System; -using NBF; using UnityEngine; -[RequireComponent(typeof(LineRenderer))] -public class FishingNodeRope : MonoBehaviour +namespace NBF { - [Header("Anchors")] [SerializeField] public Rigidbody startAnchor; - [SerializeField] public Rigidbody endAnchor; - - /// 鱼线宽度倍数 - public int LineMultiple = 1; - - [Header("Physics (Dynamic Nodes, Fixed Segment Len)")] [SerializeField, Min(0.01f), Tooltip("物理每段固定长度(越小越细致越耗)")] - private float physicsSegmentLen = 0.15f; - - [SerializeField, Range(2, 200)] private int minPhysicsNodes = 12; - - [SerializeField, Range(2, 400), Tooltip("物理节点上限(仅用于性能保护;与“最大长度不限制”不是一回事)")] - private int maxPhysicsNodes = 120; - - [SerializeField] private float gravityStrength = 2.0f; - [SerializeField, Range(0f, 1f)] private float velocityDampen = 0.95f; - - [SerializeField, Range(0.0f, 1.0f), Tooltip("约束修正强度,越大越硬。0.6~0.9 常用")] - private float stiffness = 0.8f; - - [SerializeField, Range(1, 80), Tooltip("迭代次数。鱼线 10~30 通常够用")] - private int iterations = 20; - - [SerializeField, Range(0, 16), Tooltip("主求解后追加的硬长度约束次数。只负责把 poly 拉回到 rest total,不改变可变长度逻辑")] - private int hardTightenIterations = 2; - - [Header("Length Control (No Min/Max Clamp)")] - [Tooltip("初始总长度(米)。如果为 0,则用 physicsSegmentLen*(minPhysicsNodes-1) 作为初始长度")] - [SerializeField, Min(0f)] - private float initialLength = 0f; - - [Tooltip("长度变化平滑时间(越小越跟手,越大越稳)")] [SerializeField, Min(0.0001f)] - private float lengthSmoothTime = 0.15f; - - [Tooltip("当长度在变化时,额外把速度压掉一些(防抖)。0=不额外处理,1=变化时几乎清速度(建议只在收线生效)")] [SerializeField, Range(0f, 1f)] - private float lengthChangeVelocityKill = 0.6f; - - [Header("Head Segment Clamp")] [Tooltip("第一段(起点->第1节点)允许的最小长度,避免收线时第一段被压到0导致数值炸")] [SerializeField, Min(0.0001f)] - private float headMinLen = 0.01f; - - [Header("Node Count Stability")] [SerializeField, Tooltip("节点数切换迟滞(米)。避免长度在临界点抖动导致节点数来回跳 -> 卡顿")] - private float nodeHysteresis = 0.05f; - - [Header("Simple Ground/Water Constraint (Cheap)")] [SerializeField] - private bool constrainToGround = true; - - [SerializeField] private LayerMask groundMask = ~0; - [SerializeField, Min(0f)] private float groundRadius = 0.01f; - [SerializeField, Min(0f)] private float groundCastHeight = 1.0f; - [SerializeField, Min(0.01f)] private float groundCastDistance = 2.5f; - - [SerializeField, Range(1, 8), Tooltip("每隔多少个节点做一次地面检测;越大越省")] - private int groundSampleStep = 3; - - [SerializeField, Tooltip("未采样的点用插值还是直接拷贝邻近采样值")] - private bool groundInterpolate = true; - - [SerializeField, Range(1, 8), Tooltip("每隔多少次FixedUpdate更新一次地面约束")] - private int groundUpdateEvery = 2; - - [SerializeField, Range(0, 8), Tooltip("地面约束后,再做几次长度约束,减少 poly 被地面抬长")] - private int groundPostConstraintIterations = 2; - - private int _groundFrameCounter; - - [Header("Simple Water Float (Cheap)")] [SerializeField, Tooltip("绳子落到水面以下时,是否把节点约束回水面")] - private bool constrainToWaterSurface = true; - - [SerializeField, Tooltip("静态水面高度;如果你后面接波浪水面,可改成采样函数")] - private float waterLevelY = 0f; - - [SerializeField, Min(0f), Tooltip("把线抬到水面上方一点,避免视觉穿插")] - private float waterSurfaceOffset = 0.002f; - - [SerializeField, Range(1, 8), Tooltip("每隔多少个节点做一次水面约束采样;越大越省")] - private int waterSampleStep = 2; - - [SerializeField, Tooltip("未采样节点是否插值水面高度")] - private bool waterInterpolate = true; - - [SerializeField, Range(1, 8), Tooltip("每隔多少次FixedUpdate更新一次水面约束")] - private int waterUpdateEvery = 1; - - [SerializeField, Range(0f, 1f), Tooltip("水面约束抬升强度(每次更新的插值强度),越小越渐进")] - private float waterLiftStrength = 0.25f; - - [SerializeField, Tooltip("startAnchor 在水下时,让其相邻端节点强制跟随 startAnchor,避免被抬到水面导致脱离")] - private bool keepStartAdjacentNodeFollow = true; - - [SerializeField, Range(0, 8), Tooltip("水面约束后,再做几次长度约束,减少局部折角")] - private int waterPostConstraintIterations = 2; - - private int _waterFrameCounter; - - [Header("Render (High Resolution)")] [SerializeField, Min(1), Tooltip("静止时每段物理线段插值加密数量(越大越顺,越耗)")] - private int renderSubdivisionsIdle = 6; - - [SerializeField, Min(1), Tooltip("甩动时每段物理线段插值加密数量(动态降LOD以防卡顿)")] - private int renderSubdivisionsMoving = 2; - - [SerializeField, Min(0f), Tooltip("平均速度超过该阈值认为在甩动(用于动态降 subdiv)")] - private float movingSpeedThreshold = 2.0f; - - [SerializeField, Tooltip("是否使用 Catmull-Rom 平滑(开启更顺,但更耗)")] - private bool smooth = true; - - [SerializeField, Min(0.0001f)] private float lineWidth = 0.001f; - - [Header("Performance")] [SerializeField, Tooltip("远端玩家鱼线不可见时,直接停止整条渲染线的模拟与绘制")] - private bool cullRemoteRopeWhenInvisible = true; - - [SerializeField, Tooltip("本地玩家自己的鱼线始终保持完整计算")] - private bool localOwnerAlwaysSimulate = true; - - [SerializeField, Range(1, 60), Tooltip("每隔多少个 FixedUpdate 重新判断一次可见性")] - private int visibilityCheckEvery = 10; - - [SerializeField, Range(0f, 0.5f), Tooltip("屏幕边缘额外留白,避免刚进视野就闪现")] - private float visibilityViewportPadding = 0.08f; - - [Header("Air Drag (Stable)")] [SerializeField, Range(0f, 5f), Tooltip("空气阻力(Y向),指数衰减,越大越不飘")] - private float airDrag = 0.9f; - - [SerializeField, Range(0f, 2f), Tooltip("横向额外阻力(XZ),指数衰减,越大越不左右飘")] - private float airDragXZ = 0.6f; - - private LineRenderer _lineRenderer; - - // physics - private int _physicsNodes; - private Vector3[] _pCurr; - private Vector3[] _pPrev; - - // render (一次性分配到最大,后续不再 new) - private Vector3[] _rPoints; - private int _rCapacity; - - private Vector3 _gravity; - - // length control runtime - private float _targetLength; - private float _currentLength; - private float _lengthSmoothVel; - - // rest length head - private float _headRestLen; - - // node stability - private int _lastDesiredNodes = 0; - - // precomputed - private float _dt; - private float _dt2; - private float _kY; - private float _kXZ; - private Camera _cachedCamera; - private int _visibilityCheckCounter; - private bool _isCulledByVisibility; - private int _tIdleSubdiv = -1; - private int _tMovingSubdiv = -1; - - private FRod _rod; - - public void Init(FRod rod) + /// + /// 单段鱼线节点模拟(startAnchor -> endAnchor) + /// + [RequireComponent(typeof(LineRenderer))] + public class FishingNodeRope : MonoBehaviour { - _rod = rod; - if (Application.isPlaying) - RefreshVisibilityState(true); - } + [Header("Anchors")] + public Rigidbody startAnchor; - // Catmull t caches(只缓存 idle/moving 两档,减少每帧重复乘法) - private struct TCaches - { - public float[] t; - public float[] t2; - public float[] t3; - } + public Rigidbody endAnchor; - private TCaches _tIdle; - private TCaches _tMoving; - private enum SampleConstraintMode - { - GroundMinY, - WaterSurfaceY - } + [Header("Length")] + [SerializeField, Min(0.001f)] private float segmentLength = 0.1f; + [SerializeField, Min(0.001f)] private float minLength = 0.02f; + [SerializeField] private bool initializeFromAnchorDistance = true; - private void Awake() - { - _lineRenderer = GetComponent(); - _gravity = new Vector3(0f, -gravityStrength, 0f); - _dt = Mathf.Max(Time.fixedDeltaTime, 1e-6f); - _dt2 = _dt * _dt; + [Header("Simulation")] + [SerializeField, Range(1, 60)] private int solverIterations = 14; + [SerializeField, Range(0f, 1f)] private float stiffness = 0.85f; + [SerializeField, Min(0f)] private float gravityScale = 1f; + [SerializeField, Range(0f, 1f)] private float velocityDamping = 0.98f; - InitLengthSystem(); - AllocateAndInitNodes(); - EnsureRenderCaches(); - } + [Header("Ground Check")] + [SerializeField] private bool constrainToGround = true; + [SerializeField, Range(1, 16), Tooltip("每 N 个模拟点做一次地面检测")] + private int groundSampleStep = 3; - private void OnValidate() - { - renderSubdivisionsIdle = Mathf.Max(renderSubdivisionsIdle, 1); - renderSubdivisionsMoving = Mathf.Max(renderSubdivisionsMoving, 1); - iterations = Mathf.Clamp(iterations, 1, 80); - hardTightenIterations = Mathf.Clamp(hardTightenIterations, 0, 16); - groundCastDistance = Mathf.Max(groundCastDistance, 0.01f); - groundCastHeight = Mathf.Max(groundCastHeight, 0f); - lineWidth = Mathf.Max(lineWidth, 0.0001f); + [SerializeField] private LayerMask groundMask = ~0; + [SerializeField, Min(0f)] private float groundCastHeight = 0.5f; + [SerializeField, Min(0.01f)] private float groundCastDistance = 2f; + [SerializeField, Min(0f)] private float groundOffset = 0.002f; - lengthSmoothTime = Mathf.Max(lengthSmoothTime, 0.0001f); + [Header("Bend Balance")] + [SerializeField, Range(0, 8)] private int bendIterations = 2; + [SerializeField, Range(0f, 1f)] private float bendBalance = 0.35f; - physicsSegmentLen = Mathf.Max(physicsSegmentLen, 0.01f); - minPhysicsNodes = Mathf.Max(minPhysicsNodes, 2); - maxPhysicsNodes = Mathf.Max(maxPhysicsNodes, minPhysicsNodes); + [Header("Render")] + [SerializeField, Min(0.0001f)] private float lineWidth = 0.001f; - headMinLen = Mathf.Max(headMinLen, 0.0001f); - nodeHysteresis = Mathf.Max(0f, nodeHysteresis); + private LineRenderer _lineRenderer; + private Vector3[] _pCurr; + private Vector3[] _pPrev; + private float[] _segmentRestLengths; + private int _nodeCount; + private bool _initialized; - groundSampleStep = Mathf.Max(1, groundSampleStep); - groundUpdateEvery = Mathf.Max(1, groundUpdateEvery); - groundPostConstraintIterations = Mathf.Clamp(groundPostConstraintIterations, 0, 8); + public float Length { get; private set; } - waterSampleStep = Mathf.Max(1, waterSampleStep); - waterUpdateEvery = Mathf.Max(1, waterUpdateEvery); - waterSurfaceOffset = Mathf.Max(0f, waterSurfaceOffset); - waterLiftStrength = Mathf.Clamp01(waterLiftStrength); - waterPostConstraintIterations = Mathf.Clamp(waterPostConstraintIterations, 0, 8); - visibilityCheckEvery = Mathf.Clamp(visibilityCheckEvery, 1, 60); - visibilityViewportPadding = Mathf.Clamp(visibilityViewportPadding, 0f, 0.5f); - } - - private bool ShouldAlwaysSimulate() - { - if (!localOwnerAlwaysSimulate) - return false; - - var owner = _rod?.PlayerItem?.Owner; - return owner == null || owner.IsSelf; - } - - private Vector3 GetStartPosition() - { - return startAnchor ? startAnchor.position : transform.position; - } - - private Vector3 GetEndPosition() - { - return endAnchor ? endAnchor.position : transform.position; - } - - private Camera GetActiveCamera() - { - if (BaseCamera.Main) + private void Awake() { - _cachedCamera = BaseCamera.Main; - return _cachedCamera; + _lineRenderer = GetComponent(); + _lineRenderer.startWidth = lineWidth; + _lineRenderer.endWidth = lineWidth; + _lineRenderer.positionCount = 0; } - if (!_cachedCamera) - _cachedCamera = Camera.main; - - return _cachedCamera; - } - - private static bool IsViewportPointVisible(Vector3 viewportPoint, float padding) - { - if (viewportPoint.z <= 0f) - return false; - - return viewportPoint.x >= -padding && viewportPoint.x <= 1f + padding && - viewportPoint.y >= -padding && viewportPoint.y <= 1f + padding; - } - - private bool IsVisibleToMainCamera() - { - Camera cam = GetActiveCamera(); - if (!cam) - return true; - - Vector3 start = GetStartPosition(); - Vector3 end = GetEndPosition(); - Vector3 middle = (start + end) * 0.5f; - float padding = visibilityViewportPadding; - - return IsViewportPointVisible(cam.WorldToViewportPoint(start), padding) || - IsViewportPointVisible(cam.WorldToViewportPoint(end), padding) || - IsViewportPointVisible(cam.WorldToViewportPoint(middle), padding); - } - - private void RefreshVisibilityState(bool force = false) - { - if (!cullRemoteRopeWhenInvisible || ShouldAlwaysSimulate()) + private void OnValidate() { - _isCulledByVisibility = false; - if (_lineRenderer) - _lineRenderer.enabled = true; - return; + segmentLength = Mathf.Max(0.001f, segmentLength); + minLength = Mathf.Max(0.001f, minLength); + solverIterations = Mathf.Clamp(solverIterations, 1, 60); + stiffness = Mathf.Clamp01(stiffness); + velocityDamping = Mathf.Clamp01(velocityDamping); + gravityScale = Mathf.Max(0f, gravityScale); + groundSampleStep = Mathf.Max(1, groundSampleStep); + groundCastHeight = Mathf.Max(0f, groundCastHeight); + groundCastDistance = Mathf.Max(0.01f, groundCastDistance); + groundOffset = Mathf.Max(0f, groundOffset); + bendIterations = Mathf.Clamp(bendIterations, 0, 8); + bendBalance = Mathf.Clamp01(bendBalance); + lineWidth = Mathf.Max(0.0001f, lineWidth); + + if (_lineRenderer != null) + { + _lineRenderer.startWidth = lineWidth; + _lineRenderer.endWidth = lineWidth; + } } - if (!force) + private void Start() { - _visibilityCheckCounter++; - if (_visibilityCheckCounter < visibilityCheckEvery) + EnsureInitialized(); + } + + private void FixedUpdate() + { + if (!EnsureInitialized() || _nodeCount < 2) return; - } - _visibilityCheckCounter = 0; - bool wasCulled = _isCulledByVisibility; - _isCulledByVisibility = !IsVisibleToMainCamera(); + SimulateVerlet(); - if (_lineRenderer) - _lineRenderer.enabled = !_isCulledByVisibility; - - if (wasCulled && !_isCulledByVisibility) - SyncVisibleStateAfterCulling(); - } - - private void SyncVisibleStateAfterCulling() - { - _currentLength = Mathf.Max(_targetLength, 0.01f); - UpdateNodesFromLength(); - UpdateHeadRestLenFromCurrentLength(); - ResetNodesBetweenAnchors(); - LockAnchorsHard(); - } - - private void ResetNodesBetweenAnchors() - { - if (_physicsNodes < 2) - return; - - Vector3 start = GetStartPosition(); - Vector3 end = GetEndPosition(); - int last = _physicsNodes - 1; - - for (int i = 0; i <= last; i++) - { - float t = (last > 0) ? i / (float)last : 0f; - Vector3 pos = Vector3.Lerp(start, end, t); - _pCurr[i] = pos; - _pPrev[i] = pos; - } - } - - private void EnsureRenderCaches() - { - int idle = Mathf.Max(1, renderSubdivisionsIdle); - if (_tIdleSubdiv != idle) - { - BuildTCaches(idle, ref _tIdle); - _tIdleSubdiv = idle; - } - - int moving = Mathf.Max(1, renderSubdivisionsMoving); - if (_tMovingSubdiv != moving) - { - BuildTCaches(moving, ref _tMoving); - _tMovingSubdiv = moving; - } - - int maxSubdiv = Mathf.Max(idle, moving); - int neededCapacity = (maxPhysicsNodes - 1) * maxSubdiv + 1; - if (_rPoints == null || neededCapacity > _rCapacity) - { - _rCapacity = neededCapacity; - _rPoints = new Vector3[_rCapacity]; - } - } - - private void InitLengthSystem() - { - float defaultLen = physicsSegmentLen * (Mathf.Max(minPhysicsNodes, 2) - 1); - _currentLength = (initialLength > 0f) ? initialLength : defaultLen; - _targetLength = _currentLength; - } - - private void AllocateAndInitNodes() - { - _physicsNodes = Mathf.Clamp(ComputeDesiredNodesStable(_currentLength), 2, maxPhysicsNodes); - - _pCurr = new Vector3[maxPhysicsNodes]; - _pPrev = new Vector3[maxPhysicsNodes]; - - Vector3 start = GetStartPosition(); - Vector3 dir = Vector3.down; - - for (int i = 0; i < _physicsNodes; i++) - { - Vector3 pos = start + dir * (physicsSegmentLen * i); - _pCurr[i] = pos; - _pPrev[i] = pos; - } - - UpdateHeadRestLenFromCurrentLength(); - - if (startAnchor && endAnchor) - LockAnchorsHard(); - } - - private int ComputeDesiredNodes(float lengthMeters) - { - int desired = Mathf.RoundToInt(Mathf.Max(0f, lengthMeters) / physicsSegmentLen) + 1; - desired = Mathf.Clamp(desired, minPhysicsNodes, maxPhysicsNodes); - return desired; - } - - private int ComputeDesiredNodesStable(float lengthMeters) - { - int desired = ComputeDesiredNodes(lengthMeters); - - if (_lastDesiredNodes == 0) - { - _lastDesiredNodes = desired; - return desired; - } - - if (desired == _lastDesiredNodes) - return desired; - - float boundary = (_lastDesiredNodes - 1) * physicsSegmentLen; - if (Mathf.Abs(lengthMeters - boundary) < nodeHysteresis) - return _lastDesiredNodes; - - _lastDesiredNodes = desired; - return desired; - } - - public void SetTargetLength(float lengthMeters) => _targetLength = Mathf.Max(0f, lengthMeters); - public float GetCurrentLength() => _currentLength; - public float GetTargetLength() => _targetLength; - public float GetLengthSmoothVel() => _lengthSmoothVel; - - public float GetLengthByPoints() - { - if (!smooth) - return GetPhysicsPolylineLength(); - - if (_rPoints == null || _lineRenderer == null) return 0f; - - int count = _lineRenderer.positionCount; - if (count < 2) return 0f; - - float totalLength = 0f; - for (int i = 1; i < count; i++) - { - Vector3 a = _rPoints[i - 1]; - Vector3 b = _rPoints[i]; - totalLength += Vector3.Distance(a, b); - } - - return totalLength; - } - - public float GetPhysicsPolylineLength() - { - float total = 0f; - for (int i = 1; i < _physicsNodes; i++) - total += Vector3.Distance(_pCurr[i - 1], _pCurr[i]); - return total; - } - - public void DebugLength() - { - float solverRestTotal = (_physicsNodes - 2) * physicsSegmentLen + _headRestLen; - float poly = GetPhysicsPolylineLength(); - float maxSegDelta = 0f; - float avgSegDelta = 0f; - for (int i = 1; i < _physicsNodes; i++) - { - float rest = (i == 1) ? _headRestLen : physicsSegmentLen; - float segLen = Vector3.Distance(_pCurr[i - 1], _pCurr[i]); - float delta = segLen - rest; - if (delta > maxSegDelta) maxSegDelta = delta; - avgSegDelta += delta; - } - - if (_physicsNodes > 1) - avgSegDelta /= (_physicsNodes - 1); - - Debug.Log( - $"current={_currentLength}, target={_targetLength}, nodes={_physicsNodes}, " + - $"seg={physicsSegmentLen}, head={_headRestLen}, headMin={headMinLen}, " + - $"solverRestTotal={solverRestTotal}, poly={poly}, delta={poly - solverRestTotal}, " + - $"maxSegDelta={maxSegDelta}, avgSegDelta={avgSegDelta}" - ); - } - - private void FixedUpdate() - { - if (!startAnchor || !endAnchor) return; - - RefreshVisibilityState(); - if (_isCulledByVisibility) - return; - - _dt = Time.fixedDeltaTime; - if (_dt < 1e-6f) _dt = 1e-6f; - _dt2 = _dt * _dt; - - _gravity.y = -gravityStrength; - - _kY = Mathf.Exp(-airDrag * _dt); - _kXZ = Mathf.Exp(-airDragXZ * _dt); - - UpdateLengthSmooth(); - UpdateNodesFromLength(); - UpdateHeadRestLenFromCurrentLength(); - - Simulate_VerletFast(); - - for (int it = 0; it < iterations; it++) - { - LockAnchorsHard(); - SolveDistanceConstraintsBidirectional(stiffness); - } - - SolveHardDistanceConstraints(hardTightenIterations); - LockAnchorsHard(); - - if (constrainToGround) - { - _groundFrameCounter++; - if (_groundFrameCounter >= groundUpdateEvery) + for (int it = 0; it < solverIterations; it++) + { + LockAnchorsHard(Time.fixedDeltaTime); + SolveDistanceConstraints(stiffness); + SolveBendBalance(); + } + + // 最后再硬约束一遍,保证每段长度更贴近 rest length。 + LockAnchorsHard(Time.fixedDeltaTime); + SolveDistanceConstraints(1f); + + if (constrainToGround) { - _groundFrameCounter = 0; ConstrainToGround(); - SolveHardDistanceConstraints(groundPostConstraintIterations); + LockAnchorsHard(Time.fixedDeltaTime); + SolveDistanceConstraints(1f); } + + LockAnchorsHard(Time.fixedDeltaTime); } - if (constrainToWaterSurface) + private void LateUpdate() { - _waterFrameCounter++; - if (_waterFrameCounter >= waterUpdateEvery) - { - _waterFrameCounter = 0; - ConstrainToWaterSurface(); + if (_pCurr == null || _nodeCount < 2 || _lineRenderer == null) + return; - // 水面抬升后补几次长度约束,让形状更顺一点 - SolveHardDistanceConstraints(waterPostConstraintIterations); + _lineRenderer.startWidth = lineWidth; + _lineRenderer.endWidth = lineWidth; + _lineRenderer.positionCount = _nodeCount; + _lineRenderer.SetPositions(_pCurr); + } + + public void SetLength(float length) + { + float target = Mathf.Max(length, minLength); + bool firstInit = !_initialized; + Length = target; + RebuildFromLength(target, keepShapeFromStart: !firstInit); + _initialized = true; + } + + public void AddLength(float delta) + { + if (delta <= 0f) return; + SetLength(Length + delta); + } + + public void ReduceLength(float delta) + { + if (delta <= 0f) return; + SetLength(Length - delta); + } + + private bool EnsureInitialized() + { + if (_initialized) + return true; + + if (!startAnchor || !endAnchor) + return false; + + float initialLength = initializeFromAnchorDistance + ? Vector3.Distance(GetStartPos(), GetEndPos()) + : Mathf.Max(segmentLength, minLength); + + SetLength(initialLength); + return true; + } + + private Vector3 GetStartPos() => startAnchor ? startAnchor.position : transform.position; + private Vector3 GetEndPos() => endAnchor ? endAnchor.position : transform.position; + + private void RebuildFromLength(float totalLength, bool keepShapeFromStart) + { + float clampedLength = Mathf.Max(totalLength, minLength); + Length = clampedLength; + + int fullSeg = Mathf.FloorToInt(clampedLength / segmentLength); + float rem = clampedLength - fullSeg * segmentLength; + bool hasRem = rem > 1e-4f; + int segmentCount = Mathf.Max(1, fullSeg + (hasRem ? 1 : 0)); + int desiredNodes = segmentCount + 1; + + float[] newRest = new float[segmentCount]; + for (int i = 0; i < segmentCount; i++) + newRest[i] = segmentLength; + if (hasRem) + newRest[segmentCount - 1] = rem; + + if (_pCurr == null || _pPrev == null || _nodeCount < 2 || !keepShapeFromStart) + { + BuildLinearNodes(desiredNodes, newRest); + return; } - } - LockAnchorsHard(); - } + int oldNodes = _nodeCount; + int oldSegments = oldNodes - 1; + int add = Mathf.Max(0, segmentCount - oldSegments); + int remove = Mathf.Max(0, oldSegments - segmentCount); - private void Update() - { - if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return; + Vector3[] newCurr = new Vector3[desiredNodes]; + Vector3[] newPrev = new Vector3[desiredNodes]; - if (_isCulledByVisibility) - return; - - EnsureRenderCaches(); - - int last = _physicsNodes - 1; - - Vector3 s = GetStartPosition(); - Vector3 e = GetEndPosition(); - - _pCurr[0] = s; - _pCurr[last] = e; - - DrawHighResLine_Fast(); - } - - private void UpdateLengthSmooth() - { - float minFeasible = 0.01f; - float desired = Mathf.Max(_targetLength, minFeasible); - - _currentLength = Mathf.SmoothDamp( - _currentLength, - desired, - ref _lengthSmoothVel, - lengthSmoothTime, - Mathf.Infinity, - Time.fixedDeltaTime - ); - - // 长度变化时额外压一点速度,减少收放线时抖动 - float delta = Mathf.Abs(_targetLength - _currentLength); - if (delta > 0.0001f && lengthChangeVelocityKill > 0f) - { - float keep = 1f - Mathf.Clamp01(lengthChangeVelocityKill); - for (int i = 1; i < _physicsNodes - 1; i++) + if (add > 0) { - Vector3 curr = _pCurr[i]; - Vector3 prev = _pPrev[i]; - Vector3 disp = curr - prev; - _pPrev[i] = curr - disp * keep; - } - } - } + Array.Copy(_pCurr, 1, newCurr, 1 + add, oldNodes - 1); + Array.Copy(_pPrev, 1, newPrev, 1 + add, oldNodes - 1); - private void UpdateNodesFromLength() - { - int desired = ComputeDesiredNodesStable(_currentLength); - desired = Mathf.Clamp(desired, 2, maxPhysicsNodes); - if (desired == _physicsNodes) return; + Vector3 s = GetStartPos(); + int firstOldIdx = 1 + add; + Vector3 dir = GetInitialFillDir(s, firstOldIdx < newCurr.Length ? newCurr[firstOldIdx] : GetEndPos()); + Vector3 inheritDisp = Vector3.zero; + if (firstOldIdx < newCurr.Length) + inheritDisp = newCurr[firstOldIdx] - newPrev[firstOldIdx]; - if (desired > _physicsNodes) AddNodesAtStart(desired - _physicsNodes); - else RemoveNodesAtStart(_physicsNodes - desired); - - _physicsNodes = desired; - } - - private void AddNodesAtStart(int addCount) - { - if (addCount <= 0) return; - - int oldCount = _physicsNodes; - int newCount = Mathf.Min(oldCount + addCount, maxPhysicsNodes); - addCount = newCount - oldCount; - if (addCount <= 0) return; - - Array.Copy(_pCurr, 1, _pCurr, 1 + addCount, oldCount - 1); - Array.Copy(_pPrev, 1, _pPrev, 1 + addCount, oldCount - 1); - - Vector3 s = GetStartPosition(); - - Vector3 dir = Vector3.down; - int firstOld = 1 + addCount; - if (oldCount >= 2 && firstOld < maxPhysicsNodes) - { - Vector3 toOld1 = (_pCurr[firstOld] - s); - float sq = toOld1.sqrMagnitude; - if (sq > 1e-6f) dir = toOld1 / Mathf.Sqrt(sq); - } - - Vector3 inheritDisp = Vector3.zero; - if (oldCount >= 2 && firstOld < maxPhysicsNodes) - inheritDisp = (_pCurr[firstOld] - _pPrev[firstOld]); - - for (int k = 1; k <= addCount; k++) - { - Vector3 pos = s + dir * (physicsSegmentLen * k); - _pCurr[k] = pos; - _pPrev[k] = pos - inheritDisp; - } - - LockAnchorsHard(); - } - - private void RemoveNodesAtStart(int removeCount) - { - if (removeCount <= 0) return; - - int oldCount = _physicsNodes; - int newCount = Mathf.Max(oldCount - removeCount, 2); - removeCount = oldCount - newCount; - if (removeCount <= 0) return; - - Array.Copy(_pCurr, 1 + removeCount, _pCurr, 1, newCount - 2); - Array.Copy(_pPrev, 1 + removeCount, _pPrev, 1, newCount - 2); - - LockAnchorsHard(); - } - - private void UpdateHeadRestLenFromCurrentLength() - { - int fixedSegCount = Mathf.Max(0, _physicsNodes - 2); - float baseLen = fixedSegCount * physicsSegmentLen; - - _headRestLen = _currentLength - baseLen; - _headRestLen = Mathf.Clamp(_headRestLen, headMinLen, physicsSegmentLen * 1.5f); - } - - private void Simulate_VerletFast() - { - for (int i = 1; i < _physicsNodes - 1; i++) - { - Vector3 disp = _pCurr[i] - _pPrev[i]; - - disp.x *= _kXZ; - disp.z *= _kXZ; - disp.y *= _kY; - - disp *= velocityDampen; - - Vector3 next = _pCurr[i] + disp + _gravity * _dt2; - - _pPrev[i] = _pCurr[i]; - _pCurr[i] = next; - } - } - - private void LockAnchorsHard() - { - if (!startAnchor || !endAnchor || _pCurr == null || _pPrev == null || _physicsNodes < 2) return; - - Vector3 s = GetStartPosition(); - Vector3 e = GetEndPosition(); - - _pCurr[0] = s; - _pPrev[0] = s - startAnchor.linearVelocity * _dt; - - int last = _physicsNodes - 1; - _pCurr[last] = e; - _pPrev[last] = e - endAnchor.linearVelocity * _dt; - } - - private void SolveHardDistanceConstraints(int extraIterations) - { - for (int it = 0; it < extraIterations; it++) - { - LockAnchorsHard(); - SolveDistanceConstraintsBidirectional(1f); - } - } - - private void SolveDistanceConstraintsBidirectional(float combinedStiffness) - { - int last = _physicsNodes - 1; - if (last <= 0) return; - - float clamped = Mathf.Clamp01(combinedStiffness); - float sweepStiffness = (clamped >= 0.999999f) ? 1f : 1f - Mathf.Sqrt(1f - clamped); - SolveDistanceConstraintsSweep_Fast(0, last, 1, last, sweepStiffness); - SolveDistanceConstraintsSweep_Fast(last - 1, -1, -1, last, sweepStiffness); - } - - private void SolveDistanceConstraintsSweep_Fast(int start, int endExclusive, int step, int last, - float sweepStiffness) - { - for (int i = start; i != endExclusive; i += step) - { - float rest = (i == 0) ? _headRestLen : physicsSegmentLen; - - Vector3 a = _pCurr[i]; - Vector3 b = _pCurr[i + 1]; - - Vector3 delta = b - a; - float sq = delta.sqrMagnitude; - if (sq < 1e-12f) continue; - - float dist = Mathf.Sqrt(sq); - float diff = (dist - rest) / dist; - Vector3 corr = delta * (diff * sweepStiffness); - - bool aLocked = (i == 0); - bool bLocked = (i + 1 == last); - - if (!aLocked && !bLocked) - { - _pCurr[i] = a + corr * 0.5f; - _pCurr[i + 1] = b - corr * 0.5f; - } - else if (aLocked && !bLocked) - { - _pCurr[i + 1] = b - corr; // 首段:node1 吃满 - } - else if (!aLocked) - { - _pCurr[i] = a + corr; // 尾段:last-1 吃满 - } - // 两边都锁的情况理论上不会出现 - } - } - - private void ConstrainToGround() - { - if (groundMask == 0) return; - - ApplySampledConstraint(SampleConstraintMode.GroundMinY, groundSampleStep, groundInterpolate); - } - - private float SampleGroundMinY(Vector3 p) - { - Vector3 origin = p + Vector3.up * groundCastHeight; - if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask, - QueryTriggerInteraction.Ignore)) - return hit.point.y + groundRadius; - - return float.NegativeInfinity; - } - - private void ApplyMinY(int i, float minY) - { - if (float.IsNegativeInfinity(minY)) return; - - Vector3 p = _pCurr[i]; - if (p.y < minY) - { - p.y = minY; - _pCurr[i] = p; - - // prev 同步抬上来,避免下一帧又被惯性拉回去造成抖动 - Vector3 prev = _pPrev[i]; - if (prev.y < minY) prev.y = minY; - _pPrev[i] = prev; - } - } - - private void ConstrainToWaterSurface() - { - int last = _physicsNodes - 1; - if (last <= 1) return; - - float surfaceY = waterLevelY + waterSurfaceOffset; - bool startUnderWater = _pCurr[0].y < surfaceY; - int startAdjacentIdx = GetStartAdjacentNodeIndex(last); - - ApplySampledConstraint( - SampleConstraintMode.WaterSurfaceY, - waterSampleStep, - waterInterpolate, - surfaceY, - startUnderWater, - startAdjacentIdx - ); - } - - private void ApplySampledConstraint(SampleConstraintMode mode, int sampleStep, bool interpolate, - float constantValue = 0f, bool startUnderWater = false, int startAdjacentIdx = -1) - { - int last = _physicsNodes - 1; - if (last <= 1) return; - - int step = Mathf.Max(1, sampleStep); - int prevSampleIdx = 1; - float prevValue = GetConstraintSampleValue(mode, prevSampleIdx, constantValue); - - ApplyConstraintValue(mode, prevSampleIdx, prevValue, startUnderWater, startAdjacentIdx); - - for (int i = 1 + step; i < last; i += step) - { - float nextValue = GetConstraintSampleValue(mode, i, constantValue); - ApplyConstraintValue(mode, i, nextValue, startUnderWater, startAdjacentIdx); - - if (interpolate) - { - int span = i - prevSampleIdx; - for (int j = 1; j < span; j++) + for (int i = 1; i <= add; i++) { - int idx = prevSampleIdx + j; - float t = j / (float)span; - float value = Mathf.Lerp(prevValue, nextValue, t); - ApplyConstraintValue(mode, idx, value, startUnderWater, startAdjacentIdx); + Vector3 pos = s + dir * (segmentLength * i); + newCurr[i] = pos; + newPrev[i] = pos - inheritDisp; } } + else if (remove > 0) + { + int srcStart = 1 + remove; + int copyCount = desiredNodes - 1; + Array.Copy(_pCurr, srcStart, newCurr, 1, copyCount); + Array.Copy(_pPrev, srcStart, newPrev, 1, copyCount); + } else { - for (int idx = prevSampleIdx + 1; idx < i; idx++) - ApplyConstraintValue(mode, idx, prevValue, startUnderWater, startAdjacentIdx); + Array.Copy(_pCurr, newCurr, desiredNodes); + Array.Copy(_pPrev, newPrev, desiredNodes); } - prevSampleIdx = i; - prevValue = nextValue; + _pCurr = newCurr; + _pPrev = newPrev; + _segmentRestLengths = newRest; + _nodeCount = desiredNodes; + + LockAnchorsHard(Time.fixedDeltaTime > 0f ? Time.fixedDeltaTime : 0.02f); } - for (int i = prevSampleIdx + 1; i < last; i++) - ApplyConstraintValue(mode, i, prevValue, startUnderWater, startAdjacentIdx); - } - - private float GetConstraintSampleValue(SampleConstraintMode mode, int index, float constantValue) - { - switch (mode) + private void BuildLinearNodes(int desiredNodes, float[] restLengths) { - case SampleConstraintMode.GroundMinY: - return SampleGroundMinY(_pCurr[index]); - case SampleConstraintMode.WaterSurfaceY: - return constantValue; - default: - return constantValue; - } - } + _nodeCount = Mathf.Max(2, desiredNodes); + _pCurr = new Vector3[_nodeCount]; + _pPrev = new Vector3[_nodeCount]; + _segmentRestLengths = restLengths; - private void ApplyConstraintValue(SampleConstraintMode mode, int index, float value, bool startUnderWater, - int startAdjacentIdx) - { - switch (mode) - { - case SampleConstraintMode.GroundMinY: - ApplyMinY(index, value); - break; - case SampleConstraintMode.WaterSurfaceY: - ApplyWaterSurface(index, value, startUnderWater, startAdjacentIdx); - break; - } - } + Vector3 s = GetStartPos(); + Vector3 e = GetEndPos(); + Vector3 dir = GetInitialFillDir(s, e); - private int GetStartAdjacentNodeIndex(int last) - { - if (last <= 1) return 1; + _pCurr[0] = s; + _pPrev[0] = s; - Vector3 s = _pCurr[0]; - float d1 = (_pCurr[1] - s).sqrMagnitude; - float d2 = (_pCurr[last - 1] - s).sqrMagnitude; - return d1 <= d2 ? 1 : last - 1; - } - - private void ApplyWaterSurface(int i, float surfaceY, bool startUnderWater, int startAdjacentIdx) - { - if (keepStartAdjacentNodeFollow && startUnderWater && i == startAdjacentIdx) - { - Vector3 s = _pCurr[0]; - _pCurr[i] = s; - _pPrev[i] = s; - return; - } - - Vector3 p = _pCurr[i]; - if (p.y < surfaceY) - { - p.y = Mathf.Lerp(p.y, surfaceY, waterLiftStrength); - _pCurr[i] = p; - - // 渐进同步 prev,削弱向下惯性,避免反复穿透水面 - Vector3 prev = _pPrev[i]; - if (prev.y < p.y) prev.y = Mathf.Lerp(prev.y, p.y, waterLiftStrength); - _pPrev[i] = prev; - } - } - - private void DrawHighResLine_Fast() - { - if (_pCurr == null || _physicsNodes < 2) return; - - float w = lineWidth * LineMultiple; - _lineRenderer.startWidth = w; - _lineRenderer.endWidth = w; - - if (!smooth) - { - _lineRenderer.positionCount = _physicsNodes; - _lineRenderer.SetPositions(_pCurr); - return; - } - - int subdiv = PickRenderSubdivisions_Fast(); - TCaches tc = (subdiv == renderSubdivisionsMoving) ? _tMoving : _tIdle; - - int idx = 0; - int last = _physicsNodes - 1; - - for (int seg = 0; seg < last; seg++) - { - int i0 = seg - 1; - if (i0 < 0) i0 = 0; - int i1 = seg; - int i2 = seg + 1; - int i3 = seg + 2; - if (i3 > last) i3 = last; - - Vector3 p0 = _pCurr[i0]; - Vector3 p1 = _pCurr[i1]; - Vector3 p2 = _pCurr[i2]; - Vector3 p3 = _pCurr[i3]; - - for (int s = 0; s < subdiv; s++) + float traveled = 0f; + for (int i = 1; i < _nodeCount - 1; i++) { - float t = tc.t[s]; - float t2 = tc.t2[s]; - float t3 = tc.t3[s]; + traveled += _segmentRestLengths[i - 1]; + Vector3 pos = s + dir * traveled; + _pCurr[i] = pos; + _pPrev[i] = pos; + } - Vector3 cr = - 0.5f * ( - (2f * p1) + - (-p0 + p2) * t + - (2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 + - (-p0 + 3f * p1 - 3f * p2 + p3) * t3 - ); + _pCurr[_nodeCount - 1] = e; + _pPrev[_nodeCount - 1] = e; + } - // y 也使用平滑曲线,再做单调夹紧;避免垂直时因为线性 y 插值导致切线断裂,看起来像折线。 - cr.y = ClampMonotonic(cr.y, p0.y, p1.y, p2.y, p3.y); + private Vector3 GetInitialFillDir(Vector3 start, Vector3 toward) + { + Vector3 d = toward - start; + if (d.sqrMagnitude < 1e-8f) + return Vector3.down; + return d.normalized; + } - _rPoints[idx++] = cr; + private void SimulateVerlet() + { + float dt = Mathf.Max(Time.fixedDeltaTime, 1e-6f); + float dt2 = dt * dt; + Vector3 gravity = Physics.gravity * gravityScale; + + int last = _nodeCount - 1; + for (int i = 1; i < last; i++) + { + Vector3 disp = (_pCurr[i] - _pPrev[i]) * velocityDamping; + Vector3 next = _pCurr[i] + disp + gravity * dt2; + _pPrev[i] = _pCurr[i]; + _pCurr[i] = next; } } - _rPoints[idx++] = _pCurr[last]; - - _lineRenderer.positionCount = idx; - _lineRenderer.SetPositions(_rPoints); - } - - private static float ClampMonotonic(float value, float p0, float p1, float p2, float p3) - { - bool rising = p0 <= p1 && p1 <= p2 && p2 <= p3; - bool falling = p0 >= p1 && p1 >= p2 && p2 >= p3; - if (!rising && !falling) - return value; - - float min = Mathf.Min(p1, p2); - float max = Mathf.Max(p1, p2); - return Mathf.Clamp(value, min, max); - } - - private int PickRenderSubdivisions_Fast() - { - int idle = Mathf.Max(1, renderSubdivisionsIdle); - int moving = Mathf.Max(1, renderSubdivisionsMoving); - - float thr = movingSpeedThreshold; - float thrSq = (thr * _dt) * (thr * _dt); - - float sumSq = 0f; - int count = Mathf.Max(1, _physicsNodes - 2); - - for (int i = 1; i < _physicsNodes - 1; i++) + private void LockAnchorsHard(float dt) { - Vector3 disp = _pCurr[i] - _pPrev[i]; - sumSq += disp.sqrMagnitude; + if (_nodeCount < 2) + return; + + Vector3 s = GetStartPos(); + Vector3 e = GetEndPos(); + + _pCurr[0] = s; + _pPrev[0] = startAnchor ? s - startAnchor.linearVelocity * dt : s; + + int last = _nodeCount - 1; + _pCurr[last] = e; + _pPrev[last] = endAnchor ? e - endAnchor.linearVelocity * dt : e; } - float avgSq = sumSq / count; - - return (avgSq > thrSq) ? moving : idle; - } - - private static void BuildTCaches(int subdiv, ref TCaches caches) - { - subdiv = Mathf.Max(1, subdiv); - caches.t = new float[subdiv]; - caches.t2 = new float[subdiv]; - caches.t3 = new float[subdiv]; - - float inv = 1f / subdiv; - for (int s = 0; s < subdiv; s++) + private void SolveDistanceConstraints(float solveStiffness) { - float t = s * inv; - float t2 = t * t; - caches.t[s] = t; - caches.t2[s] = t2; - caches.t3[s] = t2 * t; - } - } + int last = _nodeCount - 1; + if (last <= 0) + return; - private void OnDrawGizmosSelected() - { - if (_pCurr == null) return; - Gizmos.color = Color.yellow; - for (int i = 0; i < _physicsNodes; i++) - Gizmos.DrawSphere(_pCurr[i], 0.01f); + float k = Mathf.Clamp01(solveStiffness); + + SolveDistanceSweep(0, last, 1, last, k); + SolveDistanceSweep(last - 1, -1, -1, last, k); + } + + private void SolveDistanceSweep(int start, int endExclusive, int step, int last, float k) + { + for (int i = start; i != endExclusive; i += step) + { + float rest = _segmentRestLengths[i]; + Vector3 a = _pCurr[i]; + Vector3 b = _pCurr[i + 1]; + + Vector3 delta = b - a; + float sq = delta.sqrMagnitude; + if (sq < 1e-12f) + continue; + + float dist = Mathf.Sqrt(sq); + float diff = (dist - rest) / dist; + Vector3 corr = delta * (diff * k); + + bool aLocked = i == 0; + bool bLocked = i + 1 == last; + + if (!aLocked && !bLocked) + { + _pCurr[i] = a + corr * 0.5f; + _pCurr[i + 1] = b - corr * 0.5f; + } + else if (aLocked && !bLocked) + { + _pCurr[i + 1] = b - corr; + } + else if (!aLocked) + { + _pCurr[i] = a + corr; + } + } + } + + private void SolveBendBalance() + { + if (bendIterations <= 0 || bendBalance <= 0f || _nodeCount < 3) + return; + + int last = _nodeCount - 1; + for (int it = 0; it < bendIterations; it++) + { + for (int i = 1; i < last; i++) + { + Vector3 target = (_pCurr[i - 1] + _pCurr[i + 1]) * 0.5f; + _pCurr[i] = Vector3.Lerp(_pCurr[i], target, bendBalance); + } + } + } + + private void ConstrainToGround() + { + if (groundMask == 0 || _nodeCount < 3) + return; + + int last = _nodeCount - 1; + int step = Mathf.Max(1, groundSampleStep); + + for (int i = 1; i < last; i += step) + { + Vector3 p = _pCurr[i]; + Vector3 origin = p + Vector3.up * groundCastHeight; + if (!Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask, + QueryTriggerInteraction.Ignore)) + { + continue; + } + + float minY = hit.point.y + groundOffset; + if (p.y < minY) + { + p.y = minY; + _pCurr[i] = p; + + Vector3 prev = _pPrev[i]; + if (prev.y < minY) prev.y = minY; + _pPrev[i] = prev; + } + } + } } } diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingNodeRope.cs.meta b/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingNodeRope.cs.meta index 93d4824f7..ac08672fb 100644 --- a/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingNodeRope.cs.meta +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingNodeRope.cs.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 -guid: 98ba9d435a0e49c9bb527c34cc91894d -timeCreated: 1766759607 \ No newline at end of file +fileFormatVersion: 2 +guid: 53f61ff2ae804a41a3ac35ea7bd00b20 +timeCreated: 1776157309 \ No newline at end of file diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/Rope.cs b/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/Rope.cs new file mode 100644 index 000000000..d800e590a --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/Rope.cs @@ -0,0 +1,1043 @@ +using System; +using NBF; +using UnityEngine; + +[RequireComponent(typeof(LineRenderer))] +public class Rope : MonoBehaviour +{ + [Header("Anchors")] [SerializeField] public Rigidbody startAnchor; + [SerializeField] public Rigidbody endAnchor; + + /// 鱼线宽度倍数 + public int LineMultiple = 1; + + [Header("Physics (Dynamic Nodes, Fixed Segment Len)")] [SerializeField, Min(0.01f), Tooltip("物理每段固定长度(越小越细致越耗)")] + private float physicsSegmentLen = 0.15f; + + [SerializeField, Range(2, 200)] private int minPhysicsNodes = 12; + + [SerializeField, Range(2, 400), Tooltip("物理节点上限(仅用于性能保护;与“最大长度不限制”不是一回事)")] + private int maxPhysicsNodes = 120; + + [SerializeField] private float gravityStrength = 2.0f; + [SerializeField, Range(0f, 1f)] private float velocityDampen = 0.95f; + + [SerializeField, Range(0.0f, 1.0f), Tooltip("约束修正强度,越大越硬。0.6~0.9 常用")] + private float stiffness = 0.8f; + + [SerializeField, Range(1, 80), Tooltip("迭代次数。鱼线 10~30 通常够用")] + private int iterations = 20; + + [SerializeField, Range(0, 16), Tooltip("主求解后追加的硬长度约束次数。只负责把 poly 拉回到 rest total,不改变可变长度逻辑")] + private int hardTightenIterations = 2; + + [Header("Length Control (No Min/Max Clamp)")] + [Tooltip("初始总长度(米)。如果为 0,则用 physicsSegmentLen*(minPhysicsNodes-1) 作为初始长度")] + [SerializeField, Min(0f)] + private float initialLength = 0f; + + [Tooltip("长度变化平滑时间(越小越跟手,越大越稳)")] [SerializeField, Min(0.0001f)] + private float lengthSmoothTime = 0.15f; + + [Tooltip("当长度在变化时,额外把速度压掉一些(防抖)。0=不额外处理,1=变化时几乎清速度(建议只在收线生效)")] [SerializeField, Range(0f, 1f)] + private float lengthChangeVelocityKill = 0.6f; + + [Header("Head Segment Clamp")] [Tooltip("第一段(起点->第1节点)允许的最小长度,避免收线时第一段被压到0导致数值炸")] [SerializeField, Min(0.0001f)] + private float headMinLen = 0.01f; + + [Header("Node Count Stability")] [SerializeField, Tooltip("节点数切换迟滞(米)。避免长度在临界点抖动导致节点数来回跳 -> 卡顿")] + private float nodeHysteresis = 0.05f; + + [Header("Simple Ground/Water Constraint (Cheap)")] [SerializeField] + private bool constrainToGround = true; + + [SerializeField] private LayerMask groundMask = ~0; + [SerializeField, Min(0f)] private float groundRadius = 0.01f; + [SerializeField, Min(0f)] private float groundCastHeight = 1.0f; + [SerializeField, Min(0.01f)] private float groundCastDistance = 2.5f; + + [SerializeField, Range(1, 8), Tooltip("每隔多少个节点做一次地面检测;越大越省")] + private int groundSampleStep = 3; + + [SerializeField, Tooltip("未采样的点用插值还是直接拷贝邻近采样值")] + private bool groundInterpolate = true; + + [SerializeField, Range(1, 8), Tooltip("每隔多少次FixedUpdate更新一次地面约束")] + private int groundUpdateEvery = 2; + + [SerializeField, Range(0, 8), Tooltip("地面约束后,再做几次长度约束,减少 poly 被地面抬长")] + private int groundPostConstraintIterations = 2; + + private int _groundFrameCounter; + + [Header("Simple Water Float (Cheap)")] [SerializeField, Tooltip("绳子落到水面以下时,是否把节点约束回水面")] + private bool constrainToWaterSurface = true; + + [SerializeField, Tooltip("静态水面高度;如果你后面接波浪水面,可改成采样函数")] + private float waterLevelY = 0f; + + [SerializeField, Min(0f), Tooltip("把线抬到水面上方一点,避免视觉穿插")] + private float waterSurfaceOffset = 0.002f; + + [SerializeField, Range(1, 8), Tooltip("每隔多少个节点做一次水面约束采样;越大越省")] + private int waterSampleStep = 2; + + [SerializeField, Tooltip("未采样节点是否插值水面高度")] + private bool waterInterpolate = true; + + [SerializeField, Range(1, 8), Tooltip("每隔多少次FixedUpdate更新一次水面约束")] + private int waterUpdateEvery = 1; + + [SerializeField, Range(0f, 1f), Tooltip("水面约束抬升强度(每次更新的插值强度),越小越渐进")] + private float waterLiftStrength = 0.25f; + + [SerializeField, Tooltip("startAnchor 在水下时,让其相邻端节点强制跟随 startAnchor,避免被抬到水面导致脱离")] + private bool keepStartAdjacentNodeFollow = true; + + [SerializeField, Range(0, 8), Tooltip("水面约束后,再做几次长度约束,减少局部折角")] + private int waterPostConstraintIterations = 2; + + private int _waterFrameCounter; + + [Header("Render (High Resolution)")] [SerializeField, Min(1), Tooltip("静止时每段物理线段插值加密数量(越大越顺,越耗)")] + private int renderSubdivisionsIdle = 6; + + [SerializeField, Min(1), Tooltip("甩动时每段物理线段插值加密数量(动态降LOD以防卡顿)")] + private int renderSubdivisionsMoving = 2; + + [SerializeField, Min(0f), Tooltip("平均速度超过该阈值认为在甩动(用于动态降 subdiv)")] + private float movingSpeedThreshold = 2.0f; + + [SerializeField, Tooltip("是否使用 Catmull-Rom 平滑(开启更顺,但更耗)")] + private bool smooth = true; + + [SerializeField, Min(0.0001f)] private float lineWidth = 0.001f; + + [Header("Performance")] [SerializeField, Tooltip("远端玩家鱼线不可见时,直接停止整条渲染线的模拟与绘制")] + private bool cullRemoteRopeWhenInvisible = true; + + [SerializeField, Tooltip("本地玩家自己的鱼线始终保持完整计算")] + private bool localOwnerAlwaysSimulate = true; + + [SerializeField, Range(1, 60), Tooltip("每隔多少个 FixedUpdate 重新判断一次可见性")] + private int visibilityCheckEvery = 10; + + [SerializeField, Range(0f, 0.5f), Tooltip("屏幕边缘额外留白,避免刚进视野就闪现")] + private float visibilityViewportPadding = 0.08f; + + [Header("Air Drag (Stable)")] [SerializeField, Range(0f, 5f), Tooltip("空气阻力(Y向),指数衰减,越大越不飘")] + private float airDrag = 0.9f; + + [SerializeField, Range(0f, 2f), Tooltip("横向额外阻力(XZ),指数衰减,越大越不左右飘")] + private float airDragXZ = 0.6f; + + private LineRenderer _lineRenderer; + + // physics + private int _physicsNodes; + private Vector3[] _pCurr; + private Vector3[] _pPrev; + + // render (一次性分配到最大,后续不再 new) + private Vector3[] _rPoints; + private int _rCapacity; + + private Vector3 _gravity; + + // length control runtime + private float _targetLength; + private float _currentLength; + private float _lengthSmoothVel; + + // rest length head + private float _headRestLen; + + // node stability + private int _lastDesiredNodes = 0; + + // precomputed + private float _dt; + private float _dt2; + private float _kY; + private float _kXZ; + private Camera _cachedCamera; + private int _visibilityCheckCounter; + private bool _isCulledByVisibility; + private int _tIdleSubdiv = -1; + private int _tMovingSubdiv = -1; + + private FRod _rod; + + public void Init(FRod rod) + { + _rod = rod; + if (Application.isPlaying) + RefreshVisibilityState(true); + } + + // Catmull t caches(只缓存 idle/moving 两档,减少每帧重复乘法) + private struct TCaches + { + public float[] t; + public float[] t2; + public float[] t3; + } + + private TCaches _tIdle; + private TCaches _tMoving; + private enum SampleConstraintMode + { + GroundMinY, + WaterSurfaceY + } + + private void Awake() + { + _lineRenderer = GetComponent(); + _gravity = new Vector3(0f, -gravityStrength, 0f); + _dt = Mathf.Max(Time.fixedDeltaTime, 1e-6f); + _dt2 = _dt * _dt; + + InitLengthSystem(); + AllocateAndInitNodes(); + EnsureRenderCaches(); + } + + private void OnValidate() + { + renderSubdivisionsIdle = Mathf.Max(renderSubdivisionsIdle, 1); + renderSubdivisionsMoving = Mathf.Max(renderSubdivisionsMoving, 1); + iterations = Mathf.Clamp(iterations, 1, 80); + hardTightenIterations = Mathf.Clamp(hardTightenIterations, 0, 16); + groundCastDistance = Mathf.Max(groundCastDistance, 0.01f); + groundCastHeight = Mathf.Max(groundCastHeight, 0f); + lineWidth = Mathf.Max(lineWidth, 0.0001f); + + lengthSmoothTime = Mathf.Max(lengthSmoothTime, 0.0001f); + + physicsSegmentLen = Mathf.Max(physicsSegmentLen, 0.01f); + minPhysicsNodes = Mathf.Max(minPhysicsNodes, 2); + maxPhysicsNodes = Mathf.Max(maxPhysicsNodes, minPhysicsNodes); + + headMinLen = Mathf.Max(headMinLen, 0.0001f); + nodeHysteresis = Mathf.Max(0f, nodeHysteresis); + + groundSampleStep = Mathf.Max(1, groundSampleStep); + groundUpdateEvery = Mathf.Max(1, groundUpdateEvery); + groundPostConstraintIterations = Mathf.Clamp(groundPostConstraintIterations, 0, 8); + + waterSampleStep = Mathf.Max(1, waterSampleStep); + waterUpdateEvery = Mathf.Max(1, waterUpdateEvery); + waterSurfaceOffset = Mathf.Max(0f, waterSurfaceOffset); + waterLiftStrength = Mathf.Clamp01(waterLiftStrength); + waterPostConstraintIterations = Mathf.Clamp(waterPostConstraintIterations, 0, 8); + visibilityCheckEvery = Mathf.Clamp(visibilityCheckEvery, 1, 60); + visibilityViewportPadding = Mathf.Clamp(visibilityViewportPadding, 0f, 0.5f); + } + + private bool ShouldAlwaysSimulate() + { + if (!localOwnerAlwaysSimulate) + return false; + + var owner = _rod?.PlayerItem?.Owner; + return owner == null || owner.IsSelf; + } + + private Vector3 GetStartPosition() + { + return startAnchor ? startAnchor.position : transform.position; + } + + private Vector3 GetEndPosition() + { + return endAnchor ? endAnchor.position : transform.position; + } + + private Camera GetActiveCamera() + { + if (BaseCamera.Main) + { + _cachedCamera = BaseCamera.Main; + return _cachedCamera; + } + + if (!_cachedCamera) + _cachedCamera = Camera.main; + + return _cachedCamera; + } + + private static bool IsViewportPointVisible(Vector3 viewportPoint, float padding) + { + if (viewportPoint.z <= 0f) + return false; + + return viewportPoint.x >= -padding && viewportPoint.x <= 1f + padding && + viewportPoint.y >= -padding && viewportPoint.y <= 1f + padding; + } + + private bool IsVisibleToMainCamera() + { + Camera cam = GetActiveCamera(); + if (!cam) + return true; + + Vector3 start = GetStartPosition(); + Vector3 end = GetEndPosition(); + Vector3 middle = (start + end) * 0.5f; + float padding = visibilityViewportPadding; + + return IsViewportPointVisible(cam.WorldToViewportPoint(start), padding) || + IsViewportPointVisible(cam.WorldToViewportPoint(end), padding) || + IsViewportPointVisible(cam.WorldToViewportPoint(middle), padding); + } + + private void RefreshVisibilityState(bool force = false) + { + if (!cullRemoteRopeWhenInvisible || ShouldAlwaysSimulate()) + { + _isCulledByVisibility = false; + if (_lineRenderer) + _lineRenderer.enabled = true; + return; + } + + if (!force) + { + _visibilityCheckCounter++; + if (_visibilityCheckCounter < visibilityCheckEvery) + return; + } + + _visibilityCheckCounter = 0; + bool wasCulled = _isCulledByVisibility; + _isCulledByVisibility = !IsVisibleToMainCamera(); + + if (_lineRenderer) + _lineRenderer.enabled = !_isCulledByVisibility; + + if (wasCulled && !_isCulledByVisibility) + SyncVisibleStateAfterCulling(); + } + + private void SyncVisibleStateAfterCulling() + { + _currentLength = Mathf.Max(_targetLength, 0.01f); + UpdateNodesFromLength(); + UpdateHeadRestLenFromCurrentLength(); + ResetNodesBetweenAnchors(); + LockAnchorsHard(); + } + + private void ResetNodesBetweenAnchors() + { + if (_physicsNodes < 2) + return; + + Vector3 start = GetStartPosition(); + Vector3 end = GetEndPosition(); + int last = _physicsNodes - 1; + + for (int i = 0; i <= last; i++) + { + float t = (last > 0) ? i / (float)last : 0f; + Vector3 pos = Vector3.Lerp(start, end, t); + _pCurr[i] = pos; + _pPrev[i] = pos; + } + } + + private void EnsureRenderCaches() + { + int idle = Mathf.Max(1, renderSubdivisionsIdle); + if (_tIdleSubdiv != idle) + { + BuildTCaches(idle, ref _tIdle); + _tIdleSubdiv = idle; + } + + int moving = Mathf.Max(1, renderSubdivisionsMoving); + if (_tMovingSubdiv != moving) + { + BuildTCaches(moving, ref _tMoving); + _tMovingSubdiv = moving; + } + + int maxSubdiv = Mathf.Max(idle, moving); + int neededCapacity = (maxPhysicsNodes - 1) * maxSubdiv + 1; + if (_rPoints == null || neededCapacity > _rCapacity) + { + _rCapacity = neededCapacity; + _rPoints = new Vector3[_rCapacity]; + } + } + + private void InitLengthSystem() + { + float defaultLen = physicsSegmentLen * (Mathf.Max(minPhysicsNodes, 2) - 1); + _currentLength = (initialLength > 0f) ? initialLength : defaultLen; + _targetLength = _currentLength; + } + + private void AllocateAndInitNodes() + { + _physicsNodes = Mathf.Clamp(ComputeDesiredNodesStable(_currentLength), 2, maxPhysicsNodes); + + _pCurr = new Vector3[maxPhysicsNodes]; + _pPrev = new Vector3[maxPhysicsNodes]; + + Vector3 start = GetStartPosition(); + Vector3 dir = Vector3.down; + + for (int i = 0; i < _physicsNodes; i++) + { + Vector3 pos = start + dir * (physicsSegmentLen * i); + _pCurr[i] = pos; + _pPrev[i] = pos; + } + + UpdateHeadRestLenFromCurrentLength(); + + if (startAnchor && endAnchor) + LockAnchorsHard(); + } + + private int ComputeDesiredNodes(float lengthMeters) + { + int desired = Mathf.RoundToInt(Mathf.Max(0f, lengthMeters) / physicsSegmentLen) + 1; + desired = Mathf.Clamp(desired, minPhysicsNodes, maxPhysicsNodes); + return desired; + } + + private int ComputeDesiredNodesStable(float lengthMeters) + { + int desired = ComputeDesiredNodes(lengthMeters); + + if (_lastDesiredNodes == 0) + { + _lastDesiredNodes = desired; + return desired; + } + + if (desired == _lastDesiredNodes) + return desired; + + float boundary = (_lastDesiredNodes - 1) * physicsSegmentLen; + if (Mathf.Abs(lengthMeters - boundary) < nodeHysteresis) + return _lastDesiredNodes; + + _lastDesiredNodes = desired; + return desired; + } + + public void SetTargetLength(float lengthMeters) => _targetLength = Mathf.Max(0f, lengthMeters); + public float GetCurrentLength() => _currentLength; + public float GetTargetLength() => _targetLength; + public float GetLengthSmoothVel() => _lengthSmoothVel; + + public float GetLengthByPoints() + { + if (!smooth) + return GetPhysicsPolylineLength(); + + if (_rPoints == null || _lineRenderer == null) return 0f; + + int count = _lineRenderer.positionCount; + if (count < 2) return 0f; + + float totalLength = 0f; + for (int i = 1; i < count; i++) + { + Vector3 a = _rPoints[i - 1]; + Vector3 b = _rPoints[i]; + totalLength += Vector3.Distance(a, b); + } + + return totalLength; + } + + public float GetPhysicsPolylineLength() + { + float total = 0f; + for (int i = 1; i < _physicsNodes; i++) + total += Vector3.Distance(_pCurr[i - 1], _pCurr[i]); + return total; + } + + public void DebugLength() + { + float solverRestTotal = (_physicsNodes - 2) * physicsSegmentLen + _headRestLen; + float poly = GetPhysicsPolylineLength(); + float maxSegDelta = 0f; + float avgSegDelta = 0f; + for (int i = 1; i < _physicsNodes; i++) + { + float rest = (i == 1) ? _headRestLen : physicsSegmentLen; + float segLen = Vector3.Distance(_pCurr[i - 1], _pCurr[i]); + float delta = segLen - rest; + if (delta > maxSegDelta) maxSegDelta = delta; + avgSegDelta += delta; + } + + if (_physicsNodes > 1) + avgSegDelta /= (_physicsNodes - 1); + + Debug.Log( + $"current={_currentLength}, target={_targetLength}, nodes={_physicsNodes}, " + + $"seg={physicsSegmentLen}, head={_headRestLen}, headMin={headMinLen}, " + + $"solverRestTotal={solverRestTotal}, poly={poly}, delta={poly - solverRestTotal}, " + + $"maxSegDelta={maxSegDelta}, avgSegDelta={avgSegDelta}" + ); + } + + private void FixedUpdate() + { + if (!startAnchor || !endAnchor) return; + + RefreshVisibilityState(); + if (_isCulledByVisibility) + return; + + _dt = Time.fixedDeltaTime; + if (_dt < 1e-6f) _dt = 1e-6f; + _dt2 = _dt * _dt; + + _gravity.y = -gravityStrength; + + _kY = Mathf.Exp(-airDrag * _dt); + _kXZ = Mathf.Exp(-airDragXZ * _dt); + + UpdateLengthSmooth(); + UpdateNodesFromLength(); + UpdateHeadRestLenFromCurrentLength(); + + Simulate_VerletFast(); + + for (int it = 0; it < iterations; it++) + { + LockAnchorsHard(); + SolveDistanceConstraintsBidirectional(stiffness); + } + + SolveHardDistanceConstraints(hardTightenIterations); + LockAnchorsHard(); + + if (constrainToGround) + { + _groundFrameCounter++; + if (_groundFrameCounter >= groundUpdateEvery) + { + _groundFrameCounter = 0; + ConstrainToGround(); + SolveHardDistanceConstraints(groundPostConstraintIterations); + } + } + + if (constrainToWaterSurface) + { + _waterFrameCounter++; + if (_waterFrameCounter >= waterUpdateEvery) + { + _waterFrameCounter = 0; + ConstrainToWaterSurface(); + + // 水面抬升后补几次长度约束,让形状更顺一点 + SolveHardDistanceConstraints(waterPostConstraintIterations); + } + } + + LockAnchorsHard(); + } + + private void Update() + { + if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return; + + if (_isCulledByVisibility) + return; + + EnsureRenderCaches(); + + int last = _physicsNodes - 1; + + Vector3 s = GetStartPosition(); + Vector3 e = GetEndPosition(); + + _pCurr[0] = s; + _pCurr[last] = e; + + DrawHighResLine_Fast(); + } + + private void UpdateLengthSmooth() + { + float minFeasible = 0.01f; + float desired = Mathf.Max(_targetLength, minFeasible); + + _currentLength = Mathf.SmoothDamp( + _currentLength, + desired, + ref _lengthSmoothVel, + lengthSmoothTime, + Mathf.Infinity, + Time.fixedDeltaTime + ); + + // 长度变化时额外压一点速度,减少收放线时抖动 + float delta = Mathf.Abs(_targetLength - _currentLength); + if (delta > 0.0001f && lengthChangeVelocityKill > 0f) + { + float keep = 1f - Mathf.Clamp01(lengthChangeVelocityKill); + for (int i = 1; i < _physicsNodes - 1; i++) + { + Vector3 curr = _pCurr[i]; + Vector3 prev = _pPrev[i]; + Vector3 disp = curr - prev; + _pPrev[i] = curr - disp * keep; + } + } + } + + private void UpdateNodesFromLength() + { + int desired = ComputeDesiredNodesStable(_currentLength); + desired = Mathf.Clamp(desired, 2, maxPhysicsNodes); + if (desired == _physicsNodes) return; + + if (desired > _physicsNodes) AddNodesAtStart(desired - _physicsNodes); + else RemoveNodesAtStart(_physicsNodes - desired); + + _physicsNodes = desired; + } + + private void AddNodesAtStart(int addCount) + { + if (addCount <= 0) return; + + int oldCount = _physicsNodes; + int newCount = Mathf.Min(oldCount + addCount, maxPhysicsNodes); + addCount = newCount - oldCount; + if (addCount <= 0) return; + + Array.Copy(_pCurr, 1, _pCurr, 1 + addCount, oldCount - 1); + Array.Copy(_pPrev, 1, _pPrev, 1 + addCount, oldCount - 1); + + Vector3 s = GetStartPosition(); + + Vector3 dir = Vector3.down; + int firstOld = 1 + addCount; + if (oldCount >= 2 && firstOld < maxPhysicsNodes) + { + Vector3 toOld1 = (_pCurr[firstOld] - s); + float sq = toOld1.sqrMagnitude; + if (sq > 1e-6f) dir = toOld1 / Mathf.Sqrt(sq); + } + + Vector3 inheritDisp = Vector3.zero; + if (oldCount >= 2 && firstOld < maxPhysicsNodes) + inheritDisp = (_pCurr[firstOld] - _pPrev[firstOld]); + + for (int k = 1; k <= addCount; k++) + { + Vector3 pos = s + dir * (physicsSegmentLen * k); + _pCurr[k] = pos; + _pPrev[k] = pos - inheritDisp; + } + + LockAnchorsHard(); + } + + private void RemoveNodesAtStart(int removeCount) + { + if (removeCount <= 0) return; + + int oldCount = _physicsNodes; + int newCount = Mathf.Max(oldCount - removeCount, 2); + removeCount = oldCount - newCount; + if (removeCount <= 0) return; + + Array.Copy(_pCurr, 1 + removeCount, _pCurr, 1, newCount - 2); + Array.Copy(_pPrev, 1 + removeCount, _pPrev, 1, newCount - 2); + + LockAnchorsHard(); + } + + private void UpdateHeadRestLenFromCurrentLength() + { + int fixedSegCount = Mathf.Max(0, _physicsNodes - 2); + float baseLen = fixedSegCount * physicsSegmentLen; + + _headRestLen = _currentLength - baseLen; + _headRestLen = Mathf.Clamp(_headRestLen, headMinLen, physicsSegmentLen * 1.5f); + } + + private void Simulate_VerletFast() + { + for (int i = 1; i < _physicsNodes - 1; i++) + { + Vector3 disp = _pCurr[i] - _pPrev[i]; + + disp.x *= _kXZ; + disp.z *= _kXZ; + disp.y *= _kY; + + disp *= velocityDampen; + + Vector3 next = _pCurr[i] + disp + _gravity * _dt2; + + _pPrev[i] = _pCurr[i]; + _pCurr[i] = next; + } + } + + private void LockAnchorsHard() + { + if (!startAnchor || !endAnchor || _pCurr == null || _pPrev == null || _physicsNodes < 2) return; + + Vector3 s = GetStartPosition(); + Vector3 e = GetEndPosition(); + + _pCurr[0] = s; + _pPrev[0] = s - startAnchor.linearVelocity * _dt; + + int last = _physicsNodes - 1; + _pCurr[last] = e; + _pPrev[last] = e - endAnchor.linearVelocity * _dt; + } + + private void SolveHardDistanceConstraints(int extraIterations) + { + for (int it = 0; it < extraIterations; it++) + { + LockAnchorsHard(); + SolveDistanceConstraintsBidirectional(1f); + } + } + + private void SolveDistanceConstraintsBidirectional(float combinedStiffness) + { + int last = _physicsNodes - 1; + if (last <= 0) return; + + float clamped = Mathf.Clamp01(combinedStiffness); + float sweepStiffness = (clamped >= 0.999999f) ? 1f : 1f - Mathf.Sqrt(1f - clamped); + SolveDistanceConstraintsSweep_Fast(0, last, 1, last, sweepStiffness); + SolveDistanceConstraintsSweep_Fast(last - 1, -1, -1, last, sweepStiffness); + } + + private void SolveDistanceConstraintsSweep_Fast(int start, int endExclusive, int step, int last, + float sweepStiffness) + { + for (int i = start; i != endExclusive; i += step) + { + float rest = (i == 0) ? _headRestLen : physicsSegmentLen; + + Vector3 a = _pCurr[i]; + Vector3 b = _pCurr[i + 1]; + + Vector3 delta = b - a; + float sq = delta.sqrMagnitude; + if (sq < 1e-12f) continue; + + float dist = Mathf.Sqrt(sq); + float diff = (dist - rest) / dist; + Vector3 corr = delta * (diff * sweepStiffness); + + bool aLocked = (i == 0); + bool bLocked = (i + 1 == last); + + if (!aLocked && !bLocked) + { + _pCurr[i] = a + corr * 0.5f; + _pCurr[i + 1] = b - corr * 0.5f; + } + else if (aLocked && !bLocked) + { + _pCurr[i + 1] = b - corr; // 首段:node1 吃满 + } + else if (!aLocked) + { + _pCurr[i] = a + corr; // 尾段:last-1 吃满 + } + // 两边都锁的情况理论上不会出现 + } + } + + private void ConstrainToGround() + { + if (groundMask == 0) return; + + ApplySampledConstraint(SampleConstraintMode.GroundMinY, groundSampleStep, groundInterpolate); + } + + private float SampleGroundMinY(Vector3 p) + { + Vector3 origin = p + Vector3.up * groundCastHeight; + if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask, + QueryTriggerInteraction.Ignore)) + return hit.point.y + groundRadius; + + return float.NegativeInfinity; + } + + private void ApplyMinY(int i, float minY) + { + if (float.IsNegativeInfinity(minY)) return; + + Vector3 p = _pCurr[i]; + if (p.y < minY) + { + p.y = minY; + _pCurr[i] = p; + + // prev 同步抬上来,避免下一帧又被惯性拉回去造成抖动 + Vector3 prev = _pPrev[i]; + if (prev.y < minY) prev.y = minY; + _pPrev[i] = prev; + } + } + + private void ConstrainToWaterSurface() + { + int last = _physicsNodes - 1; + if (last <= 1) return; + + float surfaceY = waterLevelY + waterSurfaceOffset; + bool startUnderWater = _pCurr[0].y < surfaceY; + int startAdjacentIdx = GetStartAdjacentNodeIndex(last); + + ApplySampledConstraint( + SampleConstraintMode.WaterSurfaceY, + waterSampleStep, + waterInterpolate, + surfaceY, + startUnderWater, + startAdjacentIdx + ); + } + + private void ApplySampledConstraint(SampleConstraintMode mode, int sampleStep, bool interpolate, + float constantValue = 0f, bool startUnderWater = false, int startAdjacentIdx = -1) + { + int last = _physicsNodes - 1; + if (last <= 1) return; + + int step = Mathf.Max(1, sampleStep); + int prevSampleIdx = 1; + float prevValue = GetConstraintSampleValue(mode, prevSampleIdx, constantValue); + + ApplyConstraintValue(mode, prevSampleIdx, prevValue, startUnderWater, startAdjacentIdx); + + for (int i = 1 + step; i < last; i += step) + { + float nextValue = GetConstraintSampleValue(mode, i, constantValue); + ApplyConstraintValue(mode, i, nextValue, startUnderWater, startAdjacentIdx); + + if (interpolate) + { + int span = i - prevSampleIdx; + for (int j = 1; j < span; j++) + { + int idx = prevSampleIdx + j; + float t = j / (float)span; + float value = Mathf.Lerp(prevValue, nextValue, t); + ApplyConstraintValue(mode, idx, value, startUnderWater, startAdjacentIdx); + } + } + else + { + for (int idx = prevSampleIdx + 1; idx < i; idx++) + ApplyConstraintValue(mode, idx, prevValue, startUnderWater, startAdjacentIdx); + } + + prevSampleIdx = i; + prevValue = nextValue; + } + + for (int i = prevSampleIdx + 1; i < last; i++) + ApplyConstraintValue(mode, i, prevValue, startUnderWater, startAdjacentIdx); + } + + private float GetConstraintSampleValue(SampleConstraintMode mode, int index, float constantValue) + { + switch (mode) + { + case SampleConstraintMode.GroundMinY: + return SampleGroundMinY(_pCurr[index]); + case SampleConstraintMode.WaterSurfaceY: + return constantValue; + default: + return constantValue; + } + } + + private void ApplyConstraintValue(SampleConstraintMode mode, int index, float value, bool startUnderWater, + int startAdjacentIdx) + { + switch (mode) + { + case SampleConstraintMode.GroundMinY: + ApplyMinY(index, value); + break; + case SampleConstraintMode.WaterSurfaceY: + ApplyWaterSurface(index, value, startUnderWater, startAdjacentIdx); + break; + } + } + + private int GetStartAdjacentNodeIndex(int last) + { + if (last <= 1) return 1; + + Vector3 s = _pCurr[0]; + float d1 = (_pCurr[1] - s).sqrMagnitude; + float d2 = (_pCurr[last - 1] - s).sqrMagnitude; + return d1 <= d2 ? 1 : last - 1; + } + + private void ApplyWaterSurface(int i, float surfaceY, bool startUnderWater, int startAdjacentIdx) + { + if (keepStartAdjacentNodeFollow && startUnderWater && i == startAdjacentIdx) + { + Vector3 s = _pCurr[0]; + _pCurr[i] = s; + _pPrev[i] = s; + return; + } + + Vector3 p = _pCurr[i]; + if (p.y < surfaceY) + { + p.y = Mathf.Lerp(p.y, surfaceY, waterLiftStrength); + _pCurr[i] = p; + + // 渐进同步 prev,削弱向下惯性,避免反复穿透水面 + Vector3 prev = _pPrev[i]; + if (prev.y < p.y) prev.y = Mathf.Lerp(prev.y, p.y, waterLiftStrength); + _pPrev[i] = prev; + } + } + + private void DrawHighResLine_Fast() + { + if (_pCurr == null || _physicsNodes < 2) return; + + float w = lineWidth * LineMultiple; + _lineRenderer.startWidth = w; + _lineRenderer.endWidth = w; + + if (!smooth) + { + _lineRenderer.positionCount = _physicsNodes; + _lineRenderer.SetPositions(_pCurr); + return; + } + + int subdiv = PickRenderSubdivisions_Fast(); + TCaches tc = (subdiv == renderSubdivisionsMoving) ? _tMoving : _tIdle; + + int idx = 0; + int last = _physicsNodes - 1; + + for (int seg = 0; seg < last; seg++) + { + int i0 = seg - 1; + if (i0 < 0) i0 = 0; + int i1 = seg; + int i2 = seg + 1; + int i3 = seg + 2; + if (i3 > last) i3 = last; + + Vector3 p0 = _pCurr[i0]; + Vector3 p1 = _pCurr[i1]; + Vector3 p2 = _pCurr[i2]; + Vector3 p3 = _pCurr[i3]; + + for (int s = 0; s < subdiv; s++) + { + float t = tc.t[s]; + float t2 = tc.t2[s]; + float t3 = tc.t3[s]; + + Vector3 cr = + 0.5f * ( + (2f * p1) + + (-p0 + p2) * t + + (2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 + + (-p0 + 3f * p1 - 3f * p2 + p3) * t3 + ); + + // y 也使用平滑曲线,再做单调夹紧;避免垂直时因为线性 y 插值导致切线断裂,看起来像折线。 + cr.y = ClampMonotonic(cr.y, p0.y, p1.y, p2.y, p3.y); + + _rPoints[idx++] = cr; + } + } + + _rPoints[idx++] = _pCurr[last]; + + _lineRenderer.positionCount = idx; + _lineRenderer.SetPositions(_rPoints); + } + + private static float ClampMonotonic(float value, float p0, float p1, float p2, float p3) + { + bool rising = p0 <= p1 && p1 <= p2 && p2 <= p3; + bool falling = p0 >= p1 && p1 >= p2 && p2 >= p3; + if (!rising && !falling) + return value; + + float min = Mathf.Min(p1, p2); + float max = Mathf.Max(p1, p2); + return Mathf.Clamp(value, min, max); + } + + private int PickRenderSubdivisions_Fast() + { + int idle = Mathf.Max(1, renderSubdivisionsIdle); + int moving = Mathf.Max(1, renderSubdivisionsMoving); + + float thr = movingSpeedThreshold; + float thrSq = (thr * _dt) * (thr * _dt); + + float sumSq = 0f; + int count = Mathf.Max(1, _physicsNodes - 2); + + for (int i = 1; i < _physicsNodes - 1; i++) + { + Vector3 disp = _pCurr[i] - _pPrev[i]; + sumSq += disp.sqrMagnitude; + } + + float avgSq = sumSq / count; + + return (avgSq > thrSq) ? moving : idle; + } + + private static void BuildTCaches(int subdiv, ref TCaches caches) + { + subdiv = Mathf.Max(1, subdiv); + caches.t = new float[subdiv]; + caches.t2 = new float[subdiv]; + caches.t3 = new float[subdiv]; + + float inv = 1f / subdiv; + for (int s = 0; s < subdiv; s++) + { + float t = s * inv; + float t2 = t * t; + caches.t[s] = t; + caches.t2[s] = t2; + caches.t3[s] = t2 * t; + } + } + + private void OnDrawGizmosSelected() + { + if (_pCurr == null) return; + Gizmos.color = Color.yellow; + for (int i = 0; i < _physicsNodes; i++) + Gizmos.DrawSphere(_pCurr[i], 0.01f); + } +} diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/Rope.cs.meta b/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/Rope.cs.meta new file mode 100644 index 000000000..93d4824f7 --- /dev/null +++ b/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/Rope.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 98ba9d435a0e49c9bb527c34cc91894d +timeCreated: 1766759607 \ No newline at end of file diff --git a/Fishing2.sln.DotSettings.user b/Fishing2.sln.DotSettings.user index e82578fe2..e47416b78 100644 --- a/Fishing2.sln.DotSettings.user +++ b/Fishing2.sln.DotSettings.user @@ -35,6 +35,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded