3 Commits

Author SHA1 Message Date
69bb9c93fb 修改提交 2026-04-13 20:48:47 +08:00
fbe33f9f3a 代码提交 2026-04-12 23:52:07 +08:00
95700b71c1 修改线的渲染长度不一致问题 2026-04-12 23:19:41 +08:00
38 changed files with 5880 additions and 1232 deletions

View File

@@ -18553,6 +18553,21 @@ MonoBehaviour:
- {fileID: 102900000, guid: 60ac19fbbe20cec48add96b79332c113, type: 3}
FilterEnum: 0
Filter: '*'
- Path: Assets/ResRaw/Prefabs/Line/fishing line float set 1.prefab
Address: Plyaer/fishing line float set 1
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/fishing line float set.prefab
Address: Plyaer/fishing line float set
Type: GameObject
@@ -18628,21 +18643,6 @@ MonoBehaviour:
- {fileID: 102900000, guid: aa3f5467c0c153642ac320466aee0ec1, type: 3}
FilterEnum: 0
Filter: '*'
- Path: Assets/ResRaw/Prefabs/Line/Line1.prefab
Address: Plyaer/Line1
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/LineHand1.prefab
Address: Plyaer/LineHand1
Type: GameObject

Binary file not shown.

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 953400dda9b03df4d967e2e7dd8ef9f8
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 15600000
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: e5b77e954e13d8644859b63f2252924f
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 15600000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: ea6901d8aa7c41d41987d8ca92b02f6d
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,6 +1,6 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1386063717907585334
--- !u!1 &1035052809208993
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -8,283 +8,40 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4162208118158024875}
- component: {fileID: 6741752443570310990}
m_Layer: 0
m_Name: Line1
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &4162208118158024875
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1386063717907585334}
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: 1479551252035771110}
- {fileID: 153991853389016720}
- {fileID: 6385925521264460254}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &6741752443570310990
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1386063717907585334}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 78dc478e56ff48849761861244c93535, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineSolver
ConfigId: 0
LineType: 0
anchorTransform: {fileID: 0}
logicalNodes:
- {fileID: 9075018646815510674}
- {fileID: 3453378298765533283}
- {fileID: 7410869349490881487}
PinchController: {fileID: 0}
lengthLimitTolerance: 0.01
breakStretchThreshold: 0.05
--- !u!1 &2899898627497558851
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 153991853389016720}
- component: {fileID: 828578718743757123}
- component: {fileID: 9053429622791349644}
- component: {fileID: 3453378298765533283}
m_Layer: 15
m_Name: Float
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
--- !u!4 &153991853389016720
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2899898627497558851}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: -1, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4162208118158024875}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!54 &828578718743757123
Rigidbody:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2899898627497558851}
serializedVersion: 5
m_Mass: 0.1
m_LinearDamping: 1
m_AngularDamping: 0.1
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 0.001, y: 0.001, z: 0.001}
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: 0
m_UseGravity: 1
m_IsKinematic: 0
m_Interpolate: 0
m_Constraints: 0
m_CollisionDetection: 2
--- !u!153 &9053429622791349644
ConfigurableJoint:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2899898627497558851}
serializedVersion: 4
m_ConnectedBody: {fileID: 2686136634991136867}
m_ConnectedArticulationBody: {fileID: 0}
m_Anchor: {x: 0, y: -0.01, z: 0}
m_Axis: {x: 0, y: 0, z: 0}
m_AutoConfigureConnectedAnchor: 0
m_ConnectedAnchor: {x: 0, y: 0, z: 0}
m_SecondaryAxis: {x: 0, y: 0, z: 0}
m_XMotion: 1
m_YMotion: 1
m_ZMotion: 1
m_AngularXMotion: 2
m_AngularYMotion: 2
m_AngularZMotion: 2
m_LinearLimitSpring:
spring: 0
damper: 0
m_LinearLimit:
limit: 0.5
bounciness: 0
contactDistance: 0
m_AngularXLimitSpring:
spring: 0
damper: 0
m_LowAngularXLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_HighAngularXLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_AngularYZLimitSpring:
spring: 0
damper: 0
m_AngularYLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_AngularZLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_TargetPosition: {x: 0, y: 0, z: 0}
m_TargetVelocity: {x: 0, y: 0, z: 0}
m_XDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_YDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_ZDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_TargetRotation: {x: 0, y: 0, z: 0, w: 1}
m_TargetAngularVelocity: {x: 0, y: 0, z: 0}
m_RotationDriveMode: 0
m_AngularXDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_AngularYZDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_SlerpDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_ProjectionMode: 1
m_ProjectionDistance: 0
m_ProjectionAngle: 0
m_ConfiguredInWorldSpace: 0
m_SwapBodies: 0
m_BreakForce: Infinity
m_BreakTorque: Infinity
m_EnableCollision: 0
m_EnablePreprocessing: 0
m_MassScale: 1
m_ConnectedMassScale: 1
--- !u!114 &3453378298765533283
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2899898627497558851}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f89affce787d4a1cbcd68bed409183d7, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineNode
_solver: {fileID: 0}
nodeType: 1
body: {fileID: 828578718743757123}
Rope: {fileID: 0}
interaction: {fileID: 0}
_joint: {fileID: 9053429622791349644}
segmentLengthToNext: 0.5
runtimeChainIndex: -1
features: []
motionFeatures: []
activeMotionFeature: {fileID: 0}
--- !u!1 &6377338098506485345
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6385925521264460254}
- component: {fileID: 3393178238064969316}
- component: {fileID: 1271283185862622601}
- component: {fileID: 8513982619376093011}
- component: {fileID: 7410869349490881487}
- component: {fileID: 4026445325167852}
- component: {fileID: 54679398375713381}
- component: {fileID: 153611279189314279}
- component: {fileID: 135844594273256032}
- component: {fileID: 7176287465780574680}
m_Layer: 15
m_Name: Lure
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
--- !u!4 &6385925521264460254
m_IsActive: 1
--- !u!4 &4026445325167852
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6377338098506485345}
m_GameObject: {fileID: 1035052809208993}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: -1.5, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4162208118158024875}
m_Father: {fileID: 4283454774123242}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!54 &3393178238064969316
--- !u!54 &54679398375713381
Rigidbody:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6377338098506485345}
m_GameObject: {fileID: 1035052809208993}
serializedVersion: 5
m_Mass: 0.01
m_LinearDamping: 1
@@ -300,20 +57,20 @@ Rigidbody:
m_Bits: 0
m_ImplicitCom: 1
m_ImplicitTensor: 0
m_UseGravity: 1
m_UseGravity: 0
m_IsKinematic: 0
m_Interpolate: 0
m_Constraints: 0
m_CollisionDetection: 2
--- !u!153 &1271283185862622601
--- !u!153 &153611279189314279
ConfigurableJoint:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6377338098506485345}
m_GameObject: {fileID: 1035052809208993}
serializedVersion: 4
m_ConnectedBody: {fileID: 828578718743757123}
m_ConnectedBody: {fileID: 54298866000586118}
m_ConnectedArticulationBody: {fileID: 0}
m_Anchor: {x: 0, y: 0, z: 0}
m_Axis: {x: 0, y: 0, z: 0}
@@ -407,13 +164,13 @@ ConfigurableJoint:
m_EnablePreprocessing: 0
m_MassScale: 1
m_ConnectedMassScale: 1
--- !u!135 &8513982619376093011
--- !u!135 &135844594273256032
SphereCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6377338098506485345}
m_GameObject: {fileID: 1035052809208993}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
@@ -428,30 +185,21 @@ SphereCollider:
serializedVersion: 3
m_Radius: 0.003
m_Center: {x: 0, y: -0.0015, z: 0}
--- !u!114 &7410869349490881487
--- !u!114 &7176287465780574680
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6377338098506485345}
m_GameObject: {fileID: 1035052809208993}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f89affce787d4a1cbcd68bed409183d7, type: 3}
m_Script: {fileID: 11500000, guid: ee9704c2e1594f4cab270bfd4ca2210b, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineNode
_solver: {fileID: 0}
nodeType: 3
body: {fileID: 3393178238064969316}
Rope: {fileID: 0}
interaction: {fileID: 0}
_joint: {fileID: 1271283185862622601}
segmentLengthToNext: 0.5
runtimeChainIndex: -1
features: []
motionFeatures: []
activeMotionFeature: {fileID: 0}
--- !u!1 &7697635668067303442
body: {fileID: 54679398375713381}
--- !u!1 &1387836627839849
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -459,44 +207,94 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1479551252035771110}
- component: {fileID: 2686136634991136867}
- component: {fileID: 9075018646815510674}
- component: {fileID: 4283454774123242}
- component: {fileID: 7859179875146676465}
m_Layer: 0
m_Name: Start
m_Name: fishing line float set 1
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
--- !u!4 &1479551252035771110
m_IsActive: 1
--- !u!4 &4283454774123242
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7697635668067303442}
m_GameObject: {fileID: 1387836627839849}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4162208118158024875}
m_Children:
- {fileID: 4530253318796540}
- {fileID: 4026445325167852}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!54 &2686136634991136867
--- !u!114 &7859179875146676465
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1387836627839849}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fd39a2e024a0477c9ad5698d80d9a63a, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineSolver
logicalNodes:
- {fileID: 8342656697567645068}
- {fileID: 7176287465780574680}
--- !u!1 &1858052053854210
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4530253318796540}
- component: {fileID: 54298866000586118}
- component: {fileID: 153691655494134957}
- component: {fileID: 8342656697567645068}
m_Layer: 15
m_Name: Float
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &4530253318796540
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1858052053854210}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: -1, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4283454774123242}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!54 &54298866000586118
Rigidbody:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7697635668067303442}
m_GameObject: {fileID: 1858052053854210}
serializedVersion: 5
m_Mass: 1
m_LinearDamping: 0
m_AngularDamping: 0.05
m_Mass: 0.1
m_LinearDamping: 1
m_AngularDamping: 0.1
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 1, y: 1, z: 1}
m_InertiaTensor: {x: 0.001, y: 0.001, z: 0.001}
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
m_IncludeLayers:
serializedVersion: 2
@@ -504,33 +302,126 @@ Rigidbody:
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_ImplicitCom: 0
m_ImplicitCom: 1
m_ImplicitTensor: 0
m_UseGravity: 0
m_IsKinematic: 1
m_UseGravity: 1
m_IsKinematic: 0
m_Interpolate: 0
m_Constraints: 0
m_CollisionDetection: 0
--- !u!114 &9075018646815510674
m_CollisionDetection: 2
--- !u!153 &153691655494134957
ConfigurableJoint:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1858052053854210}
serializedVersion: 4
m_ConnectedBody: {fileID: 0}
m_ConnectedArticulationBody: {fileID: 0}
m_Anchor: {x: 0, y: -0.01, z: 0}
m_Axis: {x: 0, y: 0, z: 0}
m_AutoConfigureConnectedAnchor: 0
m_ConnectedAnchor: {x: 0, y: 0, z: 0}
m_SecondaryAxis: {x: 0, y: 0, z: 0}
m_XMotion: 1
m_YMotion: 1
m_ZMotion: 1
m_AngularXMotion: 2
m_AngularYMotion: 2
m_AngularZMotion: 2
m_LinearLimitSpring:
spring: 0
damper: 0
m_LinearLimit:
limit: 0.5
bounciness: 0
contactDistance: 0
m_AngularXLimitSpring:
spring: 0
damper: 0
m_LowAngularXLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_HighAngularXLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_AngularYZLimitSpring:
spring: 0
damper: 0
m_AngularYLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_AngularZLimit:
limit: 0
bounciness: 0
contactDistance: 0
m_TargetPosition: {x: 0, y: 0, z: 0}
m_TargetVelocity: {x: 0, y: 0, z: 0}
m_XDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_YDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_ZDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_TargetRotation: {x: 0, y: 0, z: 0, w: 1}
m_TargetAngularVelocity: {x: 0, y: 0, z: 0}
m_RotationDriveMode: 0
m_AngularXDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_AngularYZDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_SlerpDrive:
serializedVersion: 4
positionSpring: 0
positionDamper: 0
maximumForce: 3.4028233e+38
useAcceleration: 0
m_ProjectionMode: 1
m_ProjectionDistance: 0
m_ProjectionAngle: 0
m_ConfiguredInWorldSpace: 0
m_SwapBodies: 0
m_BreakForce: Infinity
m_BreakTorque: Infinity
m_EnableCollision: 0
m_EnablePreprocessing: 0
m_MassScale: 1
m_ConnectedMassScale: 1
--- !u!114 &8342656697567645068
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7697635668067303442}
m_GameObject: {fileID: 1858052053854210}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f89affce787d4a1cbcd68bed409183d7, type: 3}
m_Script: {fileID: 11500000, guid: ee9704c2e1594f4cab270bfd4ca2210b, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::NBF.FishingLineNode
_solver: {fileID: 0}
nodeType: 0
body: {fileID: 2686136634991136867}
Rope: {fileID: 0}
interaction: {fileID: 0}
_joint: {fileID: 0}
segmentLengthToNext: 0.5
runtimeChainIndex: -1
features: []
motionFeatures: []
activeMotionFeature: {fileID: 0}
nodeType: 1
body: {fileID: 54298866000586118}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 449040ff936513f4ba4b9715db2f4bf7
timeCreated: 1762387921
licenseType: Free
PrefabImporter:
externalObjects: {}
addedObjectFileIDs:
isPrefabVariant: 0
variantParentGUID: 00000000000000000000000000000000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -1630,8 +1630,8 @@ Camera:
y: 0
width: 1
height: 1
near clip plane: 0.1
far clip plane: 3000
near clip plane: 0.01
far clip plane: 5000
field of view: 60.000004
orthographic: 0
orthographic size: 5

View File

@@ -2,6 +2,8 @@
using Fantasy;
using Fantasy.Async;
using Fantasy.Entitas;
using NBF.Fishing2;
using RootMotion.FinalIK;
using Log = NBC.Log;
namespace NBF

View File

@@ -1,3 +1,3 @@
fileFormatVersion: 2
guid: 23b031de32454768b4cd922619ef4e8e
timeCreated: 1776227094
guid: 11c32e9f078948c880079b4903f7c8bd
timeCreated: 1776008844

View File

@@ -1,3 +1,8 @@
fileFormatVersion: 2
guid: aecb364a5aa3486d9e9c4f37ba801403
timeCreated: 1776227164
guid: 2ff164cb3132445289d22c7b3a0f4fab
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,685 @@
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 RigidbodyConstraints _defaultConstraints;
private float _draftVelocity;
private float _currentDraftDepth;
private float _ySmoothVelocity;
private float _biteOffsetY;
private float _biteOffsetYVelocity;
private bool _uprightPoseCached;
private Quaternion _cachedUprightRotation;
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);
var nextRotation = CalculateNextRotation(deltaTime);
UpdateVerticalPosition(deltaTime, nextRotation);
ApplyRotation(nextRotation);
}
#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;
_defaultConstraints = _rb.constraints;
_defaultsCached = true;
}
_currentDraftDepth = CalculateRawDraftDepth();
_draftVelocity = 0f;
_ySmoothVelocity = 0f;
_biteOffsetY = 0f;
_biteOffsetYVelocity = 0f;
_targetRotation = _rb.rotation;
if (!_uprightPoseCached)
{
_cachedUprightRotation = _rb.rotation;
_uprightPoseCached = true;
}
_uprightReferenceRotation = _cachedUprightRotation;
}
private void EnterWaterPresentationMode()
{
if (_rb == null)
{
return;
}
_mode = FishingBobberControlMode.WaterPresentation;
_waterStateInitialized = true;
_uprightReferenceRotation = _cachedUprightRotation;
_targetRotation = _rb.rotation;
_draftVelocity = 0f;
_ySmoothVelocity = 0f;
_biteOffsetYVelocity = 0f;
_currentDraftDepth = CalculateRawDraftDepth();
_rb.useGravity = false;
_rb.angularDamping = waterAngularDamping;
_rb.constraints = _defaultConstraints | RigidbodyConstraints.FreezeRotation;
_rb.angularVelocity = Vector3.zero;
}
private void ExitWaterPresentationMode()
{
_mode = FishingBobberControlMode.AirPhysics;
RestorePhysicsState();
}
private void RestorePhysicsState()
{
if (_rb == null || !_defaultsCached)
{
return;
}
_rb.useGravity = _defaultUseGravity;
_rb.angularDamping = _defaultAngularDamping;
_rb.constraints = _defaultConstraints;
}
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, Quaternion targetRotation)
{
var position = _rb.position;
var targetY = waterLevel - _currentDraftDepth - GetBottomOffsetWorldY(targetRotation) + _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);
}
_rb.MovePosition(position);
var velocity = _rb.linearVelocity;
if (Mathf.Abs(velocity.y) > 0f)
{
velocity.y = 0f;
_rb.linearVelocity = velocity;
}
}
private Quaternion CalculateNextRotation(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;
return Quaternion.Slerp(
_rb.rotation,
_targetRotation,
1f - Mathf.Exp(-Mathf.Max(0.01f, rotationLerpSpeed) * deltaTime));
}
private void ApplyRotation(Quaternion nextRotation)
{
_rb.rotation = nextRotation;
}
private float GetBottomOffsetWorldY(Quaternion rotation)
{
return (rotation * new Vector3(0f, bottomOffsetLocalY, 0f)).y;
}
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
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ca4d5d54d89446b0a10b7ce521fd7d9e
timeCreated: 1775958532

