修改鱼线节点逻辑
This commit is contained in:
@@ -18583,6 +18583,21 @@ MonoBehaviour:
|
||||
- {fileID: 102900000, guid: aa3f5467c0c153642ac320466aee0ec1, type: 3}
|
||||
FilterEnum: 0
|
||||
Filter: '*'
|
||||
- Path: Assets/ResRaw/Prefabs/Line/FishingLine1.prefab
|
||||
Address: Plyaer/FishingLine1
|
||||
Type: GameObject
|
||||
Bundle: main/plyaer.bundle
|
||||
Tags:
|
||||
Group:
|
||||
Name: Plyaer
|
||||
Enable: 1
|
||||
BundleMode: 0
|
||||
AddressMode: 2
|
||||
Tags:
|
||||
Collectors:
|
||||
- {fileID: 102900000, guid: aa3f5467c0c153642ac320466aee0ec1, type: 3}
|
||||
FilterEnum: 0
|
||||
Filter: '*'
|
||||
- Path: Assets/ResRaw/Prefabs/Line/FishingRopeLong.asset
|
||||
Address: Plyaer/FishingRopeLong
|
||||
Type: Missing
|
||||
|
||||
575
Assets/ResRaw/Prefabs/Line/FishingLine1.prefab
Normal file
575
Assets/ResRaw/Prefabs/Line/FishingLine1.prefab
Normal file
@@ -0,0 +1,575 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &2696931885206049402
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 6213026895670800501}
|
||||
- component: {fileID: 7362979975150531515}
|
||||
- component: {fileID: 5572865435543895569}
|
||||
m_Layer: 0
|
||||
m_Name: Start
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &6213026895670800501
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2696931885206049402}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 98221710317492190}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!54 &7362979975150531515
|
||||
Rigidbody:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2696931885206049402}
|
||||
serializedVersion: 5
|
||||
m_Mass: 1
|
||||
m_LinearDamping: 0
|
||||
m_AngularDamping: 0.05
|
||||
m_CenterOfMass: {x: 0, y: 0, z: 0}
|
||||
m_InertiaTensor: {x: 1, y: 1, z: 1}
|
||||
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ImplicitCom: 1
|
||||
m_ImplicitTensor: 1
|
||||
m_UseGravity: 1
|
||||
m_IsKinematic: 0
|
||||
m_Interpolate: 0
|
||||
m_Constraints: 0
|
||||
m_CollisionDetection: 0
|
||||
--- !u!114 &5572865435543895569
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2696931885206049402}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 610df5569209e4b4997cb2dbf3b94cdc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineNode
|
||||
nodeType: 0
|
||||
body: {fileID: 7362979975150531515}
|
||||
interaction: {fileID: 0}
|
||||
segmentLengthToNext: 0.5
|
||||
fixedVirtualNodesToNext: 2
|
||||
runtimeVirtualNode: 0
|
||||
runtimeChainIndex: -1
|
||||
drawDebugGizmo: 1
|
||||
debugGizmoRadius: 0.03
|
||||
logicalNodeColor: {r: 0.2, g: 0.9, b: 0.2, a: 1}
|
||||
virtualNodeColor: {r: 1, g: 0.6, b: 0.1, a: 1}
|
||||
features: []
|
||||
motionFeatures: []
|
||||
activeMotionFeature: {fileID: 0}
|
||||
--- !u!1 &5077741257619886775
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7385546574667729905}
|
||||
- component: {fileID: 5707703654405666688}
|
||||
- component: {fileID: 3979683508768218053}
|
||||
- component: {fileID: 250386986656750139}
|
||||
m_Layer: 0
|
||||
m_Name: End
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &7385546574667729905
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5077741257619886775}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 98221710317492190}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!54 &5707703654405666688
|
||||
Rigidbody:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5077741257619886775}
|
||||
serializedVersion: 5
|
||||
m_Mass: 0.01
|
||||
m_LinearDamping: 1
|
||||
m_AngularDamping: 0.2
|
||||
m_CenterOfMass: {x: 0, y: 0, z: 0}
|
||||
m_InertiaTensor: {x: 1, y: 1, z: 1}
|
||||
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ImplicitCom: 1
|
||||
m_ImplicitTensor: 1
|
||||
m_UseGravity: 1
|
||||
m_IsKinematic: 0
|
||||
m_Interpolate: 0
|
||||
m_Constraints: 0
|
||||
m_CollisionDetection: 0
|
||||
--- !u!114 &3979683508768218053
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5077741257619886775}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 610df5569209e4b4997cb2dbf3b94cdc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineNode
|
||||
nodeType: 3
|
||||
body: {fileID: 5707703654405666688}
|
||||
interaction: {fileID: 0}
|
||||
segmentLengthToNext: 0.5
|
||||
fixedVirtualNodesToNext: 2
|
||||
runtimeVirtualNode: 0
|
||||
runtimeChainIndex: -1
|
||||
drawDebugGizmo: 1
|
||||
debugGizmoRadius: 0.03
|
||||
logicalNodeColor: {r: 0.2, g: 0.9, b: 0.2, a: 1}
|
||||
virtualNodeColor: {r: 1, g: 0.6, b: 0.1, a: 1}
|
||||
features: []
|
||||
motionFeatures: []
|
||||
activeMotionFeature: {fileID: 0}
|
||||
--- !u!65 &250386986656750139
|
||||
BoxCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5077741257619886775}
|
||||
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_Size: {x: 0.02, y: 0.02, z: 0.02}
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &5252216124238432432
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3116177874895914436}
|
||||
- component: {fileID: 1033618195002336566}
|
||||
- component: {fileID: 9117070148710828175}
|
||||
- component: {fileID: 4152162740525283091}
|
||||
- component: {fileID: 2513762410452133691}
|
||||
m_Layer: 0
|
||||
m_Name: Bobber
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &3116177874895914436
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5252216124238432432}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 98221710317492190}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!54 &1033618195002336566
|
||||
Rigidbody:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5252216124238432432}
|
||||
serializedVersion: 5
|
||||
m_Mass: 1
|
||||
m_LinearDamping: 0
|
||||
m_AngularDamping: 0.05
|
||||
m_CenterOfMass: {x: 0, y: 0, z: 0}
|
||||
m_InertiaTensor: {x: 1, y: 1, z: 1}
|
||||
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ImplicitCom: 0
|
||||
m_ImplicitTensor: 0
|
||||
m_UseGravity: 1
|
||||
m_IsKinematic: 0
|
||||
m_Interpolate: 0
|
||||
m_Constraints: 0
|
||||
m_CollisionDetection: 0
|
||||
--- !u!136 &9117070148710828175
|
||||
CapsuleCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5252216124238432432}
|
||||
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: 2
|
||||
m_Radius: 0.003
|
||||
m_Height: 0.02
|
||||
m_Direction: 1
|
||||
m_Center: {x: 0, y: 0.03, z: 0}
|
||||
--- !u!114 &4152162740525283091
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5252216124238432432}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 610df5569209e4b4997cb2dbf3b94cdc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineNode
|
||||
nodeType: 1
|
||||
body: {fileID: 1033618195002336566}
|
||||
interaction: {fileID: 0}
|
||||
segmentLengthToNext: 0.5
|
||||
fixedVirtualNodesToNext: 2
|
||||
runtimeVirtualNode: 0
|
||||
runtimeChainIndex: -1
|
||||
drawDebugGizmo: 1
|
||||
debugGizmoRadius: 0.03
|
||||
logicalNodeColor: {r: 0.2, g: 0.9, b: 0.2, a: 1}
|
||||
virtualNodeColor: {r: 1, g: 0.6, b: 0.1, a: 1}
|
||||
features: []
|
||||
motionFeatures: []
|
||||
activeMotionFeature: {fileID: 0}
|
||||
--- !u!114 &2513762410452133691
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5252216124238432432}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: ca4d5d54d89446b0a10b7ce521fd7d9e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingBobberFeature
|
||||
priorityOffset: 0
|
||||
waterLevel: 0
|
||||
enterWaterDepth: 0.002
|
||||
exitWaterDepth: -0.01
|
||||
floatHeight: 0.08
|
||||
bottomOffsetLocalY: 0
|
||||
ySmoothTime: 0.08
|
||||
maxYSpeed: 2
|
||||
yDeadZone: 0.0005
|
||||
neutralBottomWeight: 1
|
||||
currentBottomWeight: 1
|
||||
baseDraftDepth: 0.02
|
||||
draftDepthPerWeight: 0.01
|
||||
minDraftDepth: 0.005
|
||||
maxDraftDepth: 0.08
|
||||
draftSmoothTime: 0.18
|
||||
biteSmoothTime: 0.03
|
||||
tapAmplitude: 0.008
|
||||
tapDuration: 0.18
|
||||
slowSinkAmplitude: 0.025
|
||||
slowSinkDuration: 1.2
|
||||
liftAmplitude: 0.015
|
||||
liftDuration: 1.2
|
||||
blackDriftAmplitude: 0.06
|
||||
blackDriftDuration: 0.8
|
||||
enableDebugInput: 1
|
||||
stopBiteKey: 114
|
||||
tapKey: 116
|
||||
slowSinkKey: 103
|
||||
liftKey: 104
|
||||
blackDriftKey: 98
|
||||
lyingWeightThreshold: 0.4
|
||||
tiltedWeightThreshold: 0.8
|
||||
uprightWeightThreshold: 1.2
|
||||
lyingAngle: 88
|
||||
tiltedAngle: 42
|
||||
uprightAngle: 0
|
||||
tiltAxis: 0
|
||||
invertTiltDirection: 0
|
||||
rotationLerpSpeed: 8
|
||||
waterAngularDamping: 999
|
||||
--- !u!1 &5438655829551842420
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 98221710317492190}
|
||||
- component: {fileID: 913314649585263376}
|
||||
- component: {fileID: 6821754774284524478}
|
||||
- component: {fileID: 1999186176030474616}
|
||||
m_Layer: 0
|
||||
m_Name: FishingLine1
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &98221710317492190
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5438655829551842420}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 6213026895670800501}
|
||||
- {fileID: 3116177874895914436}
|
||||
- {fileID: 7385546574667729905}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &913314649585263376
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5438655829551842420}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: dcd0fd8d96f994444b2d8663af6b915d, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineSolver
|
||||
anchorTransform: {fileID: 0}
|
||||
logicalNodes:
|
||||
- {fileID: 5572865435543895569}
|
||||
- {fileID: 4152162740525283091}
|
||||
- {fileID: 3979683508768218053}
|
||||
lineRenderer: {fileID: 1999186176030474616}
|
||||
firstSegmentLength: 1.2
|
||||
firstSegmentStep: 0.1
|
||||
jointSolverIterations: 12
|
||||
jointProjectionDistance: 0.02
|
||||
jointProjectionAngle: 1
|
||||
lengthLimitTolerance: 0.01
|
||||
breakStretchThreshold: 0.05
|
||||
breakLimitDuration: 0.15
|
||||
autoBuildOnStart: 1
|
||||
--- !u!120 &6821754774284524478
|
||||
LineRenderer:
|
||||
serializedVersion: 3
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5438655829551842420}
|
||||
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: 1, z: 0}
|
||||
m_Parameters:
|
||||
serializedVersion: 3
|
||||
widthMultiplier: 1
|
||||
widthCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0.01
|
||||
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!114 &1999186176030474616
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5438655829551842420}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 827786ffede4e7b4781c522e8a4ba9d0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineRenderer
|
||||
solver: {fileID: 913314649585263376}
|
||||
lineRenderer: {fileID: 6821754774284524478}
|
||||
solverIterations: 8
|
||||
damping: 0.98
|
||||
gravityScale: 1
|
||||
simulationStep: 0.0166667
|
||||
maxDeltaTime: 0.0333333
|
||||
constrainToWaterSurface: 1
|
||||
waterSurfaceTransform: {fileID: 0}
|
||||
waterSurfaceHeight: 0
|
||||
ignoreHeadNodeCount: 1
|
||||
ignoreTailNodeCount: 1
|
||||
waterSurfaceFollowSpeed: 12
|
||||
maxSubStepsPerFrame: 2
|
||||
sleepVelocityThreshold: 0.001
|
||||
sleepDistanceThreshold: 0.002
|
||||
stableFramesBeforeSleep: 4
|
||||
wakeDistanceThreshold: 0.001
|
||||
tautSegmentThreshold: 0.002
|
||||
tautTransitionRange: 0.03
|
||||
smoothCorners: 1
|
||||
minCornerAngle: 12
|
||||
maxCornerSmoothDistance: 0.03
|
||||
cornerSmoothSubdivisions: 3
|
||||
drawDebugSamples: 0
|
||||
debugLogicalSampleColor: {r: 0, g: 1, b: 1, a: 1}
|
||||
debugVirtualSampleColor: {r: 1, g: 0.55, b: 0.15, a: 1}
|
||||
debugLogicalSampleRadius: 0.018
|
||||
debugVirtualSampleRadius: 0.012
|
||||
7
Assets/ResRaw/Prefabs/Line/FishingLine1.prefab.meta
Normal file
7
Assets/ResRaw/Prefabs/Line/FishingLine1.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 488209094f0c45a41aa6801dd86e6768
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -675,7 +675,7 @@ MonoBehaviour:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1858052053854210}
|
||||
m_Enabled: 0
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 2dedfafdc2d747d98c682cde3e28e513, type: 3}
|
||||
m_Name:
|
||||
|
||||
8
Assets/Scripts/Fishing/New/View/FishingLine.meta
Normal file
8
Assets/Scripts/Fishing/New/View/FishingLine.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6901448ac9466974791a863c357f6579
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/Scripts/Fishing/New/View/FishingLine/Feature.meta
Normal file
3
Assets/Scripts/Fishing/New/View/FishingLine/Feature.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ff164cb3132445289d22c7b3a0f4fab
|
||||
timeCreated: 1775957622
|
||||
@@ -0,0 +1,664 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public enum FishingBobberControlMode
|
||||
{
|
||||
AirPhysics,
|
||||
WaterPresentation,
|
||||
}
|
||||
|
||||
public enum FishingBobberBiteType
|
||||
{
|
||||
None,
|
||||
Tap,
|
||||
SlowSink,
|
||||
Lift,
|
||||
BlackDrift,
|
||||
}
|
||||
|
||||
public enum BobberTiltAxis
|
||||
{
|
||||
LocalX,
|
||||
LocalZ,
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
public class FishingBobberFeature : FishingLineNodeMotionFeature
|
||||
{
|
||||
protected override int DefaultPriority => 100;
|
||||
|
||||
#region 检测入水
|
||||
|
||||
[Header("入水检测")]
|
||||
[Tooltip("当前测试阶段固定水面高度。")]
|
||||
public float waterLevel = 0f;
|
||||
|
||||
[Tooltip("浮漂底部进入水面达到该深度后,切换为水面表现控制。")]
|
||||
public float enterWaterDepth = 0.002f;
|
||||
|
||||
[Tooltip("浮漂底部高于该深度时退出水面表现控制。通常设为负值用于滞回。")]
|
||||
public float exitWaterDepth = -0.01f;
|
||||
|
||||
[Tooltip("浮漂总高度,单位米。")]
|
||||
public float floatHeight = 0.08f;
|
||||
|
||||
[Tooltip("如果 Pivot 在浮漂底部填 0;如果 Pivot 在模型中部,填底部相对 Pivot 的本地 Y 偏移。")]
|
||||
public float bottomOffsetLocalY;
|
||||
|
||||
[Tooltip("Y 轴控制的平滑时间。")]
|
||||
public float ySmoothTime = 0.08f;
|
||||
|
||||
[Tooltip("Y 轴平滑时允许的最大竖直速度。")]
|
||||
public float maxYSpeed = 2f;
|
||||
|
||||
[Tooltip("Y 轴死区,小范围内直接贴目标值以减少微抖。")]
|
||||
public float yDeadZone = 0.0005f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 吃水控制
|
||||
|
||||
[Header("吃水控制")]
|
||||
[Tooltip("基准底部重量。当前重量等于该值时,使用基础吃水深度。")]
|
||||
public float neutralBottomWeight = 1f;
|
||||
|
||||
[Tooltip("当前底部总重量。运行时可由其他脚本更新。")]
|
||||
public float currentBottomWeight = 1f;
|
||||
|
||||
[Tooltip("基准重量下的基础吃水深度。")]
|
||||
public float baseDraftDepth = 0.02f;
|
||||
|
||||
[Tooltip("每单位重量变化对应增加或减少的吃水深度。")]
|
||||
public float draftDepthPerWeight = 0.01f;
|
||||
|
||||
[Tooltip("吃水深度下限。")]
|
||||
public float minDraftDepth = 0.005f;
|
||||
|
||||
[Tooltip("吃水深度上限。")]
|
||||
public float maxDraftDepth = 0.08f;
|
||||
|
||||
[Tooltip("吃水深度变化的平滑时间。")]
|
||||
public float draftSmoothTime = 0.18f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 漂相动画
|
||||
|
||||
[Header("漂相动画")]
|
||||
[Tooltip("漂相位移动画的平滑时间。")]
|
||||
public float biteSmoothTime = 0.03f;
|
||||
|
||||
[Tooltip("点漂默认振幅。")]
|
||||
public float tapAmplitude = 0.008f;
|
||||
|
||||
[Tooltip("点漂默认时长。")]
|
||||
public float tapDuration = 0.18f;
|
||||
|
||||
[Tooltip("缓沉默认振幅。")]
|
||||
public float slowSinkAmplitude = 0.025f;
|
||||
|
||||
[Tooltip("缓沉默认时长。")]
|
||||
public float slowSinkDuration = 1.2f;
|
||||
|
||||
[Tooltip("顶漂默认振幅。")]
|
||||
public float liftAmplitude = 0.015f;
|
||||
|
||||
[Tooltip("顶漂默认时长。")]
|
||||
public float liftDuration = 1.2f;
|
||||
|
||||
[Tooltip("黑漂默认振幅。")]
|
||||
public float blackDriftAmplitude = 0.06f;
|
||||
|
||||
[Tooltip("黑漂默认时长。")]
|
||||
public float blackDriftDuration = 0.8f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 输入测试
|
||||
|
||||
[Header("输入测试")]
|
||||
[Tooltip("是否启用运行时按键测试漂相。")]
|
||||
public bool enableDebugInput = true;
|
||||
|
||||
[Tooltip("停止当前漂相的按键。")]
|
||||
public KeyCode stopBiteKey = KeyCode.R;
|
||||
|
||||
[Tooltip("触发点漂的按键。")]
|
||||
public KeyCode tapKey = KeyCode.T;
|
||||
|
||||
[Tooltip("触发缓沉的按键。")]
|
||||
public KeyCode slowSinkKey = KeyCode.G;
|
||||
|
||||
[Tooltip("触发顶漂的按键。")]
|
||||
public KeyCode liftKey = KeyCode.H;
|
||||
|
||||
[Tooltip("触发黑漂的按键。")]
|
||||
public KeyCode blackDriftKey = KeyCode.B;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 姿态控制
|
||||
|
||||
[Header("姿态控制")]
|
||||
[Tooltip("重量低于该值时,姿态趋向躺漂。")]
|
||||
public float lyingWeightThreshold = 0.4f;
|
||||
|
||||
[Tooltip("重量达到该值附近时,姿态趋向半躺。")]
|
||||
public float tiltedWeightThreshold = 0.8f;
|
||||
|
||||
[Tooltip("重量达到该值及以上时,姿态趋向立漂。")]
|
||||
public float uprightWeightThreshold = 1.2f;
|
||||
|
||||
[Tooltip("躺漂对应的倾角。")]
|
||||
public float lyingAngle = 88f;
|
||||
|
||||
[Tooltip("半躺对应的倾角。")]
|
||||
public float tiltedAngle = 42f;
|
||||
|
||||
[Tooltip("立漂对应的倾角,通常为 0。")]
|
||||
public float uprightAngle = 0f;
|
||||
|
||||
[Tooltip("绕哪个本地轴做倾倒。")]
|
||||
public BobberTiltAxis tiltAxis = BobberTiltAxis.LocalX;
|
||||
|
||||
[Tooltip("是否反转倾倒方向。")]
|
||||
public bool invertTiltDirection;
|
||||
|
||||
[Tooltip("姿态旋转的平滑速度。")]
|
||||
public float rotationLerpSpeed = 8f;
|
||||
|
||||
[Tooltip("入水后用于压制旋转抖动的角阻尼。")]
|
||||
public float waterAngularDamping = 999f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 运行时状态
|
||||
|
||||
public FishingBobberControlMode CurrentMode => _mode;
|
||||
public FishingBobberBiteType CurrentBiteType => _activeBiteType;
|
||||
public float CurrentDraftDepth => _currentDraftDepth;
|
||||
public float CurrentBottomWeight => currentBottomWeight;
|
||||
|
||||
private Rigidbody _rb;
|
||||
private FishingBobberControlMode _mode = FishingBobberControlMode.AirPhysics;
|
||||
|
||||
private bool _defaultsCached;
|
||||
private bool _waterStateInitialized;
|
||||
private float _defaultAngularDamping;
|
||||
private bool _defaultUseGravity;
|
||||
|
||||
private float _draftVelocity;
|
||||
private float _currentDraftDepth;
|
||||
private float _ySmoothVelocity;
|
||||
private float _biteOffsetY;
|
||||
private float _biteOffsetYVelocity;
|
||||
|
||||
private Quaternion _uprightReferenceRotation;
|
||||
private Quaternion _targetRotation;
|
||||
|
||||
private FishingBobberBiteType _activeBiteType = FishingBobberBiteType.None;
|
||||
private float _biteTimer;
|
||||
private float _biteDuration;
|
||||
private float _biteAmplitude;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity 生命周期
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
InitializeRuntimeState();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
HandleDebugInput();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 运动控制接入
|
||||
|
||||
public override bool IsSupportedNode(FishingLineNode node)
|
||||
{
|
||||
return node != null && node.Type == FishingLineNode.NodeType.Float;
|
||||
}
|
||||
|
||||
protected override void OnBind()
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
InitializeRuntimeState();
|
||||
}
|
||||
|
||||
public override bool CanControl()
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
if (_rb == null || !IsSupportedNode(Node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var submergeDepth = GetSubmergeDepth();
|
||||
if (_mode == FishingBobberControlMode.WaterPresentation)
|
||||
{
|
||||
return submergeDepth >= exitWaterDepth;
|
||||
}
|
||||
|
||||
return submergeDepth > enterWaterDepth;
|
||||
}
|
||||
|
||||
public override void OnMotionActivated()
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
EnterWaterPresentationMode();
|
||||
}
|
||||
|
||||
public override void OnMotionDeactivated()
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
ExitWaterPresentationMode();
|
||||
}
|
||||
|
||||
public override void TickMotion(float deltaTime)
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
if (_rb == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var submergeDepth = GetSubmergeDepth();
|
||||
if (submergeDepth < exitWaterDepth)
|
||||
{
|
||||
ExitWaterPresentationMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_mode != FishingBobberControlMode.WaterPresentation)
|
||||
{
|
||||
EnterWaterPresentationMode();
|
||||
}
|
||||
|
||||
UpdateBiteAnimation(deltaTime);
|
||||
UpdateDraft(deltaTime);
|
||||
UpdateVerticalPosition(deltaTime);
|
||||
UpdateRotation(deltaTime);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 漂相接口
|
||||
|
||||
public void SetBottomWeight(float weight)
|
||||
{
|
||||
currentBottomWeight = weight;
|
||||
}
|
||||
|
||||
public void PlayTap(float amplitude = -1f, float duration = -1f)
|
||||
{
|
||||
StartBite(
|
||||
FishingBobberBiteType.Tap,
|
||||
amplitude > 0f ? amplitude : tapAmplitude,
|
||||
duration > 0f ? duration : tapDuration);
|
||||
}
|
||||
|
||||
public void PlaySlowSink(float amplitude = -1f, float duration = -1f)
|
||||
{
|
||||
StartBite(
|
||||
FishingBobberBiteType.SlowSink,
|
||||
amplitude > 0f ? amplitude : slowSinkAmplitude,
|
||||
duration > 0f ? duration : slowSinkDuration);
|
||||
}
|
||||
|
||||
public void PlayLift(float amplitude = -1f, float duration = -1f)
|
||||
{
|
||||
StartBite(
|
||||
FishingBobberBiteType.Lift,
|
||||
amplitude > 0f ? amplitude : liftAmplitude,
|
||||
duration > 0f ? duration : liftDuration);
|
||||
}
|
||||
|
||||
public void PlayBlackDrift(float amplitude = -1f, float duration = -1f)
|
||||
{
|
||||
StartBite(
|
||||
FishingBobberBiteType.BlackDrift,
|
||||
amplitude > 0f ? amplitude : blackDriftAmplitude,
|
||||
duration > 0f ? duration : blackDriftDuration);
|
||||
}
|
||||
|
||||
public void StopBite()
|
||||
{
|
||||
_activeBiteType = FishingBobberBiteType.None;
|
||||
_biteTimer = 0f;
|
||||
_biteDuration = 0f;
|
||||
_biteAmplitude = 0f;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部实现
|
||||
|
||||
private void EnsureRuntimeReferences()
|
||||
{
|
||||
if (_rb == null)
|
||||
{
|
||||
_rb = Node != null && Node.Body != null ? Node.Body : GetComponent<Rigidbody>();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeRuntimeState()
|
||||
{
|
||||
if (_rb == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_defaultsCached)
|
||||
{
|
||||
_defaultAngularDamping = _rb.angularDamping;
|
||||
_defaultUseGravity = _rb.useGravity;
|
||||
_defaultsCached = true;
|
||||
}
|
||||
|
||||
_currentDraftDepth = CalculateRawDraftDepth();
|
||||
_draftVelocity = 0f;
|
||||
_ySmoothVelocity = 0f;
|
||||
_biteOffsetY = 0f;
|
||||
_biteOffsetYVelocity = 0f;
|
||||
_targetRotation = transform.rotation;
|
||||
|
||||
if (!_waterStateInitialized)
|
||||
{
|
||||
_uprightReferenceRotation = transform.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnterWaterPresentationMode()
|
||||
{
|
||||
if (_rb == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_mode = FishingBobberControlMode.WaterPresentation;
|
||||
_waterStateInitialized = true;
|
||||
_uprightReferenceRotation = transform.rotation;
|
||||
_targetRotation = transform.rotation;
|
||||
_draftVelocity = 0f;
|
||||
_ySmoothVelocity = 0f;
|
||||
_biteOffsetYVelocity = 0f;
|
||||
_currentDraftDepth = CalculateRawDraftDepth();
|
||||
|
||||
_rb.useGravity = false;
|
||||
_rb.angularDamping = waterAngularDamping;
|
||||
}
|
||||
|
||||
private void ExitWaterPresentationMode()
|
||||
{
|
||||
_mode = FishingBobberControlMode.AirPhysics;
|
||||
RestorePhysicsState();
|
||||
}
|
||||
|
||||
private void RestorePhysicsState()
|
||||
{
|
||||
if (_rb == null || !_defaultsCached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_rb.useGravity = _defaultUseGravity;
|
||||
_rb.angularDamping = _defaultAngularDamping;
|
||||
}
|
||||
|
||||
private float GetSubmergeDepth()
|
||||
{
|
||||
return waterLevel - GetBottomWorldPosition().y;
|
||||
}
|
||||
|
||||
private Vector3 GetBottomWorldPosition()
|
||||
{
|
||||
return transform.TransformPoint(new Vector3(0f, bottomOffsetLocalY, 0f));
|
||||
}
|
||||
|
||||
private float CalculateRawDraftDepth()
|
||||
{
|
||||
var weightDelta = currentBottomWeight - neutralBottomWeight;
|
||||
var targetDraft = baseDraftDepth + weightDelta * draftDepthPerWeight;
|
||||
return Mathf.Clamp(targetDraft, minDraftDepth, maxDraftDepth);
|
||||
}
|
||||
|
||||
private void UpdateDraft(float deltaTime)
|
||||
{
|
||||
var targetDraft = CalculateRawDraftDepth();
|
||||
_currentDraftDepth = Mathf.SmoothDamp(
|
||||
_currentDraftDepth,
|
||||
targetDraft,
|
||||
ref _draftVelocity,
|
||||
Mathf.Max(0.0001f, draftSmoothTime),
|
||||
Mathf.Infinity,
|
||||
deltaTime);
|
||||
}
|
||||
|
||||
private void UpdateVerticalPosition(float deltaTime)
|
||||
{
|
||||
var position = transform.position;
|
||||
var targetY = waterLevel - _currentDraftDepth - bottomOffsetLocalY + _biteOffsetY;
|
||||
|
||||
if (Mathf.Abs(position.y - targetY) < yDeadZone)
|
||||
{
|
||||
position.y = targetY;
|
||||
_ySmoothVelocity = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
position.y = Mathf.SmoothDamp(
|
||||
position.y,
|
||||
targetY,
|
||||
ref _ySmoothVelocity,
|
||||
Mathf.Max(0.0001f, ySmoothTime),
|
||||
maxYSpeed,
|
||||
deltaTime);
|
||||
}
|
||||
|
||||
transform.position = position;
|
||||
|
||||
var velocity = _rb.linearVelocity;
|
||||
if (Mathf.Abs(velocity.y) > 0f)
|
||||
{
|
||||
velocity.y = 0f;
|
||||
_rb.linearVelocity = velocity;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRotation(float deltaTime)
|
||||
{
|
||||
var targetTiltAngle = EvaluateTargetTiltAngle();
|
||||
var signedAngle = invertTiltDirection ? -targetTiltAngle : targetTiltAngle;
|
||||
var localAxis = tiltAxis == BobberTiltAxis.LocalX ? Vector3.right : Vector3.forward;
|
||||
|
||||
_targetRotation = _uprightReferenceRotation * Quaternion.AngleAxis(signedAngle, localAxis);
|
||||
_rb.angularVelocity = Vector3.zero;
|
||||
transform.rotation = Quaternion.Slerp(
|
||||
transform.rotation,
|
||||
_targetRotation,
|
||||
1f - Mathf.Exp(-Mathf.Max(0.01f, rotationLerpSpeed) * deltaTime));
|
||||
}
|
||||
|
||||
private float EvaluateTargetTiltAngle()
|
||||
{
|
||||
if (currentBottomWeight <= lyingWeightThreshold)
|
||||
{
|
||||
return lyingAngle;
|
||||
}
|
||||
|
||||
if (currentBottomWeight <= tiltedWeightThreshold)
|
||||
{
|
||||
var t = Mathf.InverseLerp(lyingWeightThreshold, tiltedWeightThreshold, currentBottomWeight);
|
||||
return Mathf.Lerp(lyingAngle, tiltedAngle, t);
|
||||
}
|
||||
|
||||
if (currentBottomWeight <= uprightWeightThreshold)
|
||||
{
|
||||
var t = Mathf.InverseLerp(tiltedWeightThreshold, uprightWeightThreshold, currentBottomWeight);
|
||||
return Mathf.Lerp(tiltedAngle, uprightAngle, t);
|
||||
}
|
||||
|
||||
return uprightAngle;
|
||||
}
|
||||
|
||||
private void StartBite(FishingBobberBiteType type, float amplitude, float duration)
|
||||
{
|
||||
if (_mode != FishingBobberControlMode.WaterPresentation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_activeBiteType = type;
|
||||
_biteTimer = 0f;
|
||||
_biteDuration = Mathf.Max(0.01f, duration);
|
||||
_biteAmplitude = Mathf.Max(0f, amplitude);
|
||||
_biteOffsetYVelocity = 0f;
|
||||
}
|
||||
|
||||
private void UpdateBiteAnimation(float deltaTime)
|
||||
{
|
||||
if (_activeBiteType == FishingBobberBiteType.None)
|
||||
{
|
||||
_biteOffsetY = Mathf.SmoothDamp(
|
||||
_biteOffsetY,
|
||||
0f,
|
||||
ref _biteOffsetYVelocity,
|
||||
Mathf.Max(0.0001f, biteSmoothTime),
|
||||
Mathf.Infinity,
|
||||
deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
_biteTimer += deltaTime;
|
||||
var t = Mathf.Clamp01(_biteTimer / _biteDuration);
|
||||
var targetOffset = 0f;
|
||||
|
||||
switch (_activeBiteType)
|
||||
{
|
||||
case FishingBobberBiteType.Tap:
|
||||
if (t < 0.35f)
|
||||
{
|
||||
var downT = t / 0.35f;
|
||||
targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, downT);
|
||||
}
|
||||
else
|
||||
{
|
||||
var upT = (t - 0.35f) / 0.65f;
|
||||
targetOffset = -Mathf.Lerp(_biteAmplitude, 0f, upT);
|
||||
}
|
||||
break;
|
||||
|
||||
case FishingBobberBiteType.SlowSink:
|
||||
targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, t);
|
||||
break;
|
||||
|
||||
case FishingBobberBiteType.Lift:
|
||||
targetOffset = Mathf.SmoothStep(0f, _biteAmplitude, t);
|
||||
break;
|
||||
|
||||
case FishingBobberBiteType.BlackDrift:
|
||||
targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, t);
|
||||
break;
|
||||
}
|
||||
|
||||
_biteOffsetY = Mathf.SmoothDamp(
|
||||
_biteOffsetY,
|
||||
targetOffset,
|
||||
ref _biteOffsetYVelocity,
|
||||
Mathf.Max(0.0001f, biteSmoothTime),
|
||||
Mathf.Infinity,
|
||||
deltaTime);
|
||||
|
||||
if (_biteTimer >= _biteDuration &&
|
||||
_activeBiteType != FishingBobberBiteType.SlowSink &&
|
||||
_activeBiteType != FishingBobberBiteType.BlackDrift)
|
||||
{
|
||||
_activeBiteType = FishingBobberBiteType.None;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDebugInput()
|
||||
{
|
||||
if (!Application.isPlaying || !enableDebugInput)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(stopBiteKey))
|
||||
{
|
||||
StopBite();
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(tapKey))
|
||||
{
|
||||
PlayTap();
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(slowSinkKey))
|
||||
{
|
||||
PlaySlowSink();
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(liftKey))
|
||||
{
|
||||
PlayLift();
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(blackDriftKey))
|
||||
{
|
||||
PlayBlackDrift();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 参数校验
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
floatHeight = Mathf.Max(0.001f, floatHeight);
|
||||
ySmoothTime = Mathf.Max(0.001f, ySmoothTime);
|
||||
maxYSpeed = Mathf.Max(0.01f, maxYSpeed);
|
||||
yDeadZone = Mathf.Max(0f, yDeadZone);
|
||||
|
||||
neutralBottomWeight = Mathf.Max(0f, neutralBottomWeight);
|
||||
currentBottomWeight = Mathf.Max(0f, currentBottomWeight);
|
||||
minDraftDepth = Mathf.Max(0f, minDraftDepth);
|
||||
maxDraftDepth = Mathf.Max(minDraftDepth, maxDraftDepth);
|
||||
baseDraftDepth = Mathf.Clamp(baseDraftDepth, minDraftDepth, maxDraftDepth);
|
||||
draftDepthPerWeight = Mathf.Max(0f, draftDepthPerWeight);
|
||||
draftSmoothTime = Mathf.Max(0.001f, draftSmoothTime);
|
||||
|
||||
biteSmoothTime = Mathf.Max(0.001f, biteSmoothTime);
|
||||
tapAmplitude = Mathf.Max(0f, tapAmplitude);
|
||||
tapDuration = Mathf.Max(0.01f, tapDuration);
|
||||
slowSinkAmplitude = Mathf.Max(0f, slowSinkAmplitude);
|
||||
slowSinkDuration = Mathf.Max(0.01f, slowSinkDuration);
|
||||
liftAmplitude = Mathf.Max(0f, liftAmplitude);
|
||||
liftDuration = Mathf.Max(0.01f, liftDuration);
|
||||
blackDriftAmplitude = Mathf.Max(0f, blackDriftAmplitude);
|
||||
blackDriftDuration = Mathf.Max(0.01f, blackDriftDuration);
|
||||
|
||||
lyingWeightThreshold = Mathf.Max(0f, lyingWeightThreshold);
|
||||
tiltedWeightThreshold = Mathf.Max(lyingWeightThreshold, tiltedWeightThreshold);
|
||||
uprightWeightThreshold = Mathf.Max(tiltedWeightThreshold, uprightWeightThreshold);
|
||||
uprightAngle = Mathf.Clamp(uprightAngle, 0f, 89f);
|
||||
tiltedAngle = Mathf.Clamp(tiltedAngle, uprightAngle, 89f);
|
||||
lyingAngle = Mathf.Clamp(lyingAngle, tiltedAngle, 89.9f);
|
||||
rotationLerpSpeed = Mathf.Max(0.01f, rotationLerpSpeed);
|
||||
waterAngularDamping = Mathf.Max(0f, waterAngularDamping);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca4d5d54d89446b0a10b7ce521fd7d9e
|
||||
timeCreated: 1775958532
|
||||
@@ -0,0 +1,46 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认物理组件
|
||||
/// </summary>
|
||||
public class FishingDefaultPhysicsFeature : FishingLineNodeMotionFeature
|
||||
{
|
||||
[Header("Physics")] [SerializeField] private bool useGravity = true;
|
||||
|
||||
protected override int DefaultPriority => 0;
|
||||
|
||||
public override bool IsSupportedNode(FishingLineNode node)
|
||||
{
|
||||
return node != null
|
||||
&& !node.IsRuntimeVirtualNode
|
||||
&& node.Type != FishingLineNode.NodeType.Start;
|
||||
}
|
||||
|
||||
public override bool CanControl()
|
||||
{
|
||||
return Node != null && Node.Body != null && IsSupportedNode(Node);
|
||||
}
|
||||
|
||||
public override void OnMotionActivated()
|
||||
{
|
||||
ApplyPhysicsState();
|
||||
}
|
||||
|
||||
public override void TickMotion(float deltaTime)
|
||||
{
|
||||
ApplyPhysicsState();
|
||||
}
|
||||
|
||||
private void ApplyPhysicsState()
|
||||
{
|
||||
if (Node == null || Node.Body == null || !IsSupportedNode(Node))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Node.Body.useGravity = useGravity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ebfa4366b504ba0a3f398eded17df31
|
||||
timeCreated: 1775957743
|
||||
@@ -0,0 +1,944 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public interface IWaterSurfaceProvider
|
||||
{
|
||||
float GetWaterHeight(Vector3 worldPos);
|
||||
Vector3 GetWaterNormal(Vector3 worldPos);
|
||||
}
|
||||
|
||||
public enum BobberControlMode
|
||||
{
|
||||
AirPhysics,
|
||||
WaterPresentation,
|
||||
}
|
||||
|
||||
public enum BobberBiteType
|
||||
{
|
||||
None,
|
||||
Tap,
|
||||
SlowSink,
|
||||
Lift,
|
||||
BlackDrift,
|
||||
}
|
||||
|
||||
public enum BobberPosture
|
||||
{
|
||||
Lying,
|
||||
Tilted,
|
||||
Upright,
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
public class FishingFloatFeature : FishingLineNodeMotionFeature
|
||||
{
|
||||
protected override int DefaultPriority => 100;
|
||||
|
||||
[Header("Water")]
|
||||
[Tooltip("没有水提供器时使用固定水位")]
|
||||
public float fallbackWaterLevel;
|
||||
|
||||
[Tooltip("可选:挂实现了 IWaterSurfaceProvider 的组件")]
|
||||
public MonoBehaviour waterProviderBehaviour;
|
||||
|
||||
[Header("Enter Water")]
|
||||
[Tooltip("底部进入水面多少米后切换为漂像控制")]
|
||||
public float enterWaterDepth = 0.002f;
|
||||
|
||||
[Tooltip("离开水面多少米后回到空中物理。一般给负值做滞回")]
|
||||
public float exitWaterDepth = -0.01f;
|
||||
|
||||
[Header("Geometry")]
|
||||
[Tooltip("浮漂总高度(米)")]
|
||||
public float floatHeight = 0.08f;
|
||||
|
||||
[Tooltip("如果 Pivot 在浮漂底部,这里填 0;如果 Pivot 在模型中心,就填底部相对 Pivot 的本地 Y")]
|
||||
public float bottomOffsetLocalY;
|
||||
|
||||
[Header("Base Float")]
|
||||
[Tooltip("基础吃铅比例,决定静止时有多少在水下")]
|
||||
[Range(0.05f, 0.95f)]
|
||||
public float baseSubmergeRatio = 0.28f;
|
||||
|
||||
[Tooltip("Y 轴平滑时间,越小响应越快")]
|
||||
public float ySmoothTime = 0.08f;
|
||||
|
||||
[Tooltip("最大竖直速度限制(用于 SmoothDamp)")]
|
||||
public float maxYSpeed = 2f;
|
||||
|
||||
[Tooltip("静止小死区,减少微抖")]
|
||||
public float yDeadZone = 0.0005f;
|
||||
|
||||
[Header("Surface Motion")]
|
||||
[Tooltip("是否启用轻微水面起伏")]
|
||||
public bool enableSurfaceBobbing = true;
|
||||
|
||||
[Tooltip("水面轻微起伏振幅(米)")]
|
||||
public float surfaceBobAmplitude = 0.0015f;
|
||||
|
||||
[Tooltip("水面轻微起伏频率")]
|
||||
public float surfaceBobFrequency = 1.2f;
|
||||
|
||||
[Header("XZ Motion")]
|
||||
[Tooltip("入水后是否锁定 XZ 到入水点附近")]
|
||||
public bool lockXZAroundAnchor = true;
|
||||
|
||||
[Tooltip("XZ 跟随平滑时间")]
|
||||
public float xzSmoothTime = 0.15f;
|
||||
|
||||
[Tooltip("水流/拖拽带来的额外平面偏移最大值")]
|
||||
public float maxPlanarOffset = 0.15f;
|
||||
|
||||
[Header("Sink By Weight / Tension")]
|
||||
[Tooltip("外部向下拉力映射为下沉量的系数。你可以把钩/铅/线组的等效向下拉力喂进来")]
|
||||
public float downForceToSink = 0.0025f;
|
||||
|
||||
[Tooltip("向下拉力下沉的最大附加量")]
|
||||
public float maxExtraSink = 0.08f;
|
||||
|
||||
[Header("Bottom Touch")]
|
||||
[Tooltip("触底时是否启用修正")]
|
||||
public bool enableBottomTouchAdjust = true;
|
||||
|
||||
[Tooltip("触底后减少的下沉量(例如铅坠到底,漂会回升一点)")]
|
||||
public float bottomTouchLift = 0.01f;
|
||||
|
||||
[Header("Posture Source")]
|
||||
[Tooltip("下方 Lure / 钩组 / 铅坠的刚体。姿态主要根据它和浮漂的相对位置判断")]
|
||||
public Rigidbody lureBody;
|
||||
|
||||
[Tooltip("用于归一化的参考长度。一般填:浮漂到 Lure 在“正常拉直”时的大致长度")]
|
||||
public float referenceLength = 0.30f;
|
||||
|
||||
[Header("Posture Threshold")]
|
||||
[Tooltip("最小入水比例。不够时优先躺漂")]
|
||||
public float minSubmergeToStand = 0.16f;
|
||||
|
||||
[Tooltip("垂直分量比低于该值时,优先躺漂")]
|
||||
public float verticalLieThreshold = 0.18f;
|
||||
|
||||
[Tooltip("垂直分量比高于该值,且水平分量较小时,允许立漂")]
|
||||
public float verticalUprightThreshold = 0.75f;
|
||||
|
||||
[Tooltip("水平分量比高于该值时,不允许完全立漂")]
|
||||
public float planarTiltThreshold = 0.30f;
|
||||
|
||||
[Tooltip("水平分量明显大于垂直分量时,优先躺漂")]
|
||||
public float planarDominanceMultiplier = 1.20f;
|
||||
|
||||
[Tooltip("姿态切换滞回")]
|
||||
public float postureHysteresis = 0.04f;
|
||||
|
||||
[Header("Posture Stability")]
|
||||
[Tooltip("候选姿态需持续多久才真正切换")]
|
||||
public float postureConfirmTime = 0.08f;
|
||||
|
||||
[Tooltip("姿态切换后的最短冷却时间,避免来回闪烁")]
|
||||
public float postureSwitchCooldown = 0.10f;
|
||||
|
||||
[Header("Posture Rotation")]
|
||||
[Tooltip("倾斜状态角度")]
|
||||
public float tiltedAngle = 38f;
|
||||
|
||||
[Tooltip("躺漂角度")]
|
||||
public float lyingAngle = 88f;
|
||||
|
||||
[Tooltip("立漂时允许的最大附加倾角")]
|
||||
public float uprightMaxTiltAngle = 8f;
|
||||
|
||||
[Tooltip("平面方向对立漂/斜漂附加倾角的影响强度")]
|
||||
public float planarTiltFactor = 120f;
|
||||
|
||||
[Tooltip("平面方向死区,小于该值时保持上一帧方向")]
|
||||
public float planarDirectionDeadZone = 0.01f;
|
||||
|
||||
[Tooltip("平面方向平滑速度")]
|
||||
public float planarDirectionLerpSpeed = 10f;
|
||||
|
||||
[Tooltip("姿态平滑速度")]
|
||||
public float rotationLerpSpeed = 8f;
|
||||
|
||||
[Header("Debug Input")]
|
||||
public bool debugResetKey = true;
|
||||
public bool debugTapKey = true;
|
||||
public bool debugSlowSinkKey = true;
|
||||
public bool debugLiftKey = true;
|
||||
public bool debugBlackDriftKey = true;
|
||||
|
||||
[Header("Debug")]
|
||||
public bool drawDebug;
|
||||
|
||||
public bool UseTestPosture;
|
||||
public BobberPosture TestPosture;
|
||||
|
||||
public BobberControlMode CurrentMode => _mode;
|
||||
public BobberPosture CurrentPosture => _posture;
|
||||
public float CurrentVerticalRatio => _verticalRatio;
|
||||
public float CurrentPlanarRatio => _planarRatio;
|
||||
|
||||
public float ExternalDownForce { get; set; }
|
||||
public bool IsBottomTouched { get; set; }
|
||||
public Vector2 ExternalPlanarOffset { get; set; }
|
||||
|
||||
private Rigidbody _rb;
|
||||
private IWaterSurfaceProvider _waterProvider;
|
||||
private BobberControlMode _mode = BobberControlMode.AirPhysics;
|
||||
private BobberPosture _posture = BobberPosture.Lying;
|
||||
|
||||
private float _defaultLinearDamping;
|
||||
private float _defaultAngularDamping;
|
||||
private bool _defaultUseGravity;
|
||||
private bool _defaultsCached;
|
||||
|
||||
private Vector3 _waterAnchorPos;
|
||||
private Vector3 _xzSmoothVelocity;
|
||||
private float _ySmoothVelocity;
|
||||
private float _biteOffsetY;
|
||||
private float _biteOffsetYVelocity;
|
||||
private Quaternion _targetRotation;
|
||||
|
||||
private BobberBiteType _activeBiteType = BobberBiteType.None;
|
||||
private float _biteTimer;
|
||||
private float _biteDuration;
|
||||
private float _biteAmplitude;
|
||||
private Vector3 _blackDriftDirection;
|
||||
|
||||
private float _verticalRatio;
|
||||
private float _planarRatio;
|
||||
private float _verticalDistance;
|
||||
private float _planarDistance;
|
||||
private BobberPosture _pendingPosture;
|
||||
private float _pendingPostureTimer;
|
||||
private float _postureCooldownTimer;
|
||||
private Vector3 _stablePlanarDir = Vector3.forward;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
InitializeRuntimeState();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
HandleDebugKeys();
|
||||
}
|
||||
|
||||
public override bool IsSupportedNode(FishingLineNode node)
|
||||
{
|
||||
return node != null && node.Type == FishingLineNode.NodeType.Float;
|
||||
}
|
||||
|
||||
protected override void OnBind()
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
InitializeRuntimeState();
|
||||
}
|
||||
|
||||
public override bool CanControl()
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
if (_rb == null || !IsSupportedNode(Node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var waterY = GetWaterHeight(transform.position);
|
||||
var submergeDepth = waterY - GetBottomWorldPosition().y;
|
||||
if (_mode == BobberControlMode.WaterPresentation)
|
||||
{
|
||||
return submergeDepth >= exitWaterDepth;
|
||||
}
|
||||
|
||||
return submergeDepth > enterWaterDepth;
|
||||
}
|
||||
|
||||
public override void OnMotionActivated()
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
EnterWaterPresentationMode();
|
||||
}
|
||||
|
||||
public override void OnMotionDeactivated()
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
ExitWaterPresentationMode();
|
||||
}
|
||||
|
||||
public override void TickMotion(float deltaTime)
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
if (_rb == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var waterY = GetWaterHeight(transform.position);
|
||||
var submergeDepth = waterY - GetBottomWorldPosition().y;
|
||||
UpdateWaterPresentation(waterY, submergeDepth, deltaTime);
|
||||
|
||||
if (drawDebug)
|
||||
{
|
||||
DrawDebug(waterY);
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayTap(float amplitude = 0.008f, float duration = 0.18f)
|
||||
{
|
||||
StartBite(BobberBiteType.Tap, amplitude, duration);
|
||||
}
|
||||
|
||||
public void PlaySlowSink(float amplitude = 0.025f, float duration = 1.2f)
|
||||
{
|
||||
StartBite(BobberBiteType.SlowSink, amplitude, duration);
|
||||
}
|
||||
|
||||
public void PlayLift(float amplitude = 0.015f, float duration = 1.2f)
|
||||
{
|
||||
StartBite(BobberBiteType.Lift, amplitude, duration);
|
||||
}
|
||||
|
||||
public void PlayBlackDrift(float amplitude = 0.06f, float duration = 0.8f, Vector3? driftDirection = null)
|
||||
{
|
||||
StartBite(BobberBiteType.BlackDrift, amplitude, duration);
|
||||
_blackDriftDirection = (driftDirection ?? transform.forward).normalized;
|
||||
}
|
||||
|
||||
public void StopBite()
|
||||
{
|
||||
_activeBiteType = BobberBiteType.None;
|
||||
_biteTimer = 0f;
|
||||
_biteDuration = 0f;
|
||||
_biteAmplitude = 0f;
|
||||
_biteOffsetY = 0f;
|
||||
_biteOffsetYVelocity = 0f;
|
||||
}
|
||||
|
||||
private void EnsureRuntimeReferences()
|
||||
{
|
||||
if (_rb == null)
|
||||
{
|
||||
_rb = Node != null && Node.Body != null ? Node.Body : GetComponent<Rigidbody>();
|
||||
}
|
||||
|
||||
if (_waterProvider == null && waterProviderBehaviour != null)
|
||||
{
|
||||
_waterProvider = waterProviderBehaviour as IWaterSurfaceProvider;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void InitializeRuntimeState()
|
||||
{
|
||||
if (_rb == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_defaultsCached)
|
||||
{
|
||||
_defaultLinearDamping = _rb.linearDamping;
|
||||
_defaultAngularDamping = _rb.angularDamping;
|
||||
_defaultUseGravity = _rb.useGravity;
|
||||
_defaultsCached = true;
|
||||
}
|
||||
|
||||
_pendingPosture = _posture;
|
||||
_pendingPostureTimer = 0f;
|
||||
_postureCooldownTimer = 0f;
|
||||
_stablePlanarDir = Vector3.ProjectOnPlane(transform.forward, Vector3.up);
|
||||
if (_stablePlanarDir.sqrMagnitude < 1e-6f)
|
||||
{
|
||||
_stablePlanarDir = Vector3.forward;
|
||||
}
|
||||
else
|
||||
{
|
||||
_stablePlanarDir.Normalize();
|
||||
}
|
||||
|
||||
_targetRotation = transform.rotation;
|
||||
}
|
||||
|
||||
private void UpdateWaterPresentation(float waterY, float submergeDepth, float deltaTime)
|
||||
{
|
||||
if (submergeDepth < exitWaterDepth)
|
||||
{
|
||||
ExitWaterPresentationMode();
|
||||
return;
|
||||
}
|
||||
|
||||
_rb.useGravity = false;
|
||||
_rb.linearVelocity = Vector3.zero;
|
||||
_rb.angularVelocity = Vector3.zero;
|
||||
_rb.linearDamping = 999f;
|
||||
_rb.angularDamping = 999f;
|
||||
|
||||
UpdateBiteAnimation(deltaTime);
|
||||
|
||||
var pos = transform.position;
|
||||
var targetY = CalculateTargetY(waterY);
|
||||
if (Mathf.Abs(pos.y - targetY) < yDeadZone)
|
||||
{
|
||||
pos.y = targetY;
|
||||
_ySmoothVelocity = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
pos.y = Mathf.SmoothDamp(
|
||||
pos.y,
|
||||
targetY,
|
||||
ref _ySmoothVelocity,
|
||||
Mathf.Max(0.0001f, ySmoothTime),
|
||||
maxYSpeed,
|
||||
deltaTime);
|
||||
}
|
||||
|
||||
var targetXZ = CalculateTargetXZ();
|
||||
var planarPos = new Vector3(pos.x, 0f, pos.z);
|
||||
var planarTarget = new Vector3(targetXZ.x, 0f, targetXZ.z);
|
||||
planarPos = Vector3.SmoothDamp(
|
||||
planarPos,
|
||||
planarTarget,
|
||||
ref _xzSmoothVelocity,
|
||||
Mathf.Max(0.0001f, xzSmoothTime),
|
||||
Mathf.Infinity,
|
||||
deltaTime);
|
||||
|
||||
pos.x = planarPos.x;
|
||||
pos.z = planarPos.z;
|
||||
transform.position = pos;
|
||||
|
||||
EvaluatePostureByComponents(waterY, deltaTime);
|
||||
UpdateTargetRotationByPosture(deltaTime);
|
||||
|
||||
transform.rotation = Quaternion.Slerp(
|
||||
transform.rotation,
|
||||
_targetRotation,
|
||||
1f - Mathf.Exp(-rotationLerpSpeed * deltaTime));
|
||||
}
|
||||
|
||||
private void EnterWaterPresentationMode()
|
||||
{
|
||||
if (_rb == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_mode = BobberControlMode.WaterPresentation;
|
||||
_waterAnchorPos = transform.position;
|
||||
_ySmoothVelocity = 0f;
|
||||
_xzSmoothVelocity = Vector3.zero;
|
||||
_biteOffsetY = 0f;
|
||||
_biteOffsetYVelocity = 0f;
|
||||
_activeBiteType = BobberBiteType.None;
|
||||
_biteTimer = 0f;
|
||||
|
||||
_posture = BobberPosture.Lying;
|
||||
_verticalRatio = 0f;
|
||||
_planarRatio = 0f;
|
||||
_verticalDistance = 0f;
|
||||
_planarDistance = 0f;
|
||||
_pendingPosture = _posture;
|
||||
_pendingPostureTimer = 0f;
|
||||
_postureCooldownTimer = 0f;
|
||||
_stablePlanarDir = Vector3.ProjectOnPlane(transform.forward, Vector3.up);
|
||||
if (_stablePlanarDir.sqrMagnitude < 1e-6f)
|
||||
{
|
||||
_stablePlanarDir = Vector3.forward;
|
||||
}
|
||||
else
|
||||
{
|
||||
_stablePlanarDir.Normalize();
|
||||
}
|
||||
|
||||
_rb.useGravity = false;
|
||||
_rb.linearVelocity = Vector3.zero;
|
||||
_rb.angularVelocity = Vector3.zero;
|
||||
_rb.linearDamping = 999f;
|
||||
_rb.angularDamping = 999f;
|
||||
}
|
||||
|
||||
private void ExitWaterPresentationMode()
|
||||
{
|
||||
_mode = BobberControlMode.AirPhysics;
|
||||
RestoreAirPhysicsState();
|
||||
}
|
||||
|
||||
private void RestoreAirPhysicsState()
|
||||
{
|
||||
if (_rb == null || !_defaultsCached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_rb.useGravity = _defaultUseGravity;
|
||||
_rb.linearDamping = _defaultLinearDamping;
|
||||
_rb.angularDamping = _defaultAngularDamping;
|
||||
}
|
||||
|
||||
private float CalculateTargetY(float waterY)
|
||||
{
|
||||
var baseSinkDepth = floatHeight * Mathf.Clamp01(baseSubmergeRatio);
|
||||
var sinkByForce = Mathf.Clamp(ExternalDownForce * downForceToSink, 0f, maxExtraSink);
|
||||
|
||||
var bottomAdjust = 0f;
|
||||
if (enableBottomTouchAdjust && IsBottomTouched)
|
||||
{
|
||||
bottomAdjust -= bottomTouchLift;
|
||||
}
|
||||
|
||||
var surfaceBob = 0f;
|
||||
if (enableSurfaceBobbing)
|
||||
{
|
||||
surfaceBob = Mathf.Sin(Time.time * surfaceBobFrequency * Mathf.PI * 2f) * surfaceBobAmplitude;
|
||||
}
|
||||
|
||||
var totalSink = baseSinkDepth + sinkByForce + bottomAdjust;
|
||||
var targetBottomY = waterY - totalSink;
|
||||
return targetBottomY - bottomOffsetLocalY + surfaceBob + _biteOffsetY;
|
||||
}
|
||||
|
||||
private Vector3 CalculateTargetXZ()
|
||||
{
|
||||
var planarOffset = Vector2.ClampMagnitude(ExternalPlanarOffset, maxPlanarOffset);
|
||||
var basePos = lockXZAroundAnchor ? _waterAnchorPos : transform.position;
|
||||
|
||||
if (_activeBiteType == BobberBiteType.BlackDrift)
|
||||
{
|
||||
var t = Mathf.Clamp01(_biteDuration > 0f ? _biteTimer / _biteDuration : 1f);
|
||||
var drift = Mathf.SmoothStep(0f, 1f, t) * 0.08f;
|
||||
var blackDrift = _blackDriftDirection * drift;
|
||||
basePos += new Vector3(blackDrift.x, 0f, blackDrift.z);
|
||||
}
|
||||
|
||||
return new Vector3(
|
||||
basePos.x + planarOffset.x,
|
||||
transform.position.y,
|
||||
basePos.z + planarOffset.y);
|
||||
}
|
||||
|
||||
private void EvaluatePostureByComponents(float waterY, float deltaTime)
|
||||
{
|
||||
var submergeRatio = Mathf.Clamp01(
|
||||
(waterY - GetBottomWorldPosition().y) / Mathf.Max(0.0001f, floatHeight));
|
||||
|
||||
var hasLure = lureBody != null;
|
||||
if (!hasLure)
|
||||
{
|
||||
_verticalDistance = 0f;
|
||||
_planarDistance = 0f;
|
||||
_verticalRatio = 0f;
|
||||
_planarRatio = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
var bobberPos = _rb.worldCenterOfMass;
|
||||
var lurePos = lureBody.worldCenterOfMass;
|
||||
var delta = lurePos - bobberPos;
|
||||
|
||||
_verticalDistance = Mathf.Max(0f, Vector3.Dot(delta, Vector3.down));
|
||||
_planarDistance = Vector3.ProjectOnPlane(delta, Vector3.up).magnitude;
|
||||
|
||||
var refLen = Mathf.Max(0.0001f, referenceLength);
|
||||
_verticalRatio = _verticalDistance / refLen;
|
||||
_planarRatio = _planarDistance / refLen;
|
||||
}
|
||||
|
||||
var desiredPosture = DeterminePostureState(submergeRatio, hasLure);
|
||||
ApplyPostureWithStability(desiredPosture, deltaTime);
|
||||
}
|
||||
|
||||
private BobberPosture DeterminePostureState(float submergeRatio, bool hasLure)
|
||||
{
|
||||
if (UseTestPosture)
|
||||
{
|
||||
return TestPosture;
|
||||
}
|
||||
|
||||
if (!hasLure)
|
||||
{
|
||||
if (submergeRatio < minSubmergeToStand)
|
||||
{
|
||||
return BobberPosture.Lying;
|
||||
}
|
||||
|
||||
if (ExternalPlanarOffset.magnitude > 0.01f)
|
||||
{
|
||||
return BobberPosture.Tilted;
|
||||
}
|
||||
|
||||
return BobberPosture.Upright;
|
||||
}
|
||||
|
||||
switch (_posture)
|
||||
{
|
||||
case BobberPosture.Lying:
|
||||
{
|
||||
var canStandUpright =
|
||||
submergeRatio >= minSubmergeToStand &&
|
||||
_verticalRatio > verticalUprightThreshold + postureHysteresis &&
|
||||
_planarRatio < planarTiltThreshold - postureHysteresis;
|
||||
|
||||
var canTilt =
|
||||
submergeRatio >= minSubmergeToStand * 0.8f &&
|
||||
_verticalRatio > verticalLieThreshold + postureHysteresis;
|
||||
|
||||
if (canStandUpright)
|
||||
{
|
||||
return BobberPosture.Upright;
|
||||
}
|
||||
|
||||
if (canTilt)
|
||||
{
|
||||
return BobberPosture.Tilted;
|
||||
}
|
||||
|
||||
return BobberPosture.Lying;
|
||||
}
|
||||
|
||||
case BobberPosture.Tilted:
|
||||
{
|
||||
var shouldLie =
|
||||
submergeRatio < minSubmergeToStand * 0.75f ||
|
||||
_verticalRatio < verticalLieThreshold - postureHysteresis ||
|
||||
_planarDistance > _verticalDistance * planarDominanceMultiplier;
|
||||
|
||||
var shouldStand =
|
||||
submergeRatio >= minSubmergeToStand &&
|
||||
_verticalRatio > verticalUprightThreshold + postureHysteresis &&
|
||||
_planarRatio < planarTiltThreshold - postureHysteresis;
|
||||
|
||||
if (shouldLie)
|
||||
{
|
||||
return BobberPosture.Lying;
|
||||
}
|
||||
|
||||
if (shouldStand)
|
||||
{
|
||||
return BobberPosture.Upright;
|
||||
}
|
||||
|
||||
return BobberPosture.Tilted;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
var shouldLie =
|
||||
submergeRatio < minSubmergeToStand * 0.75f ||
|
||||
_verticalRatio < verticalLieThreshold - postureHysteresis ||
|
||||
_planarDistance > _verticalDistance * (planarDominanceMultiplier + 0.15f);
|
||||
|
||||
var shouldTilt =
|
||||
_verticalRatio < verticalUprightThreshold - postureHysteresis ||
|
||||
_planarRatio > planarTiltThreshold + postureHysteresis;
|
||||
|
||||
if (shouldLie)
|
||||
{
|
||||
return BobberPosture.Lying;
|
||||
}
|
||||
|
||||
if (shouldTilt)
|
||||
{
|
||||
return BobberPosture.Tilted;
|
||||
}
|
||||
|
||||
return BobberPosture.Upright;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyPostureWithStability(BobberPosture desiredPosture, float deltaTime)
|
||||
{
|
||||
_postureCooldownTimer = Mathf.Max(0f, _postureCooldownTimer - deltaTime);
|
||||
if (desiredPosture == _posture)
|
||||
{
|
||||
_pendingPosture = _posture;
|
||||
_pendingPostureTimer = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_postureCooldownTimer > 0f)
|
||||
{
|
||||
_pendingPosture = desiredPosture;
|
||||
_pendingPostureTimer = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pendingPosture != desiredPosture)
|
||||
{
|
||||
_pendingPosture = desiredPosture;
|
||||
_pendingPostureTimer = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingPostureTimer += deltaTime;
|
||||
if (_pendingPostureTimer >= Mathf.Max(0f, postureConfirmTime))
|
||||
{
|
||||
_posture = desiredPosture;
|
||||
_pendingPosture = _posture;
|
||||
_pendingPostureTimer = 0f;
|
||||
_postureCooldownTimer = Mathf.Max(0f, postureSwitchCooldown);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTargetRotationByPosture(float deltaTime)
|
||||
{
|
||||
var candidateDir = Vector3.zero;
|
||||
if (lureBody != null)
|
||||
{
|
||||
var delta = lureBody.worldCenterOfMass - _rb.worldCenterOfMass;
|
||||
candidateDir = Vector3.ProjectOnPlane(delta, Vector3.up);
|
||||
}
|
||||
|
||||
if (candidateDir.sqrMagnitude < 1e-6f)
|
||||
{
|
||||
candidateDir = new Vector3(_xzSmoothVelocity.x, 0f, _xzSmoothVelocity.z);
|
||||
}
|
||||
|
||||
if (candidateDir.sqrMagnitude < 1e-6f)
|
||||
{
|
||||
candidateDir = new Vector3(ExternalPlanarOffset.x, 0f, ExternalPlanarOffset.y);
|
||||
}
|
||||
|
||||
if (_stablePlanarDir.sqrMagnitude < 1e-6f)
|
||||
{
|
||||
_stablePlanarDir = Vector3.ProjectOnPlane(transform.forward, Vector3.up);
|
||||
if (_stablePlanarDir.sqrMagnitude < 1e-6f)
|
||||
{
|
||||
_stablePlanarDir = Vector3.forward;
|
||||
}
|
||||
}
|
||||
|
||||
_stablePlanarDir.Normalize();
|
||||
|
||||
var dirDeadZone = Mathf.Max(0.0001f, planarDirectionDeadZone);
|
||||
if (candidateDir.sqrMagnitude > dirDeadZone * dirDeadZone)
|
||||
{
|
||||
candidateDir.Normalize();
|
||||
if (Vector3.Dot(candidateDir, _stablePlanarDir) < 0f)
|
||||
{
|
||||
candidateDir = -candidateDir;
|
||||
}
|
||||
|
||||
var k = 1f - Mathf.Exp(-Mathf.Max(0.01f, planarDirectionLerpSpeed) * deltaTime);
|
||||
_stablePlanarDir = Vector3.Slerp(_stablePlanarDir, candidateDir, k);
|
||||
_stablePlanarDir.Normalize();
|
||||
}
|
||||
|
||||
var planarDir = _stablePlanarDir;
|
||||
var tiltAxis = Vector3.Cross(Vector3.up, planarDir);
|
||||
if (tiltAxis.sqrMagnitude < 1e-6f)
|
||||
{
|
||||
tiltAxis = transform.right;
|
||||
}
|
||||
|
||||
var angle = _posture switch
|
||||
{
|
||||
BobberPosture.Lying => lyingAngle,
|
||||
BobberPosture.Tilted => tiltedAngle,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
_targetRotation = Quaternion.AngleAxis(angle, tiltAxis.normalized);
|
||||
}
|
||||
|
||||
private void StartBite(BobberBiteType type, float amplitude, float duration)
|
||||
{
|
||||
if (_mode != BobberControlMode.WaterPresentation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_activeBiteType = type;
|
||||
_biteTimer = 0f;
|
||||
_biteDuration = Mathf.Max(0.01f, duration);
|
||||
_biteAmplitude = amplitude;
|
||||
_biteOffsetYVelocity = 0f;
|
||||
|
||||
if (type == BobberBiteType.BlackDrift && _blackDriftDirection.sqrMagnitude < 1e-6f)
|
||||
{
|
||||
_blackDriftDirection =
|
||||
transform.forward.sqrMagnitude > 1e-6f ? transform.forward.normalized : Vector3.forward;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateBiteAnimation(float deltaTime)
|
||||
{
|
||||
if (_activeBiteType == BobberBiteType.None)
|
||||
{
|
||||
_biteOffsetY = Mathf.SmoothDamp(
|
||||
_biteOffsetY,
|
||||
0f,
|
||||
ref _biteOffsetYVelocity,
|
||||
0.08f,
|
||||
Mathf.Infinity,
|
||||
deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
_biteTimer += deltaTime;
|
||||
var t = Mathf.Clamp01(_biteTimer / _biteDuration);
|
||||
var targetOffset = 0f;
|
||||
|
||||
switch (_activeBiteType)
|
||||
{
|
||||
case BobberBiteType.Tap:
|
||||
if (t < 0.35f)
|
||||
{
|
||||
var k = t / 0.35f;
|
||||
targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, k);
|
||||
}
|
||||
else
|
||||
{
|
||||
var k = (t - 0.35f) / 0.65f;
|
||||
targetOffset = -Mathf.Lerp(_biteAmplitude, 0f, k);
|
||||
}
|
||||
break;
|
||||
|
||||
case BobberBiteType.SlowSink:
|
||||
targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, t);
|
||||
break;
|
||||
|
||||
case BobberBiteType.Lift:
|
||||
targetOffset = Mathf.SmoothStep(0f, _biteAmplitude, t);
|
||||
break;
|
||||
|
||||
case BobberBiteType.BlackDrift:
|
||||
targetOffset = -Mathf.SmoothStep(0f, _biteAmplitude, t);
|
||||
break;
|
||||
}
|
||||
|
||||
_biteOffsetY = Mathf.SmoothDamp(
|
||||
_biteOffsetY,
|
||||
targetOffset,
|
||||
ref _biteOffsetYVelocity,
|
||||
0.03f,
|
||||
Mathf.Infinity,
|
||||
deltaTime);
|
||||
|
||||
if (_biteTimer >= _biteDuration &&
|
||||
_activeBiteType != BobberBiteType.SlowSink &&
|
||||
_activeBiteType != BobberBiteType.BlackDrift)
|
||||
{
|
||||
_activeBiteType = BobberBiteType.None;
|
||||
}
|
||||
}
|
||||
|
||||
private float GetWaterHeight(Vector3 worldPos)
|
||||
{
|
||||
if (_waterProvider != null)
|
||||
{
|
||||
return _waterProvider.GetWaterHeight(worldPos);
|
||||
}
|
||||
|
||||
return fallbackWaterLevel;
|
||||
}
|
||||
|
||||
private Vector3 GetBottomWorldPosition()
|
||||
{
|
||||
return transform.TransformPoint(new Vector3(0f, bottomOffsetLocalY, 0f));
|
||||
}
|
||||
|
||||
private void HandleDebugKeys()
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (debugResetKey && Input.GetKeyDown(KeyCode.R))
|
||||
{
|
||||
StopBite();
|
||||
}
|
||||
|
||||
if (debugTapKey && Input.GetKeyDown(KeyCode.T))
|
||||
{
|
||||
PlayTap();
|
||||
}
|
||||
|
||||
if (debugSlowSinkKey && Input.GetKeyDown(KeyCode.G))
|
||||
{
|
||||
PlaySlowSink();
|
||||
}
|
||||
|
||||
if (debugLiftKey && Input.GetKeyDown(KeyCode.H))
|
||||
{
|
||||
PlayLift();
|
||||
}
|
||||
|
||||
if (debugBlackDriftKey && Input.GetKeyDown(KeyCode.B))
|
||||
{
|
||||
PlayBlackDrift();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDebug(float waterY)
|
||||
{
|
||||
var p = transform.position;
|
||||
var b = GetBottomWorldPosition();
|
||||
|
||||
Debug.DrawLine(
|
||||
new Vector3(p.x - 0.05f, waterY, p.z),
|
||||
new Vector3(p.x + 0.05f, waterY, p.z),
|
||||
Color.cyan);
|
||||
|
||||
Debug.DrawLine(b, b + Vector3.up * floatHeight, Color.yellow);
|
||||
|
||||
if (_mode == BobberControlMode.WaterPresentation)
|
||||
{
|
||||
var a = _waterAnchorPos;
|
||||
Debug.DrawLine(a + Vector3.left * 0.03f, a + Vector3.right * 0.03f, Color.green);
|
||||
Debug.DrawLine(a + Vector3.forward * 0.03f, a + Vector3.back * 0.03f, Color.green);
|
||||
}
|
||||
|
||||
if (lureBody != null)
|
||||
{
|
||||
var bobber = _rb.worldCenterOfMass;
|
||||
var lure = lureBody.worldCenterOfMass;
|
||||
Debug.DrawLine(bobber, lure, Color.magenta);
|
||||
|
||||
var verticalEnd = bobber + Vector3.down * _verticalDistance;
|
||||
Debug.DrawLine(bobber, verticalEnd, Color.red);
|
||||
|
||||
var planar = Vector3.ProjectOnPlane(lure - bobber, Vector3.up);
|
||||
Debug.DrawLine(verticalEnd, verticalEnd + planar, Color.blue);
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
floatHeight = Mathf.Max(0.001f, floatHeight);
|
||||
ySmoothTime = Mathf.Max(0.001f, ySmoothTime);
|
||||
maxYSpeed = Mathf.Max(0.01f, maxYSpeed);
|
||||
xzSmoothTime = Mathf.Max(0.001f, xzSmoothTime);
|
||||
rotationLerpSpeed = Mathf.Max(0.01f, rotationLerpSpeed);
|
||||
|
||||
maxPlanarOffset = Mathf.Max(0f, maxPlanarOffset);
|
||||
downForceToSink = Mathf.Max(0f, downForceToSink);
|
||||
maxExtraSink = Mathf.Max(0f, maxExtraSink);
|
||||
surfaceBobAmplitude = Mathf.Max(0f, surfaceBobAmplitude);
|
||||
surfaceBobFrequency = Mathf.Max(0f, surfaceBobFrequency);
|
||||
yDeadZone = Mathf.Max(0f, yDeadZone);
|
||||
|
||||
referenceLength = Mathf.Max(0.0001f, referenceLength);
|
||||
minSubmergeToStand = Mathf.Clamp01(minSubmergeToStand);
|
||||
verticalLieThreshold = Mathf.Clamp(verticalLieThreshold, 0f, 2f);
|
||||
verticalUprightThreshold = Mathf.Max(verticalLieThreshold, verticalUprightThreshold);
|
||||
planarTiltThreshold = Mathf.Clamp(planarTiltThreshold, 0f, 2f);
|
||||
planarDominanceMultiplier = Mathf.Max(0.1f, planarDominanceMultiplier);
|
||||
postureHysteresis = Mathf.Clamp(postureHysteresis, 0f, 0.3f);
|
||||
postureConfirmTime = Mathf.Max(0f, postureConfirmTime);
|
||||
postureSwitchCooldown = Mathf.Max(0f, postureSwitchCooldown);
|
||||
|
||||
tiltedAngle = Mathf.Clamp(tiltedAngle, 0f, 89f);
|
||||
lyingAngle = Mathf.Clamp(lyingAngle, tiltedAngle, 89.9f);
|
||||
uprightMaxTiltAngle = Mathf.Clamp(uprightMaxTiltAngle, 0f, tiltedAngle);
|
||||
planarTiltFactor = Mathf.Max(0f, planarTiltFactor);
|
||||
planarDirectionDeadZone = Mathf.Max(0.0001f, planarDirectionDeadZone);
|
||||
planarDirectionLerpSpeed = Mathf.Max(0.01f, planarDirectionLerpSpeed);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40a38940e81046e2854add979cedbef9
|
||||
timeCreated: 1775958531
|
||||
@@ -0,0 +1,63 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public abstract class FishingLineNodeFeature : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前功能组件所属的节点。
|
||||
/// </summary>
|
||||
public FishingLineNode Node { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前功能组件所属的鱼线求解器。
|
||||
/// </summary>
|
||||
public FishingLineSolver Solver { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 将当前功能组件绑定到指定节点和求解器。
|
||||
/// </summary>
|
||||
public void Bind(FishingLineNode node, FishingLineSolver solver)
|
||||
{
|
||||
Node = node;
|
||||
Solver = solver;
|
||||
|
||||
if (!IsSupportedNode(node))
|
||||
{
|
||||
Debug.LogWarning($"{GetType().Name} 不适用于节点 {node.name} 的当前配置。", this);
|
||||
}
|
||||
|
||||
OnBind();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前功能组件是否支持挂在该节点上。
|
||||
/// 子类可按节点类型、尾节点类型或产品标识做限制。
|
||||
/// </summary>
|
||||
public virtual bool IsSupportedNode(FishingLineNode node)
|
||||
{
|
||||
return node != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 节点与求解器绑定完成后的回调。
|
||||
/// </summary>
|
||||
protected virtual void OnBind()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鱼线链路重建完成后的回调。
|
||||
/// </summary>
|
||||
public virtual void OnLineBuilt()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鱼线达到断线条件后的回调。
|
||||
/// </summary>
|
||||
public virtual void OnLineBreakRequested()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7ea70945db841deb0ee8df85f0e15ec
|
||||
timeCreated: 1775957663
|
||||
@@ -0,0 +1,47 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public abstract class FishingLineNodeMotionFeature : FishingLineNodeFeature
|
||||
{
|
||||
[Header("Motion Control")] [SerializeField]
|
||||
private int priorityOffset;
|
||||
|
||||
/// <summary>
|
||||
/// 当前运动控制组件的优先级。
|
||||
/// 值越大,越容易取得节点运动控制权。
|
||||
/// 最终优先级 = 默认优先级 + 调整值。
|
||||
/// </summary>
|
||||
public int Priority => DefaultPriority + priorityOffset;
|
||||
|
||||
/// <summary>
|
||||
/// 当前运动控制组件的默认优先级。
|
||||
/// 子类可通过重写该值,决定自己相对默认物理的抢占能力。
|
||||
/// </summary>
|
||||
protected virtual int DefaultPriority => 0;
|
||||
|
||||
/// <summary>
|
||||
/// 当前帧该运动控制组件是否希望接管节点运动。
|
||||
/// </summary>
|
||||
public abstract bool CanControl();
|
||||
|
||||
/// <summary>
|
||||
/// 当前运动控制组件开始接管节点时的回调。
|
||||
/// </summary>
|
||||
public virtual void OnMotionActivated()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前运动控制组件失去节点控制权时的回调。
|
||||
/// </summary>
|
||||
public virtual void OnMotionDeactivated()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前运动控制组件正在接管节点时,每个 FixedUpdate 执行的逻辑。
|
||||
/// </summary>
|
||||
public abstract void TickMotion(float deltaTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e75217306a64f8f868f5f9127772de2
|
||||
timeCreated: 1775957711
|
||||
305
Assets/Scripts/Fishing/New/View/FishingLine/FishingLineNode.cs
Normal file
305
Assets/Scripts/Fishing/New/View/FishingLine/FishingLineNode.cs
Normal file
@@ -0,0 +1,305 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class FishingLineNode : MonoBehaviour
|
||||
{
|
||||
public enum NodeType
|
||||
{
|
||||
Start,
|
||||
Float,
|
||||
Weight,
|
||||
Tail,
|
||||
Virtual,
|
||||
}
|
||||
|
||||
private FishingLineSolver _solver;
|
||||
|
||||
[Header("Node")] [SerializeField] private NodeType nodeType = NodeType.Tail;
|
||||
[SerializeField] private Rigidbody body;
|
||||
[SerializeField] private MonoBehaviour interaction;
|
||||
|
||||
[Header("Segment To Next Logical Node")] [Min(0f)] [SerializeField]
|
||||
private float segmentLengthToNext = 0.5f;
|
||||
|
||||
[Min(0)] [SerializeField] private int fixedVirtualNodesToNext = 2;
|
||||
|
||||
[Header("Runtime")] [SerializeField] private bool runtimeVirtualNode;
|
||||
[SerializeField] private int runtimeChainIndex = -1;
|
||||
|
||||
[Header("Debug")] [SerializeField] private bool drawDebugGizmo = true;
|
||||
[Min(0.001f)] [SerializeField] private float debugGizmoRadius = 0.03f;
|
||||
[SerializeField] private Color logicalNodeColor = new(0.2f, 0.9f, 0.2f, 1f);
|
||||
[SerializeField] private Color virtualNodeColor = new(1f, 0.6f, 0.1f, 1f);
|
||||
|
||||
|
||||
|
||||
[SerializeField] private List<FishingLineNodeFeature> features = new();
|
||||
[SerializeField] private List<FishingLineNodeMotionFeature> motionFeatures = new();
|
||||
private bool featureCacheReady;
|
||||
[SerializeField] private FishingLineNodeMotionFeature activeMotionFeature;
|
||||
|
||||
/// <summary>
|
||||
/// 当前正在接管节点运动的组件。
|
||||
/// </summary>
|
||||
public FishingLineNodeMotionFeature ActiveMotionFeature => activeMotionFeature;
|
||||
|
||||
public NodeType Type
|
||||
{
|
||||
get => nodeType;
|
||||
set => nodeType = value;
|
||||
}
|
||||
|
||||
public Rigidbody Body => body;
|
||||
|
||||
public MonoBehaviour Interaction => interaction;
|
||||
|
||||
public float SegmentLengthToNext
|
||||
{
|
||||
get => segmentLengthToNext;
|
||||
set => segmentLengthToNext = Mathf.Max(0f, value);
|
||||
}
|
||||
|
||||
public int FixedVirtualNodesToNext
|
||||
{
|
||||
get => fixedVirtualNodesToNext;
|
||||
set => fixedVirtualNodesToNext = Mathf.Max(0, value);
|
||||
}
|
||||
|
||||
public bool IsRuntimeVirtualNode => runtimeVirtualNode;
|
||||
|
||||
public int RuntimeChainIndex => runtimeChainIndex;
|
||||
|
||||
public Vector3 Position => transform.position;
|
||||
|
||||
public string GetDebugName()
|
||||
{
|
||||
return runtimeVirtualNode
|
||||
? $"V[{runtimeChainIndex}]"
|
||||
: $"{nodeType}[{runtimeChainIndex}]";
|
||||
}
|
||||
|
||||
public string GetDebugSummary()
|
||||
{
|
||||
var bodySummary = body == null
|
||||
? "NoBody"
|
||||
: $"v:{body.linearVelocity.magnitude:F2}";
|
||||
|
||||
return $"{GetDebugName()} pos:{Position} next:{segmentLengthToNext:F2} {bodySummary}";
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
TryGetComponent(out body);
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_solver = GetComponentInParent<FishingLineSolver>();
|
||||
EnsureFeatureCache();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
BindFeatures(_solver);
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
EnsureFeatureCache();
|
||||
UpdateMotionControl(Time.fixedDeltaTime);
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (body == null)
|
||||
{
|
||||
TryGetComponent(out body);
|
||||
}
|
||||
|
||||
segmentLengthToNext = Mathf.Max(0f, segmentLengthToNext);
|
||||
fixedVirtualNodesToNext = Mathf.Max(0, fixedVirtualNodesToNext);
|
||||
}
|
||||
|
||||
public void SetRuntimeVirtual(bool isVirtual, int chainIndex)
|
||||
{
|
||||
runtimeVirtualNode = isVirtual;
|
||||
runtimeChainIndex = chainIndex;
|
||||
|
||||
if (isVirtual)
|
||||
{
|
||||
nodeType = NodeType.Virtual;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetVisualPosition(Vector3 position)
|
||||
{
|
||||
transform.position = position;
|
||||
}
|
||||
|
||||
#region Feature
|
||||
|
||||
/// <summary>
|
||||
/// 获取节点上的第一个指定类型功能组件。
|
||||
/// </summary>
|
||||
public T GetFeature<T>() where T : FishingLineNodeFeature
|
||||
{
|
||||
EnsureFeatureCache();
|
||||
for (var i = 0; i < features.Count; i++)
|
||||
{
|
||||
if (features[i] is T result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取节点上的指定类型功能组件。
|
||||
/// </summary>
|
||||
public bool TryGetFeature<T>(out T feature) where T : FishingLineNodeFeature
|
||||
{
|
||||
feature = GetFeature<T>();
|
||||
return feature != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新并重新绑定当前节点上的功能组件。
|
||||
/// </summary>
|
||||
public void BindFeatures(FishingLineSolver solver)
|
||||
{
|
||||
EnsureFeatureCache();
|
||||
foreach (var t in features)
|
||||
{
|
||||
t.Bind(this, solver);
|
||||
}
|
||||
|
||||
ResolveMotionFeature(forceRefresh: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知当前节点上的所有功能组件,鱼线已重建完成。
|
||||
/// </summary>
|
||||
public void NotifyLineBuilt()
|
||||
{
|
||||
EnsureFeatureCache();
|
||||
foreach (var t in features)
|
||||
{
|
||||
t.OnLineBuilt();
|
||||
}
|
||||
|
||||
ResolveMotionFeature(forceRefresh: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知当前节点上的所有功能组件,鱼线已经达到断线条件。
|
||||
/// </summary>
|
||||
public void NotifyLineBreakRequested()
|
||||
{
|
||||
EnsureFeatureCache();
|
||||
foreach (var t in features)
|
||||
{
|
||||
t.OnLineBreakRequested();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void EnsureFeatureCache()
|
||||
{
|
||||
if (!featureCacheReady)
|
||||
{
|
||||
RefreshFeatures();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshFeatures()
|
||||
{
|
||||
features.Clear();
|
||||
motionFeatures.Clear();
|
||||
GetComponents(features);
|
||||
for (var i = 0; i < features.Count; i++)
|
||||
{
|
||||
if (features[i] is FishingLineNodeMotionFeature motionFeature)
|
||||
{
|
||||
motionFeatures.Add(motionFeature);
|
||||
}
|
||||
}
|
||||
|
||||
activeMotionFeature = null;
|
||||
featureCacheReady = true;
|
||||
}
|
||||
|
||||
private void UpdateMotionControl(float deltaTime)
|
||||
{
|
||||
var motionFeature = ResolveMotionFeature(forceRefresh: false);
|
||||
if (motionFeature == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
motionFeature.TickMotion(deltaTime);
|
||||
}
|
||||
|
||||
private FishingLineNodeMotionFeature ResolveMotionFeature(bool forceRefresh)
|
||||
{
|
||||
EnsureFeatureCache();
|
||||
|
||||
var bestMotionFeature = default(FishingLineNodeMotionFeature);
|
||||
var bestPriority = int.MinValue;
|
||||
|
||||
foreach (var motionFeature in motionFeatures)
|
||||
{
|
||||
var r = !motionFeature.IsSupportedNode(this);
|
||||
var n = !motionFeature.CanControl();
|
||||
if (motionFeature == null || !motionFeature.IsSupportedNode(this) || !motionFeature.CanControl())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bestMotionFeature != null && motionFeature.Priority <= bestPriority)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestMotionFeature = motionFeature;
|
||||
bestPriority = motionFeature.Priority;
|
||||
}
|
||||
|
||||
if (!forceRefresh && ReferenceEquals(activeMotionFeature, bestMotionFeature))
|
||||
{
|
||||
return activeMotionFeature;
|
||||
}
|
||||
|
||||
if (activeMotionFeature != null && !ReferenceEquals(activeMotionFeature, bestMotionFeature))
|
||||
{
|
||||
activeMotionFeature.OnMotionDeactivated();
|
||||
}
|
||||
|
||||
activeMotionFeature = bestMotionFeature;
|
||||
if (activeMotionFeature != null)
|
||||
{
|
||||
activeMotionFeature.OnMotionActivated();
|
||||
}
|
||||
|
||||
return activeMotionFeature;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (!drawDebugGizmo)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Gizmos.color = runtimeVirtualNode ? virtualNodeColor : logicalNodeColor;
|
||||
Gizmos.DrawSphere(transform.position, debugGizmoRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 610df5569209e4b4997cb2dbf3b94cdc
|
||||
@@ -0,0 +1,717 @@
|
||||
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;
|
||||
|
||||
[Header("Verlet")]
|
||||
[Min(1)]
|
||||
[SerializeField] private int solverIterations = 8;
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float damping = 0.98f;
|
||||
[Min(0f)]
|
||||
[SerializeField] private float gravityScale = 1f;
|
||||
[Min(0.001f)]
|
||||
[SerializeField] private float simulationStep = 0.0166667f;
|
||||
[Min(0.001f)]
|
||||
[SerializeField] private float maxDeltaTime = 0.0333333f;
|
||||
|
||||
[Header("Water Surface")]
|
||||
[SerializeField] private bool constrainToWaterSurface = true;
|
||||
[SerializeField] private Transform waterSurfaceTransform;
|
||||
[SerializeField] private float waterSurfaceHeight;
|
||||
[Min(0)]
|
||||
[SerializeField] private int ignoreHeadNodeCount = 1;
|
||||
[Min(0)]
|
||||
[SerializeField] private int ignoreTailNodeCount = 1;
|
||||
[Min(0f)]
|
||||
[SerializeField] private float waterSurfaceFollowSpeed = 12f;
|
||||
|
||||
[Header("Stability")]
|
||||
[Min(1)]
|
||||
[SerializeField] private int maxSubStepsPerFrame = 2;
|
||||
[Min(0f)]
|
||||
[SerializeField] private float sleepVelocityThreshold = 0.001f;
|
||||
[Min(0f)]
|
||||
[SerializeField] private float sleepDistanceThreshold = 0.002f;
|
||||
[Min(1)]
|
||||
[SerializeField] private int stableFramesBeforeSleep = 4;
|
||||
[Min(0f)]
|
||||
[SerializeField] private float wakeDistanceThreshold = 0.001f;
|
||||
[Min(0f)]
|
||||
[SerializeField] private float tautSegmentThreshold = 0.002f;
|
||||
[Min(0.001f)]
|
||||
[SerializeField] private float tautTransitionRange = 0.03f;
|
||||
|
||||
[Header("Corner Smoothing")]
|
||||
[SerializeField] private bool smoothCorners = true;
|
||||
[Range(0f, 180f)]
|
||||
[SerializeField] private float minCornerAngle = 12f;
|
||||
[Min(0f)]
|
||||
[SerializeField] private float maxCornerSmoothDistance = 0.03f;
|
||||
[Min(1)]
|
||||
[SerializeField] private int cornerSmoothSubdivisions = 3;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool drawDebugSamples;
|
||||
[SerializeField] private Color debugLogicalSampleColor = Color.cyan;
|
||||
[SerializeField] private Color debugVirtualSampleColor = new(1f, 0.55f, 0.15f, 1f);
|
||||
[Min(0.001f)]
|
||||
[SerializeField] private float debugLogicalSampleRadius = 0.018f;
|
||||
[Min(0.001f)]
|
||||
[SerializeField] private float debugVirtualSampleRadius = 0.012f;
|
||||
|
||||
private readonly List<Vector3> positions = new();
|
||||
private readonly List<Vector3> renderPositions = new();
|
||||
private readonly List<Vector3> previousPositions = new();
|
||||
private readonly List<long> sampledPointKeys = new();
|
||||
private readonly List<Vector3> lastPinnedPointPositions = new();
|
||||
private readonly List<float> lastRestLengths = new();
|
||||
private bool[] pinnedFlags = System.Array.Empty<bool>();
|
||||
private float accumulatedTime;
|
||||
private bool isSleeping;
|
||||
private int stableFrameCounter;
|
||||
|
||||
public int SampleCount => positions.Count;
|
||||
|
||||
public float CurrentRenderedLength
|
||||
{
|
||||
get
|
||||
{
|
||||
var total = 0f;
|
||||
var source = renderPositions.Count > 0 ? renderPositions : positions;
|
||||
for (var i = 0; i < source.Count - 1; i++)
|
||||
{
|
||||
total += Vector3.Distance(source[i], source[i + 1]);
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
TryGetComponent(out lineRenderer);
|
||||
if (solver == null)
|
||||
{
|
||||
TryGetComponent(out solver);
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (lineRenderer == null)
|
||||
{
|
||||
TryGetComponent(out lineRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
public void Render(FishingLineSolver sourceSolver, float deltaTime)
|
||||
{
|
||||
if (lineRenderer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
solver = sourceSolver;
|
||||
var points = solver.ChainPoints;
|
||||
var restLengths = solver.RestLengths;
|
||||
var pinnedIndices = solver.PinnedIndices;
|
||||
if (points.Count == 0)
|
||||
{
|
||||
lineRenderer.positionCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var topologyChanged = EnsureBuffers(points, pinnedIndices);
|
||||
if (topologyChanged || ShouldWake(points, restLengths))
|
||||
{
|
||||
WakeUp();
|
||||
}
|
||||
|
||||
Simulate(points, restLengths, deltaTime);
|
||||
ApplyToRenderer();
|
||||
CacheFrameState(points, restLengths);
|
||||
}
|
||||
|
||||
private bool EnsureBuffers(
|
||||
IReadOnlyList<FishingLineSolver.ChainPoint> points,
|
||||
IReadOnlyList<int> pinnedIndices)
|
||||
{
|
||||
var topologyChanged = sampledPointKeys.Count != points.Count;
|
||||
if (!topologyChanged)
|
||||
{
|
||||
for (var i = 0; i < points.Count; i++)
|
||||
{
|
||||
if (sampledPointKeys[i] == points[i].Key)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
topologyChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var previousPositionMap = new Dictionary<long, Vector3>(sampledPointKeys.Count);
|
||||
var previousHistoryMap = new Dictionary<long, Vector3>(sampledPointKeys.Count);
|
||||
for (var i = 0; i < sampledPointKeys.Count; i++)
|
||||
{
|
||||
previousPositionMap[sampledPointKeys[i]] = positions[i];
|
||||
previousHistoryMap[sampledPointKeys[i]] = previousPositions[i];
|
||||
}
|
||||
|
||||
positions.Clear();
|
||||
previousPositions.Clear();
|
||||
sampledPointKeys.Clear();
|
||||
pinnedFlags = new bool[points.Count];
|
||||
|
||||
for (var i = 0; i < points.Count; i++)
|
||||
{
|
||||
var point = points[i];
|
||||
sampledPointKeys.Add(point.Key);
|
||||
|
||||
if (previousPositionMap.TryGetValue(point.Key, out var preservedPosition))
|
||||
{
|
||||
positions.Add(preservedPosition);
|
||||
previousPositions.Add(previousHistoryMap[point.Key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
positions.Add(point.Position);
|
||||
previousPositions.Add(point.Position);
|
||||
}
|
||||
|
||||
for (var i = 0; i < pinnedIndices.Count; i++)
|
||||
{
|
||||
var pinnedIndex = pinnedIndices[i];
|
||||
if (pinnedIndex >= 0 && pinnedIndex < pinnedFlags.Length)
|
||||
{
|
||||
pinnedFlags[pinnedIndex] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return topologyChanged;
|
||||
}
|
||||
|
||||
private void Simulate(
|
||||
IReadOnlyList<FishingLineSolver.ChainPoint> points,
|
||||
IReadOnlyList<float> restLengths,
|
||||
float deltaTime)
|
||||
{
|
||||
if (isSleeping)
|
||||
{
|
||||
PinLogicalPoints(points);
|
||||
return;
|
||||
}
|
||||
|
||||
var clampedDelta = Mathf.Clamp(deltaTime, 0f, maxDeltaTime);
|
||||
accumulatedTime = Mathf.Min(accumulatedTime + clampedDelta, simulationStep * maxSubStepsPerFrame);
|
||||
|
||||
var subStepCount = 0;
|
||||
while (accumulatedTime >= simulationStep && subStepCount < maxSubStepsPerFrame)
|
||||
{
|
||||
SimulateStep(points, restLengths, simulationStep);
|
||||
accumulatedTime -= simulationStep;
|
||||
subStepCount++;
|
||||
}
|
||||
|
||||
if (subStepCount == 0)
|
||||
{
|
||||
PinLogicalPoints(points);
|
||||
ApplySleep();
|
||||
}
|
||||
|
||||
EvaluateSleepState(restLengths);
|
||||
}
|
||||
|
||||
private void SimulateStep(
|
||||
IReadOnlyList<FishingLineSolver.ChainPoint> points,
|
||||
IReadOnlyList<float> restLengths,
|
||||
float stepDelta)
|
||||
{
|
||||
var gravity = Physics.gravity * gravityScale * stepDelta * stepDelta;
|
||||
|
||||
for (var i = 0; i < points.Count; i++)
|
||||
{
|
||||
if (pinnedFlags[i])
|
||||
{
|
||||
positions[i] = points[i].Position;
|
||||
previousPositions[i] = points[i].Position;
|
||||
continue;
|
||||
}
|
||||
|
||||
var current = positions[i];
|
||||
var velocity = (current - previousPositions[i]) * damping;
|
||||
previousPositions[i] = current;
|
||||
positions[i] = current + velocity + gravity;
|
||||
}
|
||||
|
||||
SolveDistanceConstraints(points, restLengths);
|
||||
|
||||
ApplyWaterSurfaceConstraint(stepDelta);
|
||||
SolveDistanceConstraints(points, restLengths);
|
||||
StraightenTautLogicalSegments(points, restLengths);
|
||||
SolveDistanceConstraints(points, restLengths);
|
||||
PinLogicalPoints(points);
|
||||
ApplySleep();
|
||||
}
|
||||
|
||||
private void SolveDistanceConstraints(
|
||||
IReadOnlyList<FishingLineSolver.ChainPoint> points,
|
||||
IReadOnlyList<float> restLengths)
|
||||
{
|
||||
for (var iteration = 0; iteration < solverIterations; iteration++)
|
||||
{
|
||||
PinLogicalPoints(points);
|
||||
|
||||
for (var segmentIndex = 0; segmentIndex < restLengths.Count; segmentIndex++)
|
||||
{
|
||||
SatisfyDistanceConstraint(segmentIndex, restLengths[segmentIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PinLogicalPoints(IReadOnlyList<FishingLineSolver.ChainPoint> points)
|
||||
{
|
||||
for (var i = 0; i < points.Count; i++)
|
||||
{
|
||||
if (!pinnedFlags[i])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
positions[i] = points[i].Position;
|
||||
previousPositions[i] = points[i].Position;
|
||||
}
|
||||
}
|
||||
|
||||
private void SatisfyDistanceConstraint(int segmentIndex, float restLength)
|
||||
{
|
||||
var pointA = positions[segmentIndex];
|
||||
var pointB = positions[segmentIndex + 1];
|
||||
var delta = pointB - pointA;
|
||||
var distance = delta.magnitude;
|
||||
if (distance <= 0.0001f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var correctionScale = (distance - restLength) / distance;
|
||||
if (Mathf.Approximately(correctionScale, 0f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var pointAPinned = pinnedFlags[segmentIndex];
|
||||
var pointBPinned = pinnedFlags[segmentIndex + 1];
|
||||
|
||||
if (pointAPinned && pointBPinned)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (pointAPinned)
|
||||
{
|
||||
positions[segmentIndex + 1] -= delta * correctionScale;
|
||||
return;
|
||||
}
|
||||
|
||||
if (pointBPinned)
|
||||
{
|
||||
positions[segmentIndex] += delta * correctionScale;
|
||||
return;
|
||||
}
|
||||
|
||||
var correction = delta * (correctionScale * 0.5f);
|
||||
positions[segmentIndex] += correction;
|
||||
positions[segmentIndex + 1] -= correction;
|
||||
}
|
||||
|
||||
private void StraightenTautLogicalSegments(
|
||||
IReadOnlyList<FishingLineSolver.ChainPoint> points,
|
||||
IReadOnlyList<float> restLengths)
|
||||
{
|
||||
if (points.Count < 2 || restLengths.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var segmentStartIndex = 0;
|
||||
while (segmentStartIndex < points.Count - 1)
|
||||
{
|
||||
if (!points[segmentStartIndex].IsLogical)
|
||||
{
|
||||
segmentStartIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var segmentEndIndex = segmentStartIndex + 1;
|
||||
while (segmentEndIndex < points.Count && !points[segmentEndIndex].IsLogical)
|
||||
{
|
||||
segmentEndIndex++;
|
||||
}
|
||||
|
||||
if (segmentEndIndex >= points.Count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ProjectLogicalSegmentIfTaut(segmentStartIndex, segmentEndIndex, restLengths);
|
||||
segmentStartIndex = segmentEndIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProjectLogicalSegmentIfTaut(
|
||||
int startIndex,
|
||||
int endIndex,
|
||||
IReadOnlyList<float> restLengths)
|
||||
{
|
||||
if (endIndex - startIndex <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var segmentRestLength = 0f;
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
segmentRestLength += restLengths[i];
|
||||
}
|
||||
|
||||
var start = positions[startIndex];
|
||||
var end = positions[endIndex];
|
||||
var delta = end - start;
|
||||
var endpointDistance = delta.magnitude;
|
||||
if (endpointDistance <= 0.0001f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fullTautDistance = Mathf.Max(0f, segmentRestLength - tautSegmentThreshold);
|
||||
var blendStartDistance = Mathf.Max(0f, fullTautDistance - tautTransitionRange);
|
||||
if (endpointDistance < blendStartDistance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var straightenBlend = blendStartDistance >= fullTautDistance
|
||||
? 1f
|
||||
: Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(blendStartDistance, fullTautDistance, endpointDistance));
|
||||
|
||||
var direction = delta / endpointDistance;
|
||||
var accumulatedDistance = 0f;
|
||||
for (var pointIndex = startIndex + 1; pointIndex < endIndex; pointIndex++)
|
||||
{
|
||||
accumulatedDistance += restLengths[pointIndex - 1];
|
||||
var projectedPosition = start + direction * accumulatedDistance;
|
||||
var currentPosition = positions[pointIndex];
|
||||
var blendedPosition = Vector3.Lerp(currentPosition, projectedPosition, straightenBlend);
|
||||
positions[pointIndex] = blendedPosition;
|
||||
previousPositions[pointIndex] = Vector3.Lerp(previousPositions[pointIndex], blendedPosition, straightenBlend);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyWaterSurfaceConstraint(float stepDelta)
|
||||
{
|
||||
if (!constrainToWaterSurface || positions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var surfaceHeight = waterSurfaceTransform != null ? waterSurfaceTransform.position.y : waterSurfaceHeight;
|
||||
var startIndex = Mathf.Clamp(ignoreHeadNodeCount, 0, positions.Count);
|
||||
var endExclusive = Mathf.Clamp(positions.Count - ignoreTailNodeCount, startIndex, positions.Count);
|
||||
var followFactor = Mathf.Clamp01(waterSurfaceFollowSpeed * stepDelta);
|
||||
|
||||
for (var i = startIndex; i < endExclusive; i++)
|
||||
{
|
||||
if (pinnedFlags[i])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var current = positions[i];
|
||||
if (current.y >= surfaceHeight)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nextY = Mathf.Lerp(current.y, surfaceHeight, followFactor);
|
||||
positions[i] = new Vector3(current.x, nextY, current.z);
|
||||
|
||||
var previous = previousPositions[i];
|
||||
previousPositions[i] = new Vector3(
|
||||
previous.x,
|
||||
Mathf.Lerp(previous.y, nextY, followFactor),
|
||||
previous.z);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySleep()
|
||||
{
|
||||
for (var i = 0; i < positions.Count; i++)
|
||||
{
|
||||
if (pinnedFlags[i])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var velocityMagnitude = (positions[i] - previousPositions[i]).magnitude;
|
||||
if (velocityMagnitude <= sleepVelocityThreshold)
|
||||
{
|
||||
previousPositions[i] = positions[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EvaluateSleepState(IReadOnlyList<float> restLengths)
|
||||
{
|
||||
var isStable = true;
|
||||
for (var i = 0; i < positions.Count; i++)
|
||||
{
|
||||
if (pinnedFlags[i])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((positions[i] - previousPositions[i]).magnitude > sleepVelocityThreshold)
|
||||
{
|
||||
isStable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isStable)
|
||||
{
|
||||
for (var i = 0; i < restLengths.Count; i++)
|
||||
{
|
||||
var error = Mathf.Abs(Vector3.Distance(positions[i], positions[i + 1]) - restLengths[i]);
|
||||
if (error > sleepDistanceThreshold)
|
||||
{
|
||||
isStable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStable)
|
||||
{
|
||||
stableFrameCounter = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
stableFrameCounter++;
|
||||
if (stableFrameCounter < stableFramesBeforeSleep)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isSleeping = true;
|
||||
accumulatedTime = 0f;
|
||||
for (var i = 0; i < positions.Count; i++)
|
||||
{
|
||||
previousPositions[i] = positions[i];
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldWake(
|
||||
IReadOnlyList<FishingLineSolver.ChainPoint> points,
|
||||
IReadOnlyList<float> restLengths)
|
||||
{
|
||||
if (!isSleeping)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lastPinnedPointPositions.Count != points.Count || lastRestLengths.Count != restLengths.Count)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var i = 0; i < points.Count; i++)
|
||||
{
|
||||
if (!pinnedFlags[i])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Vector3.Distance(points[i].Position, lastPinnedPointPositions[i]) > wakeDistanceThreshold)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < restLengths.Count; i++)
|
||||
{
|
||||
if (Mathf.Abs(restLengths[i] - lastRestLengths[i]) > wakeDistanceThreshold)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CacheFrameState(
|
||||
IReadOnlyList<FishingLineSolver.ChainPoint> points,
|
||||
IReadOnlyList<float> restLengths)
|
||||
{
|
||||
lastPinnedPointPositions.Clear();
|
||||
for (var i = 0; i < points.Count; i++)
|
||||
{
|
||||
lastPinnedPointPositions.Add(points[i].Position);
|
||||
}
|
||||
|
||||
lastRestLengths.Clear();
|
||||
for (var i = 0; i < restLengths.Count; i++)
|
||||
{
|
||||
lastRestLengths.Add(restLengths[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void WakeUp()
|
||||
{
|
||||
isSleeping = false;
|
||||
stableFrameCounter = 0;
|
||||
accumulatedTime = 0f;
|
||||
}
|
||||
|
||||
private void ApplyToRenderer()
|
||||
{
|
||||
BuildRenderPositions();
|
||||
|
||||
lineRenderer.positionCount = renderPositions.Count;
|
||||
for (var i = 0; i < renderPositions.Count; i++)
|
||||
{
|
||||
lineRenderer.SetPosition(i, renderPositions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildRenderPositions()
|
||||
{
|
||||
renderPositions.Clear();
|
||||
if (positions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!smoothCorners || positions.Count < 3)
|
||||
{
|
||||
renderPositions.AddRange(positions);
|
||||
return;
|
||||
}
|
||||
|
||||
renderPositions.Add(positions[0]);
|
||||
|
||||
for (var i = 1; i < positions.Count - 1; i++)
|
||||
{
|
||||
var previous = positions[i - 1];
|
||||
var current = positions[i];
|
||||
var next = positions[i + 1];
|
||||
|
||||
if (!TryBuildSmoothedCorner(previous, current, next, out var entry, out var exit))
|
||||
{
|
||||
AddRenderPointIfDistinct(current);
|
||||
continue;
|
||||
}
|
||||
|
||||
AddRenderPointIfDistinct(entry);
|
||||
for (var subdivision = 1; subdivision <= cornerSmoothSubdivisions; subdivision++)
|
||||
{
|
||||
var t = subdivision / (cornerSmoothSubdivisions + 1f);
|
||||
var pointOnCurve = EvaluateQuadraticBezier(entry, current, exit, t);
|
||||
AddRenderPointIfDistinct(pointOnCurve);
|
||||
}
|
||||
|
||||
AddRenderPointIfDistinct(exit);
|
||||
}
|
||||
|
||||
AddRenderPointIfDistinct(positions[^1]);
|
||||
}
|
||||
|
||||
private bool TryBuildSmoothedCorner(
|
||||
Vector3 previous,
|
||||
Vector3 current,
|
||||
Vector3 next,
|
||||
out Vector3 entry,
|
||||
out Vector3 exit)
|
||||
{
|
||||
entry = current;
|
||||
exit = current;
|
||||
|
||||
var incoming = current - previous;
|
||||
var outgoing = next - current;
|
||||
var incomingLength = incoming.magnitude;
|
||||
var outgoingLength = outgoing.magnitude;
|
||||
if (incomingLength <= 0.0001f || outgoingLength <= 0.0001f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var cornerAngle = Vector3.Angle(incoming, outgoing);
|
||||
if (cornerAngle < minCornerAngle)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimDistance = Mathf.Min(
|
||||
maxCornerSmoothDistance,
|
||||
incomingLength * 0.5f,
|
||||
outgoingLength * 0.5f);
|
||||
|
||||
if (trimDistance <= 0.0001f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
entry = current - (incoming / incomingLength) * trimDistance;
|
||||
exit = current + (outgoing / outgoingLength) * trimDistance;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AddRenderPointIfDistinct(Vector3 point)
|
||||
{
|
||||
if (renderPositions.Count > 0 && Vector3.Distance(renderPositions[^1], point) <= 0.0001f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
renderPositions.Add(point);
|
||||
}
|
||||
|
||||
private static Vector3 EvaluateQuadraticBezier(Vector3 start, Vector3 control, Vector3 end, float t)
|
||||
{
|
||||
var oneMinusT = 1f - t;
|
||||
return (oneMinusT * oneMinusT * start)
|
||||
+ (2f * oneMinusT * t * control)
|
||||
+ (t * t * end);
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!drawDebugSamples || positions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < positions.Count; i++)
|
||||
{
|
||||
var isLogicalPoint = solver != null
|
||||
&& solver.ChainPoints != null
|
||||
&& i < solver.ChainPoints.Count
|
||||
&& solver.ChainPoints[i].IsLogical;
|
||||
|
||||
Gizmos.color = isLogicalPoint ? debugLogicalSampleColor : debugVirtualSampleColor;
|
||||
Gizmos.DrawSphere(
|
||||
positions[i],
|
||||
isLogicalPoint ? debugLogicalSampleRadius : debugVirtualSampleRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 827786ffede4e7b4781c522e8a4ba9d0
|
||||
926
Assets/Scripts/Fishing/New/View/FishingLine/FishingLineSolver.cs
Normal file
926
Assets/Scripts/Fishing/New/View/FishingLine/FishingLineSolver.cs
Normal file
@@ -0,0 +1,926 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class FishingLineSolver : MonoBehaviour
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class ChainPoint
|
||||
{
|
||||
public long Key;
|
||||
public Vector3 Position;
|
||||
public bool IsLogical;
|
||||
public FishingLineNode LogicalNode;
|
||||
public int SegmentIndex;
|
||||
public int StableIndex;
|
||||
|
||||
public string GetDebugName()
|
||||
{
|
||||
if (IsLogical)
|
||||
{
|
||||
return LogicalNode != null ? LogicalNode.GetDebugName() : $"L[{StableIndex}]";
|
||||
}
|
||||
|
||||
return $"V[S{SegmentIndex}:{StableIndex}]";
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private struct SegmentLayout
|
||||
{
|
||||
public float[] GapLengths;
|
||||
|
||||
public int VirtualNodeCount => Mathf.Max(0, GapLengths.Length - 1);
|
||||
}
|
||||
|
||||
[Header("References")] [SerializeField]
|
||||
private Transform anchorTransform;
|
||||
|
||||
[SerializeField] private FishingLineNode[] logicalNodes = Array.Empty<FishingLineNode>();
|
||||
[SerializeField] private FishingLineRenderer lineRenderer;
|
||||
|
||||
[Header("First Segment")] [Min(0f)] [SerializeField]
|
||||
private float firstSegmentLength = 1.2f;
|
||||
|
||||
[Min(0.001f)] [SerializeField] private float firstSegmentStep = 0.1f;
|
||||
|
||||
[Header("Joint")] [Min(1)] [SerializeField]
|
||||
private int jointSolverIterations = 12;
|
||||
|
||||
[SerializeField] private float jointProjectionDistance = 0.02f;
|
||||
[SerializeField] private float jointProjectionAngle = 1f;
|
||||
|
||||
[Header("Limit Detection")]
|
||||
[Min(0f)]
|
||||
// 极限判定的长度容差,允许链路在总长或单段长度上存在少量误差。
|
||||
[SerializeField]
|
||||
private float lengthLimitTolerance = 0.01f;
|
||||
|
||||
[Min(0f)]
|
||||
// 达到极限后,只有当前超长值大于该阈值时,才开始进入断线候选计时。
|
||||
[SerializeField]
|
||||
private float breakStretchThreshold = 0.05f;
|
||||
|
||||
[Min(0f)]
|
||||
// 断线候选状态允许持续的最大时间;超过后会发出一次断线消息。
|
||||
[SerializeField]
|
||||
private float breakLimitDuration = 0.15f;
|
||||
|
||||
[Header("Runtime Debug")] [SerializeField]
|
||||
private bool autoBuildOnStart = true;
|
||||
|
||||
private readonly List<ChainPoint> chainPoints = new();
|
||||
private readonly List<float> restLengths = new();
|
||||
private readonly List<int> pinnedIndices = new();
|
||||
private readonly List<ConfigurableJoint> runtimeJoints = new();
|
||||
|
||||
private bool chainDirty = true;
|
||||
private int runtimeVirtualPointCount;
|
||||
|
||||
public float FirstSegmentLength => firstSegmentLength;
|
||||
|
||||
public float FirstSegmentStep => firstSegmentStep;
|
||||
|
||||
public IReadOnlyList<ChainPoint> ChainPoints => chainPoints;
|
||||
|
||||
public IReadOnlyList<float> RestLengths => restLengths;
|
||||
|
||||
public IReadOnlyList<int> PinnedIndices => pinnedIndices;
|
||||
|
||||
/// <summary>
|
||||
/// 当前配置的逻辑节点只读列表。
|
||||
/// 外部可读取节点顺序,但不应直接修改数组内容。
|
||||
/// </summary>
|
||||
public IReadOnlyList<FishingLineNode> LogicalNodes => logicalNodes;
|
||||
|
||||
/// <summary>
|
||||
/// 当前整条鱼线的配置总长度,等于所有段的静止长度之和。
|
||||
/// </summary>
|
||||
public float TotalLineLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前逻辑节点链路的实际总长度,按相邻逻辑节点的实际距离累加。
|
||||
/// </summary>
|
||||
public float CurrentLogicalChainLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前首尾逻辑节点之间的直线距离,仅作为端点跨度观察值。
|
||||
/// </summary>
|
||||
public float CurrentEndpointDistance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前逻辑链总长度超出配置总长度的部分,小于等于零时记为 0。
|
||||
/// </summary>
|
||||
public float CurrentStretchLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前所有逻辑段中,单段超出配置长度的最大值。
|
||||
/// </summary>
|
||||
public float MaxSegmentStretchLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前超限最明显的逻辑段索引;为 -1 表示没有段处于超限。
|
||||
/// </summary>
|
||||
public int MaxOverstretchedSegmentIndex { get; private set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 当前受拉比例。
|
||||
/// 该值取“单段实际长度 / 单段配置长度”和“整链实际长度 / 整链配置长度”中的最大值。
|
||||
/// 约等于 1 表示接近拉直,大于 1 表示已经出现超限拉伸。
|
||||
/// </summary>
|
||||
public float CurrentTensionRatio { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 断线候选拉伸阈值。
|
||||
/// 只有当前处于极限状态,且超长值大于该阈值时,才会开始累计断线计时。
|
||||
/// </summary>
|
||||
public float BreakStretchThreshold => breakStretchThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// 断线候选状态可持续的最大时间。
|
||||
/// 当断线计时超过该值时,会发出一次断线消息。
|
||||
/// </summary>
|
||||
public float BreakLimitDuration => breakLimitDuration;
|
||||
|
||||
/// <summary>
|
||||
/// 当前拉力极限百分比。
|
||||
/// 当超长值小于等于 lengthLimitTolerance 时为 0;
|
||||
/// 当超长值大于等于 breakStretchThreshold 时为 100;
|
||||
/// 中间区间按线性比例映射,供 UI 显示使用。
|
||||
/// </summary>
|
||||
public float CurrentBreakStretchPercent => EvaluateBreakStretchPercent(CurrentStretchLength);
|
||||
|
||||
/// <summary>
|
||||
/// 当前是否处于极限状态。
|
||||
/// 只要整链超出总长度容差,或任一逻辑段超出单段容差,即认为到达极限。
|
||||
/// </summary>
|
||||
public bool IsAtLimit { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前断线候选状态的累计时间。
|
||||
/// 只有在处于极限状态,且 CurrentStretchLength 大于断线阈值时才会累加;否则重置为 0。
|
||||
/// </summary>
|
||||
public float LimitStateTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前是否正在进行断线候选计时。
|
||||
/// </summary>
|
||||
public bool IsBreakCountdownActive => IsAtLimit && CurrentStretchLength > breakStretchThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// 当前极限断线消息是否已经发出过。
|
||||
/// 在退出断线候选状态前只会发一次,避免重复通知。
|
||||
/// </summary>
|
||||
public bool HasBreakNotificationSent { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当鱼线达到断线条件时发出的一次性消息。
|
||||
/// 外部可订阅该事件,在回调中执行切线、播放表现或状态切换。
|
||||
/// </summary>
|
||||
public event Action<FishingLineSolver> OnLineBreakRequested;
|
||||
|
||||
public int LogicalNodeCount => logicalNodes?.Length ?? 0;
|
||||
|
||||
public int RuntimeVirtualNodeCount => runtimeVirtualPointCount;
|
||||
|
||||
public int ActiveRuntimeVirtualNodeCount => runtimeVirtualPointCount;
|
||||
|
||||
public int OrderedNodeCount => chainPoints.Count;
|
||||
|
||||
public int SegmentCount => restLengths.Count;
|
||||
|
||||
public bool IsChainDirty => chainDirty;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
if (lineRenderer == null)
|
||||
{
|
||||
TryGetComponent(out lineRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (autoBuildOnStart)
|
||||
{
|
||||
BuildLine();
|
||||
}
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (logicalNodes == null || logicalNodes.Length == 0)
|
||||
{
|
||||
ResetLimitState();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateAnchorNode();
|
||||
|
||||
if (chainDirty)
|
||||
{
|
||||
RebuildRuntimeChain();
|
||||
}
|
||||
|
||||
EvaluateLimitState(Time.fixedDeltaTime);
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (chainDirty)
|
||||
{
|
||||
RebuildRuntimeChain();
|
||||
}
|
||||
|
||||
SyncLogicalPointPositions();
|
||||
|
||||
if (lineRenderer != null && chainPoints.Count > 1)
|
||||
{
|
||||
lineRenderer.Render(this, Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
firstSegmentLength = Mathf.Max(0f, firstSegmentLength);
|
||||
firstSegmentStep = Mathf.Max(0.001f, firstSegmentStep);
|
||||
jointSolverIterations = Mathf.Max(1, jointSolverIterations);
|
||||
lengthLimitTolerance = Mathf.Max(0f, lengthLimitTolerance);
|
||||
breakStretchThreshold = Mathf.Max(0f, breakStretchThreshold);
|
||||
breakLimitDuration = Mathf.Max(0f, breakLimitDuration);
|
||||
chainDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按当前配置重建整条鱼线的运行时链路,并立即刷新极限状态。
|
||||
/// </summary>
|
||||
[ContextMenu("Build Line")]
|
||||
public void BuildLine()
|
||||
{
|
||||
ConfigureStartNode();
|
||||
ConfigureLogicalJoints();
|
||||
RebuildRuntimeChain();
|
||||
EvaluateLimitState(0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定逻辑段的配置长度。
|
||||
/// segmentIndex 为 0 时表示第一段;大于 0 时表示对应逻辑节点到下一个逻辑节点的线长。
|
||||
/// </summary>
|
||||
public void SetLenght(float length, int segmentIndex = 0)
|
||||
{
|
||||
var clamped = Mathf.Max(0f, length);
|
||||
var currentLength = GetSegmentLength(segmentIndex);
|
||||
if (Mathf.Approximately(clamped, currentLength))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (segmentIndex <= 0)
|
||||
{
|
||||
firstSegmentLength = clamped;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (logicalNodes == null || segmentIndex >= logicalNodes.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceNode = logicalNodes[segmentIndex];
|
||||
if (sourceNode == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sourceNode.SegmentLengthToNext = clamped;
|
||||
}
|
||||
|
||||
UpdateJointLimit(segmentIndex + 1, clamped);
|
||||
chainDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即按当前配置重建鱼线。
|
||||
/// </summary>
|
||||
public void RebuildNow()
|
||||
{
|
||||
BuildLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定顺序索引的逻辑节点。
|
||||
/// 索引基于 logicalNodes 配置顺序;超出范围或节点为空时返回 null。
|
||||
/// </summary>
|
||||
public FishingLineNode GetLogicalNode(int logicalIndex)
|
||||
{
|
||||
if (logicalNodes == null || logicalIndex < 0 || logicalIndex >= logicalNodes.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return logicalNodes[logicalIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取指定顺序索引的逻辑节点。
|
||||
/// 获取失败时返回 false,并将 node 置为 null。
|
||||
/// </summary>
|
||||
public bool TryGetLogicalNode(int logicalIndex, out FishingLineNode node)
|
||||
{
|
||||
node = GetLogicalNode(logicalIndex);
|
||||
return node != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前起点逻辑节点。
|
||||
/// 会返回配置顺序中第一个非空节点。
|
||||
/// </summary>
|
||||
public FishingLineNode GetStartNode()
|
||||
{
|
||||
return FindFirstValidLogicalNode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前终点逻辑节点。
|
||||
/// 会返回配置顺序中最后一个非空节点。
|
||||
/// </summary>
|
||||
public FishingLineNode GetEndNode()
|
||||
{
|
||||
return FindLastValidLogicalNode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前运行时链路中相邻采样点的实际距离。
|
||||
/// 这里的采样点包含逻辑节点和虚拟节点。
|
||||
/// </summary>
|
||||
public float GetActualDistance(int segmentIndex)
|
||||
{
|
||||
if (segmentIndex < 0 || segmentIndex >= chainPoints.Count - 1)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return Vector3.Distance(chainPoints[segmentIndex].Position, chainPoints[segmentIndex + 1].Position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前鱼线运行时调试摘要,包含链路结构、长度和极限状态信息。
|
||||
/// </summary>
|
||||
public string GetRuntimeDebugSummary()
|
||||
{
|
||||
var builder = new StringBuilder(512);
|
||||
builder.Append("chain:")
|
||||
.Append(chainDirty ? "dirty" : "ready")
|
||||
.Append(" logical:")
|
||||
.Append(LogicalNodeCount)
|
||||
.Append(" runtimeVirtual:")
|
||||
.Append(RuntimeVirtualNodeCount)
|
||||
.Append(" ordered:")
|
||||
.Append(OrderedNodeCount)
|
||||
.Append(" total:")
|
||||
.Append(TotalLineLength.ToString("F2"))
|
||||
.Append("m")
|
||||
.Append(" chain:")
|
||||
.Append(CurrentLogicalChainLength.ToString("F2"))
|
||||
.Append("m")
|
||||
.Append(" tension:")
|
||||
.Append(CurrentTensionRatio.ToString("F3"))
|
||||
.Append(" breakPercent:")
|
||||
.Append(CurrentBreakStretchPercent.ToString("F1"))
|
||||
.Append('%')
|
||||
.Append(" limit:")
|
||||
.Append(IsAtLimit ? "yes" : "no")
|
||||
.Append(" limitTime:")
|
||||
.Append(LimitStateTime.ToString("F2"))
|
||||
.Append("s");
|
||||
|
||||
for (var i = 0; i < chainPoints.Count; i++)
|
||||
{
|
||||
var point = chainPoints[i];
|
||||
builder.AppendLine()
|
||||
.Append('#')
|
||||
.Append(i)
|
||||
.Append(' ')
|
||||
.Append(point.GetDebugName())
|
||||
.Append(" pos:")
|
||||
.Append(point.Position);
|
||||
|
||||
if (point.IsLogical && point.LogicalNode != null)
|
||||
{
|
||||
builder.Append(" body:")
|
||||
.Append(point.LogicalNode.Body != null ? "yes" : "no");
|
||||
}
|
||||
|
||||
if (i < restLengths.Count)
|
||||
{
|
||||
builder.Append(" seg rest:")
|
||||
.Append(restLengths[i].ToString("F3"))
|
||||
.Append(" actual:")
|
||||
.Append(GetActualDistance(i).ToString("F3"));
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private void ConfigureStartNode()
|
||||
{
|
||||
if (logicalNodes == null || logicalNodes.Length == 0 || logicalNodes[0] == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var startNode = logicalNodes[0];
|
||||
startNode.Type = FishingLineNode.NodeType.Start;
|
||||
|
||||
if (startNode.Body != null)
|
||||
{
|
||||
startNode.Body.isKinematic = true;
|
||||
startNode.Body.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
startNode.Body.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
|
||||
}
|
||||
|
||||
UpdateAnchorNode();
|
||||
}
|
||||
|
||||
private void ConfigureLogicalJoints()
|
||||
{
|
||||
runtimeJoints.Clear();
|
||||
|
||||
if (logicalNodes == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 1; i < logicalNodes.Length; i++)
|
||||
{
|
||||
var current = logicalNodes[i];
|
||||
var previous = logicalNodes[i - 1];
|
||||
if (current == null || previous == null || current.Body == null || previous.Body == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
current.Body.solverIterations = jointSolverIterations;
|
||||
current.Body.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
current.Body.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
|
||||
|
||||
var joint = current.GetComponent<ConfigurableJoint>();
|
||||
if (joint == null)
|
||||
{
|
||||
joint = current.gameObject.AddComponent<ConfigurableJoint>();
|
||||
}
|
||||
|
||||
joint.autoConfigureConnectedAnchor = true;
|
||||
joint.connectedBody = previous.Body;
|
||||
joint.xMotion = ConfigurableJointMotion.Limited;
|
||||
joint.yMotion = ConfigurableJointMotion.Limited;
|
||||
joint.zMotion = ConfigurableJointMotion.Limited;
|
||||
joint.angularXMotion = ConfigurableJointMotion.Free;
|
||||
joint.angularYMotion = ConfigurableJointMotion.Free;
|
||||
joint.angularZMotion = ConfigurableJointMotion.Free;
|
||||
joint.projectionMode = JointProjectionMode.PositionAndRotation;
|
||||
joint.projectionDistance = jointProjectionDistance;
|
||||
joint.projectionAngle = jointProjectionAngle;
|
||||
|
||||
var limit = joint.linearLimit;
|
||||
limit.limit = GetSegmentLength(i - 1);
|
||||
joint.linearLimit = limit;
|
||||
|
||||
runtimeJoints.Add(joint);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAnchorNode()
|
||||
{
|
||||
if (anchorTransform == null || logicalNodes == null || logicalNodes.Length == 0 || logicalNodes[0] == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var startNode = logicalNodes[0];
|
||||
startNode.transform.SetPositionAndRotation(anchorTransform.position, anchorTransform.rotation);
|
||||
|
||||
if (startNode.Body != null)
|
||||
{
|
||||
if (!startNode.Body.isKinematic)
|
||||
{
|
||||
startNode.Body.linearVelocity = Vector3.zero;
|
||||
startNode.Body.angularVelocity = Vector3.zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EvaluateLimitState(float deltaTime)
|
||||
{
|
||||
CurrentLogicalChainLength = 0f;
|
||||
CurrentEndpointDistance = 0f;
|
||||
CurrentStretchLength = 0f;
|
||||
MaxSegmentStretchLength = 0f;
|
||||
MaxOverstretchedSegmentIndex = -1;
|
||||
CurrentTensionRatio = 0f;
|
||||
|
||||
if (logicalNodes == null || logicalNodes.Length < 2)
|
||||
{
|
||||
SetLimitState(false);
|
||||
UpdateBreakCountdown(deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
FishingLineNode firstNode = null;
|
||||
FishingLineNode lastNode = null;
|
||||
|
||||
for (var segmentIndex = 0; segmentIndex < logicalNodes.Length; segmentIndex++)
|
||||
{
|
||||
var node = logicalNodes[segmentIndex];
|
||||
if (node == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
firstNode ??= node;
|
||||
lastNode = node;
|
||||
|
||||
if (segmentIndex >= logicalNodes.Length - 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nextNode = logicalNodes[segmentIndex + 1];
|
||||
if (nextNode == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var actualDistance = Vector3.Distance(node.Position, nextNode.Position);
|
||||
var configuredLength = GetSegmentLength(segmentIndex);
|
||||
CurrentLogicalChainLength += actualDistance;
|
||||
|
||||
if (configuredLength > 0.0001f)
|
||||
{
|
||||
CurrentTensionRatio = Mathf.Max(CurrentTensionRatio, actualDistance / configuredLength);
|
||||
}
|
||||
|
||||
var segmentStretchLength = Mathf.Max(0f, actualDistance - configuredLength);
|
||||
if (segmentStretchLength > MaxSegmentStretchLength)
|
||||
{
|
||||
MaxSegmentStretchLength = segmentStretchLength;
|
||||
MaxOverstretchedSegmentIndex = segmentStretchLength > 0f ? segmentIndex : -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstNode != null && lastNode != null && !ReferenceEquals(firstNode, lastNode))
|
||||
{
|
||||
CurrentEndpointDistance = Vector3.Distance(firstNode.Position, lastNode.Position);
|
||||
}
|
||||
|
||||
if (TotalLineLength > 0.0001f)
|
||||
{
|
||||
CurrentStretchLength = Mathf.Max(0f, CurrentLogicalChainLength - TotalLineLength);
|
||||
CurrentTensionRatio = Mathf.Max(CurrentTensionRatio, CurrentLogicalChainLength / TotalLineLength);
|
||||
}
|
||||
else if (CurrentLogicalChainLength > 0f)
|
||||
{
|
||||
CurrentStretchLength = CurrentLogicalChainLength;
|
||||
CurrentTensionRatio = Mathf.Max(CurrentTensionRatio, 1f);
|
||||
}
|
||||
|
||||
var exceedsTotalLength = CurrentStretchLength > lengthLimitTolerance;
|
||||
var exceedsSegmentLength = MaxSegmentStretchLength > lengthLimitTolerance;
|
||||
SetLimitState(exceedsTotalLength || exceedsSegmentLength);
|
||||
UpdateBreakCountdown(deltaTime);
|
||||
}
|
||||
|
||||
private void SetLimitState(bool isAtLimit)
|
||||
{
|
||||
IsAtLimit = isAtLimit;
|
||||
}
|
||||
|
||||
private void UpdateBreakCountdown(float deltaTime)
|
||||
{
|
||||
if (!IsBreakCountdownActive)
|
||||
{
|
||||
LimitStateTime = 0f;
|
||||
HasBreakNotificationSent = false;
|
||||
return;
|
||||
}
|
||||
|
||||
LimitStateTime += Mathf.Max(0f, deltaTime);
|
||||
|
||||
if (HasBreakNotificationSent || LimitStateTime < breakLimitDuration)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HasBreakNotificationSent = true;
|
||||
NotifyLineBreakRequested();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发出鱼线达到断线条件的消息。
|
||||
/// 这里预留给外部订阅,当前不在求解器内部直接执行断线逻辑。
|
||||
/// </summary>
|
||||
private void NotifyLineBreakRequested()
|
||||
{
|
||||
OnLineBreakRequested?.Invoke(this);
|
||||
}
|
||||
|
||||
private void ResetLimitState()
|
||||
{
|
||||
CurrentLogicalChainLength = 0f;
|
||||
CurrentEndpointDistance = 0f;
|
||||
CurrentStretchLength = 0f;
|
||||
MaxSegmentStretchLength = 0f;
|
||||
MaxOverstretchedSegmentIndex = -1;
|
||||
CurrentTensionRatio = 0f;
|
||||
IsAtLimit = false;
|
||||
LimitStateTime = 0f;
|
||||
HasBreakNotificationSent = false;
|
||||
}
|
||||
|
||||
private float EvaluateBreakStretchPercent(float stretchLength)
|
||||
{
|
||||
if (stretchLength <= lengthLimitTolerance)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
if (stretchLength >= breakStretchThreshold)
|
||||
{
|
||||
return 100f;
|
||||
}
|
||||
|
||||
if (breakStretchThreshold <= lengthLimitTolerance)
|
||||
{
|
||||
return 100f;
|
||||
}
|
||||
|
||||
return Mathf.InverseLerp(lengthLimitTolerance, breakStretchThreshold, stretchLength) * 100f;
|
||||
}
|
||||
|
||||
private void RebuildRuntimeChain()
|
||||
{
|
||||
chainPoints.Clear();
|
||||
restLengths.Clear();
|
||||
pinnedIndices.Clear();
|
||||
TotalLineLength = 0f;
|
||||
runtimeVirtualPointCount = 0;
|
||||
|
||||
if (logicalNodes == null || logicalNodes.Length < 2)
|
||||
{
|
||||
chainDirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var segmentLayouts = new SegmentLayout[logicalNodes.Length - 1];
|
||||
for (var segmentIndex = 0; segmentIndex < segmentLayouts.Length; segmentIndex++)
|
||||
{
|
||||
segmentLayouts[segmentIndex] = BuildSegmentLayout(segmentIndex);
|
||||
}
|
||||
|
||||
AddLogicalPoint(logicalNodes[0], 0);
|
||||
pinnedIndices.Add(0);
|
||||
|
||||
for (var segmentIndex = 0; segmentIndex < segmentLayouts.Length; segmentIndex++)
|
||||
{
|
||||
var fromNode = logicalNodes[segmentIndex];
|
||||
var toNode = logicalNodes[segmentIndex + 1];
|
||||
if (fromNode == null || toNode == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var layout = segmentLayouts[segmentIndex];
|
||||
AddVirtualPoints(fromNode.Position, toNode.Position, layout, segmentIndex);
|
||||
|
||||
for (var gapIndex = 0; gapIndex < layout.GapLengths.Length; gapIndex++)
|
||||
{
|
||||
restLengths.Add(layout.GapLengths[gapIndex]);
|
||||
TotalLineLength += layout.GapLengths[gapIndex];
|
||||
}
|
||||
|
||||
AddLogicalPoint(toNode, segmentIndex + 1);
|
||||
pinnedIndices.Add(chainPoints.Count - 1);
|
||||
}
|
||||
|
||||
chainDirty = false;
|
||||
}
|
||||
|
||||
private SegmentLayout BuildSegmentLayout(int segmentIndex)
|
||||
{
|
||||
var totalLength = GetSegmentLength(segmentIndex);
|
||||
if (segmentIndex == 0)
|
||||
{
|
||||
return new SegmentLayout
|
||||
{
|
||||
GapLengths = BuildFirstSegmentGaps(totalLength, firstSegmentStep),
|
||||
};
|
||||
}
|
||||
|
||||
var sourceNode = logicalNodes[segmentIndex];
|
||||
var virtualCount = sourceNode != null ? sourceNode.FixedVirtualNodesToNext : 0;
|
||||
var gapCount = Mathf.Max(1, virtualCount + 1);
|
||||
var gapLength = totalLength / gapCount;
|
||||
var gaps = new float[gapCount];
|
||||
for (var i = 0; i < gaps.Length; i++)
|
||||
{
|
||||
gaps[i] = gapLength;
|
||||
}
|
||||
|
||||
return new SegmentLayout
|
||||
{
|
||||
GapLengths = gaps,
|
||||
};
|
||||
}
|
||||
|
||||
private void AddLogicalPoint(FishingLineNode logicalNode, int logicalIndex)
|
||||
{
|
||||
if (logicalNode == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
chainPoints.Add(new ChainPoint
|
||||
{
|
||||
Key = BuildLogicalPointKey(logicalIndex),
|
||||
Position = logicalNode.Position,
|
||||
IsLogical = true,
|
||||
LogicalNode = logicalNode,
|
||||
SegmentIndex = logicalIndex,
|
||||
StableIndex = logicalIndex,
|
||||
});
|
||||
}
|
||||
|
||||
private void AddVirtualPoints(Vector3 fromPosition, Vector3 toPosition, SegmentLayout layout, int segmentIndex)
|
||||
{
|
||||
if (layout.VirtualNodeCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var direction = toPosition - fromPosition;
|
||||
var distance = direction.magnitude;
|
||||
var normalizedDirection = distance > 0.0001f ? direction / distance : Vector3.down;
|
||||
var accumulatedDistance = 0f;
|
||||
|
||||
for (var virtualIndex = 0; virtualIndex < layout.VirtualNodeCount; virtualIndex++)
|
||||
{
|
||||
accumulatedDistance += layout.GapLengths[virtualIndex];
|
||||
var stableIndex = segmentIndex == 0
|
||||
? layout.VirtualNodeCount - 1 - virtualIndex
|
||||
: virtualIndex;
|
||||
|
||||
chainPoints.Add(new ChainPoint
|
||||
{
|
||||
Key = BuildVirtualPointKey(segmentIndex, stableIndex),
|
||||
Position = fromPosition + normalizedDirection * accumulatedDistance,
|
||||
IsLogical = false,
|
||||
LogicalNode = null,
|
||||
SegmentIndex = segmentIndex,
|
||||
StableIndex = stableIndex,
|
||||
});
|
||||
runtimeVirtualPointCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncLogicalPointPositions()
|
||||
{
|
||||
for (var i = 0; i < chainPoints.Count; i++)
|
||||
{
|
||||
var point = chainPoints[i];
|
||||
if (!point.IsLogical || point.LogicalNode == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
point.Position = point.LogicalNode.Position;
|
||||
}
|
||||
}
|
||||
|
||||
private static float[] BuildFirstSegmentGaps(float totalLength, float step)
|
||||
{
|
||||
if (totalLength <= 0f)
|
||||
{
|
||||
return new[] { 0f };
|
||||
}
|
||||
|
||||
if (totalLength < step)
|
||||
{
|
||||
return new[] { totalLength };
|
||||
}
|
||||
|
||||
var fullStepCount = Mathf.FloorToInt(totalLength / step);
|
||||
var remainder = totalLength - (fullStepCount * step);
|
||||
if (remainder > 0.0001f)
|
||||
{
|
||||
var gaps = new float[fullStepCount + 1];
|
||||
gaps[0] = remainder;
|
||||
for (var i = 1; i < gaps.Length; i++)
|
||||
{
|
||||
gaps[i] = step;
|
||||
}
|
||||
|
||||
return gaps;
|
||||
}
|
||||
|
||||
var divisibleGaps = new float[fullStepCount];
|
||||
for (var i = 0; i < divisibleGaps.Length; i++)
|
||||
{
|
||||
divisibleGaps[i] = step;
|
||||
}
|
||||
|
||||
return divisibleGaps;
|
||||
}
|
||||
|
||||
|
||||
private void UpdateJointLimit(int logicalNodeIndex, float limitValue)
|
||||
{
|
||||
if (logicalNodeIndex <= 0 || logicalNodeIndex >= logicalNodes.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var node = logicalNodes[logicalNodeIndex];
|
||||
if (node == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var joint = node.GetComponent<ConfigurableJoint>();
|
||||
if (joint == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var limit = joint.linearLimit;
|
||||
limit.limit = limitValue;
|
||||
joint.linearLimit = limit;
|
||||
}
|
||||
|
||||
private float GetSegmentLength(int segmentIndex)
|
||||
{
|
||||
if (segmentIndex <= 0)
|
||||
{
|
||||
return firstSegmentLength;
|
||||
}
|
||||
|
||||
if (logicalNodes == null || segmentIndex >= logicalNodes.Length)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var sourceNode = logicalNodes[segmentIndex];
|
||||
return sourceNode != null ? sourceNode.SegmentLengthToNext : 0f;
|
||||
}
|
||||
|
||||
private FishingLineNode FindFirstValidLogicalNode()
|
||||
{
|
||||
if (logicalNodes == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var i = 0; i < logicalNodes.Length; i++)
|
||||
{
|
||||
if (logicalNodes[i] != null)
|
||||
{
|
||||
return logicalNodes[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private FishingLineNode FindLastValidLogicalNode()
|
||||
{
|
||||
if (logicalNodes == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var i = logicalNodes.Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (logicalNodes[i] != null)
|
||||
{
|
||||
return logicalNodes[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long BuildLogicalPointKey(int logicalIndex)
|
||||
{
|
||||
return (1L << 62) | (uint)logicalIndex;
|
||||
}
|
||||
|
||||
private static long BuildVirtualPointKey(int segmentIndex, int stableIndex)
|
||||
{
|
||||
return ((long)(segmentIndex + 1) << 32) | (uint)stableIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dcd0fd8d96f994444b2d8663af6b915d
|
||||
@@ -0,0 +1,103 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class FishingLineTestController : MonoBehaviour
|
||||
{
|
||||
[Header("References")] [SerializeField]
|
||||
private FishingLineSolver solver;
|
||||
|
||||
[Header("Length Test")] [Min(0f)] [SerializeField]
|
||||
private float initialFirstSegmentLength = 1.2f;
|
||||
|
||||
[Min(0f)] [SerializeField] private float minFirstSegmentLength = 0.1f;
|
||||
[Min(0f)] [SerializeField] private float maxFirstSegmentLength = 5f;
|
||||
[Min(0f)] [SerializeField] private float lineAdjustSpeed = 1f;
|
||||
|
||||
[Header("Input")] [SerializeField] private KeyCode extendKey = KeyCode.UpArrow;
|
||||
[SerializeField] private KeyCode retractKey = KeyCode.DownArrow;
|
||||
|
||||
private float targetFirstSegmentLength;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
if (solver == null)
|
||||
{
|
||||
solver = GetComponent<FishingLineSolver>();
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (solver == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
targetFirstSegmentLength =
|
||||
Mathf.Clamp(initialFirstSegmentLength, minFirstSegmentLength, maxFirstSegmentLength);
|
||||
solver.SetLenght(targetFirstSegmentLength);
|
||||
solver.BuildLine();
|
||||
|
||||
solver.OnLineBreakRequested += OnLineBreakRequested;
|
||||
}
|
||||
|
||||
private void OnLineBreakRequested(FishingLineSolver lineSolver)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"当前拉力达到极限,切线,极限时间={lineSolver.LimitStateTime} CurrentStretchLength={lineSolver.CurrentStretchLength} CurrentTensionRatio={lineSolver.CurrentTensionRatio}");
|
||||
var endNode = lineSolver.GetEndNode();
|
||||
if (endNode != null)
|
||||
{
|
||||
endNode.Body.isKinematic = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (solver == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var input = 0f;
|
||||
if (Input.GetKey(extendKey))
|
||||
{
|
||||
input += 1f;
|
||||
}
|
||||
|
||||
if (Input.GetKey(retractKey))
|
||||
{
|
||||
input -= 1f;
|
||||
}
|
||||
|
||||
if (!Mathf.Approximately(input, 0f))
|
||||
{
|
||||
targetFirstSegmentLength += input * lineAdjustSpeed * Time.deltaTime;
|
||||
targetFirstSegmentLength =
|
||||
Mathf.Clamp(targetFirstSegmentLength, minFirstSegmentLength, maxFirstSegmentLength);
|
||||
solver.SetLenght(targetFirstSegmentLength);
|
||||
}
|
||||
|
||||
if (solver.CurrentBreakStretchPercent > 0)
|
||||
{
|
||||
// Debug.LogError(solver.CurrentBreakStretchPercent);
|
||||
}
|
||||
// if (solver.IsAtLimit)
|
||||
// {
|
||||
// if (solver.CurrentStretchLength > 0.04)
|
||||
// Debug.LogError($"CurrentStretchLength={solver.CurrentStretchLength}");
|
||||
// if (solver.CurrentStretchLength > 0.1 && solver.LimitStateTime > 2f)
|
||||
// {
|
||||
// Debug.LogError(
|
||||
// $"当前拉力达到极限,切线,极限时间={solver.LimitStateTime} CurrentStretchLength={solver.CurrentStretchLength} CurrentTensionRatio={solver.CurrentTensionRatio}");
|
||||
// var endNode = solver.GetEndNode();
|
||||
// if (endNode != null)
|
||||
// {
|
||||
// endNode.Body.isKinematic = false;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77af114bb80f3904a83cdeaacd5af508
|
||||
Reference in New Issue
Block a user