View File

@@ -0,0 +1,44 @@
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.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;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2ebfa4366b504ba0a3f398eded17df31
timeCreated: 1775957743

View File

@@ -0,0 +1,890 @@
using UnityEngine;
namespace NBF
{
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 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>();
}
}
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)
{
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
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 40a38940e81046e2854add979cedbef9
timeCreated: 1775958531

View File

@@ -12,12 +12,12 @@ namespace NBF
/// <summary>
/// 当前功能组件所属的鱼线求解器。
/// </summary>
public FLine Solver { get; private set; }
public FishingLineSolver Solver { get; private set; }
/// <summary>
/// 将当前功能组件绑定到指定节点和求解器。
/// </summary>
public void Bind(FishingLineNode node, FLine solver)
public void Bind(FishingLineNode node, FishingLineSolver solver)
{
Node = node;
Solver = solver;

View File

@@ -1,3 +1,3 @@
fileFormatVersion: 2
guid: ad2d8ec3c7054440819bc7a15991f724
timeCreated: 1776227197
guid: c7ea70945db841deb0ee8df85f0e15ec
timeCreated: 1775957663

View File

@@ -1,3 +1,3 @@
fileFormatVersion: 2
guid: aabd7c367ac74642942c4a4499e35281
timeCreated: 1776227237
guid: 7e75217306a64f8f868f5f9127772de2
timeCreated: 1775957711

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine;
namespace NBF
{
@@ -14,29 +12,9 @@ namespace NBF
Tail
}
[SerializeField] private FLine _solver;
private FishingLineSolver _solver;
[Header("Node")] [SerializeField] private NodeType nodeType = NodeType.Tail;
[SerializeField] public Rigidbody body;
public Rope Rope;
[SerializeField] private MonoBehaviour interaction;
[SerializeField] private ConfigurableJoint _joint;
[Header("Segment To Next Logical Node")] [Min(0f)] [SerializeField]
private float segmentLengthToNext = 0.5f;
[SerializeField] private int runtimeChainIndex = -1;
[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;
[SerializeField] private Rigidbody body;
public NodeType Type
{
@@ -44,218 +22,12 @@ namespace NBF
set => nodeType = value;
}
public float Lenght => _joint != null ? _joint.linearLimit.limit : 0f;
public Rigidbody Body => body;
public MonoBehaviour Interaction => interaction;
public ConfigurableJoint Joint => _joint;
public int RuntimeChainIndex => runtimeChainIndex;
public Vector3 Position => transform.position;
private void Reset()
{
TryGetComponent(out body);
}
private void Awake()
{
if (!_solver) _solver = GetComponentInParent<FLine>();
if (!_joint) _joint = GetComponent<ConfigurableJoint>();
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);
}
#region Line
public void SetLenght(float lenght)
{
if (!_joint) return;
if (!Mathf.Approximately(lenght, _joint.linearLimit.limit))
{
_joint.linearLimit = new SoftJointLimit() { limit = lenght };
Rope.SetTargetLength(lenght - 0.1f);
}
}
#endregion
#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(FLine 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()
{
_solver = GetComponentInParent<FishingLineSolver>();
body = GetComponent<Rigidbody>();
}
}
}

View File

@@ -1,3 +1,3 @@
fileFormatVersion: 2
guid: f89affce787d4a1cbcd68bed409183d7
timeCreated: 1776227097
guid: ee9704c2e1594f4cab270bfd4ca2210b
timeCreated: 1776008915

View File

@@ -0,0 +1,19 @@
using UnityEngine;
namespace NBF
{
[RequireComponent(typeof(LineRenderer))]
public class FishingLineRenderer : MonoBehaviour
{
[Header("References")] [SerializeField]
private FishingLineSolver solver;
[SerializeField] private LineRenderer lineRenderer;
private void Awake()
{
lineRenderer = GetComponent<LineRenderer>();
solver = GetComponent<FishingLineSolver>();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 14537bf263784fcd8aaf130eb3c51350
timeCreated: 1776084149

View File

@@ -1,122 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace NBF
{
public enum LineType
public class FishingLineSolver : MonoBehaviour
{
Hand,
HandDouble,
Spinning,
SpinningFloat,
}
public class FishingLineSolver : FGearBase
{
[SerializeField] public LineType LineType;
[Header("References")] [SerializeField]
private Transform anchorTransform;
[SerializeField] private FishingLineNode[] logicalNodes = Array.Empty<FishingLineNode>();
public JointPinchController PinchController;
protected override void OnInit()
{
// var tipRb = Rod.Asset.LineConnectorRigidbody;
// anchorTransform = tipRb.transform;
//
// GetComponentsInChildren<Transform>(includeInactive: true).ToList().ForEach(delegate(Transform i)
// {
// i.gameObject.SetActive(true);
// });
}
private void Awake()
{
}
private void Start()
{
InitRenderer();
GetComponentsInChildren<Transform>(includeInactive: true).ToList().ForEach(delegate(Transform i)
{
i.gameObject.SetActive(true);
});
}
private void FixedUpdate()
{
UpdateAnchorNode();
UpdateStretchLength();
}
#region Start Node
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 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;
}
}
}
#endregion
#region Line
/// <summary>
/// 设置指定逻辑段的配置长度。
/// segmentIndex 为 0 时表示第一段;大于 0 时表示对应逻辑节点到下一个逻辑节点的线长。
/// </summary>
public void SetLenght(float length, int index = 0)
{
ConfigureStartNode();
var node = logicalNodes[index + 1];
if (node != null)
{
node.SetLenght(length);
}
}
#endregion
#region LineNode
@@ -126,23 +17,6 @@ namespace NBF
/// </summary>
public IReadOnlyList<FishingLineNode> LogicalNodes => logicalNodes;
/// <summary>
/// 根据类型获取逻辑节点类型
/// </summary>
/// <param name="nodeType"></param>
/// <returns></returns>
public FishingLineNode GetLogicalNode(FishingLineNode.NodeType nodeType)
{
foreach (var fishingLineNode in logicalNodes)
{
if (fishingLineNode.Type == nodeType)
{
return fishingLineNode;
}
}
return null;
}
/// <summary>
/// 获取指定顺序索引的逻辑节点。
@@ -213,100 +87,6 @@ namespace NBF
}
#endregion
#region
/// <summary>
/// 当前逻辑链总长度超出配置总长度的部分,小于等于零时记为 0。
/// </summary>
[Header("Limit Detection")]
public float CurrentStretchLength { get; private set; }
[Min(0f)]
// 极限判定的长度容差,允许链路在总长或单段长度上存在少量误差。
[SerializeField]
private float lengthLimitTolerance = 0.01f;
[Min(0f)]
// 达到极限后,只有当前超长值大于该阈值时,才开始进入断线候选计时。
[SerializeField]
private float breakStretchThreshold = 0.05f;
/// <summary>
/// 当鱼线达到断线条件时发出的一次性消息。
/// 外部可订阅该事件,在回调中执行切线、播放表现或状态切换。
/// </summary>
public event Action<FishingLineSolver> OnLineBreakRequested;
/// <summary>
/// 当前断线候选状态的累计时间。
/// 只有在处于极限状态,且 CurrentStretchLength 大于断线阈值时才会累加;否则重置为 0。
/// </summary>
public float LimitStateTime { get; private set; }
/// <summary>
/// 当前拉力极限百分比。
/// 当超长值小于等于 lengthLimitTolerance 时为 0
/// 当超长值大于等于 breakStretchThreshold 时为 100
/// 中间区间按线性比例映射,供 UI 显示使用。
/// </summary>
public float CurrentBreakStretchPercent => EvaluateBreakStretchPercent(CurrentStretchLength);
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 UpdateStretchLength()
{
}
#endregion
#region Renderer
private Transform _ropeRoot;
private void InitRenderer()
{
var root = new GameObject("RopeRoot");
_ropeRoot = root.transform;
_ropeRoot.SetParent(transform);
CreateRopes();
}
private void CreateRopes()
{
foreach (var node in LogicalNodes)
{
if (node.Type == FishingLineNode.NodeType.Start) continue;
var ropeObject = new GameObject($"rope_{node.Type}");
ropeObject.transform.SetParent(_ropeRoot);
var rope = ropeObject.AddComponent<Rope>();
node.Rope = rope;
// rope.groundMask = LayerMask.GetMask("Terrain");
rope.startAnchor = node.Joint.connectedBody;
rope.endAnchor = node.body;
}
}
#endregion
}
}

View File

@@ -1,3 +1,3 @@
fileFormatVersion: 2
guid: 78dc478e56ff48849761861244c93535
timeCreated: 1776227360
guid: fd39a2e024a0477c9ad5698d80d9a63a
timeCreated: 1776008869

View File

@@ -1,88 +0,0 @@
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.SetLenght(0.5f, 1);
// solver.BuildLine();
solver.OnLineBreakRequested += OnLineBreakRequested;
}
private void OnLineBreakRequested(FishingLineSolver lineSolver)
{
Debug.LogError($"当前拉力达到极限,切线,极限时间={lineSolver.LimitStateTime}");
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);
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 5382d66f55f6463cb469c5094b0e7a6b
timeCreated: 1776234046

View File

@@ -8,12 +8,20 @@ using UnityEngine;
namespace NBF
{
public enum LineType
{
Hand,
HandDouble,
Spinning,
SpinningFloat,
}
public class FLine : FGearBase
{
public LineType LineType;
[SerializeField] private bool isLureConnect;
[SerializeField] private RodLine rodLine;
/// <summary>
/// 主线
@@ -33,9 +41,6 @@ namespace NBF
public float LinelenghtDiferent;
public float Length { get; private set; }
protected override void OnInit()
{
var tipRb = Rod.Asset.LineConnectorRigidbody;
@@ -60,6 +65,17 @@ namespace NBF
});
StartCoroutine(LureUseGravity());
if (isLureConnect)
{
fishingRope.Init(Rod);
}
else
{
fishingRope.Init(Rod);
bobberRope.Init(Rod);
}
// rodLine.GenerateLineRendererRope(guides.ToArray(), _LineThickness);
}
public void InitTest(Rigidbody tipRb)
@@ -85,6 +101,15 @@ namespace NBF
});
StartCoroutine(LureUseGravity());
if (isLureConnect)
{
fishingRope.Init(Rod);
}
else
{
fishingRope.Init(Rod);
bobberRope.Init(Rod);
}
}
private IEnumerator LureUseGravity()
@@ -132,7 +157,7 @@ namespace NBF
}
// return 0;
//第一个节点到竿稍的位置-第一段鱼线长度
return Vector3.Distance(Bobber.transform.position, Bobber.JointRb.transform.position) -
fishingRope.GetCurrentLength();
@@ -144,61 +169,5 @@ namespace NBF
}
#endregion
#region
[Header("Limit Detection")]
[Min(0f)]
// 极限判定的长度容差,允许链路在总长或单段长度上存在少量误差。
[SerializeField]
private float lengthLimitTolerance = 0.01f;
[Min(0f)]
// 达到极限后,只有当前超长值大于该阈值时,才开始进入断线候选计时。
[SerializeField]
private float breakStretchThreshold = 0.05f;
/// <summary>
/// 当鱼线达到断线条件时发出的一次性消息。
/// 外部可订阅该事件,在回调中执行切线、播放表现或状态切换。
/// </summary>
public event Action<FLine> OnLineBreakRequested;
/// <summary>
/// 当前断线候选状态的累计时间。
/// 只有在处于极限状态,且 CurrentStretchLength 大于断线阈值时才会累加;否则重置为 0。
/// </summary>
public float LimitStateTime { get; private set; }
/// <summary>
/// 当前拉力极限百分比。
/// 当超长值小于等于 lengthLimitTolerance 时为 0
/// 当超长值大于等于 breakStretchThreshold 时为 100
/// 中间区间按线性比例映射,供 UI 显示使用。
/// </summary>
public float CurrentBreakStretchPercent => EvaluateBreakStretchPercent(Length);
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;
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1330e7f73f804eea8ae09724f6bb1039
timeCreated: 1766759962

View File

@@ -0,0 +1,55 @@
// using Obi;
using UnityEngine;
public class RodLine : MonoBehaviour
{
// private ObiRope obiRope;
private LineRenderer lineRenderer;
private Transform[] points;
public void GenerateLineRendererRope(Transform[] points, float thickness)
{
if (points.Length < 2)
{
Debug.LogError("LineRenderer requires at least two points.");
return;
}
this.points = points;
if (lineRenderer == null)
{
lineRenderer = base.gameObject.GetComponent<LineRenderer>();
if (lineRenderer == null)
{
lineRenderer = base.gameObject.AddComponent<LineRenderer>();
}
}
lineRenderer.positionCount = points.Length;
lineRenderer.startWidth = thickness;
lineRenderer.endWidth = thickness;
for (int i = 0; i < points.Length; i++)
{
lineRenderer.SetPosition(i, points[i].position);
}
}
private void LateUpdate()
{
if (lineRenderer == null)
{
return;
}
for (var i = 0; i < points.Length; i++)
{
var point = points[i];
if (point)
{
lineRenderer.SetPosition(i, point.position);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5625b86b9e4b4482b82d83b962d0c873
timeCreated: 1766759973

View File

@@ -14,19 +14,19 @@ public class Rope : MonoBehaviour
[Header("Physics (Dynamic Nodes, Fixed Segment Len)")] [SerializeField, Min(0.01f), Tooltip("物理每段固定长度(越小越细致越耗)")]
private float physicsSegmentLen = 0.15f;
[SerializeField, Range(2, 200)] private int minPhysicsNodes = 2;
[SerializeField, Range(2, 200)] private int minPhysicsNodes = 12;
[SerializeField, Range(2, 400), Tooltip("物理节点上限(仅用于性能保护;与“最大长度不限制”不是一回事)")]
private int maxPhysicsNodes = 200;
private int maxPhysicsNodes = 120;
[SerializeField] private float gravityStrength = 6.0f;
[SerializeField] private float gravityStrength = 2.0f;
[SerializeField, Range(0f, 1f)] private float velocityDampen = 0.95f;
[SerializeField, Range(0.0f, 1.0f), Tooltip("约束修正强度越大越硬。0.6~0.9 常用")]
private float stiffness = 0.8f;
[SerializeField, Range(1, 80), Tooltip("迭代次数")]
private int iterations = 10;
[SerializeField, Range(1, 80), Tooltip("迭代次数。鱼线 10~30 通常够用")]
private int iterations = 20;
[SerializeField, Range(0, 16), Tooltip("主求解后追加的硬长度约束次数。只负责把 poly 拉回到 rest total不改变可变长度逻辑")]
private int hardTightenIterations = 2;
@@ -46,7 +46,7 @@ public class Rope : MonoBehaviour
private float lengthSmoothTime = 0.15f;
[Tooltip("当长度在变化时额外把速度压掉一些防抖。0=不额外处理1=变化时几乎清速度(建议只在收线生效)")] [SerializeField, Range(0f, 1f)]
private float lengthChangeVelocityKill = 0.4f;
private float lengthChangeVelocityKill = 0.6f;
[Tooltip("允许的最小松弛余量(避免目标长度刚好等于锚点距离时抖动)")] [SerializeField, Min(0f)]
private float minSlack = 0.002f;
@@ -58,9 +58,9 @@ public class Rope : MonoBehaviour
private float nodeHysteresis = 0.05f;
[Header("Simple Ground/Water Constraint (Cheap)")] [SerializeField]
private bool constrainToGround = false;
private bool constrainToGround = true;
[SerializeField] private LayerMask groundMask = 0;
[SerializeField] private LayerMask groundMask = ~0;
[SerializeField, Min(0f)] private float groundRadius = 0.01f;
[SerializeField, Min(0f)] private float groundCastHeight = 1.0f;
[SerializeField, Min(0.01f)] private float groundCastDistance = 2.5f;
@@ -72,7 +72,7 @@ public class Rope : MonoBehaviour
private bool groundInterpolate = true;
[SerializeField, Range(1, 8), Tooltip("每隔多少次FixedUpdate更新一次地面约束")]
private int groundUpdateEvery = 1;
private int groundUpdateEvery = 2;
[SerializeField, Range(0, 8), Tooltip("地面约束后,再做几次长度约束,减少 poly 被地面抬长")]
private int groundPostConstraintIterations = 2;
@@ -80,7 +80,7 @@ public class Rope : MonoBehaviour
private int _groundFrameCounter;
[Header("Simple Water Float (Cheap)")] [SerializeField, Tooltip("绳子落到水面以下时,是否把节点约束回水面")]
private bool constrainToWaterSurface = false;
private bool constrainToWaterSurface = true;
[SerializeField, Tooltip("静态水面高度;如果你后面接波浪水面,可改成采样函数")]
private float waterLevelY = 0f;
@@ -135,7 +135,7 @@ public class Rope : MonoBehaviour
private float visibilityViewportPadding = 0.08f;
[Header("Air Drag (Stable)")] [SerializeField, Range(0f, 5f), Tooltip("空气阻力Y向指数衰减越大越不飘")]
private float airDrag = 0.2f;
private float airDrag = 0.9f;
[SerializeField, Range(0f, 2f), Tooltip("横向额外阻力XZ指数衰减越大越不左右飘")]
private float airDragXZ = 0.6f;
@@ -179,6 +179,13 @@ public class Rope : MonoBehaviour
private int _tIdleSubdiv = -1;
private int _tMovingSubdiv = -1;
private FRod _rod;
public void Init(FRod rod)
{
_rod = rod;
if (Application.isPlaying)
RefreshVisibilityState(true);
}
// Catmull t caches只缓存 idle/moving 两档,减少每帧重复乘法)
private struct TCaches
@@ -204,7 +211,6 @@ public class Rope : MonoBehaviour
RefreshVisibilityState(true);
}
private void OnValidate()
{
renderSubdivisionsIdle = Mathf.Max(renderSubdivisionsIdle, 1);
@@ -245,6 +251,14 @@ public class Rope : MonoBehaviour
_endTr = endAnchor ? endAnchor.transform : null;
}
private bool ShouldAlwaysSimulate()
{
if (!localOwnerAlwaysSimulate)
return false;
var owner = _rod?.PlayerItem?.Owner;
return owner == null || owner.IsSelf;
}
private Transform GetActiveCameraTransform()
{
@@ -298,7 +312,7 @@ public class Rope : MonoBehaviour
private void RefreshVisibilityState(bool force = false)
{
if (!cullRemoteRopeWhenInvisible)
if (!cullRemoteRopeWhenInvisible || ShouldAlwaysSimulate())
{
_isCulledByVisibility = false;
if (_lineRenderer)
@@ -584,10 +598,9 @@ public class Rope : MonoBehaviour
Time.fixedDeltaTime
);
// 仅在收线(目标长度小于当前长度)时额外压速度;
// 放线时不要压速度,否则新增节点下落会出现“顿一下再突然加速”。
float reelInDelta = _currentLength - _targetLength;
if (reelInDelta > 0.0001f && lengthChangeVelocityKill > 0f)
// 长度变化时额外压一点速度,减少收放线时抖动
float delta = Mathf.Abs(_targetLength - _currentLength);
if (delta > 0.0001f && lengthChangeVelocityKill > 0f)
{
float keep = 1f - Mathf.Clamp01(lengthChangeVelocityKill);
for (int i = 1; i < _physicsNodes - 1; i++)
@@ -752,8 +765,7 @@ public class Rope : MonoBehaviour
SolveDistanceConstraintsSweep_Fast(last - 1, -1, -1, last, sweepStiffness);
}
private void SolveDistanceConstraintsSweep_Fast(int start, int endExclusive, int step, int last,
float sweepStiffness)
private void SolveDistanceConstraintsSweep_Fast(int start, int endExclusive, int step, int last, float sweepStiffness)
{
for (int i = start; i != endExclusive; i += step)
{
@@ -1094,4 +1106,4 @@ public class Rope : MonoBehaviour
for (int i = 0; i < _physicsNodes; i++)
Gizmos.DrawSphere(_pCurr[i], 0.01f);
}
}
}

View File

@@ -15,34 +15,34 @@ EditorUserSettings:
value: 2550581500
flags: 0
RecentlyUsedSceneGuid-0:
value: 050402550007590a0f565f2714200c44144e492f2f70753175711f66e0b8303c
value: 55540305570d0f0e0c5e5e2115710d44174e4e2b7b7e77662f2d1c61b5b06069
flags: 0
RecentlyUsedSceneGuid-1:
value: 06070c5f5c075c5e5e085476427a0a44474e1c2f7f7a73362f2d4d36b5b1633d
value: 5452500303515f0a5f5b5a7445775e46401519787c717f677d784860e3b1676c
flags: 0
RecentlyUsedSceneGuid-2:
value: 0005505f515750595e5f5f23412507441216497f2d7f24367e711c64b6b86c61
value: 050402550007590a0f565f2714200c44144e492f2f70753175711f66e0b8303c
flags: 0
RecentlyUsedSceneGuid-3:
value: 54070c5452075002590c0871127b5a4443161c2f797176312c2f1e6bb1b4353d
value: 06070c5f5c075c5e5e085476427a0a44474e1c2f7f7a73362f2d4d36b5b1633d
flags: 0
RecentlyUsedSceneGuid-4:
value: 5309035757065a0a54575f7216265c4444151d28792e72627d2f1935bbb8673a
value: 0005505f515750595e5f5f23412507441216497f2d7f24367e711c64b6b86c61
flags: 0
RecentlyUsedSceneGuid-5:
value: 00050c5150005f5f54560f2640270d4410161c28282b72357e7c4835e4b63760
value: 54070c5452075002590c0871127b5a4443161c2f797176312c2f1e6bb1b4353d
flags: 0
RecentlyUsedSceneGuid-6:
value: 06090c5f54015f5a0f085b7b11765d444e4e1e287429773178704561b3b23561
value: 5309035757065a0a54575f7216265c4444151d28792e72627d2f1935bbb8673a
flags: 0
RecentlyUsedSceneGuid-7:
value: 0257035f51050d090f0f5d734521094414164e797e7a20667d7a4536e0e36461
value: 00050c5150005f5f54560f2640270d4410161c28282b72357e7c4835e4b63760
flags: 0
RecentlyUsedSceneGuid-8:
value: 5505015f5c515a085f5b092149760f441716407a787d7564287b1b36e7e1366e
value: 06090c5f54015f5a0f085b7b11765d444e4e1e287429773178704561b3b23561
flags: 0
RecentlyUsedSceneGuid-9:
value: 07060c5454040c0a545b547240700a441216417e7f2e7268752c4966b4b0663d
value: 5505015f5c515a085f5b092149760f441716407a787d7564287b1b36e7e1366e
flags: 0
UnityEditor.ShaderGraph.Blackboard:
value: 18135939215a0a5004000b0e15254b524c030a3f2964643d120d1230e9e93a3fd6e826abbd2e2d293c4ead313b08042de6030a0afa240c0d020be94c4ba75e435d8715fa32c70d15d11612dacc11fee5d3c5d1fe9ab1bf968e93e2ffcbc3e7e2f0b3ffe0e8b0be9afeffa9ffff8e85dd8390e2969e8899daa7