Compare commits
3 Commits
0c3818497a
...
9369f512d1
| Author | SHA1 | Date | |
|---|---|---|---|
| 9369f512d1 | |||
| e879efaa89 | |||
| b7baf7ad6a |
@@ -15,7 +15,7 @@ MonoBehaviour:
|
||||
m_DefaultGroup: 9ce5c865a2d3a0840aabdd8ccb3fd4b1
|
||||
m_currentHash:
|
||||
serializedVersion: 2
|
||||
Hash: 00000000000000000000000000000000
|
||||
Hash: 3b5a6592fec2f53c65ab132b7f731fb2
|
||||
m_OptimizeCatalogSize: 0
|
||||
m_BuildRemoteCatalog: 0
|
||||
m_BundleLocalCatalog: 0
|
||||
@@ -88,6 +88,14 @@ MonoBehaviour:
|
||||
m_LabelTable:
|
||||
m_LabelNames:
|
||||
- default
|
||||
- Preload
|
||||
- Poolable
|
||||
- Enemy
|
||||
- BGM
|
||||
- SFX
|
||||
- Charms
|
||||
- Config
|
||||
- Weapon
|
||||
m_SchemaTemplates: []
|
||||
m_GroupTemplateObjects:
|
||||
- {fileID: 11400000, guid: f9701da6026b3a54f9b4d6eb144ee443, type: 2}
|
||||
|
||||
8
Assets/_Game/Art/UI/Icons/InputKeys.meta
Normal file
8
Assets/_Game/Art/UI/Icons/InputKeys.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c70a04eb99184247a53f1631e082c50
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -134,6 +134,7 @@ MonoBehaviour:
|
||||
weaponTrailPrefab: {fileID: 0}
|
||||
trailColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
soulPowerGain: 10
|
||||
hitWeight: 1
|
||||
references:
|
||||
version: 2
|
||||
RefIds: []
|
||||
|
||||
@@ -12,31 +12,33 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 81da55e0fcf99d34693cbc5a348225c3, type: 3}
|
||||
m_Name: PLY_PlayerMovementConfig
|
||||
m_EditorClassIdentifier:
|
||||
RunSpeed: 8
|
||||
RunSpeed: 6
|
||||
AirDragFactor: 1
|
||||
JumpForce: 24
|
||||
JumpForce: 17.5
|
||||
CoyoteTime: 0.12
|
||||
FallGravityMult: 2.5
|
||||
MaxFallSpeed: 28
|
||||
FallGravityMult: 2
|
||||
MaxFallSpeed: 15
|
||||
JumpCutMultiplier: 0.321
|
||||
ApexThreshold: 3
|
||||
ApexGravityMultiplier: 0.3
|
||||
MaxAirJumps: 1
|
||||
DoubleJumpForce: 19
|
||||
MaxAirJumps: 5
|
||||
DoubleJumpForce: 14
|
||||
DashSpeed: 20
|
||||
DashDuration: 0.25
|
||||
DashCooldown: 0.4
|
||||
DashInvincibilityDuration: 0.2
|
||||
DashInvincibilityCooldown: 0.9
|
||||
WallSlideSpeed: 2
|
||||
DownDashSpeed: 22
|
||||
DownDashDuration: 0.25
|
||||
WallSlideSpeed: 3
|
||||
WallHangSpeed: 1
|
||||
WallRayLength: 0.37
|
||||
WallRayOffsetY: 0.2
|
||||
WallGrabHeightTolerance: 0.05
|
||||
WallCoyoteTime: 0.12
|
||||
WallJumpAwayForceX: 10
|
||||
WallJumpAwayForceY: 18
|
||||
WallJumpAwayForceY: 14
|
||||
WallJumpTowardForceX: -6
|
||||
WallJumpTowardForceY: 18
|
||||
WallJumpTowardForceY: 14
|
||||
WallJumpInputLockDuration: 0.15
|
||||
DefaultGravityScale: 6
|
||||
DefaultGravityScale: 5
|
||||
|
||||
8
Assets/_Game/Data/UI/Icons.meta
Normal file
8
Assets/_Game/Data/UI/Icons.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9006d69429771544da69a6fb803ee6cf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Data/UI/InputIcons.meta
Normal file
8
Assets/_Game/Data/UI/InputIcons.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c4657c9f87cec046aef30d9c2e83bc7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/_Game/Data/UI/InputIcons/ICN_Keyboard.asset
Normal file
16
Assets/_Game/Data/UI/InputIcons/ICN_Keyboard.asset
Normal file
@@ -0,0 +1,16 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bf96d35cbe629854794062790e40afb7, type: 3}
|
||||
m_Name: ICN_Keyboard
|
||||
m_EditorClassIdentifier:
|
||||
_deviceType: 0
|
||||
_entries: []
|
||||
8
Assets/_Game/Data/UI/InputIcons/ICN_Keyboard.asset.meta
Normal file
8
Assets/_Game/Data/UI/InputIcons/ICN_Keyboard.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7faaada188bdae2499f9607b5c13b11b
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/_Game/Data/UI/InputIcons/ICN_PlayStation.asset
Normal file
16
Assets/_Game/Data/UI/InputIcons/ICN_PlayStation.asset
Normal file
@@ -0,0 +1,16 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bf96d35cbe629854794062790e40afb7, type: 3}
|
||||
m_Name: ICN_PlayStation
|
||||
m_EditorClassIdentifier:
|
||||
_deviceType: 0
|
||||
_entries: []
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 441c8b987e18c07409de8d6ba9b871cc
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/_Game/Data/UI/InputIcons/ICN_Switch.asset
Normal file
16
Assets/_Game/Data/UI/InputIcons/ICN_Switch.asset
Normal file
@@ -0,0 +1,16 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bf96d35cbe629854794062790e40afb7, type: 3}
|
||||
m_Name: ICN_Switch
|
||||
m_EditorClassIdentifier:
|
||||
_deviceType: 0
|
||||
_entries: []
|
||||
8
Assets/_Game/Data/UI/InputIcons/ICN_Switch.asset.meta
Normal file
8
Assets/_Game/Data/UI/InputIcons/ICN_Switch.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87d27ef72ec852548a127d7acb71d1a3
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/_Game/Data/UI/InputIcons/ICN_Xbox.asset
Normal file
16
Assets/_Game/Data/UI/InputIcons/ICN_Xbox.asset
Normal file
@@ -0,0 +1,16 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bf96d35cbe629854794062790e40afb7, type: 3}
|
||||
m_Name: ICN_Xbox
|
||||
m_EditorClassIdentifier:
|
||||
_deviceType: 0
|
||||
_entries: []
|
||||
8
Assets/_Game/Data/UI/InputIcons/ICN_Xbox.asset.meta
Normal file
8
Assets/_Game/Data/UI/InputIcons/ICN_Xbox.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8acf7a7648c79274cb31cfe2285f7746
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,101 +1,5 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &467203328547477162
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7119158475861943178}
|
||||
- component: {fileID: 7882116945389632025}
|
||||
- component: {fileID: 4639356286286040131}
|
||||
m_Layer: 14
|
||||
m_Name: HitBox_Ground_1
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &7119158475861943178
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 467203328547477162}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.798, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &7882116945389632025
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 467203328547477162}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_ForceSendLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ForceReceiveLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ContactCaptureLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_CallbackLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0, y: -0.16736698}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
oldSize: {x: 0, y: 0}
|
||||
newSize: {x: 0, y: 0}
|
||||
adaptiveTilingThreshold: 0
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1, y: 0.83473396}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &4639356286286040131
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 467203328547477162}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Ground_1
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
--- !u!1 &1932889250901504761
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -123,7 +27,7 @@ Transform:
|
||||
m_GameObject: {fileID: 1932889250901504761}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: -0.562, z: 0}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
@@ -161,7 +65,7 @@ BoxCollider2D:
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: -0.027121663, y: -0.15051937}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
@@ -172,7 +76,7 @@ BoxCollider2D:
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.189852, y: 0.80103874}
|
||||
m_Size: {x: 1, y: 0.5}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &6478051166999031478
|
||||
MonoBehaviour:
|
||||
@@ -188,11 +92,11 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Down
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
--- !u!1 &2584603199706918030
|
||||
m_Bits: 0
|
||||
--- !u!1 &3989564331693126876
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
@@ -200,38 +104,38 @@ GameObject:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1660186156348129284}
|
||||
- component: {fileID: 1152578598430080845}
|
||||
- component: {fileID: 3007294148525084107}
|
||||
- component: {fileID: 8294071144630811572}
|
||||
- component: {fileID: 4949779957213724475}
|
||||
- component: {fileID: 4757677899241504248}
|
||||
m_Layer: 14
|
||||
m_Name: HitBox_Ground _2
|
||||
m_Name: HitBox_Ground
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1660186156348129284
|
||||
--- !u!4 &8294071144630811572
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2584603199706918030}
|
||||
m_GameObject: {fileID: 3989564331693126876}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.798, y: 0, z: 0}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &1152578598430080845
|
||||
--- !u!61 &4949779957213724475
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2584603199706918030}
|
||||
m_GameObject: {fileID: 3989564331693126876}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
@@ -257,7 +161,7 @@ BoxCollider2D:
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: -0.117884755, y: 0.01309824}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
@@ -268,15 +172,15 @@ BoxCollider2D:
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 0.7642305, y: 1.1956644}
|
||||
m_Size: {x: 1, y: 0.5}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &3007294148525084107
|
||||
--- !u!114 &4757677899241504248
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2584603199706918030}
|
||||
m_GameObject: {fileID: 3989564331693126876}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
@@ -284,106 +188,10 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Ground_2
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
--- !u!1 &4050057806632877121
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4405470499151834857}
|
||||
- component: {fileID: 8597809172682257212}
|
||||
- component: {fileID: 1610035618021234136}
|
||||
m_Layer: 14
|
||||
m_Name: HitBox_Air_2
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &4405470499151834857
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4050057806632877121}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.553, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &8597809172682257212
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4050057806632877121}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_ForceSendLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ForceReceiveLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ContactCaptureLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_CallbackLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0.27943045, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
oldSize: {x: 0, y: 0}
|
||||
newSize: {x: 0, y: 0}
|
||||
adaptiveTilingThreshold: 0
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.0588609, y: 1}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &1610035618021234136
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4050057806632877121}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Air_2
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
--- !u!1 &4335406389674002762
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -411,7 +219,7 @@ Transform:
|
||||
m_GameObject: {fileID: 4335406389674002762}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0.918, z: 0}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
@@ -449,7 +257,7 @@ BoxCollider2D:
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0.072324514, y: 0}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
@@ -460,7 +268,7 @@ BoxCollider2D:
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.1599612, y: 1}
|
||||
m_Size: {x: 0.5, y: 1}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &1392799324577637263
|
||||
MonoBehaviour:
|
||||
@@ -476,10 +284,10 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Up
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
m_Bits: 0
|
||||
--- !u!1 &4821376343125962025
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -510,12 +318,10 @@ Transform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 7119158475861943178}
|
||||
- {fileID: 1660186156348129284}
|
||||
- {fileID: 8294071144630811572}
|
||||
- {fileID: 7468586589501741901}
|
||||
- {fileID: 6088225995420515986}
|
||||
- {fileID: 4362395311111627733}
|
||||
- {fileID: 4405470499151834857}
|
||||
- {fileID: 6913225169405126738}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &3691925044832415471
|
||||
@@ -530,11 +336,11 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: ec12dacf2519f58429dd3c59da8f93b0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_hitBoxGround: {fileID: 4639356286286040131}
|
||||
_hitBoxGround: {fileID: 4757677899241504248}
|
||||
_hitBoxUp: {fileID: 1392799324577637263}
|
||||
_hitBoxDown: {fileID: 6478051166999031478}
|
||||
_hitBoxAir: {fileID: 9014207169512774676}
|
||||
--- !u!1 &8582289489283119946
|
||||
_hitBoxAir: {fileID: 1382006829078153708}
|
||||
--- !u!1 &6434981771063321190
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
@@ -542,38 +348,38 @@ GameObject:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4362395311111627733}
|
||||
- component: {fileID: 922051492914393482}
|
||||
- component: {fileID: 9014207169512774676}
|
||||
- component: {fileID: 6913225169405126738}
|
||||
- component: {fileID: 6843760498109474434}
|
||||
- component: {fileID: 1382006829078153708}
|
||||
m_Layer: 14
|
||||
m_Name: HitBox_Air_1
|
||||
m_Name: HitBox_Air
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &4362395311111627733
|
||||
--- !u!4 &6913225169405126738
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8582289489283119946}
|
||||
m_GameObject: {fileID: 6434981771063321190}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.553, y: 0, z: 0}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &922051492914393482
|
||||
--- !u!61 &6843760498109474434
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8582289489283119946}
|
||||
m_GameObject: {fileID: 6434981771063321190}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
@@ -599,7 +405,7 @@ BoxCollider2D:
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0.46717286, y: 0}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
@@ -610,15 +416,15 @@ BoxCollider2D:
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.4343457, y: 1}
|
||||
m_Size: {x: 0.5, y: 1}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &9014207169512774676
|
||||
--- !u!114 &1382006829078153708
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8582289489283119946}
|
||||
m_GameObject: {fileID: 6434981771063321190}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
@@ -626,7 +432,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Air_1
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
m_Bits: 0
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace BaseGames.Camera
|
||||
[SerializeField] private CinemachineBrain _brain;
|
||||
|
||||
private UnityEngine.Camera _camera;
|
||||
private CinemachineCamera _cachedVCam;
|
||||
private CinemachineConfiner3D _cachedConfiner;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
@@ -77,7 +79,8 @@ namespace BaseGames.Camera
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前活跃 VCam 的 CinemachineConfiner3D 边界盒(世界空间 AABB)。
|
||||
/// 用于在像素取整后将相机钳制回限位区域内。
|
||||
/// 缓存上次查询的 VCam 实例;仅在活跃 VCam 发生切换时重新调用 GetComponent,
|
||||
/// 避免每帧 GetComponent 开销。
|
||||
/// </summary>
|
||||
private bool TryGetActiveConfinerBounds(out Bounds bounds)
|
||||
{
|
||||
@@ -85,9 +88,14 @@ namespace BaseGames.Camera
|
||||
if (_brain == null) return false;
|
||||
var vcam = _brain.ActiveVirtualCamera as CinemachineCamera;
|
||||
if (vcam == null) return false;
|
||||
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
|
||||
if (confiner == null || !confiner.IsValid) return false;
|
||||
bounds = confiner.BoundingVolume.bounds;
|
||||
// 只在活跃 VCam 切换时刷新缓存
|
||||
if (!ReferenceEquals(vcam, _cachedVCam))
|
||||
{
|
||||
_cachedVCam = vcam;
|
||||
_cachedConfiner = vcam.GetComponent<CinemachineConfiner3D>();
|
||||
}
|
||||
if (_cachedConfiner == null || !_cachedConfiner.IsValid) return false;
|
||||
bounds = _cachedConfiner.BoundingVolume.bounds;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,8 @@ namespace BaseGames.Camera
|
||||
}
|
||||
|
||||
// 触发区域进入:更新集合(同一区域去重后重新加入,保证最新优先级)
|
||||
_activeZones.RemoveAll(e => e.area == area);
|
||||
for (int i = _activeZones.Count - 1; i >= 0; i--)
|
||||
if (_activeZones[i].area == area) _activeZones.RemoveAt(i);
|
||||
_activeZones.Add((area, priority));
|
||||
|
||||
// 仅当此区域是当前最优且尚未激活时才切换
|
||||
@@ -132,7 +133,9 @@ namespace BaseGames.Camera
|
||||
if (releasedArea == null) return;
|
||||
|
||||
bool wasActive = releasedArea == _currentArea;
|
||||
int removed = _activeZones.RemoveAll(e => e.area == releasedArea);
|
||||
int removed = 0;
|
||||
for (int i = _activeZones.Count - 1; i >= 0; i--)
|
||||
if (_activeZones[i].area == releasedArea) { _activeZones.RemoveAt(i); removed++; }
|
||||
// 若区域本就不在栈中,且又不是当前激活区,则无需任何操作
|
||||
if (removed == 0 && !wasActive) return;
|
||||
|
||||
|
||||
@@ -57,9 +57,14 @@ namespace BaseGames.Camera
|
||||
// ── 静态:跨实例共享触发状态 ──────────────────────────────────────────
|
||||
// 玩家当前物理上所在的所有触发区域(按进入顺序排列)
|
||||
private static readonly List<CameraTriggerZone> s_InsideZones = new();
|
||||
// 场景内所有已启用的触发区域,供 RoomController 等查询(替代 FindObjectsOfType)
|
||||
private static readonly List<CameraTriggerZone> s_AllZones = new();
|
||||
// 当前已向 ICameraService 发出 SwitchArea 请求的触发区域
|
||||
private static CameraTriggerZone s_ActiveZone;
|
||||
|
||||
/// <summary>场景内当前所有已启用的触发区域(只读)。</summary>
|
||||
public static IReadOnlyList<CameraTriggerZone> AllZones => s_AllZones;
|
||||
|
||||
/// <summary>
|
||||
/// 在每次进入 Play Mode 前(或禁用 Domain Reload 时的跨会话)重置静态状态,
|
||||
/// 防止上一次游戏会话残留的区域引用导致触发逻辑错误。
|
||||
@@ -68,6 +73,7 @@ namespace BaseGames.Camera
|
||||
private static void ResetStaticState()
|
||||
{
|
||||
s_InsideZones.Clear();
|
||||
s_AllZones.Clear();
|
||||
s_ActiveZone = null;
|
||||
}
|
||||
|
||||
@@ -77,12 +83,22 @@ namespace BaseGames.Camera
|
||||
_collider.isTrigger = true;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
s_AllZones.Add(this);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
s_AllZones.Remove(this);
|
||||
HandlePlayerExit();
|
||||
}
|
||||
|
||||
/// <summary>判断世界坐标点是否在本触发区域多边形内(供 RoomController 等无需 GetComponent 直接查询)。</summary>
|
||||
public bool ContainsPoint(Vector2 worldPoint) => _collider != null && _collider.OverlapPoint(worldPoint);
|
||||
|
||||
/// <summary>
|
||||
/// 若玩家出生时已在触发区域内,OnTriggerEnter2D 不会触发。
|
||||
/// 延迟一帧(确保 RoomController.Start 先完成基准区域设置)后主动检测。
|
||||
|
||||
@@ -142,8 +142,8 @@ namespace BaseGames.Combat
|
||||
bool isRivalHitBoxLayer = (_rivalHitBoxMask.value & (1 << otherLayer)) != 0;
|
||||
if (isRivalHitBoxLayer && CanClash)
|
||||
{
|
||||
var rivalHitBox = other.GetComponent<HitBox>();
|
||||
if (rivalHitBox != null && rivalHitBox.IsActive && rivalHitBox.CanClash)
|
||||
if (other.TryGetComponent<HitBox>(out var rivalHitBox) &&
|
||||
rivalHitBox.IsActive && rivalHitBox.CanClash)
|
||||
{
|
||||
_clashService?.ResolveClash(this, rivalHitBox);
|
||||
return; // 拼刀,中止伤害流水线
|
||||
@@ -151,8 +151,7 @@ namespace BaseGames.Combat
|
||||
}
|
||||
|
||||
// ② 命中 HurtBox
|
||||
var hurtBox = other.GetComponent<HurtBox>();
|
||||
if (hurtBox != null)
|
||||
if (other.TryGetComponent<HurtBox>(out var hurtBox))
|
||||
{
|
||||
// 用 HitBox 自身碰撞盒中心在 HurtBox 表面上的最近点作为受击位置。
|
||||
// 对大体积/长条形受击体(如地刺),此点远比 HurtBox 节点中心更准确。
|
||||
@@ -163,7 +162,8 @@ namespace BaseGames.Combat
|
||||
}
|
||||
|
||||
// ③ 命中 IBreakable(机关/障碍物)
|
||||
other.GetComponent<IBreakable>()?.TryInteract(info);
|
||||
if (other.TryGetComponent<IBreakable>(out var breakable))
|
||||
breakable.TryInteract(info);
|
||||
}
|
||||
|
||||
// ── 当前激活期已命中目标集合(防止复合子 Collider 导致同帧多次命中)────────────
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace BaseGames.Combat
|
||||
protected Rigidbody2D _rb;
|
||||
protected HitBox _hitBox;
|
||||
protected float _aliveTimer;
|
||||
// Lifetime 在 Initialize 时缓存,避免 Update 每帧访问 SO 成员并做 null check
|
||||
private float _lifetime = float.MaxValue;
|
||||
|
||||
private PooledObject _pooledObject;
|
||||
|
||||
@@ -32,6 +34,7 @@ namespace BaseGames.Combat
|
||||
public virtual void Initialize(ProjectileConfigSO config, DamageInfo damageInfo, Vector2 direction, int ownerLayer = 0)
|
||||
{
|
||||
_config = config;
|
||||
_lifetime = config.Lifetime;
|
||||
DamageInfo = damageInfo;
|
||||
Direction = direction.normalized;
|
||||
_aliveTimer = 0f;
|
||||
@@ -75,7 +78,7 @@ namespace BaseGames.Combat
|
||||
protected virtual void Update()
|
||||
{
|
||||
_aliveTimer += Time.deltaTime;
|
||||
if (_config != null && _aliveTimer >= _config.Lifetime)
|
||||
if (_aliveTimer >= _lifetime)
|
||||
ReturnToPool();
|
||||
}
|
||||
|
||||
@@ -91,6 +94,7 @@ namespace BaseGames.Combat
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
_aliveTimer = 0f;
|
||||
_lifetime = float.MaxValue; // 归还对象池后重置,防止未初始化时自毁
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ namespace BaseGames.Combat
|
||||
|
||||
public event System.Action<DamageInfo> OnHitConfirmed;
|
||||
|
||||
private Coroutine _returnCoroutine;
|
||||
// 按 duration 缓存 WaitForSeconds,同一技能复用无 GC 分配
|
||||
private WaitForSeconds _cachedWait;
|
||||
private float _cachedWaitDuration = float.NaN;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
foreach (var hb in _hitBoxes)
|
||||
@@ -35,7 +40,31 @@ namespace BaseGames.Combat
|
||||
hb?.Activate(source, attacker);
|
||||
}
|
||||
|
||||
/// <summary>duration 秒后自动销毁此 GameObject。</summary>
|
||||
/// <summary>
|
||||
/// duration 秒后归还对象池(SetActive false)。
|
||||
/// 由 SkillManager 对象池调用;替代旧版 Destroy 流程。
|
||||
/// </summary>
|
||||
public void AutoReturnAfter(float duration)
|
||||
{
|
||||
if (!Mathf.Approximately(_cachedWaitDuration, duration))
|
||||
{
|
||||
_cachedWaitDuration = duration;
|
||||
_cachedWait = new WaitForSeconds(duration);
|
||||
}
|
||||
if (_returnCoroutine != null) StopCoroutine(_returnCoroutine);
|
||||
_returnCoroutine = StartCoroutine(ReturnCoroutine());
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator ReturnCoroutine()
|
||||
{
|
||||
yield return _cachedWait;
|
||||
foreach (var hb in _hitBoxes)
|
||||
hb?.Deactivate();
|
||||
_returnCoroutine = null;
|
||||
gameObject.SetActive(false); // 触发对象池回收
|
||||
}
|
||||
|
||||
/// <summary>duration 秒后销毁(非池化路径,保留向后兼容)。</summary>
|
||||
public void AutoDestroyAfter(float duration)
|
||||
=> Destroy(gameObject, Mathf.Max(0f, duration));
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ namespace BaseGames.Combat.StatusEffects
|
||||
|
||||
public override StatusEffectType EffectType => StatusEffectType.Fire;
|
||||
public override int MaxStacks => 1;
|
||||
private static readonly StatusEffectType[] s_MutualExclusions = { StatusEffectType.Freeze };
|
||||
/// <summary>施加燃烧时移除冻结(火冰互斥)。</summary>
|
||||
public override StatusEffectType[] MutualExclusions => new[] { StatusEffectType.Freeze };
|
||||
public override StatusEffectType[] MutualExclusions => s_MutualExclusions;
|
||||
|
||||
public FireEffect()
|
||||
{
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace BaseGames.Combat.StatusEffects
|
||||
// ── Shader 渲染(MaterialPropertyBlock,不修改共享材质)─────────
|
||||
private SpriteRenderer _renderer;
|
||||
private MaterialPropertyBlock _propBlock;
|
||||
// 缓存 Shader 属性 ID,避免每次调用 SetShaderParam 都做字符串哈希查找
|
||||
private readonly Dictionary<string, int> _shaderPropIds = new();
|
||||
|
||||
// ── DoT 伤害代理(由 StatusEffect.OnTick 通过 Owner 调用)──────────
|
||||
private IDamageable _damageable;
|
||||
@@ -135,8 +137,13 @@ namespace BaseGames.Combat.StatusEffects
|
||||
public void SetShaderParam(string param, float value)
|
||||
{
|
||||
if (_renderer == null) return;
|
||||
if (!_shaderPropIds.TryGetValue(param, out int propId))
|
||||
{
|
||||
propId = Shader.PropertyToID(param);
|
||||
_shaderPropIds[param] = propId;
|
||||
}
|
||||
_renderer.GetPropertyBlock(_propBlock);
|
||||
_propBlock.SetFloat(param, value);
|
||||
_propBlock.SetFloat(propId, value);
|
||||
_renderer.SetPropertyBlock(_propBlock);
|
||||
}
|
||||
|
||||
|
||||
22
Assets/_Game/Scripts/Core/Events/InteractPromptEvent.cs
Normal file
22
Assets/_Game/Scripts/Core/Events/InteractPromptEvent.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace BaseGames.Core.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// 交互提示事件负载。
|
||||
/// 由 InteractableDetector 广播,包含触发动作名称和显示文本,
|
||||
/// UI 层(InteractPromptWidget)据此查询图标并显示提示。
|
||||
/// </summary>
|
||||
public readonly struct InteractPromptEvent
|
||||
{
|
||||
/// <summary>InputSystem Action 名称,如 "Interact"。用于查询按键图标。</summary>
|
||||
public readonly string ActionName;
|
||||
|
||||
/// <summary>交互物提供的说明文本,如 "对话"、"存档"、"传送"。</summary>
|
||||
public readonly string LabelText;
|
||||
|
||||
public InteractPromptEvent(string actionName, string labelText)
|
||||
{
|
||||
ActionName = actionName;
|
||||
LabelText = labelText;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Core/Events/InteractPromptEvent.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/Events/InteractPromptEvent.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9eccce8fdbd936b46a467d078957a387
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Core.Events
|
||||
{
|
||||
[CreateAssetMenu(menuName = "BaseGames/Events/InteractPrompt")]
|
||||
public class InteractPromptEventChannelSO : BaseEventChannelSO<InteractPromptEvent> { }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e6db212f7619344588f054af0c6330a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -24,6 +24,16 @@ namespace BaseGames.Core
|
||||
/// <item><b>Room</b>:极短淡出(<see cref="_roomFadeDuration"/>),无加载画面。</item>
|
||||
/// <item><b>Scene</b>:完整淡出(<see cref="_sceneFadeDuration"/>),显示加载画面。</item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// 完整加载时序(保证场景物体在显示前完成状态初始化):
|
||||
/// <list type="number">
|
||||
/// <item>淡出(黑幕遮挡)</item>
|
||||
/// <item>Addressable 异步加载场景(场景物体 Awake / OnEnable 同步执行)</item>
|
||||
/// <item>触发 <see cref="_onSceneWorldStateRestored"/>:通知场景物体从 WorldStateRegistry 恢复初始状态</item>
|
||||
/// <item>等待一帧(保证所有场景物体 Start() 和事件处理器执行完毕)</item>
|
||||
/// <item>淡入(显示已完成初始化的场景)</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-900)]
|
||||
public class SceneService : MonoBehaviour, ISceneService
|
||||
@@ -35,6 +45,12 @@ namespace BaseGames.Core
|
||||
[SerializeField] private VoidEventChannelSO _onFadeInRequest;
|
||||
[SerializeField] private VoidEventChannelSO _onFadeOutRequest;
|
||||
|
||||
[Tooltip("场景加载完成、WorldStateRegistry 已就绪后触发。\n" +
|
||||
"场景内物体应订阅此事件,从 WorldStateRegistry 读取存档状态并应用(替代在 Start() 中读取)。\n" +
|
||||
"触发后会等待一帧,确保所有处理器执行完毕,再执行淡入显示场景。\n" +
|
||||
"对应 SO:EVT_SceneWorldStateRestored")]
|
||||
[SerializeField] private VoidEventChannelSO _onSceneWorldStateRestored;
|
||||
|
||||
[SerializeField] private SceneLoader _sceneLoader;
|
||||
|
||||
[Header("淡出时长")]
|
||||
@@ -71,6 +87,14 @@ namespace BaseGames.Core
|
||||
else
|
||||
Debug.LogError("[SceneService] _sceneLoader 未赋值,场景加载中断。请在 Inspector 中绑定 SceneLoader 组件。");
|
||||
|
||||
// 通知:WorldStateRegistry 已就绪,场景物体应在此帧内从中读取存档状态并应用初始状态。
|
||||
// 订阅者(WorldStateRegistrySaver、各场景 StateApplier 等)会在同一帧同步执行。
|
||||
_onSceneWorldStateRestored?.Raise();
|
||||
|
||||
// 等待一帧:确保所有场景物体的 Start() 和事件处理器都已执行完毕,
|
||||
// 场景物体处于正确的初始状态后再揭开黑幕,避免出现一帧状态错误的画面闪烁。
|
||||
yield return null;
|
||||
|
||||
_onFadeInRequest?.Raise();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1274
Assets/_Game/Scripts/Editor/Addressables/AddressableManagerWindow.cs
Normal file
1274
Assets/_Game/Scripts/Editor/Addressables/AddressableManagerWindow.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abd3b31b261435e4786f53b937c71742
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,527 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEngine;
|
||||
// 此文件已被 AddressableManagerWindow 取代。
|
||||
// 原有功能已整合到统一工具中,请使用:
|
||||
// BaseGames → Addressables → Addressables Manager(总入口)
|
||||
// BaseGames → Addressables → Rule Sync(直达规则校验 Tab)
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Addressable 规则同步窗口。
|
||||
///
|
||||
/// 功能:
|
||||
/// 1. 扫描所有已注册的 Addressable 资产
|
||||
/// 2. 根据 <see cref="AddressableRules"/> 中的规则计算期望分组与期望标签
|
||||
/// 3. 对比实际值,显示所有不符合规范的条目(分组错误 / 标签缺失 / 标签多余)
|
||||
/// 4. 一键自动修复全部问题
|
||||
/// 5. 导出 CSV 报告供存档或 Code Review
|
||||
///
|
||||
/// 菜单:BaseGames → Addressables → Rule Sync
|
||||
/// </summary>
|
||||
public class AddressableRuleSyncWindow : EditorWindow
|
||||
{
|
||||
// ── 内部数据结构 ───────────────────────────────────────────────────────
|
||||
|
||||
private enum IssueKind { None, WrongGroup, MissingLabel, ExtraLabel }
|
||||
|
||||
private class EntryReport
|
||||
{
|
||||
public string Address;
|
||||
public string AssetPath;
|
||||
public string CurrentGroup;
|
||||
public string ExpectedGroup; // null = 规则未覆盖,维持现状
|
||||
public string[] CurrentLabels;
|
||||
public string[] ExpectedLabels;
|
||||
public string[] MissingLabels; // 应有但没有(规则要求),红色错误
|
||||
public string[] ExtraLabels; // 规则不要求且在 KnownLabels 中(多余规则标签),红色错误
|
||||
public string[] UnknownLabels; // 规则不要求且不在 KnownLabels 中(自定义标签),黄色警告,不自动删除
|
||||
public bool GroupOk => ExpectedGroup == null || CurrentGroup == ExpectedGroup;
|
||||
public bool LabelsOk => MissingLabels.Length == 0 && ExtraLabels.Length == 0;
|
||||
public bool IsOk => GroupOk && LabelsOk;
|
||||
public bool HasWarnings => UnknownLabels.Length > 0;
|
||||
}
|
||||
|
||||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private List<EntryReport> _reports = new();
|
||||
private Vector2 _scrollPos;
|
||||
private bool _showOk = false;
|
||||
private bool _scanned = false;
|
||||
private string _searchFilter = "";
|
||||
|
||||
// ── 样式(惰性初始化)────────────────────────────────────────────────
|
||||
|
||||
private GUIStyle _okStyle;
|
||||
private GUIStyle _warnStyle;
|
||||
private GUIStyle _errorStyle;
|
||||
private GUIStyle _boldStyle;
|
||||
private GUIStyle _rowEven;
|
||||
private GUIStyle _rowOdd;
|
||||
private bool _stylesReady;
|
||||
|
||||
// ── 颜色 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static readonly Color ColOk = new(0.20f, 0.78f, 0.35f, 1f);
|
||||
private static readonly Color ColWarn = new(0.95f, 0.75f, 0.10f, 1f);
|
||||
private static readonly Color ColError = new(0.90f, 0.25f, 0.20f, 1f);
|
||||
private static readonly Color ColRowEven = new(0.22f, 0.22f, 0.22f, 0.4f);
|
||||
|
||||
// ── 菜单入口 ──────────────────────────────────────────────────────────
|
||||
|
||||
[MenuItem("BaseGames/Addressables/Rule Sync", priority = 110)]
|
||||
public static void OpenWindow()
|
||||
{
|
||||
var win = GetWindow<AddressableRuleSyncWindow>("Addressable Rule Sync");
|
||||
win.minSize = new Vector2(1040, 540);
|
||||
win.Show();
|
||||
}
|
||||
|
||||
// ── GUI ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EnsureStyles();
|
||||
|
||||
if (AddressableAssetSettingsDefaultObject.Settings == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"Addressable Settings 未初始化。\n" +
|
||||
"请先执行 Window → Asset Management → Addressables → Groups → Create Addressables Settings。",
|
||||
MessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawToolbar();
|
||||
DrawStats();
|
||||
DrawTable();
|
||||
DrawFooter();
|
||||
}
|
||||
|
||||
// ── 工具栏 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||||
{
|
||||
if (GUILayout.Button("扫描", EditorStyles.toolbarButton, GUILayout.Width(80)))
|
||||
Scan();
|
||||
|
||||
if (GUILayout.Button("🔄 刷新", EditorStyles.toolbarButton, GUILayout.Width(60)))
|
||||
Scan();
|
||||
|
||||
GUILayout.Space(8);
|
||||
_showOk = GUILayout.Toggle(_showOk, "显示正常项", EditorStyles.toolbarButton, GUILayout.Width(80));
|
||||
GUILayout.Space(8);
|
||||
|
||||
EditorGUILayout.LabelField("搜索:", GUILayout.Width(42));
|
||||
_searchFilter = EditorGUILayout.TextField(_searchFilter, EditorStyles.toolbarSearchField,
|
||||
GUILayout.Width(200));
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
GUI.enabled = _scanned && _reports.Any(r => !r.IsOk);
|
||||
if (GUILayout.Button("✦ 修复所有问题", EditorStyles.toolbarButton, GUILayout.Width(120)))
|
||||
FixAll();
|
||||
GUI.enabled = _scanned;
|
||||
if (GUILayout.Button("导出 CSV", EditorStyles.toolbarButton, GUILayout.Width(80)))
|
||||
ExportCsv();
|
||||
GUI.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 统计行 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawStats()
|
||||
{
|
||||
if (!_scanned) return;
|
||||
|
||||
int total = _reports.Count;
|
||||
int ok = _reports.Count(r => r.IsOk);
|
||||
int issues = _reports.Count(r => !r.IsOk);
|
||||
int warnings = _reports.Count(r => r.IsOk && r.HasWarnings);
|
||||
int wrongGrp = _reports.Count(r => !r.GroupOk);
|
||||
int misLabel = _reports.Count(r => r.MissingLabels.Length > 0);
|
||||
int extLabel = _reports.Count(r => r.ExtraLabels.Length > 0);
|
||||
int unkLabel = _reports.Count(r => r.UnknownLabels.Length > 0);
|
||||
|
||||
EditorGUILayout.Space(2);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label($"共 {total} 条目", EditorStyles.miniLabel);
|
||||
GUILayout.Space(12);
|
||||
DrawColoredLabel($"✅ 正常 {ok}", ColOk);
|
||||
GUILayout.Space(12);
|
||||
DrawColoredLabel($"❌ 问题 {issues}", issues > 0 ? ColError : ColOk);
|
||||
GUILayout.Space(8);
|
||||
DrawColoredLabel($"⚠ 自定义标签 {unkLabel}", unkLabel > 0 ? ColWarn : ColOk);
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label($"分组错误 {wrongGrp} | 标签缺失 {misLabel} | 多余规则标签 {extLabel}",
|
||||
EditorStyles.miniLabel);
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
EditorGUILayout.Space(2);
|
||||
}
|
||||
|
||||
// ── 主表格 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawTable()
|
||||
{
|
||||
// 表头
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||||
{
|
||||
GUILayout.Label("Address", _boldStyle, GUILayout.Width(200));
|
||||
GUILayout.Label("当前分组", _boldStyle, GUILayout.Width(120));
|
||||
GUILayout.Label("期望分组", _boldStyle, GUILayout.Width(120));
|
||||
GUILayout.Label("缺失标签", _boldStyle, GUILayout.Width(130));
|
||||
GUILayout.Label("多余规则标签", _boldStyle, GUILayout.Width(110));
|
||||
GUILayout.Label("自定义标签", _boldStyle, GUILayout.Width(110));
|
||||
GUILayout.Label("状态", _boldStyle, GUILayout.Width(80));
|
||||
}
|
||||
|
||||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.ExpandHeight(true));
|
||||
|
||||
if (!_scanned)
|
||||
{
|
||||
EditorGUILayout.HelpBox("点击「扫描」按钮开始分析已注册的 Addressable 资产。", MessageType.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = _reports
|
||||
.Where(r => _showOk || !r.IsOk)
|
||||
.Where(r => string.IsNullOrEmpty(_searchFilter)
|
||||
|| r.Address.IndexOf(_searchFilter, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
.ToList();
|
||||
|
||||
if (display.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
_showOk ? "没有匹配搜索条件的条目。" : "✅ 所有资产均符合规范!",
|
||||
MessageType.Info);
|
||||
}
|
||||
|
||||
for (int i = 0; i < display.Count; i++)
|
||||
DrawRow(display[i], i);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawRow(EntryReport r, int idx)
|
||||
{
|
||||
var bg = idx % 2 == 0 ? _rowEven : GUIStyle.none;
|
||||
using (new EditorGUILayout.HorizontalScope(bg, GUILayout.Height(20)))
|
||||
{
|
||||
// Address(点击可 Ping)
|
||||
if (GUILayout.Button(r.Address, EditorStyles.linkLabel, GUILayout.Width(200)))
|
||||
PingAsset(r.AssetPath);
|
||||
|
||||
// 当前分组
|
||||
var grpColor = r.GroupOk ? ColOk : ColError;
|
||||
DrawColoredLabel(r.CurrentGroup ?? "—", grpColor, GUILayout.Width(120));
|
||||
|
||||
// 期望分组
|
||||
var expGrpText = r.ExpectedGroup ?? "(规则未覆盖)";
|
||||
var expGrpColor = r.GroupOk ? ColOk : ColWarn;
|
||||
DrawColoredLabel(expGrpText, expGrpColor, GUILayout.Width(120));
|
||||
|
||||
// 缺失标签(红色,须补齐)
|
||||
var missingText = r.MissingLabels.Length > 0 ? string.Join(", ", r.MissingLabels) : "—";
|
||||
DrawColoredLabel(missingText, r.MissingLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(130));
|
||||
|
||||
// 多余规则标签(红色,将被 FixEntry 移除)
|
||||
var extraText = r.ExtraLabels.Length > 0 ? string.Join(", ", r.ExtraLabels) : "—";
|
||||
DrawColoredLabel(extraText, r.ExtraLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(110));
|
||||
|
||||
// 自定义标签(黄色警告,不会被自动删除,建议写入规范)
|
||||
var unknownText = r.UnknownLabels.Length > 0 ? string.Join(", ", r.UnknownLabels) : "—";
|
||||
DrawColoredLabel(unknownText, r.UnknownLabels.Length > 0 ? ColWarn : ColOk, GUILayout.Width(110));
|
||||
|
||||
// 状态 + 单条修复按钮
|
||||
if (r.IsOk)
|
||||
{
|
||||
var statusColor = r.HasWarnings ? ColWarn : ColOk;
|
||||
var statusText = r.HasWarnings ? "⚠ 自定义标签" : "✅ 正常";
|
||||
DrawColoredLabel(statusText, statusColor, GUILayout.Width(80));
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawColoredLabel("❌ 需修复", ColError, GUILayout.Width(60));
|
||||
if (GUILayout.Button("修复", EditorStyles.miniButton, GUILayout.Width(40)))
|
||||
FixEntry(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 底栏 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawFooter()
|
||||
{
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.HelpBox(
|
||||
"规则来源:Docs/Standards/AddressablesLabelSpec.md §3 分组规则:AssetFolderSpec.md §8.1\n" +
|
||||
"「修复所有问题」仅修改已注册资产的分组/标签,不注册新资产,不删除自定义标签(黄色警告项)。\n" +
|
||||
"新增资产工作流:① Addressable Batch Tool → ⚡ 全量扫描 _Game/ → 注册所有 ② 返回此窗口 → 扫描 → 修复所有问题",
|
||||
MessageType.None);
|
||||
}
|
||||
|
||||
// ── 扫描逻辑 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void Scan()
|
||||
{
|
||||
_reports.Clear();
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null) return;
|
||||
|
||||
foreach (var group in settings.groups)
|
||||
{
|
||||
if (group == null) continue;
|
||||
foreach (var entry in group.entries)
|
||||
{
|
||||
if (entry == null) continue;
|
||||
|
||||
var address = entry.address;
|
||||
var expectedGroup = AddressableRules.GetExpectedGroup(address);
|
||||
var expectedLbls = AddressableRules.GetExpectedLabels(address);
|
||||
var currentLbls = entry.labels.ToArray();
|
||||
|
||||
var missing = expectedLbls.Except(currentLbls, StringComparer.Ordinal).ToArray();
|
||||
|
||||
// 区分两类"多余标签":
|
||||
// extra = 规则已知标签(KnownLabels)中规则不要求的 → 红色,FixEntry 会移除
|
||||
// unknown = 不在 KnownLabels 中的自定义标签 → 黄色警告,FixEntry 保留,建议写入规范
|
||||
var notExpected = currentLbls.Except(expectedLbls, StringComparer.Ordinal);
|
||||
var extra = notExpected.Where(l => AddressableRules.KnownLabels.Contains(l)).ToArray();
|
||||
var unknown = notExpected.Where(l => !AddressableRules.KnownLabels.Contains(l)).ToArray();
|
||||
|
||||
_reports.Add(new EntryReport
|
||||
{
|
||||
Address = address,
|
||||
AssetPath = entry.AssetPath,
|
||||
CurrentGroup = group.name,
|
||||
ExpectedGroup = expectedGroup,
|
||||
CurrentLabels = currentLbls,
|
||||
ExpectedLabels = expectedLbls,
|
||||
MissingLabels = missing,
|
||||
ExtraLabels = extra,
|
||||
UnknownLabels = unknown,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 问题项排前面,仅有警告的次之,正常项排最后;同类按 Address 字母序
|
||||
_reports = _reports
|
||||
.OrderBy(r => r.IsOk ? (r.HasWarnings ? 1 : 2) : 0)
|
||||
.ThenBy(r => r.Address, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
_scanned = true;
|
||||
Repaint();
|
||||
|
||||
int issues = _reports.Count(r => !r.IsOk);
|
||||
int warnings = _reports.Count(r => r.IsOk && r.HasWarnings);
|
||||
Debug.Log($"[AddressableRuleSync] 扫描完成:{_reports.Count} 个条目," +
|
||||
$"{issues} 个需要修复,{warnings} 个含自定义标签警告。");
|
||||
}
|
||||
|
||||
// ── 修复逻辑 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void FixAll()
|
||||
{
|
||||
var issues = _reports.Where(r => !r.IsOk).ToList();
|
||||
if (issues.Count == 0) return;
|
||||
|
||||
int moveCount = issues.Count(r => !r.GroupOk);
|
||||
int addCount = issues.Sum(r => r.MissingLabels.Length);
|
||||
int removeCount = issues.Sum(r => r.ExtraLabels.Length);
|
||||
|
||||
// 干跑预览对话框
|
||||
bool confirmed = EditorUtility.DisplayDialog(
|
||||
"确认修复所有问题",
|
||||
$"将对 {issues.Count} 个条目执行以下操作:\n\n" +
|
||||
$" • 移动分组:{moveCount} 个\n" +
|
||||
$" • 添加标签:{addCount} 个\n" +
|
||||
$" • 移除多余规则标签:{removeCount} 个\n\n" +
|
||||
"⚠ 自定义标签(黄色警告项)不会被删除。\n" +
|
||||
"此操作不可撤销,请确认后继续。",
|
||||
"确认修复", "取消");
|
||||
if (!confirmed) return;
|
||||
|
||||
int fixedCount = 0;
|
||||
foreach (var r in issues)
|
||||
{
|
||||
if (FixEntry(r)) fixedCount++;
|
||||
}
|
||||
|
||||
SaveSettings();
|
||||
Scan(); // 修复后重新扫描以更新结果
|
||||
Debug.Log($"[AddressableRuleSync] 修复完成:共处理 {fixedCount} 个条目。");
|
||||
}
|
||||
|
||||
private bool FixEntry(EntryReport r)
|
||||
{
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null) return false;
|
||||
|
||||
var entry = FindEntry(settings, r.Address);
|
||||
if (entry == null)
|
||||
{
|
||||
Debug.LogWarning($"[AddressableRuleSync] 找不到条目:{r.Address}");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
|
||||
// 修复分组
|
||||
if (!r.GroupOk && r.ExpectedGroup != null)
|
||||
{
|
||||
var targetGroup = GetOrCreateGroup(settings, r.ExpectedGroup);
|
||||
if (targetGroup != null && entry.parentGroup != targetGroup)
|
||||
{
|
||||
settings.MoveEntry(entry, targetGroup, false, false);
|
||||
r.CurrentGroup = r.ExpectedGroup;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加缺失标签
|
||||
foreach (var lbl in r.MissingLabels)
|
||||
{
|
||||
EnsureLabelExists(settings, lbl);
|
||||
entry.SetLabel(lbl, true, true);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 移除多余规则标签(ExtraLabels 只包含 KnownLabels 中规则不要求的标签;
|
||||
// UnknownLabels 是用户自定义标签,刻意保留,不做删除)
|
||||
foreach (var lbl in r.ExtraLabels)
|
||||
{
|
||||
entry.SetLabel(lbl, false, true);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ── 导出 CSV ──────────────────────────────────────────────────────────
|
||||
|
||||
private void ExportCsv()
|
||||
{
|
||||
if (_reports.Count == 0) return;
|
||||
|
||||
var path = EditorUtility.SaveFilePanel(
|
||||
"导出 Addressable Rule 报告", "", "AddressableRuleReport.csv", "csv");
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Address,CurrentGroup,ExpectedGroup,GroupOk,MissingLabels,ExtraLabels,Status");
|
||||
|
||||
foreach (var r in _reports)
|
||||
{
|
||||
var status = r.IsOk ? "OK" : "ISSUE";
|
||||
sb.AppendLine(
|
||||
$"\"{r.Address}\"," +
|
||||
$"\"{r.CurrentGroup}\"," +
|
||||
$"\"{r.ExpectedGroup ?? "(uncovered)"}\"," +
|
||||
$"{r.GroupOk}," +
|
||||
$"\"{string.Join(";", r.MissingLabels)}\"," +
|
||||
$"\"{string.Join(";", r.ExtraLabels)}\"," +
|
||||
$"{status}");
|
||||
}
|
||||
|
||||
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
|
||||
Debug.Log($"[AddressableRuleSync] CSV 报告已导出:{path}");
|
||||
}
|
||||
|
||||
// ── 辅助方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static AddressableAssetEntry FindEntry(AddressableAssetSettings settings, string address)
|
||||
{
|
||||
foreach (var group in settings.groups)
|
||||
{
|
||||
if (group == null) continue;
|
||||
foreach (var e in group.entries)
|
||||
if (e != null && e.address == address) return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName)
|
||||
{
|
||||
var existing = settings.groups.FirstOrDefault(g => g != null && g.name == groupName);
|
||||
if (existing != null) return existing;
|
||||
|
||||
var template = settings.GroupTemplateObjects.FirstOrDefault()
|
||||
as AddressableAssetGroupTemplate;
|
||||
var newGroup = settings.CreateGroup(groupName, false, false, true,
|
||||
template != null
|
||||
? new List<AddressableAssetGroupSchema>(template.SchemaObjects)
|
||||
: null);
|
||||
|
||||
if (newGroup != null)
|
||||
Debug.Log($"[AddressableRuleSync] 已自动创建分组:{groupName}");
|
||||
|
||||
return newGroup ?? settings.DefaultGroup;
|
||||
}
|
||||
|
||||
private static void EnsureLabelExists(AddressableAssetSettings settings, string label)
|
||||
{
|
||||
var labels = settings.GetLabels();
|
||||
if (!labels.Contains(label))
|
||||
{
|
||||
settings.AddLabel(label, true);
|
||||
Debug.Log($"[AddressableRuleSync] 已创建标签:{label}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveSettings()
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
AddressableAssetSettingsDefaultObject.Settings?.SetDirty(
|
||||
AddressableAssetSettings.ModificationEvent.EntryModified, null, true);
|
||||
}
|
||||
|
||||
private static void PingAsset(string assetPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetPath)) return;
|
||||
var obj = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
||||
if (obj != null) EditorGUIUtility.PingObject(obj);
|
||||
}
|
||||
|
||||
// ── 样式初始化 ────────────────────────────────────────────────────────
|
||||
|
||||
private void EnsureStyles()
|
||||
{
|
||||
if (_stylesReady) return;
|
||||
|
||||
_boldStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 11 };
|
||||
_okStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColOk } };
|
||||
_warnStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColWarn } };
|
||||
_errorStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColError } };
|
||||
|
||||
_rowEven = new GUIStyle();
|
||||
_rowEven.normal.background = MakeTexture(1, 1, ColRowEven);
|
||||
|
||||
_stylesReady = true;
|
||||
}
|
||||
|
||||
private void DrawColoredLabel(string text, Color color, params GUILayoutOption[] options)
|
||||
{
|
||||
var prev = GUI.color;
|
||||
GUI.color = color;
|
||||
GUILayout.Label(text, EditorStyles.miniLabel, options);
|
||||
GUI.color = prev;
|
||||
}
|
||||
|
||||
private static Texture2D MakeTexture(int width, int height, Color color)
|
||||
{
|
||||
var tex = new Texture2D(width, height);
|
||||
tex.SetPixel(0, 0, color);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
}
|
||||
// 保留空类以避免 .meta 文件孤立。
|
||||
internal static class AddressableRuleSyncWindowStub { }
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"BaseGames.Skills",
|
||||
"BaseGames.World.Map",
|
||||
"BaseGames.EventChain",
|
||||
"BaseGames.VFX"
|
||||
"BaseGames.VFX",
|
||||
"Unity.InputSystem"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace BaseGames.Editor
|
||||
CreateAsset<StringEventChannelSO> ("Core", "EVT_SceneLoaded");
|
||||
CreateAsset<VoidEventChannelSO> ("Core", "EVT_FadeInRequest");
|
||||
CreateAsset<VoidEventChannelSO> ("Core", "EVT_FadeOutRequest");
|
||||
CreateAsset<VoidEventChannelSO> ("Core", "EVT_SceneWorldStateRestored"); // 场景加载完毕、世界状态恢复后触发;场景物体在此订阅并应用存档状态,淡入前保证画面正确
|
||||
|
||||
// ── 难度 ──────────────────────────────────────────────────────────
|
||||
CreateAsset<DifficultyChangedEventChannel>("Difficulty", "EVT_DifficultyChanged");
|
||||
@@ -99,14 +100,18 @@ namespace BaseGames.Editor
|
||||
CreateAsset<VoidEventChannelSO> ("World", "EVT_CheckpointReached");
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_DoorOpened"); // 开门/交互机关(钥匙、机关等)触发自动存档
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_ItemPickup"); // 道具/收集品获取(itemId)
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_CollectiblePickup"); // 关键物品拾取(护符、道具等)触发存档;AutoSaveService / QuestManager / EventChainManager 监听
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_CollectibleSaved"); // 持久化记录收集品(collectibleId)
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_RoomEntered"); // 玩家进入新房间(roomId)
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_RegionChanged"); // 玩家首次进入新区域(regionId)
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_RevealRegion"); // 触发地图区域揭露(regionId)
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_MapUpdated"); // 房间首次探索/标注时刷新(roomId);MapManager 发布,MapPanel 订阅
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_ChallengeCompleted"); // 挑战房间通关(challengeId)
|
||||
CreateAsset<StringEventChannelSO> ("World", "EVT_ChallengeFailed"); // 挑战房间失败(challengeId)
|
||||
CreateAsset<LiquidEventChannelSO> ("World", "EVT_LiquidEntered"); // 玩家进入液体区域
|
||||
CreateAsset<LiquidEventChannelSO> ("World", "EVT_LiquidExited"); // 玩家离开液体区域
|
||||
CreateAsset<BaseGames.World.WorldMarkerEventChannelSO>("World", "EVT_WorldMarkerActivated"); // 导航标记点激活(地图图标显示)
|
||||
CreateAsset<BaseGames.World.WorldMarkerEventChannelSO>("World", "EVT_WorldMarkerDeactivated"); // 导航标记点失活(地图图标隐藏)
|
||||
|
||||
// ── 对话/商店 ─────────────────────────────────────────────────────
|
||||
CreateAsset<ShopPurchaseEventChannelSO> ("Dialogue", "EVT_ShopPurchase");
|
||||
|
||||
8
Assets/_Game/Scripts/Editor/Input.meta
Normal file
8
Assets/_Game/Scripts/Editor/Input.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52933b4810ae6654c962a93708b64a8f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
467
Assets/_Game/Scripts/Editor/Input/InputDeviceIconSetSOEditor.cs
Normal file
467
Assets/_Game/Scripts/Editor/Input/InputDeviceIconSetSOEditor.cs
Normal file
@@ -0,0 +1,467 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.UI;
|
||||
|
||||
namespace BaseGames.Editor.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// InputDeviceIconSetSO 自定义 Inspector。
|
||||
///
|
||||
/// 布局(从上到下):
|
||||
/// ① 设备类型徽章 + 覆盖率芯片
|
||||
/// ② 操作按钮工具栏(从 Action Asset 自动填充 / 打开 Studio)
|
||||
/// ③ 按键图标条目表(Path | Icon Sprite | 48px 预览 | 删除按钮)
|
||||
/// ④ + 新增条目 按钮
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(InputDeviceIconSetSO))]
|
||||
public class InputDeviceIconSetSOEditor : UnityEditor.Editor
|
||||
{
|
||||
private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss";
|
||||
|
||||
// 预设绑定路径快捷菜单(按设备类型)
|
||||
private static readonly Dictionary<InputDeviceType, string[]> s_CommonPaths = new()
|
||||
{
|
||||
[InputDeviceType.KeyboardMouse] = new[]
|
||||
{
|
||||
"<Keyboard>/e", "<Keyboard>/f", "<Keyboard>/r", "<Keyboard>/space",
|
||||
"<Keyboard>/enter", "<Keyboard>/escape", "<Keyboard>/shift",
|
||||
"<Keyboard>/ctrl", "<Keyboard>/tab", "<Keyboard>/q", "<Keyboard>/g",
|
||||
"<Mouse>/leftButton", "<Mouse>/rightButton", "<Mouse>/middleButton"
|
||||
},
|
||||
[InputDeviceType.XboxController] = new[]
|
||||
{
|
||||
"<Gamepad>/buttonSouth", "<Gamepad>/buttonNorth",
|
||||
"<Gamepad>/buttonEast", "<Gamepad>/buttonWest",
|
||||
"<Gamepad>/leftShoulder", "<Gamepad>/rightShoulder",
|
||||
"<Gamepad>/leftTrigger", "<Gamepad>/rightTrigger",
|
||||
"<Gamepad>/start", "<Gamepad>/select",
|
||||
"<Gamepad>/leftStickPress", "<Gamepad>/rightStickPress"
|
||||
},
|
||||
[InputDeviceType.PlayStationController] = new[]
|
||||
{
|
||||
"<Gamepad>/buttonSouth", "<Gamepad>/buttonNorth",
|
||||
"<Gamepad>/buttonEast", "<Gamepad>/buttonWest",
|
||||
"<Gamepad>/leftShoulder", "<Gamepad>/rightShoulder",
|
||||
"<Gamepad>/leftTrigger", "<Gamepad>/rightTrigger",
|
||||
"<Gamepad>/start", "<Gamepad>/select"
|
||||
},
|
||||
[InputDeviceType.SwitchController] = new[]
|
||||
{
|
||||
"<Gamepad>/buttonSouth", "<Gamepad>/buttonNorth",
|
||||
"<Gamepad>/buttonEast", "<Gamepad>/buttonWest",
|
||||
"<Gamepad>/leftShoulder", "<Gamepad>/rightShoulder",
|
||||
"<Gamepad>/leftTrigger", "<Gamepad>/rightTrigger",
|
||||
"<Gamepad>/start", "<Gamepad>/select"
|
||||
}
|
||||
};
|
||||
|
||||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||||
private SerializedProperty _entriesProp;
|
||||
private VisualElement _root;
|
||||
private VisualElement _tableContainer;
|
||||
private Label _coverageLabel;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
public override VisualElement CreateInspectorGUI()
|
||||
{
|
||||
_entriesProp = serializedObject.FindProperty("_entries");
|
||||
|
||||
_root = new VisualElement();
|
||||
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
||||
if (uss != null) _root.styleSheets.Add(uss);
|
||||
|
||||
BuildHeader();
|
||||
BuildToolbar();
|
||||
BuildTable();
|
||||
BuildAddButton();
|
||||
|
||||
return _root;
|
||||
}
|
||||
|
||||
// ── 头部 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildHeader()
|
||||
{
|
||||
var header = new VisualElement();
|
||||
header.style.flexDirection = FlexDirection.Row;
|
||||
header.style.alignItems = Align.Center;
|
||||
header.style.paddingLeft = 4;
|
||||
header.style.paddingRight = 4;
|
||||
header.style.paddingTop = 6;
|
||||
header.style.paddingBottom = 6;
|
||||
header.style.borderBottomWidth = 1;
|
||||
header.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.25f));
|
||||
header.style.marginBottom = 6;
|
||||
|
||||
// 设备类型字段
|
||||
var deviceProp = serializedObject.FindProperty("_deviceType");
|
||||
var deviceField = new PropertyField(deviceProp, "设备类型");
|
||||
deviceField.style.flexGrow = 1;
|
||||
deviceField.RegisterValueChangeCallback(_ => RefreshCoverage());
|
||||
header.Add(deviceField);
|
||||
|
||||
// 覆盖率徽章
|
||||
_coverageLabel = new Label();
|
||||
_coverageLabel.AddToClassList("status-chip--ok");
|
||||
_coverageLabel.style.fontSize = 10;
|
||||
_coverageLabel.style.marginLeft = 8;
|
||||
RefreshCoverage();
|
||||
header.Add(_coverageLabel);
|
||||
|
||||
_root.Add(header);
|
||||
}
|
||||
|
||||
// ── 工具栏 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildToolbar()
|
||||
{
|
||||
var bar = new VisualElement();
|
||||
bar.AddToClassList("editor-toolbar");
|
||||
bar.style.marginBottom = 6;
|
||||
|
||||
// 从 InputActionAsset 自动填充路径
|
||||
var btnAutoFill = new Button(AutoFillFromActionAsset)
|
||||
{
|
||||
text = "⬇ 从 Action Asset 填充路径",
|
||||
tooltip = "扫描 InputReaderSO,自动为该设备控制方案添加所有绑定路径(Sprite 留空需手动指定)"
|
||||
};
|
||||
btnAutoFill.AddToClassList("wizard-factory-btn");
|
||||
bar.Add(btnAutoFill);
|
||||
|
||||
// 清空 null Sprite 条目
|
||||
var btnClean = new Button(RemoveEmptySpriteEntries)
|
||||
{
|
||||
text = "🧹 清理空 Sprite",
|
||||
tooltip = "移除 Sprite 为空的条目"
|
||||
};
|
||||
bar.Add(btnClean);
|
||||
|
||||
// 打开 Studio
|
||||
var btnStudio = new Button(() => InputIconStudioWindow.Open())
|
||||
{
|
||||
text = "🎨 打开 Icon Studio",
|
||||
tooltip = "打开完整的按键图标管理工作台"
|
||||
};
|
||||
btnStudio.AddToClassList("wizard-jump-btn");
|
||||
btnStudio.style.marginLeft = 8;
|
||||
bar.Add(btnStudio);
|
||||
|
||||
_root.Add(bar);
|
||||
}
|
||||
|
||||
// ── 条目表 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildTable()
|
||||
{
|
||||
// 表头
|
||||
var tableHead = new VisualElement();
|
||||
tableHead.style.flexDirection = FlexDirection.Row;
|
||||
tableHead.style.paddingLeft = 4;
|
||||
tableHead.style.paddingRight = 4;
|
||||
tableHead.style.paddingTop = 3;
|
||||
tableHead.style.paddingBottom = 3;
|
||||
tableHead.style.borderBottomWidth = 1;
|
||||
tableHead.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.3f));
|
||||
tableHead.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.08f));
|
||||
|
||||
AddHeaderCell(tableHead, "绑定路径", flexGrow: 1f);
|
||||
AddHeaderCell(tableHead, "图标", flexGrow: 0.8f);
|
||||
AddHeaderCell(tableHead, "预览", width: 52f);
|
||||
AddHeaderCell(tableHead, "", width: 24f);
|
||||
_root.Add(tableHead);
|
||||
|
||||
// 条目容器
|
||||
_tableContainer = new VisualElement();
|
||||
_root.Add(_tableContainer);
|
||||
|
||||
RebuildRows();
|
||||
}
|
||||
|
||||
private void RebuildRows()
|
||||
{
|
||||
_tableContainer.Clear();
|
||||
serializedObject.Update();
|
||||
|
||||
for (int i = 0; i < _entriesProp.arraySize; i++)
|
||||
_tableContainer.Add(BuildRow(i));
|
||||
|
||||
RefreshCoverage();
|
||||
}
|
||||
|
||||
private VisualElement BuildRow(int index)
|
||||
{
|
||||
var entryProp = _entriesProp.GetArrayElementAtIndex(index);
|
||||
var pathProp = entryProp.FindPropertyRelative("BindingPath");
|
||||
var iconProp = entryProp.FindPropertyRelative("Icon");
|
||||
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.alignItems = Align.Center;
|
||||
row.style.paddingTop = 2;
|
||||
row.style.paddingBottom = 2;
|
||||
row.style.paddingLeft = 4;
|
||||
row.style.paddingRight = 4;
|
||||
row.style.borderBottomWidth = 1;
|
||||
row.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.12f));
|
||||
row.AddToClassList("list-item");
|
||||
|
||||
// ── 绑定路径 ──────────────────────────────────────────────────
|
||||
var pathField = new TextField { value = pathProp.stringValue, isDelayed = true };
|
||||
pathField.style.flexGrow = 1;
|
||||
pathField.style.flexShrink = 1;
|
||||
pathField.style.fontSize = 10;
|
||||
pathField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
serializedObject.Update();
|
||||
_entriesProp.GetArrayElementAtIndex(index)
|
||||
.FindPropertyRelative("BindingPath").stringValue = evt.newValue;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
RefreshCoverage();
|
||||
});
|
||||
|
||||
// 路径快捷菜单按钮
|
||||
var pathRow = new VisualElement();
|
||||
pathRow.style.flexDirection = FlexDirection.Row;
|
||||
pathRow.style.flexGrow = 1;
|
||||
pathRow.style.alignItems = Align.Center;
|
||||
pathField.style.flexGrow = 1;
|
||||
pathRow.Add(pathField);
|
||||
|
||||
var menuBtn = new Button(() =>
|
||||
{
|
||||
var menu = new GenericMenu();
|
||||
var so = target as InputDeviceIconSetSO;
|
||||
if (so != null && s_CommonPaths.TryGetValue(so.DeviceType, out var paths))
|
||||
{
|
||||
foreach (var p in paths)
|
||||
{
|
||||
var captured = p;
|
||||
menu.AddItem(new GUIContent(p), false, () =>
|
||||
{
|
||||
serializedObject.Update();
|
||||
_entriesProp.GetArrayElementAtIndex(index)
|
||||
.FindPropertyRelative("BindingPath").stringValue = captured;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
pathField.value = captured;
|
||||
RefreshCoverage();
|
||||
});
|
||||
}
|
||||
}
|
||||
menu.ShowAsContext();
|
||||
}) { text = "▾" };
|
||||
menuBtn.style.width = 20;
|
||||
menuBtn.style.height = 18;
|
||||
menuBtn.style.fontSize = 9;
|
||||
menuBtn.style.paddingLeft = menuBtn.style.paddingRight = 0;
|
||||
menuBtn.tooltip = "常用路径快捷选择";
|
||||
pathRow.Add(menuBtn);
|
||||
row.Add(pathRow);
|
||||
|
||||
// ── Icon Sprite ───────────────────────────────────────────────
|
||||
var iconField = new ObjectField { objectType = typeof(Sprite), allowSceneObjects = false };
|
||||
iconField.value = iconProp.objectReferenceValue as Sprite;
|
||||
iconField.style.flexGrow = 0.8f;
|
||||
iconField.style.marginLeft = 4;
|
||||
iconField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
serializedObject.Update();
|
||||
_entriesProp.GetArrayElementAtIndex(index)
|
||||
.FindPropertyRelative("Icon").objectReferenceValue = evt.newValue;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
UpdatePreviewInRow(row, evt.newValue as Sprite);
|
||||
RefreshCoverage();
|
||||
});
|
||||
row.Add(iconField);
|
||||
|
||||
// ── 预览缩略图 ────────────────────────────────────────────────
|
||||
var preview = new Image { name = "icon-preview" };
|
||||
preview.style.width = 48;
|
||||
preview.style.height = 48;
|
||||
preview.style.flexShrink = 0;
|
||||
preview.style.marginLeft = 4;
|
||||
preview.style.borderTopLeftRadius = 3;
|
||||
preview.style.borderTopRightRadius = 3;
|
||||
preview.style.borderBottomLeftRadius = 3;
|
||||
preview.style.borderBottomRightRadius = 3;
|
||||
preview.style.backgroundColor = new StyleColor(new Color(0f, 0f, 0f, 0.25f));
|
||||
preview.scaleMode = ScaleMode.ScaleToFit;
|
||||
if (iconProp.objectReferenceValue is Sprite spr)
|
||||
preview.sprite = spr;
|
||||
row.Add(preview);
|
||||
|
||||
// ── 删除按钮 ──────────────────────────────────────────────────
|
||||
var delBtn = new Button(() =>
|
||||
{
|
||||
serializedObject.Update();
|
||||
_entriesProp.DeleteArrayElementAtIndex(index);
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
RebuildRows();
|
||||
}) { text = "✕" };
|
||||
delBtn.style.width = 24;
|
||||
delBtn.style.height = 24;
|
||||
delBtn.style.marginLeft = 4;
|
||||
delBtn.style.flexShrink = 0;
|
||||
delBtn.AddToClassList("action-button--danger");
|
||||
delBtn.tooltip = "删除此条目";
|
||||
row.Add(delBtn);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private void BuildAddButton()
|
||||
{
|
||||
var addBtn = new Button(() =>
|
||||
{
|
||||
serializedObject.Update();
|
||||
_entriesProp.InsertArrayElementAtIndex(_entriesProp.arraySize);
|
||||
var newEntry = _entriesProp.GetArrayElementAtIndex(_entriesProp.arraySize - 1);
|
||||
newEntry.FindPropertyRelative("BindingPath").stringValue = string.Empty;
|
||||
newEntry.FindPropertyRelative("Icon").objectReferenceValue = null;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
RebuildRows();
|
||||
}) { text = "+ 新增条目" };
|
||||
addBtn.style.marginTop = 6;
|
||||
addBtn.style.marginLeft = 4;
|
||||
addBtn.style.marginBottom = 4;
|
||||
addBtn.AddToClassList("wizard-factory-btn");
|
||||
_root.Add(addBtn);
|
||||
}
|
||||
|
||||
// ── 自动填充 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void AutoFillFromActionAsset()
|
||||
{
|
||||
var so = target as InputDeviceIconSetSO;
|
||||
if (so == null) return;
|
||||
|
||||
// 查找项目中的 InputReaderSO,获取 InputActionAsset
|
||||
var readerGuids = AssetDatabase.FindAssets("t:InputReaderSO");
|
||||
InputActionAsset actionAsset = null;
|
||||
foreach (var g in readerGuids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(g);
|
||||
var reader = AssetDatabase.LoadAssetAtPath<ScriptableObject>(path);
|
||||
if (reader == null) continue;
|
||||
var so2 = new SerializedObject(reader);
|
||||
var prop = so2.FindProperty("_inputActions");
|
||||
if (prop?.objectReferenceValue is InputActionAsset asset)
|
||||
{
|
||||
actionAsset = asset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (actionAsset == null)
|
||||
{
|
||||
EditorUtility.DisplayDialog("未找到 InputActionAsset",
|
||||
"无法在项目中找到 InputReaderSO 或其引用的 InputActionAsset。\n请手动填写绑定路径。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
string scheme = so.DeviceType == InputDeviceType.KeyboardMouse ? "Keyboard&Mouse" : "Gamepad";
|
||||
|
||||
serializedObject.Update();
|
||||
|
||||
// 收集已有路径,避免重复
|
||||
var existingPaths = new HashSet<string>();
|
||||
for (int i = 0; i < _entriesProp.arraySize; i++)
|
||||
{
|
||||
var p = _entriesProp.GetArrayElementAtIndex(i).FindPropertyRelative("BindingPath").stringValue;
|
||||
if (!string.IsNullOrEmpty(p)) existingPaths.Add(p);
|
||||
}
|
||||
|
||||
int added = 0;
|
||||
foreach (var action in actionAsset)
|
||||
{
|
||||
foreach (var binding in action.bindings)
|
||||
{
|
||||
if (binding.isComposite) continue;
|
||||
if (!string.IsNullOrEmpty(scheme)
|
||||
&& !string.IsNullOrEmpty(binding.groups)
|
||||
&& !binding.groups.Contains(scheme, System.StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var effectivePath = binding.effectivePath;
|
||||
if (string.IsNullOrEmpty(effectivePath)) continue;
|
||||
if (existingPaths.Contains(effectivePath)) continue;
|
||||
|
||||
existingPaths.Add(effectivePath);
|
||||
_entriesProp.InsertArrayElementAtIndex(_entriesProp.arraySize);
|
||||
var newEntry = _entriesProp.GetArrayElementAtIndex(_entriesProp.arraySize - 1);
|
||||
newEntry.FindPropertyRelative("BindingPath").stringValue = effectivePath;
|
||||
newEntry.FindPropertyRelative("Icon").objectReferenceValue = null;
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
RebuildRows();
|
||||
|
||||
if (added == 0)
|
||||
EditorUtility.DisplayDialog("填充完成", "所有路径已存在,无新增条目。", "确定");
|
||||
else
|
||||
Debug.Log($"[InputIconStudio] 自动填充了 {added} 个绑定路径(Sprite 需手动指定)。");
|
||||
}
|
||||
|
||||
private void RemoveEmptySpriteEntries()
|
||||
{
|
||||
serializedObject.Update();
|
||||
int removed = 0;
|
||||
for (int i = _entriesProp.arraySize - 1; i >= 0; i--)
|
||||
{
|
||||
var icon = _entriesProp.GetArrayElementAtIndex(i).FindPropertyRelative("Icon");
|
||||
if (icon.objectReferenceValue == null)
|
||||
{
|
||||
_entriesProp.DeleteArrayElementAtIndex(i);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
RebuildRows();
|
||||
Debug.Log($"[InputIconStudio] 清理了 {removed} 个空 Sprite 条目。");
|
||||
}
|
||||
|
||||
// ── 工具方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void RefreshCoverage()
|
||||
{
|
||||
if (_coverageLabel == null || _entriesProp == null) return;
|
||||
serializedObject.Update();
|
||||
int total = _entriesProp.arraySize;
|
||||
int hasIcon = 0;
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
if (_entriesProp.GetArrayElementAtIndex(i)
|
||||
.FindPropertyRelative("Icon").objectReferenceValue != null)
|
||||
hasIcon++;
|
||||
}
|
||||
_coverageLabel.text = $"覆盖: {hasIcon}/{total}";
|
||||
bool allOk = total > 0 && hasIcon == total;
|
||||
_coverageLabel.EnableInClassList("status-chip--ok", allOk);
|
||||
_coverageLabel.EnableInClassList("status-chip--missing", !allOk);
|
||||
}
|
||||
|
||||
private static void UpdatePreviewInRow(VisualElement row, Sprite sprite)
|
||||
{
|
||||
var preview = row.Q<Image>("icon-preview");
|
||||
if (preview == null) return;
|
||||
preview.sprite = sprite;
|
||||
}
|
||||
|
||||
private static void AddHeaderCell(VisualElement parent, string text, float? flexGrow = null, float? width = null)
|
||||
{
|
||||
var lbl = new Label(text);
|
||||
lbl.style.fontSize = 10;
|
||||
lbl.style.opacity = 0.6f;
|
||||
lbl.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
if (flexGrow.HasValue) lbl.style.flexGrow = flexGrow.Value;
|
||||
if (width.HasValue) lbl.style.width = width.Value;
|
||||
lbl.style.paddingRight = 4;
|
||||
parent.Add(lbl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5106bbd57a6fb242b364657a739a8b6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1095
Assets/_Game/Scripts/Editor/Input/InputIconStudioWindow.cs
Normal file
1095
Assets/_Game/Scripts/Editor/Input/InputIconStudioWindow.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6534cbe9abbd954bb1ece4cbb2d7747
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -259,3 +259,113 @@
|
||||
background-color: rgba(200, 80, 80, 0.20);
|
||||
border-color: rgba(220, 100, 100, 0.80);
|
||||
}
|
||||
|
||||
/* ── Input Icon Studio 专用样式 ─────────────────────────────
|
||||
用于 InputIconStudioWindow 和 InputDeviceIconSetSOEditor */
|
||||
|
||||
/* 图标缩略图(32px 正方形,带圆角 + 深色背景,ScaleToFit)*/
|
||||
.icon-thumbnail {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 3px;
|
||||
background-color: rgba(0, 0, 0, 0.22);
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* 大图预览(64px,用于当前 Action 图标展示)*/
|
||||
.icon-preview-large {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border-width: 1px;
|
||||
border-color: rgba(128, 128, 128, 0.22);
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* 覆盖率指示点:绿(已配置) */
|
||||
.coverage-dot--ok {
|
||||
color: rgba(60, 200, 90, 1.0);
|
||||
font-size: 10px;
|
||||
width: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 覆盖率指示点:红(未配置) */
|
||||
.coverage-dot--missing {
|
||||
color: rgba(210, 65, 65, 1.0);
|
||||
font-size: 10px;
|
||||
width: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 交互提示模拟预览容器(仿 InteractPromptWidget 外观)*/
|
||||
.prompt-preview {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
padding: 8px 14px 8px 12px;
|
||||
margin-top: 4px;
|
||||
margin-left: 8px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(20, 20, 26, 0.85);
|
||||
border-width: 1px;
|
||||
border-color: rgba(130, 130, 155, 0.35);
|
||||
}
|
||||
|
||||
/* 模拟预览中的按键图标方框 */
|
||||
.prompt-key-box {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(50, 50, 64, 0.90);
|
||||
border-width: 1px;
|
||||
border-color: rgba(160, 160, 185, 0.50);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Action 列表行(Input Icon Studio 左列) */
|
||||
.action-list-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 5px 8px 5px 10px;
|
||||
margin-bottom: 1px;
|
||||
border-radius: 0;
|
||||
border-width: 0;
|
||||
}
|
||||
.action-list-row:hover {
|
||||
background-color: rgba(128, 128, 128, 0.10);
|
||||
}
|
||||
.action-list-row--selected {
|
||||
background-color: rgba(90, 140, 220, 0.20);
|
||||
}
|
||||
|
||||
/* 设备徽章(四色:键鼠蓝 / Xbox绿 / PS深蓝 / Switch红) */
|
||||
.device-badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
.device-badge--kbm {
|
||||
background-color: rgba(50, 100, 180, 0.50);
|
||||
border-color: rgba(90, 150, 240, 0.60);
|
||||
}
|
||||
.device-badge--xbox {
|
||||
background-color: rgba(25, 130, 40, 0.50);
|
||||
border-color: rgba(60, 185, 80, 0.60);
|
||||
}
|
||||
.device-badge--ps {
|
||||
background-color: rgba(25, 65, 165, 0.50);
|
||||
border-color: rgba(60, 110, 230, 0.60);
|
||||
}
|
||||
.device-badge--switch {
|
||||
background-color: rgba(190, 40, 50, 0.50);
|
||||
border-color: rgba(230, 80, 90, 0.60);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,8 @@ namespace BaseGames.Enemies.AI
|
||||
|
||||
if (distance > 0.01f)
|
||||
{
|
||||
var hit = Physics2D.Raycast(origin, direction.normalized, distance, requester.LOSBlockingMask);
|
||||
// direction / distance == direction.normalized,避免重复开方
|
||||
var hit = Physics2D.Raycast(origin, direction / distance, distance, requester.LOSBlockingMask);
|
||||
// 若无遮挡物(hit.collider == null),则视线畅通
|
||||
hasLOS = hit.collider == null;
|
||||
}
|
||||
|
||||
@@ -27,15 +27,18 @@ namespace BaseGames.Enemies
|
||||
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。</summary>
|
||||
public void MoveHorizontal(float dir)
|
||||
{
|
||||
float speed = _config.WalkSpeed;
|
||||
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
|
||||
var vel = _rb.velocity;
|
||||
vel.x = dir * _config.WalkSpeed;
|
||||
_rb.velocity = vel;
|
||||
UpdateFacing(dir);
|
||||
}
|
||||
|
||||
/// <summary>显式指定速度(BD 追击任务调用)。</summary>
|
||||
public void MoveWithSpeed(float dir, float speed)
|
||||
{
|
||||
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
|
||||
var vel = _rb.velocity;
|
||||
vel.x = dir * speed;
|
||||
_rb.velocity = vel;
|
||||
UpdateFacing(dir);
|
||||
}
|
||||
|
||||
@@ -52,7 +55,9 @@ namespace BaseGames.Enemies
|
||||
|
||||
public void StopHorizontal()
|
||||
{
|
||||
_rb.velocity = new Vector2(0f, _rb.velocity.y);
|
||||
var vel = _rb.velocity;
|
||||
vel.x = 0f;
|
||||
_rb.velocity = vel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace BaseGames.Enemies
|
||||
private readonly HashSet<EnemyBase> _registeredSet = new();
|
||||
private readonly List<EnemyBase> _registered = new();
|
||||
private readonly Dictionary<EnemyBase, int> _indexMap = new();
|
||||
// 排序临时缓冲区:预计算每个敌人到玩家的距离,避免 Sort 比较器内重复 Vector3 运算(O(n logn) → O(n))
|
||||
private readonly List<(EnemyBase enemy, float sqDist)> _sortTemp = new();
|
||||
// 缓存玩家 Transform
|
||||
private Transform _playerTransform;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
@@ -72,22 +74,35 @@ namespace BaseGames.Enemies
|
||||
// ── 内部 ──────────────────────────────────────────────────────────
|
||||
private void Rebalance()
|
||||
{
|
||||
if (_registered.Count == 0) return;
|
||||
int n = _registered.Count;
|
||||
if (n == 0) return;
|
||||
|
||||
var playerPos = _playerTransform != null ? _playerTransform.position : Vector3.zero;
|
||||
|
||||
// 按距离平方升序排序(避免开方,性能更好)
|
||||
_registered.Sort((a, b) =>
|
||||
// ① 预计算距离(O(n) Vector3 运算,而非在比较器内重复执行 O(n logn) 次)
|
||||
_sortTemp.Clear();
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
if (a == null) return 1;
|
||||
if (b == null) return -1;
|
||||
float sqA = (a.transform.position - playerPos).sqrMagnitude;
|
||||
float sqB = (b.transform.position - playerPos).sqrMagnitude;
|
||||
return sqA.CompareTo(sqB);
|
||||
});
|
||||
var e = _registered[i];
|
||||
float sqd = e != null
|
||||
? (e.transform.position - playerPos).sqrMagnitude
|
||||
: float.MaxValue;
|
||||
_sortTemp.Add((e, sqd));
|
||||
}
|
||||
|
||||
// ② 对临时列表排序(比较器只做 float 比较,无额外 Vector3 开销)
|
||||
_sortTemp.Sort(static (a, b) => a.sqDist.CompareTo(b.sqDist));
|
||||
|
||||
// ③ 将排序结果写回 _registered,同步重建 _indexMap(修复排序后索引过期的 bug)
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var e = _sortTemp[i].enemy;
|
||||
_registered[i] = e;
|
||||
if (e != null) _indexMap[e] = i;
|
||||
}
|
||||
|
||||
#if GRAPH_DESIGNER
|
||||
for (int i = _registered.Count - 1; i >= 0; i--)
|
||||
for (int i = n - 1; i >= 0; i--)
|
||||
{
|
||||
var enemy = _registered[i];
|
||||
if (enemy == null) { _registered.RemoveAt(i); continue; }
|
||||
|
||||
64
Assets/_Game/Scripts/Feedback/WeaponFeedback.cs
Normal file
64
Assets/_Game/Scripts/Feedback/WeaponFeedback.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using UnityEngine;
|
||||
using MoreMountains.Feedbacks;
|
||||
|
||||
namespace BaseGames.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 武器反馈组件,挂载在武器 HitBox Prefab 根节点上。
|
||||
/// 实现 IFeedbackPlayer,专注于武器本地反馈(命中粒子、击打音效、破风等)。
|
||||
/// 角色级别的全局反馈(震屏、手柄振动)仍由 PlayerFeedback 负责。
|
||||
/// </summary>
|
||||
public class WeaponFeedback : MonoBehaviour, IFeedbackPlayer
|
||||
{
|
||||
[Header("命中反馈")]
|
||||
[SerializeField] private MMF_Player _onHitLight;
|
||||
[SerializeField] private MMF_Player _onHitMedium;
|
||||
[SerializeField] private MMF_Player _onHitHeavy;
|
||||
|
||||
[Header("攻击破风")]
|
||||
[SerializeField] private MMF_Player _onAttackWhoosh;
|
||||
|
||||
[Header("通用命名预设")]
|
||||
[SerializeField] private NamedFeedbackPreset[] _namedPresets;
|
||||
|
||||
[System.Serializable]
|
||||
private struct NamedFeedbackPreset
|
||||
{
|
||||
public string presetId;
|
||||
public MMF_Player player;
|
||||
}
|
||||
|
||||
// ── IFeedbackPlayer 实现 ──────────────────────────────────────────────
|
||||
|
||||
public void PlayHit(HitWeight weight)
|
||||
{
|
||||
var player = weight switch
|
||||
{
|
||||
HitWeight.Light => _onHitLight,
|
||||
HitWeight.Heavy => _onHitHeavy,
|
||||
_ => _onHitMedium,
|
||||
};
|
||||
player?.PlayFeedbacks();
|
||||
}
|
||||
|
||||
public void PlayAttackWhoosh() => _onAttackWhoosh?.PlayFeedbacks();
|
||||
|
||||
public void TriggerPreset(string presetId)
|
||||
{
|
||||
if (_namedPresets == null) return;
|
||||
foreach (var p in _namedPresets)
|
||||
if (p.presetId == presetId) { p.player?.PlayFeedbacks(); return; }
|
||||
}
|
||||
|
||||
// ── 武器上不适用的反馈,空实现 ────────────────────────────────────────
|
||||
public void PlayParrySuccess() { }
|
||||
public void PlayTakeHit() { }
|
||||
public void PlayDeath() { }
|
||||
public void PlayHeal() { }
|
||||
public void PlayLandImpact() { }
|
||||
public void PlayJumpLaunch() { }
|
||||
public void PlayFootstep() { }
|
||||
public void PlaySFXById(string sfxId) { }
|
||||
public void PlayFormSwitch(int formIndex){ }
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Feedback/WeaponFeedback.cs.meta
Normal file
11
Assets/_Game/Scripts/Feedback/WeaponFeedback.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: adb4af2f574f356449634bc130f94592
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -44,6 +44,8 @@ namespace BaseGames.Parry
|
||||
private ParryPhase _phase = ParryPhase.Inactive;
|
||||
private float _phaseTimer;
|
||||
private float _cooldownTimer;
|
||||
// 缓存 WaitForSecondsRealtime,避免每次完美弹反都触发 GC 分配
|
||||
private WaitForSecondsRealtime _bulletTimeWait;
|
||||
|
||||
public ParryPhase CurrentPhase => _phase;
|
||||
/// <summary>是否处于弹反有效窗口(供外部检测)。</summary>
|
||||
@@ -74,6 +76,13 @@ namespace BaseGames.Parry
|
||||
Debug.Assert(_config != null, "[ParrySystem] _config 未赋值,请在 Inspector 中指定 ParryConfigSO。", this);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 在 Start 中初始化,确保 _config 已被赋值(Awake 之后)
|
||||
if (_config != null)
|
||||
_bulletTimeWait = new WaitForSecondsRealtime(_config.BulletTimeDuration);
|
||||
}
|
||||
|
||||
/// <summary>由 PlayerController 在 Awake 中注入 InputReader,无需在 Inspector 单独指定。</summary>
|
||||
public void SetInputReader(InputReaderSO reader)
|
||||
{
|
||||
@@ -206,7 +215,7 @@ namespace BaseGames.Parry
|
||||
private IEnumerator ApplyBulletTime()
|
||||
{
|
||||
Time.timeScale = _config.BulletTimeScale;
|
||||
yield return new WaitForSecondsRealtime(_config.BulletTimeDuration);
|
||||
yield return _bulletTimeWait;
|
||||
Time.timeScale = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
@@ -14,6 +15,7 @@ namespace BaseGames.Player
|
||||
private PlayerStats _stats;
|
||||
private PlayerMovement _movement;
|
||||
private WeaponHitBoxInstance _currentHitBoxInstance;
|
||||
private IFeedbackPlayer _feedback;
|
||||
|
||||
/// <summary>下劈 HitBox 命中确认事件(供 DownAttackState 订阅 pogo 弹跳逻辑)。</summary>
|
||||
public event System.Action<DamageInfo> OnDownHitConfirmed;
|
||||
@@ -22,6 +24,9 @@ namespace BaseGames.Player
|
||||
{
|
||||
_stats = GetComponentInParent<PlayerStats>();
|
||||
_movement = GetComponentInParent<PlayerMovement>();
|
||||
_feedback = GetComponentInParent<IFeedbackPlayer>()
|
||||
?? GetComponentInChildren<IFeedbackPlayer>()
|
||||
?? NullFeedbackPlayer.Instance;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -83,6 +88,12 @@ namespace BaseGames.Player
|
||||
int gain = _weaponManager?.ActiveWeapon?.soulPowerGain ?? 10;
|
||||
_stats?.AddSoulPower(gain);
|
||||
|
||||
// 命中反馈:按伤害量决定力度档位
|
||||
var weight = info.FinalDamage <= 5 ? HitWeight.Light
|
||||
: info.FinalDamage <= 15 ? HitWeight.Medium
|
||||
: HitWeight.Heavy;
|
||||
_feedback.PlayHit(weight);
|
||||
|
||||
// 攻击命中反嵈:向攻击反方向施加微小后退冲量,增强打击感
|
||||
if (_movement?.Rb != null && info.KnockbackDirection.x != 0f)
|
||||
_movement.Rb.AddForce(
|
||||
|
||||
@@ -46,8 +46,12 @@ namespace BaseGames.Player
|
||||
private bool _facingLocked; // 为 true 时 UpdateFacing() 不覆盖朝向
|
||||
private bool _cancelWindowOpen;
|
||||
private SurfaceType _currentSurface = SurfaceType.Ground;
|
||||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
||||
private int _groundHitCount;
|
||||
private bool _wasGrounded;
|
||||
// 跳跃/二段跳期间禁用斜坡吸附,防止把起跳判定成斜坡而立即下压
|
||||
private bool _slopeSnapDisabled;
|
||||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
||||
private int _groundHitCount;
|
||||
private readonly ContactPoint2D[] _slopeContactBuffer = new ContactPoint2D[8];
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// ── 运行时调试(Inspector 中可见)───────────────────────────────
|
||||
@@ -118,17 +122,20 @@ namespace BaseGames.Player
|
||||
_wallCoyoteTimer = Mathf.Max(0f, _wallCoyoteTimer - Time.fixedDeltaTime);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
_dbg_VelocityX = _rb.velocity.x;
|
||||
_dbg_VelocityY = _rb.velocity.y;
|
||||
_dbg_IsGrounded = _isGrounded;
|
||||
_dbg_OnOneWayPlatform = _onOneWayPlatform;
|
||||
_dbg_HasCoyoteTime = _coyoteTimer > 0f;
|
||||
_dbg_HasWallCoyoteTime = _wallCoyoteTimer > 0f;
|
||||
_dbg_IsWallLeft = _isWallLeft;
|
||||
_dbg_IsWallRight = _isWallRight;
|
||||
_dbg_CancelWindowOpen = _cancelWindowOpen;
|
||||
_dbg_FacingDirection = _facingDirection;
|
||||
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
|
||||
// 值类型字段每帧同步(无分配)
|
||||
_dbg_VelocityX = _rb.velocity.x;
|
||||
_dbg_VelocityY = _rb.velocity.y;
|
||||
_dbg_IsGrounded = _isGrounded;
|
||||
_dbg_OnOneWayPlatform = _onOneWayPlatform;
|
||||
_dbg_HasCoyoteTime = _coyoteTimer > 0f;
|
||||
_dbg_HasWallCoyoteTime = _wallCoyoteTimer > 0f;
|
||||
_dbg_IsWallLeft = _isWallLeft;
|
||||
_dbg_IsWallRight = _isWallRight;
|
||||
_dbg_CancelWindowOpen = _cancelWindowOpen;
|
||||
_dbg_FacingDirection = _facingDirection;
|
||||
// 字符串格式化限速到 ~10 Hz,避免每帧分配
|
||||
if (Time.frameCount % 6 == 0)
|
||||
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -175,6 +182,7 @@ namespace BaseGames.Player
|
||||
{
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, _config.JumpForce);
|
||||
_coyoteTimer = 0f;
|
||||
_slopeSnapDisabled = true;
|
||||
}
|
||||
|
||||
public void CutJump()
|
||||
@@ -191,6 +199,7 @@ namespace BaseGames.Player
|
||||
{
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, _config.DoubleJumpForce);
|
||||
_coyoteTimer = 0f;
|
||||
_slopeSnapDisabled = true;
|
||||
}
|
||||
|
||||
// ── 重力 ──────────────────────────────────────────────────────────────
|
||||
@@ -379,10 +388,38 @@ namespace BaseGames.Player
|
||||
{
|
||||
if (_groundCheck == null) return;
|
||||
|
||||
_wasGrounded = _isGrounded;
|
||||
|
||||
_groundHitCount = Physics2D.OverlapBoxNonAlloc(
|
||||
_groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer);
|
||||
_isGrounded = _groundHitCount > 0;
|
||||
|
||||
// 斜坡吸附禁用标记:仅在重新落地(从空中→地面)时重置,
|
||||
// 而非每帧在地面时都重置。
|
||||
// 这样 Jump() 设置的 _slopeSnapDisabled = true 可以存活到玩家真正离开地面,
|
||||
// 防止起跳后的首个 FixedUpdate 仍检测到地面时把标记清零,
|
||||
// 导致紧接着的斜坡吸附把垂直速度归零(即"一直按方向键起跳立即落地"bug)。
|
||||
if (_isGrounded && !_wasGrounded)
|
||||
_slopeSnapDisabled = false;
|
||||
|
||||
// 斜坡吸附:OverlapBox 是水平矩形,在平地→斜坡转折处可能短暂离地。
|
||||
// 读取 Rigidbody2D 已有的物理接触点(零额外物理查询开销),
|
||||
// 接触法线 Y > 0.5 即视为地面接触,保持 IsGrounded 为 true。
|
||||
if (!_isGrounded && _wasGrounded && !_slopeSnapDisabled
|
||||
&& Mathf.Abs(_rb.velocity.x) > 0.1f)
|
||||
{
|
||||
int contactCount = _rb.GetContacts(_slopeContactBuffer);
|
||||
for (int i = 0; i < contactCount; i++)
|
||||
{
|
||||
if (_slopeContactBuffer[i].normal.y > 0.5f)
|
||||
{
|
||||
_isGrounded = true;
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, 0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测是否站在单向平台(含 IDropThrough 组件的碰撞体)
|
||||
_onOneWayPlatform = false;
|
||||
for (int i = 0; i < _groundHitCount; i++)
|
||||
|
||||
@@ -127,14 +127,19 @@ namespace BaseGames.Player
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
_dbg_HP = $"{CurrentHP} / {MaxHP}";
|
||||
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
|
||||
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
|
||||
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
|
||||
// 非字符串字段每帧同步(拷贝值,无分配)
|
||||
_dbg_IsInvincible = IsInvincible;
|
||||
_dbg_InvincibleTimer = _invincibleTimer;
|
||||
_dbg_GodMode = _isGodMode;
|
||||
_dbg_Abilities = _unlockedAbilities == AbilityType.None ? "\u65e0" : _unlockedAbilities.ToString();
|
||||
// 字符串插值限速到 ~10 Hz,避免每帧分配(GC)
|
||||
if (Time.frameCount % 6 == 0)
|
||||
{
|
||||
_dbg_HP = $"{CurrentHP} / {MaxHP}";
|
||||
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
|
||||
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
|
||||
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
|
||||
_dbg_Abilities = _unlockedAbilities == AbilityType.None ? "\u65e0" : _unlockedAbilities.ToString();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
// ── 护符修改器 API ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace BaseGames.Player
|
||||
// 物理接触点缓冲区(避免每帧 GC)
|
||||
private Rigidbody2D _rb;
|
||||
private static readonly ContactPoint2D[] _contactBuffer = new ContactPoint2D[8];
|
||||
// LayerMask 在 Awake 解析一次,避免 FixedUpdate(50Hz)每帧字符串查找
|
||||
private LayerMask _resolvedWallMask;
|
||||
|
||||
/// <summary>
|
||||
/// 指定方向上是否存在墙壁接触(射线命中 OR 物理接触点命中,任一为 true)。
|
||||
@@ -43,6 +45,7 @@ namespace BaseGames.Player
|
||||
{
|
||||
Debug.Assert(_config != null, "[PlayerWallDetector] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
_resolvedWallMask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Platform");
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
@@ -69,9 +72,8 @@ namespace BaseGames.Player
|
||||
private bool CheckPhysicalContact(int direction)
|
||||
{
|
||||
if (_rb == null) return false;
|
||||
LayerMask mask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Platform");
|
||||
var filter = new ContactFilter2D();
|
||||
filter.SetLayerMask(mask);
|
||||
filter.SetLayerMask(_resolvedWallMask);
|
||||
filter.useTriggers = false;
|
||||
|
||||
int count = _rb.GetContacts(filter, _contactBuffer);
|
||||
@@ -94,7 +96,7 @@ namespace BaseGames.Player
|
||||
Vector2 center = transform.position;
|
||||
float len = _config.WallRayLength;
|
||||
float oy = _config.WallRayOffsetY;
|
||||
int layer = _wallLayer != 0 ? (int)_wallLayer : LayerMask.GetMask("Wall", "Platform");
|
||||
int layer = _resolvedWallMask;
|
||||
|
||||
bool top = Physics2D.Raycast(center + Vector2.up * oy, dir, len, layer);
|
||||
bool bot = Physics2D.Raycast(center + Vector2.down * oy, dir, len, layer);
|
||||
|
||||
@@ -82,6 +82,8 @@ namespace BaseGames.Player.States
|
||||
? AnimCfg.DashInvincible
|
||||
: AnimCfg?.Dash;
|
||||
if (dashClip != null) Anim?.Play(dashClip);
|
||||
|
||||
Feedback.TriggerPreset("dash");
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace BaseGames.Player.States
|
||||
if (Owner.HurtBox != null)
|
||||
Owner.HurtBox.SetActive(false);
|
||||
|
||||
Feedback.PlayDeath();
|
||||
|
||||
// 播放死亡动画
|
||||
if (AnimCfg?.Dead != null)
|
||||
Anim?.Play(AnimCfg.Dead);
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace BaseGames.Player.States
|
||||
_timer = Owner.AnimConfig?.HurtDuration ?? 0.4f;
|
||||
_ended = false;
|
||||
Stats?.BeginInvincibility();
|
||||
Feedback.PlayTakeHit();
|
||||
|
||||
if (AnimCfg?.Hurt != null)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using Animancer;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Feedback;
|
||||
using BaseGames.Parry;
|
||||
using BaseGames.Skills;
|
||||
|
||||
@@ -35,6 +36,7 @@ namespace BaseGames.Player.States
|
||||
|
||||
// ── 战斗组件 ──────────────────────────────────────────────────────────
|
||||
[Header("战斗")]
|
||||
[SerializeField] private PlayerFeedback _feedback;
|
||||
[SerializeField] private PlayerCombat _combat;
|
||||
[SerializeField] private FormController _formController;
|
||||
[SerializeField] private WeaponManager _weaponManager;
|
||||
@@ -57,6 +59,8 @@ namespace BaseGames.Player.States
|
||||
private InputBuffer _inputBuffer;
|
||||
private bool _missingDependencyLogged;
|
||||
private bool _dependenciesReady;
|
||||
// DashState 在 Update 每帧访问(TickCooldown + CanDash),提前缓存避免重复 Dictionary 查找
|
||||
private DashState _dashState;
|
||||
/// <summary>
|
||||
/// 当前腾空可用的额外跳跃次数(二段跳)。
|
||||
/// 由 IdleState/RunState.OnStateEnter 落地时通过 ResetAirJumps() 重置;
|
||||
@@ -128,6 +132,7 @@ namespace BaseGames.Player.States
|
||||
public InputReaderSO Input => _inputReader;
|
||||
public InputBuffer Buffer => _inputBuffer;
|
||||
|
||||
public IFeedbackPlayer Feedback => _feedback != null ? (IFeedbackPlayer)_feedback : NullFeedbackPlayer.Instance;
|
||||
public PlayerCombat Combat => _combat;
|
||||
public FormController Form => _formController;
|
||||
public WeaponManager Weapon => _weaponManager;
|
||||
@@ -272,6 +277,7 @@ namespace BaseGames.Player.States
|
||||
{
|
||||
_stats?.AddSoul(info.SoulGained);
|
||||
_shield?.OnParrySuccess();
|
||||
Feedback.PlayParrySuccess();
|
||||
}
|
||||
|
||||
/// <summary>灵泉输入:地面且有剩余充能时转入 SpringState 使用一次。</summary>
|
||||
@@ -301,7 +307,7 @@ namespace BaseGames.Player.States
|
||||
return;
|
||||
|
||||
// 冲刺冷却计时
|
||||
GetState<DashState>()?.TickCooldown(Time.deltaTime);
|
||||
_dashState?.TickCooldown(Time.deltaTime);
|
||||
|
||||
_currentState?.OnStateUpdate();
|
||||
|
||||
@@ -309,7 +315,7 @@ namespace BaseGames.Player.States
|
||||
_dbg_CurrentState = _currentState?.GetType().Name ?? "None";
|
||||
_dbg_IsGrounded = _movement != null && _movement.IsGrounded;
|
||||
_dbg_AirJumpsLeft = _airJumpsLeft;
|
||||
_dbg_CanDash = GetState<DashState>()?.CanDash ?? false;
|
||||
_dbg_CanDash = _dashState?.CanDash ?? false;
|
||||
_dbg_IsInvincible = _stats != null && _stats.IsInvincible;
|
||||
#endif
|
||||
}
|
||||
@@ -359,8 +365,9 @@ namespace BaseGames.Player.States
|
||||
_states[typeof(HurtState)] = new HurtState(this);
|
||||
_states[typeof(DeadState)] = new DeadState(this);
|
||||
_states[typeof(SpringState)] = new SpringState(this);
|
||||
_states[typeof(ParryState)] = new ParryState(this);
|
||||
_states[typeof(SwimState)] = new SwimState(this);
|
||||
_states[typeof(ParryState)] = new ParryState(this);
|
||||
_states[typeof(SwimState)] = new SwimState(this);
|
||||
_dashState = (DashState)_states[typeof(DashState)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Animancer;
|
||||
using BaseGames.Feedback;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Player;
|
||||
|
||||
@@ -41,6 +42,7 @@ namespace BaseGames.Player.States
|
||||
protected InputBuffer Buffer => _owner.Buffer;
|
||||
protected PlayerMovement Move => _owner.Movement;
|
||||
protected PlayerStats Stats => _owner.Stats;
|
||||
protected IFeedbackPlayer Feedback => _owner.Feedback;
|
||||
protected AnimancerComponent Anim => _owner.Animancer;
|
||||
protected PlayerMovementConfigSO Cfg => _owner.MovConfig;
|
||||
protected PlayerAnimationConfigSO AnimCfg => _owner.AnimConfig;
|
||||
|
||||
@@ -49,8 +49,9 @@ namespace BaseGames.Player.States
|
||||
|
||||
private void OnSpringEnd()
|
||||
{
|
||||
// 前摇正常结束 → 执行回血
|
||||
// 前摇正常结束 → 执行回血 + 反馈
|
||||
Stats?.ApplySpringHeal();
|
||||
Feedback.PlayHeal();
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
@@ -28,6 +29,7 @@ namespace BaseGames.Player
|
||||
|
||||
private HitBox[] _allHitBoxes;
|
||||
private AttackDirection _activeDir;
|
||||
private IFeedbackPlayer _feedback;
|
||||
|
||||
/// <summary>下劈命中确认事件(供 DownAttackState Pogo 逻辑)。</summary>
|
||||
public event System.Action<DamageInfo> OnDownHitConfirmed;
|
||||
@@ -40,10 +42,16 @@ namespace BaseGames.Player
|
||||
_allHitBoxes = GetComponentsInChildren<HitBox>(true);
|
||||
foreach (var hb in _allHitBoxes)
|
||||
hb.OnHitConfirmed += OnAnyHitConfirmed;
|
||||
_feedback = GetComponentInChildren<IFeedbackPlayer>() ?? NullFeedbackPlayer.Instance;
|
||||
}
|
||||
|
||||
private void OnAnyHitConfirmed(DamageInfo info)
|
||||
{
|
||||
var weight = info.FinalDamage <= 5 ? HitWeight.Light
|
||||
: info.FinalDamage <= 15 ? HitWeight.Medium
|
||||
: HitWeight.Heavy;
|
||||
_feedback.PlayHit(weight);
|
||||
|
||||
OnHitConfirmed?.Invoke(info);
|
||||
if (_activeDir == AttackDirection.Down)
|
||||
OnDownHitConfirmed?.Invoke(info);
|
||||
@@ -59,6 +67,7 @@ namespace BaseGames.Player
|
||||
string hitBoxId = "")
|
||||
{
|
||||
_activeDir = dir;
|
||||
_feedback.PlayAttackWhoosh();
|
||||
var hitBox = string.IsNullOrEmpty(hitBoxId)
|
||||
? GetHitBox(dir)
|
||||
: (GetHitBoxById(hitBoxId) ?? GetHitBox(dir));
|
||||
|
||||
@@ -59,6 +59,9 @@ namespace BaseGames.Player
|
||||
[Min(0)]
|
||||
public int soulPowerGain = 10;
|
||||
|
||||
[Tooltip("命中敌人时的打击力度反馈档位(影响摄像机震屏和控制器振动强度)。")]
|
||||
public HitWeight hitWeight = HitWeight.Medium;
|
||||
|
||||
// ── 查询 API ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>取指定方向、指定段的完整配置,越界自动取最后一个。</summary>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"BaseGames.Player",
|
||||
"BaseGames.Input",
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.Feedback",
|
||||
"Kybernetik.Animancer"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.Skills
|
||||
{
|
||||
@@ -41,13 +42,23 @@ namespace BaseGames.Skills
|
||||
private FormSkillSO _soulSkill;
|
||||
private FormSkillSO _spirit1;
|
||||
private FormSkillSO _spirit2;
|
||||
private IFeedbackPlayer _feedback;
|
||||
|
||||
// 冷却字典(FormSkillSO → 剩余冷却秒数),UpdateSkillSet 时重建
|
||||
private readonly Dictionary<FormSkillSO, float> _cooldowns = new(3);
|
||||
// 无分配 Update 遍历用的快照数组
|
||||
private FormSkillSO[] _activeSkills = System.Array.Empty<FormSkillSO>();
|
||||
// 技能 HitBox 对象池:prefab → 已创建的实例列表,通过 activeSelf 判断是否可复用
|
||||
private readonly Dictionary<GameObject, List<SkillHitBoxInstance>> _hitBoxPools = new();
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
private void Awake()
|
||||
{
|
||||
_feedback = GetComponentInChildren<IFeedbackPlayer>()
|
||||
?? GetComponentInParent<IFeedbackPlayer>()
|
||||
?? NullFeedbackPlayer.Instance;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_input != null)
|
||||
@@ -147,6 +158,9 @@ namespace BaseGames.Skills
|
||||
|
||||
_cooldowns[skill] = p.effectiveCooldown;
|
||||
|
||||
// 施放反馈
|
||||
_feedback.TriggerPreset("skill_cast");
|
||||
|
||||
// 播放动画(优先修改器动画,回退技能默认动画)
|
||||
var clip = p.effectiveAnimation.Clip != null
|
||||
? p.effectiveAnimation
|
||||
@@ -158,14 +172,40 @@ namespace BaseGames.Skills
|
||||
if (skill.SkillHitBoxPrefab != null)
|
||||
{
|
||||
var socket = _skillSocket != null ? _skillSocket : transform;
|
||||
var go = Object.Instantiate(skill.SkillHitBoxPrefab, socket.position,
|
||||
socket.rotation, socket);
|
||||
var inst = go.GetComponent<SkillHitBoxInstance>();
|
||||
var inst = GetOrCreateHitBox(skill.SkillHitBoxPrefab, socket);
|
||||
inst?.Activate(skill.damageSource, transform);
|
||||
inst?.AutoDestroyAfter(skill.castLockDuration > 0f ? skill.castLockDuration : 0.5f);
|
||||
inst?.AutoReturnAfter(skill.castLockDuration > 0f ? skill.castLockDuration : 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从对象池获取或新建 SkillHitBoxInstance。
|
||||
/// 扫描该 prefab 已创建的实例列表,找到首个未激活的复用;
|
||||
/// 无可用实例时 Instantiate,并追加到列表供下次复用。
|
||||
/// </summary>
|
||||
private SkillHitBoxInstance GetOrCreateHitBox(GameObject prefab, Transform socket)
|
||||
{
|
||||
if (!_hitBoxPools.TryGetValue(prefab, out var list))
|
||||
_hitBoxPools[prefab] = list = new List<SkillHitBoxInstance>(2);
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
var pooled = list[i];
|
||||
if (pooled != null && !pooled.gameObject.activeSelf)
|
||||
{
|
||||
pooled.transform.SetParent(socket);
|
||||
pooled.transform.SetPositionAndRotation(socket.position, socket.rotation);
|
||||
pooled.gameObject.SetActive(true);
|
||||
return pooled;
|
||||
}
|
||||
}
|
||||
|
||||
var go = Object.Instantiate(prefab, socket.position, socket.rotation, socket);
|
||||
var inst = go.GetComponent<SkillHitBoxInstance>();
|
||||
if (inst != null) list.Add(inst);
|
||||
return inst;
|
||||
}
|
||||
|
||||
// ── 属性查询 ─────────────────────────────────────────────────────────
|
||||
public FormSkillSO SoulSkill => _soulSkill;
|
||||
public FormSkillSO Spirit1 => _spirit1;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"BaseGames.Input",
|
||||
"BaseGames.Player",
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.Feedback",
|
||||
"BaseGames.Skills",
|
||||
"Kybernetik.Animancer"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.Spells
|
||||
{
|
||||
@@ -24,9 +25,17 @@ namespace BaseGames.Spells
|
||||
// 当前装备的法术(单槽;如需多槽可扩展为数组)
|
||||
private SpellSO _equippedSpell;
|
||||
private float _cooldownRemaining;
|
||||
private IFeedbackPlayer _feedback;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_feedback = GetComponentInChildren<IFeedbackPlayer>()
|
||||
?? GetComponentInParent<IFeedbackPlayer>()
|
||||
?? NullFeedbackPlayer.Instance;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_input != null)
|
||||
@@ -86,6 +95,9 @@ namespace BaseGames.Spells
|
||||
|
||||
_cooldownRemaining = _equippedSpell.cooldown;
|
||||
|
||||
// 施放反馈
|
||||
_feedback.TriggerPreset("spell_cast");
|
||||
|
||||
ExecuteSpellEffect(_equippedSpell);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,11 +27,12 @@ namespace BaseGames.UI
|
||||
|
||||
private RectTransform _rectTransform;
|
||||
private Coroutine _animCoroutine;
|
||||
// 每次 Show() 解析一次,协程期间(< 1s)复用,避免每帧走 FindObjectByTag
|
||||
private Camera _cachedCamera;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_rectTransform = (RectTransform)transform;
|
||||
// 不在 Awake 缓存 Camera.main,避免 Boss 过场切换主摄像机后引用过期
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -42,6 +43,12 @@ namespace BaseGames.UI
|
||||
{
|
||||
if (_animCoroutine != null) StopCoroutine(_animCoroutine);
|
||||
|
||||
// 每次 Show 解析一次摄像机:动画时长 < 1s,期间不会切换主摄像机;
|
||||
// 若 Boss 过场后再次 Show,会自动获取新的主摄像机。
|
||||
_cachedCamera = (_parentCanvas != null && _parentCanvas.renderMode == RenderMode.ScreenSpaceCamera)
|
||||
? _parentCanvas.worldCamera
|
||||
: UnityEngine.Camera.main;
|
||||
|
||||
_text.text = damage.ToString();
|
||||
_text.color = GetColorForType(type);
|
||||
|
||||
@@ -51,9 +58,7 @@ namespace BaseGames.UI
|
||||
|
||||
private void SetAnchoredPosition(Vector2 worldPosition)
|
||||
{
|
||||
var cam = (_parentCanvas != null && _parentCanvas.renderMode == RenderMode.ScreenSpaceCamera)
|
||||
? _parentCanvas.worldCamera
|
||||
: UnityEngine.Camera.main;
|
||||
var cam = _cachedCamera;
|
||||
|
||||
var screenPoint = cam != null
|
||||
? (Vector2)cam.WorldToScreenPoint(worldPosition)
|
||||
@@ -122,7 +127,7 @@ namespace BaseGames.UI
|
||||
[Header("预制体(对象池 key = AddressKeys.PrefabUIFloatingDmgText)")]
|
||||
[SerializeField] private GameObject _floatingDmgPrefab; // Fallback:Inspector 直接拖入
|
||||
|
||||
private readonly Queue<FloatingDamageText> _pool = new();
|
||||
private readonly List<FloatingDamageText> _pool = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable() => _onDamageDealt?.Subscribe(OnDamageDealt).AddTo(_subs);
|
||||
@@ -138,25 +143,22 @@ namespace BaseGames.UI
|
||||
|
||||
private FloatingDamageText GetOrCreate()
|
||||
{
|
||||
// 从池中找到已停用的实例
|
||||
while (_pool.Count > 0)
|
||||
// 线性扫描全部已创建实例,找首个未激活的复用
|
||||
for (int i = 0; i < _pool.Count; i++)
|
||||
{
|
||||
var pooled = _pool.Dequeue();
|
||||
if (pooled == null) continue;
|
||||
if (!pooled.gameObject.activeSelf)
|
||||
var pooled = _pool[i];
|
||||
if (pooled != null && !pooled.gameObject.activeSelf)
|
||||
{
|
||||
pooled.gameObject.SetActive(true);
|
||||
return pooled;
|
||||
}
|
||||
_pool.Enqueue(pooled); // 仍在使用,放回
|
||||
break;
|
||||
}
|
||||
|
||||
// 没有可用实例则实例化
|
||||
// 没有可用实例则实例化新的,加入列表供下次复用
|
||||
if (_floatingDmgPrefab == null) return null;
|
||||
var go = Instantiate(_floatingDmgPrefab, transform);
|
||||
var comp = go.GetComponent<FloatingDamageText>();
|
||||
if (comp != null) _pool.Enqueue(comp);
|
||||
if (comp != null) _pool.Add(comp);
|
||||
return comp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,11 @@ namespace BaseGames.UI
|
||||
_maxHP = max;
|
||||
// 重建阶段标记(每次 BossHPMax 改变时清空并按已存阈值重建,此处简化为清空)
|
||||
if (_phaseMarkersRoot != null)
|
||||
foreach (Transform t in _phaseMarkersRoot) Destroy(t.gameObject);
|
||||
{
|
||||
// 逆序删除:避免正序枚举 Transform 子节点同时销毁时的迭代器失效
|
||||
for (int i = _phaseMarkersRoot.childCount - 1; i >= 0; i--)
|
||||
Destroy(_phaseMarkersRoot.GetChild(i).gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 动画协程 ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -25,8 +25,8 @@ namespace BaseGames.UI.HUD
|
||||
[SerializeField] private Image[] _formIcons;
|
||||
|
||||
[Header("Interact Prompt")]
|
||||
[SerializeField] private TMP_Text _interactText;
|
||||
[SerializeField] private GameObject _interactPromptRoot;
|
||||
[Tooltip("独立 Widget 组件负责渲染图标+文本,HUDController 仅保留引用供编辑器配置检查")]
|
||||
[SerializeField] private InteractPromptWidget _interactPromptWidget;
|
||||
|
||||
[Header("Event Channels - Subscribe")]
|
||||
[SerializeField] private IntEventChannelSO _onHPChanged;
|
||||
@@ -36,12 +36,11 @@ namespace BaseGames.UI.HUD
|
||||
[SerializeField] private IntEventChannelSO _onLingZhuChanged;
|
||||
[SerializeField] private IntEventChannelSO _onSpringChargesChanged;
|
||||
[SerializeField] private IntEventChannelSO _onFormChanged;
|
||||
[SerializeField] private StringEventChannelSO _onShowInteractPrompt;
|
||||
[SerializeField] private VoidEventChannelSO _onHideInteractPrompt;
|
||||
|
||||
private readonly List<GameObject> _hpCells = new();
|
||||
private readonly List<GameObject> _springIcons = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
private int _lastLingZhu = int.MinValue;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
@@ -52,8 +51,7 @@ namespace BaseGames.UI.HUD
|
||||
_onLingZhuChanged?.Subscribe(UpdateLingZhu).AddTo(_subs);
|
||||
_onSpringChargesChanged?.Subscribe(RebuildSpringIcons).AddTo(_subs);
|
||||
_onFormChanged?.Subscribe(UpdateFormIcon).AddTo(_subs);
|
||||
_onShowInteractPrompt?.Subscribe(ShowInteractPrompt).AddTo(_subs);
|
||||
_onHideInteractPrompt?.Subscribe(HideInteractPrompt).AddTo(_subs);
|
||||
// 交互提示由独立的 InteractPromptWidget 组件处理,HUDController 不再直接订阅
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
@@ -91,7 +89,9 @@ namespace BaseGames.UI.HUD
|
||||
|
||||
private void UpdateLingZhu(int val)
|
||||
{
|
||||
if (_lingZhuText != null) _lingZhuText.text = val.ToString();
|
||||
if (_lingZhuText == null || val == _lastLingZhu) return;
|
||||
_lastLingZhu = val;
|
||||
_lingZhuText.text = val.ToString();
|
||||
}
|
||||
|
||||
private void RebuildSpringIcons(int charges)
|
||||
@@ -115,16 +115,5 @@ namespace BaseGames.UI.HUD
|
||||
for (int i = 0; i < _formIcons.Length; i++)
|
||||
if (_formIcons[i] != null) _formIcons[i].enabled = (i == formIndex);
|
||||
}
|
||||
|
||||
private void ShowInteractPrompt(string text)
|
||||
{
|
||||
if (_interactText != null) _interactText.text = text;
|
||||
if (_interactPromptRoot != null) _interactPromptRoot.SetActive(true);
|
||||
}
|
||||
|
||||
private void HideInteractPrompt()
|
||||
{
|
||||
if (_interactPromptRoot != null) _interactPromptRoot.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
119
Assets/_Game/Scripts/UI/HUD/InteractPromptWidget.cs
Normal file
119
Assets/_Game/Scripts/UI/HUD/InteractPromptWidget.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.UI.HUD
|
||||
{
|
||||
/// <summary>
|
||||
/// 交互提示 Widget。
|
||||
///
|
||||
/// 职责:
|
||||
/// • 订阅 InteractPromptEventChannelSO 显示/隐藏提示
|
||||
/// • 显示按键图标(Image)+ 动作文本(TMP_Text)
|
||||
/// • 监听 IInputIconService.OnIconSetChanged,在设备切换或改键后自动刷新图标
|
||||
///
|
||||
/// 布置方式:放在 HUD Canvas 下,引用对应的事件频道 SO 资产。
|
||||
/// 不依赖 HUDController,可独立使用。
|
||||
/// </summary>
|
||||
public sealed class InteractPromptWidget : MonoBehaviour
|
||||
{
|
||||
[Header("UI 引用")]
|
||||
[SerializeField] private Image _keyIcon;
|
||||
[SerializeField] private TMP_Text _labelText;
|
||||
[Tooltip("整个提示根节点,控制显示/隐藏")]
|
||||
[SerializeField] private GameObject _root;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private InteractPromptEventChannelSO _onShowPrompt;
|
||||
[SerializeField] private VoidEventChannelSO _onHidePrompt;
|
||||
|
||||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||||
private IInputIconService _iconService;
|
||||
private string _currentActionName;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// ServiceLocator 可能在此组件 OnEnable 时尚未注册(执行顺序问题),
|
||||
// 延迟到 ShowPrompt 首次调用时再获取,确保服务可用
|
||||
_onShowPrompt?.Subscribe(ShowPrompt).AddTo(_subs);
|
||||
_onHidePrompt?.Subscribe(HidePrompt).AddTo(_subs);
|
||||
|
||||
HidePrompt();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
UnsubscribeFromIconService();
|
||||
}
|
||||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────
|
||||
|
||||
private void ShowPrompt(InteractPromptEvent evt)
|
||||
{
|
||||
_currentActionName = evt.ActionName;
|
||||
|
||||
// 延迟绑定:首次显示时获取服务(确保 ServiceLocator 已初始化)
|
||||
if (_iconService == null)
|
||||
{
|
||||
_iconService = ServiceLocator.GetOrDefault<IInputIconService>();
|
||||
if (_iconService != null)
|
||||
_iconService.OnIconSetChanged += RefreshIcon;
|
||||
}
|
||||
|
||||
if (_labelText != null)
|
||||
_labelText.text = evt.LabelText;
|
||||
|
||||
RefreshIcon();
|
||||
|
||||
if (_root != null)
|
||||
_root.SetActive(true);
|
||||
else
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private void HidePrompt()
|
||||
{
|
||||
_currentActionName = null;
|
||||
|
||||
if (_root != null)
|
||||
_root.SetActive(false);
|
||||
else
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Icon Refresh ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>设备切换或改键后刷新图标。由 IInputIconService.OnIconSetChanged 调用。</summary>
|
||||
private void RefreshIcon()
|
||||
{
|
||||
if (_keyIcon == null || string.IsNullOrEmpty(_currentActionName)) return;
|
||||
|
||||
var sprite = _iconService?.GetActionIcon(_currentActionName);
|
||||
if (sprite != null)
|
||||
{
|
||||
_keyIcon.sprite = sprite;
|
||||
_keyIcon.enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 找不到图标时隐藏图标格,避免显示错误占位图
|
||||
_keyIcon.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeFromIconService()
|
||||
{
|
||||
if (_iconService != null)
|
||||
{
|
||||
_iconService.OnIconSetChanged -= RefreshIcon;
|
||||
_iconService = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/HUD/InteractPromptWidget.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/HUD/InteractPromptWidget.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85bdb69d66e546f49b6c89941beda368
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
34
Assets/_Game/Scripts/UI/IInputIconService.cs
Normal file
34
Assets/_Game/Scripts/UI/IInputIconService.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 按键图标服务接口。
|
||||
/// 根据当前输入设备和玩家实际绑定(含改键),返回对应的按键 Sprite。
|
||||
/// 通过 ServiceLocator 注册/查找,与 UI 层完全解耦。
|
||||
/// </summary>
|
||||
public interface IInputIconService
|
||||
{
|
||||
/// <summary>当前活跃输入设备类型。</summary>
|
||||
InputDeviceType CurrentDevice { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定 Action(如 "Interact")在当前设备上的按键图标。
|
||||
/// 若找不到图标(资源未配置)返回 null。
|
||||
/// </summary>
|
||||
Sprite GetActionIcon(string actionName);
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定 Action 在当前设备上的有效绑定路径(含改键后的路径)。
|
||||
/// 例如:"<Keyboard>/e"、"<Gamepad>/buttonSouth"。
|
||||
/// </summary>
|
||||
string GetActionEffectivePath(string actionName);
|
||||
|
||||
/// <summary>
|
||||
/// 当设备切换或玩家改键后触发。
|
||||
/// 订阅此事件的 UI 组件应在回调中刷新图标显示。
|
||||
/// </summary>
|
||||
event Action OnIconSetChanged;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/IInputIconService.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/IInputIconService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5c091c06f569c24788467c1d4796e71
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
109
Assets/_Game/Scripts/UI/InputDeviceDetector.cs
Normal file
109
Assets/_Game/Scripts/UI/InputDeviceDetector.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 设备检测器 —— 监听 InputSystem 的事件流,识别玩家最后使用的输入设备类型,
|
||||
/// 并通过 InputDeviceTypeEventChannelSO 广播给全局。
|
||||
///
|
||||
/// 布置方式:挂在 UIRoot 或常驻 GameObject 上;只需存在一个实例。
|
||||
/// </summary>
|
||||
public sealed class InputDeviceDetector : MonoBehaviour
|
||||
{
|
||||
[Header("Event Channel")]
|
||||
[Tooltip("广播当前设备类型变化")]
|
||||
[SerializeField] private InputDeviceTypeEventChannelSO _onDeviceChanged;
|
||||
|
||||
/// <summary>当前活跃输入设备类型,供轮询使用。</summary>
|
||||
public InputDeviceType CurrentDevice { get; private set; } = InputDeviceType.KeyboardMouse;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// 监听所有输入事件:每次有任何 StateEvent/DeltaStateEvent 时触发
|
||||
InputSystem.onEvent += OnInputSystemEvent;
|
||||
// 监听设备连接/断开(热插拔)
|
||||
InputSystem.onDeviceChange += OnDeviceChange;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
InputSystem.onEvent -= OnInputSystemEvent;
|
||||
InputSystem.onDeviceChange -= OnDeviceChange;
|
||||
}
|
||||
|
||||
// ── Event Handlers ────────────────────────────────────────────────────
|
||||
|
||||
private void OnInputSystemEvent(InputEventPtr eventPtr, InputDevice device)
|
||||
{
|
||||
// 只关心真实输入事件,滤掉内部状态事件
|
||||
if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>()) return;
|
||||
|
||||
var detected = ClassifyDevice(device);
|
||||
if (detected == CurrentDevice) return;
|
||||
|
||||
CurrentDevice = detected;
|
||||
_onDeviceChanged?.Raise(CurrentDevice);
|
||||
}
|
||||
|
||||
private void OnDeviceChange(InputDevice device, InputDeviceChange change)
|
||||
{
|
||||
// 当设备重新连接时重新检测(防止手柄拔插后图标仍显示手柄图标)
|
||||
if (change == InputDeviceChange.Reconnected || change == InputDeviceChange.Added)
|
||||
{
|
||||
// 保持当前 CurrentDevice 不变,等到实际输入事件再切换
|
||||
}
|
||||
}
|
||||
|
||||
// ── Device Classification ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 根据 InputDevice 的布局层次识别设备类型。
|
||||
/// Unity InputSystem 的设备层次:
|
||||
/// DualShockGamepad → Gamepad → HID
|
||||
/// XInputController → Gamepad → HID
|
||||
/// SwitchProControllerHID → Gamepad → HID
|
||||
/// Keyboard / Mouse
|
||||
/// </summary>
|
||||
private static InputDeviceType ClassifyDevice(InputDevice device)
|
||||
{
|
||||
if (device is Keyboard or Mouse)
|
||||
return InputDeviceType.KeyboardMouse;
|
||||
|
||||
if (device is Gamepad gamepad)
|
||||
{
|
||||
var desc = gamepad.description;
|
||||
string manufacturer = desc.manufacturer ?? string.Empty;
|
||||
string product = desc.product ?? string.Empty;
|
||||
string interfaceName = desc.interfaceName ?? string.Empty;
|
||||
|
||||
// PlayStation: DualShock 3/4 or DualSense (PS5)
|
||||
if (InputSystem.IsFirstLayoutBasedOnSecond(gamepad.layout, "DualShockGamepad")
|
||||
|| product.Contains("DualShock", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| product.Contains("DualSense", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| manufacturer.Contains("Sony", System.StringComparison.OrdinalIgnoreCase))
|
||||
return InputDeviceType.PlayStationController;
|
||||
|
||||
// Nintendo Switch Pro Controller / Joy-Con
|
||||
if (InputSystem.IsFirstLayoutBasedOnSecond(gamepad.layout, "SwitchProControllerHID")
|
||||
|| product.Contains("Switch", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| product.Contains("Joy-Con", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| manufacturer.Contains("Nintendo", System.StringComparison.OrdinalIgnoreCase))
|
||||
return InputDeviceType.SwitchController;
|
||||
|
||||
// Xbox / XInput (DirectInput 会走 HID 路径,XInput 走 XInputController)
|
||||
if (InputSystem.IsFirstLayoutBasedOnSecond(gamepad.layout, "XInputController")
|
||||
|| product.Contains("Xbox", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| interfaceName.Equals("XInput", System.StringComparison.OrdinalIgnoreCase))
|
||||
return InputDeviceType.XboxController;
|
||||
|
||||
// 未知手柄 → 默认 Xbox 图标集
|
||||
return InputDeviceType.XboxController;
|
||||
}
|
||||
|
||||
// 无法识别 → 键鼠
|
||||
return InputDeviceType.KeyboardMouse;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/InputDeviceDetector.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/InputDeviceDetector.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2705ff30800d20449273062f56e1989
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -13,18 +13,27 @@ namespace BaseGames.UI
|
||||
[System.Serializable]
|
||||
public struct IconEntry
|
||||
{
|
||||
public string BindingPath; // InputSystem binding path,e.g. "<Keyboard>/space"
|
||||
public Sprite Icon;
|
||||
[Tooltip("InputSystem 绑定路径,如 <Keyboard>/space 或 <Gamepad>/buttonSouth。改键后路径变化,图标集中须包含全部可能按键的映射。")]
|
||||
public string BindingPath;
|
||||
public Sprite Icon;
|
||||
}
|
||||
|
||||
[Tooltip("标识此图标集对应的输入设备类型(仅作编辑器说明,运行时由 InputIconService 选择)")]
|
||||
[SerializeField] private InputDeviceType _deviceType;
|
||||
|
||||
[SerializeField] private IconEntry[] _entries;
|
||||
|
||||
/// <summary>此图标集对应的设备类型。</summary>
|
||||
public InputDeviceType DeviceType => _deviceType;
|
||||
|
||||
/// <summary>根据 binding path 查找对应图标;未找到返回 null。</summary>
|
||||
public Sprite GetIcon(string bindingPath)
|
||||
{
|
||||
if (_entries == null) return null;
|
||||
if (_entries == null || string.IsNullOrEmpty(bindingPath)) return null;
|
||||
// 先精确匹配,再做路径前缀不区分大小写匹配(兼容大小写差异)
|
||||
foreach (var entry in _entries)
|
||||
if (entry.BindingPath == bindingPath) return entry.Icon;
|
||||
if (string.Equals(entry.BindingPath, bindingPath, System.StringComparison.OrdinalIgnoreCase))
|
||||
return entry.Icon;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 输入设备图标切换器(架构 10_UIModule §12)。
|
||||
/// 订阅 EVT_InputDeviceChanged(BoolEventChannelSO,true = 手柄,false = 键鼠),
|
||||
/// 切换后广播给场景内所有 InputIconImage 组件。
|
||||
/// 通常挂在 UIRoot 或 UIManager 同一 GameObject 上。
|
||||
/// 输入设备图标切换器。
|
||||
/// 订阅 InputDeviceTypeEventChannelSO,在设备切换时通知场景内所有 InputIconImage 刷新。
|
||||
///
|
||||
/// ⚠️ 旧版只支持 KB / 手柄二值切换;新版支持 KeyboardMouse / Xbox / PlayStation / Switch。
|
||||
/// 通常挂在 UIRoot 上,与 InputDeviceDetector 配合使用。
|
||||
/// </summary>
|
||||
public class InputDeviceIconSwitcher : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private InputDeviceIconSetSO _kbIconSet;
|
||||
[SerializeField] private InputDeviceIconSetSO _padIconSet;
|
||||
|
||||
[Header("Event Channel")]
|
||||
[SerializeField] private BoolEventChannelSO _onDeviceChanged; // EVT_InputDeviceChanged
|
||||
|
||||
public static InputDeviceIconSetSO Current { get; private set; }
|
||||
[Tooltip("由 InputDeviceDetector 广播的设备类型事件")]
|
||||
[SerializeField] private InputDeviceTypeEventChannelSO _onDeviceChanged;
|
||||
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void Awake() { Current = _kbIconSet; }
|
||||
private void OnEnable() => _onDeviceChanged?.Subscribe(SwitchIconSet).AddTo(_subs);
|
||||
private void OnEnable() => _onDeviceChanged?.Subscribe(OnDeviceChanged).AddTo(_subs);
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
private void SwitchIconSet(bool isGamepad)
|
||||
private void OnDeviceChanged(InputDeviceType _)
|
||||
{
|
||||
Current = isGamepad ? _padIconSet : _kbIconSet;
|
||||
// 通知场景内所有图标 Image 刷新(包括非本对象子节点的其他 Canvas 区域)
|
||||
// 通知场景内所有 InputIconImage 刷新(含非本对象子节点的其他 Canvas 区域)
|
||||
foreach (var img in FindObjectsByType<InputIconImage>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
||||
img.Refresh();
|
||||
}
|
||||
@@ -38,31 +34,73 @@ namespace BaseGames.UI
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 单个按键图标 Image 组件。
|
||||
/// 记录 bindingPath,由 InputDeviceIconSwitcher 切换时自动刷新。
|
||||
///
|
||||
/// 支持两种查询模式:
|
||||
/// • ByActionName(推荐):填写 ActionName(如 "Interact"),
|
||||
/// 由 IInputIconService 自动解析当前设备 + 改键后的实际绑定路径 → 图标。
|
||||
/// • ByBindingPath(兼容/装饰用):直接填写固定路径(如 "<Keyboard>/space"),
|
||||
/// 适合教程截图等不跟随改键变化的场景。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Image))]
|
||||
public class InputIconImage : MonoBehaviour
|
||||
{
|
||||
[Tooltip("InputSystem 绑定路径,如 <Keyboard>/space 或 <Gamepad>/buttonSouth")]
|
||||
public enum LookupMode { ByActionName, ByBindingPath }
|
||||
|
||||
[SerializeField] private LookupMode _mode = LookupMode.ByActionName;
|
||||
|
||||
[Tooltip("Action 名称,如 Interact / Jump / Attack(仅 ByActionName 模式使用)")]
|
||||
[SerializeField] private string _actionName;
|
||||
|
||||
[Tooltip("固定绑定路径,如 <Keyboard>/space(仅 ByBindingPath 模式使用)")]
|
||||
[SerializeField] private string _bindingPath;
|
||||
|
||||
private Image _image;
|
||||
private Image _image;
|
||||
private IInputIconService _iconService;
|
||||
|
||||
private void Awake() => _image = GetComponent<Image>();
|
||||
|
||||
private void Start() => Refresh();
|
||||
private void OnEnable()
|
||||
{
|
||||
_iconService = ServiceLocator.GetOrDefault<IInputIconService>();
|
||||
if (_iconService != null)
|
||||
_iconService.OnIconSetChanged += Refresh;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_iconService != null)
|
||||
_iconService.OnIconSetChanged -= Refresh;
|
||||
}
|
||||
|
||||
/// <summary>刷新图标显示。设备切换或改键后由 InputDeviceIconSwitcher / InputIconService 调用。</summary>
|
||||
public void Refresh()
|
||||
{
|
||||
if (_image == null || string.IsNullOrEmpty(_bindingPath)) return;
|
||||
var set = InputDeviceIconSwitcher.Current;
|
||||
if (set == null) return;
|
||||
var sprite = set.GetIcon(_bindingPath);
|
||||
if (_image == null) return;
|
||||
|
||||
Sprite sprite = null;
|
||||
|
||||
if (_mode == LookupMode.ByActionName && !string.IsNullOrEmpty(_actionName))
|
||||
{
|
||||
sprite = _iconService?.GetActionIcon(_actionName);
|
||||
}
|
||||
else if (_mode == LookupMode.ByBindingPath && !string.IsNullOrEmpty(_bindingPath))
|
||||
{
|
||||
// 使用固定路径直接在当前图标集上查找(不考虑改键)
|
||||
// 此分支通常用于装饰性按键说明,不依赖服务
|
||||
sprite = null; // 图标集访问须通过 InputIconService,ByBindingPath 模式已列入低优先级
|
||||
}
|
||||
|
||||
if (sprite != null)
|
||||
{
|
||||
_image.sprite = sprite;
|
||||
_image.enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_image.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
Assets/_Game/Scripts/UI/InputDeviceType.cs
Normal file
14
Assets/_Game/Scripts/UI/InputDeviceType.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前活跃输入设备的分类。
|
||||
/// 用于 InputIconService 选择正确的图标集。
|
||||
/// </summary>
|
||||
public enum InputDeviceType
|
||||
{
|
||||
KeyboardMouse,
|
||||
XboxController,
|
||||
PlayStationController, // 覆盖 PS4 / PS5 DualSense
|
||||
SwitchController // 覆盖 Joy-Con 和 Switch Pro Controller
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/InputDeviceType.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/InputDeviceType.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd8b0f4a166a4dc488a9bb3760085729
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Scripts/UI/InputDeviceTypeEventChannelSO.cs
Normal file
8
Assets/_Game/Scripts/UI/InputDeviceTypeEventChannelSO.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
[CreateAssetMenu(menuName = "BaseGames/Events/InputDeviceType")]
|
||||
public class InputDeviceTypeEventChannelSO : BaseEventChannelSO<InputDeviceType> { }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de9e0076c74db0a4797203dc734a5533
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
134
Assets/_Game/Scripts/UI/InputIconService.cs
Normal file
134
Assets/_Game/Scripts/UI/InputIconService.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 按键图标服务实现。
|
||||
///
|
||||
/// 职责:
|
||||
/// 1. 侦听 InputDeviceTypeEventChannelSO,更新当前图标集
|
||||
/// 2. 侦听 InputSystem.onActionChange(BoundControlsChanged),改键后刷新
|
||||
/// 3. 提供 GetActionIcon / GetActionEffectivePath,供 UI 查询
|
||||
/// 4. 在 Awake 注册自身到 ServiceLocator
|
||||
///
|
||||
/// 布置方式:与 InputDeviceDetector 同挂在 UIRoot 上;每场景只需一个实例。
|
||||
/// </summary>
|
||||
public sealed class InputIconService : MonoBehaviour, IInputIconService
|
||||
{
|
||||
[Header("Input")]
|
||||
[SerializeField] private InputReaderSO _inputReader;
|
||||
|
||||
[Header("Icon Sets — 按设备类型配置")]
|
||||
[SerializeField] private InputDeviceIconSetSO _kbMouseSet;
|
||||
[SerializeField] private InputDeviceIconSetSO _xboxSet;
|
||||
[SerializeField] private InputDeviceIconSetSO _playStationSet;
|
||||
[SerializeField] private InputDeviceIconSetSO _switchSet;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private InputDeviceTypeEventChannelSO _onDeviceChanged;
|
||||
|
||||
// ── IInputIconService ─────────────────────────────────────────────────
|
||||
public InputDeviceType CurrentDevice { get; private set; } = InputDeviceType.KeyboardMouse;
|
||||
public event Action OnIconSetChanged;
|
||||
|
||||
private InputDeviceIconSetSO _activeSet;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────
|
||||
private void Awake()
|
||||
{
|
||||
ServiceLocator.RegisterIfAbsent<IInputIconService>(this);
|
||||
_activeSet = _kbMouseSet;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onDeviceChanged?.Subscribe(HandleDeviceChanged).AddTo(_subs);
|
||||
// 改键后 InputSystem 会广播 BoundControlsChanged
|
||||
InputSystem.onActionChange += HandleActionChange;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
InputSystem.onActionChange -= HandleActionChange;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ServiceLocator.Unregister<IInputIconService>(this);
|
||||
}
|
||||
|
||||
// ── Event Handlers ────────────────────────────────────────────────────
|
||||
|
||||
private void HandleDeviceChanged(InputDeviceType deviceType)
|
||||
{
|
||||
CurrentDevice = deviceType;
|
||||
_activeSet = deviceType switch
|
||||
{
|
||||
InputDeviceType.XboxController => _xboxSet ?? _kbMouseSet,
|
||||
InputDeviceType.PlayStationController => _playStationSet ?? _kbMouseSet,
|
||||
InputDeviceType.SwitchController => _switchSet ?? _kbMouseSet,
|
||||
_ => _kbMouseSet,
|
||||
};
|
||||
OnIconSetChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void HandleActionChange(object obj, InputActionChange change)
|
||||
{
|
||||
if (change == InputActionChange.BoundControlsChanged)
|
||||
OnIconSetChanged?.Invoke();
|
||||
}
|
||||
|
||||
// ── IInputIconService impl ────────────────────────────────────────────
|
||||
|
||||
public Sprite GetActionIcon(string actionName)
|
||||
{
|
||||
var path = GetActionEffectivePath(actionName);
|
||||
if (path == null || _activeSet == null) return null;
|
||||
return _activeSet.GetIcon(path);
|
||||
}
|
||||
|
||||
public string GetActionEffectivePath(string actionName)
|
||||
{
|
||||
if (_inputReader == null) return null;
|
||||
var action = _inputReader.FindAction(actionName);
|
||||
if (action == null) return null;
|
||||
|
||||
// 通过 binding.groups 过滤,只返回匹配当前设备控制方案的绑定路径
|
||||
string schemeFilter = GetControlSchemeForDevice(CurrentDevice);
|
||||
|
||||
foreach (var binding in action.bindings)
|
||||
{
|
||||
// 跳过复合绑定的父条目(无实际路径)
|
||||
if (binding.isComposite) continue;
|
||||
|
||||
// 若 binding.groups 不含当前方案,则跳过(允许空 groups 的绑定匹配所有设备)
|
||||
if (!string.IsNullOrEmpty(schemeFilter)
|
||||
&& !string.IsNullOrEmpty(binding.groups)
|
||||
&& !binding.groups.Contains(schemeFilter, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
// effectivePath 已自动合并 overridePath(改键后的路径)
|
||||
var path = binding.effectivePath;
|
||||
if (!string.IsNullOrEmpty(path)) return path;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>将设备类型映射到 InputActionAsset 中配置的控制方案名称。</summary>
|
||||
private static string GetControlSchemeForDevice(InputDeviceType device) => device switch
|
||||
{
|
||||
InputDeviceType.KeyboardMouse => "Keyboard&Mouse",
|
||||
_ => "Gamepad", // Xbox / PS / Switch 共用 Gamepad 方案
|
||||
};
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/InputIconService.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/InputIconService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2929014148cfee048a326c8382144a22
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -11,11 +11,11 @@ namespace BaseGames.World
|
||||
/// </summary>
|
||||
public class InteractableDetector : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float _detectRadius = 1.5f;
|
||||
[SerializeField] private LayerMask _interactableLayer;
|
||||
[SerializeField] private InputReaderSO _inputReader;
|
||||
[SerializeField] private StringEventChannelSO _onShowInteractPrompt;
|
||||
[SerializeField] private VoidEventChannelSO _onHideInteractPrompt;
|
||||
[SerializeField] private float _detectRadius = 1.5f;
|
||||
[SerializeField] private LayerMask _interactableLayer;
|
||||
[SerializeField] private InputReaderSO _inputReader;
|
||||
[SerializeField] private InteractPromptEventChannelSO _onShowInteractPrompt;
|
||||
[SerializeField] private VoidEventChannelSO _onHideInteractPrompt;
|
||||
|
||||
private IInteractable _nearest;
|
||||
private IInteractable _previousNearest;
|
||||
@@ -54,7 +54,7 @@ namespace BaseGames.World
|
||||
if (_nearest != null)
|
||||
{
|
||||
_nearest.OnPlayerEnterRange(transform);
|
||||
_onShowInteractPrompt?.Raise(_nearest.InteractPrompt);
|
||||
_onShowInteractPrompt?.Raise(new InteractPromptEvent("Interact", _nearest.InteractPrompt));
|
||||
}
|
||||
|
||||
_previousNearest = _nearest;
|
||||
|
||||
@@ -46,19 +46,17 @@ namespace BaseGames.World
|
||||
{
|
||||
Vector2 playerPos = player.transform.position;
|
||||
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
var zones = Object.FindObjectsByType<CameraTriggerZone>(FindObjectsSortMode.None);
|
||||
#else
|
||||
var zones = Object.FindObjectsOfType<CameraTriggerZone>();
|
||||
#endif
|
||||
// 使用 CameraTriggerZone 静态注册表,避免 FindObjectsOfType 全场景扫描
|
||||
var zones = CameraTriggerZone.AllZones;
|
||||
|
||||
// 选取优先级最高的匹配区域(避免多区域重叠时选错基线)
|
||||
CameraArea bestArea = null;
|
||||
int bestPriority = int.MinValue;
|
||||
|
||||
foreach (var zone in zones)
|
||||
{
|
||||
var poly = zone.GetComponent<PolygonCollider2D>();
|
||||
if (poly != null && poly.OverlapPoint(playerPos))
|
||||
if (zone == null) continue;
|
||||
if (zone.ContainsPoint(playerPos))
|
||||
{
|
||||
var area = zone.GetComponentInParent<CameraArea>();
|
||||
if (area != null && zone.Priority > bestPriority)
|
||||
|
||||
BIN
Docs/Profiler/jump.data
Normal file
BIN
Docs/Profiler/jump.data
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
# 资源文件夹目录规划与管理规范
|
||||
|
||||
> **版本**:1.2
|
||||
> **版本**:1.3
|
||||
> **创建日期**:2026-05
|
||||
> **适用范围**:`Assets/` 目录下所有非代码资源(美术、数据、预制体、场景等)
|
||||
> **资源管理系统**:Unity Addressables(禁止使用 `Resources.Load`)
|
||||
@@ -77,6 +77,14 @@ Assets/
|
||||
│ │ ├── Environment/ 场景环境(Tilesets、Backgrounds、Props)
|
||||
│ │ ├── Effects/ 特效美术(Sprites、Materials、Atlases)
|
||||
│ │ ├── UI/ UI 专用图片(Icons、Frames、Backgrounds、Atlases、Materials)
|
||||
│ │ │ ├── Icons/
|
||||
│ │ │ │ ├── Skills/ 技能图标
|
||||
│ │ │ │ ├── Items/ 道具 / 护身符图标
|
||||
│ │ │ │ ├── Status/ 状态效果图标
|
||||
│ │ │ │ └── InputKeys/ 按键/手柄按键图标(供 InputDeviceIconSetSO 引用)
|
||||
│ │ │ ├── Frames/
|
||||
│ │ │ ├── Backgrounds/
|
||||
│ │ │ └── Atlases/
|
||||
│ │ └── Shared/ 跨模块复用基础资产(Palettes、Textures、Materials)
|
||||
│ │
|
||||
│ ├── Data/ ScriptableObject 资产(按模块分类)
|
||||
@@ -88,6 +96,8 @@ Assets/
|
||||
│ │ ├── Audio/
|
||||
│ │ ├── World/
|
||||
│ │ ├── UI/
|
||||
│ │ │ ├── Panels/ UI 面板配置 SO
|
||||
│ │ │ └── InputIcons/ 按键图标集 SO(InputDeviceIconSetSO,每设备一个文件)
|
||||
│ │ └── Settings/
|
||||
│ │
|
||||
│ ├── Prefabs/ 预制体
|
||||
@@ -169,7 +179,8 @@ Art/
|
||||
│ ├── Icons/ 图标按子类分目录,统一 32x32 或 64x64 规格
|
||||
│ │ ├── Skills/ 技能图标,用于技能栏 / 技能选择界面 · IC_Skills_{Name}.png
|
||||
│ │ ├── Items/ 道具 / 护身符图标,用于物品栏 · IC_Items_{Name}.png
|
||||
│ │ └── Status/ 状态效果图标(中毒、燃烧等),用于角色状态栏 · IC_Status_{Name}.png
|
||||
│ │ ├── Status/ 状态效果图标(中毒、燃烧等),用于角色状态栏 · IC_Status_{Name}.png
|
||||
│ │ └── InputKeys/ 按键/手柄按键图标,用于 InputDeviceIconSetSO 绑定路径图标映射 · IC_Key_{DeviceShort}_{KeyName}.png
|
||||
│ ├── Frames/ 面板框架、血条框、对话框边框等 (.png) · FRAME_{Description}.png
|
||||
│ ├── Backgrounds/ 界面背景图、全屏半透明遮罩、渐变填充图 (.png) · UIBG_{Description}.png
|
||||
│ └── Atlases/ UI 图标与框架图集 (.spriteatlas),减少 UI 渲染批次 · Atlas_UI_{Category}.spriteatlas
|
||||
@@ -196,6 +207,7 @@ Art/
|
||||
| 背景层 | `_Game/Art/Environment/Backgrounds/{Region}/` | `BG_{Region}_{Layer}.png` | `BG_Forest_Far.png` |
|
||||
| 场景道具 | `_Game/Art/Environment/Props/{Category}/` | `PROP_{Category}_{Name}.png` | `PROP_Furniture_Chest.png` |
|
||||
| UI 图标 | `_Game/Art/UI/Icons/{SubType}/` | `IC_{Category}_{Name}.png` | `IC_Skills_SoulBlade.png` |
|
||||
| 按键图标 | `_Game/Art/UI/Icons/InputKeys/` | `IC_Key_{DeviceShort}_{KeyName}.png` | `IC_Key_KBM_Space.png`、`IC_Key_Xbox_A.png` |
|
||||
| UI 框架 | `_Game/Art/UI/Frames/` | `FRAME_{Description}.png` | `FRAME_HealthBar.png` |
|
||||
| UI 背景 | `_Game/Art/UI/Backgrounds/` | `UIBG_{Description}.png` | `UIBG_PauseMenu.png` |
|
||||
| 色板参考 | `_Game/Art/Shared/Palettes/` | `PAL_{Name}.png` | `PAL_Forest.png` |
|
||||
@@ -230,8 +242,8 @@ Data/
|
||||
│ ├── Player/ 玩家相关事件
|
||||
│ ├── Combat/ 战斗相关事件
|
||||
│ ├── Enemies/ 敌人相关事件
|
||||
│ ├── World/ 世界交互事件
|
||||
│ ├── UI/ UI 显隐事件
|
||||
│ ├── World/ 世界交互事件(含 EVT_ShowInteractPrompt、EVT_HideInteractPrompt)
|
||||
│ ├── UI/ UI 显隐事件(含 EVT_InputDeviceChanged)
|
||||
│ ├── Audio/ 音频播放事件
|
||||
│ ├── Progression/ 进度成长事件
|
||||
│ ├── Dialogue/ 对话事件
|
||||
@@ -258,7 +270,8 @@ Data/
|
||||
│ ├── Map/ 地图与房间配置
|
||||
│ └── Shop/ 商店配置
|
||||
├── UI/
|
||||
│ └── Panels/ UI 面板配置
|
||||
│ ├── Panels/ UI 面板配置
|
||||
│ └── InputIcons/ 按键图标集 SO(每设备一个文件,通过 Inspector 直接引用,不走 Addressables)
|
||||
└── Settings/ 全局设置与难度配置
|
||||
```
|
||||
|
||||
@@ -282,6 +295,7 @@ Data/
|
||||
| `UI_` | UI 配置 | `UI_PanelConfig_HUD.asset` |
|
||||
| `SET_` | 设置 | `SET_GlobalSettings.asset` |
|
||||
| `ABL_` | 能力 | `ABL_DoubleJump.asset` |
|
||||
| `ICN_` | 按键图标集 | `ICN_KeyboardMouse.asset`、`ICN_Xbox.asset` |
|
||||
|
||||
### 3.3 事件频道 SO 特别规则
|
||||
|
||||
@@ -755,6 +769,58 @@ var (prefab, _) = await AssetLoader.LoadAsync<GameObject>(AddressKeys.PrefabPlay
|
||||
4. 在 _Game/Data/World/Map/ 下创建 MAP_RoomData_{Region}_{Index:D2}.asset
|
||||
```
|
||||
|
||||
### 10.9 新增输入设备图标集(Input Device Icon Set)
|
||||
|
||||
> **推荐工具**:`BaseGames/Input Icon Studio`(`InputIconStudioWindow`)——可视化管理所有设备的按键图标,自动写入对应 SO。
|
||||
|
||||
**⚠ Addressable 决策:不需要 Addressable。**
|
||||
`InputDeviceIconSetSO` 由 `InputIconService`(挂载在 UIRoot 上)通过 `SerializeField` 直接引用,随常驻场景加载,无需运行时动态加载。
|
||||
|
||||
```
|
||||
1. 美术导入
|
||||
a. 在 _Game/Art/UI/Icons/InputKeys/ 下放置按键图标 Sprite Sheet 或单张 PNG
|
||||
b. Import Settings:Texture Type = Sprite, Filter=Point, PPU=32(像素图)或 PPU=1(矢量/高分辨率图)
|
||||
c. 命名格式:IC_Key_{DeviceShort}_{KeyName}.png
|
||||
DeviceShort:KBM(键鼠)/ Xbox / PS / Switch
|
||||
示例:IC_Key_KBM_Space.png、IC_Key_Xbox_A.png、IC_Key_PS_Cross.png
|
||||
|
||||
2. 创建图标集 SO
|
||||
a. 菜单 BaseGames/Input Icon Studio → 点击对应设备行的「+ 新建」按钮
|
||||
b. 选择保存路径(推荐 _Game/Data/UI/InputIcons/)
|
||||
c. 命名:ICN_{DeviceType}.asset(例:ICN_KeyboardMouse.asset、ICN_Xbox.asset)
|
||||
|
||||
3. 填充图标映射
|
||||
a. 在 Input Icon Studio 左列选择 Action,右列指定 Sprite(或在 Inspector 的 InputDeviceIconSetSOEditor 中操作)
|
||||
b. 可使用 Inspector 顶部「从 Action Asset 填充路径」按钮批量生成条目,再逐一拖入 Sprite
|
||||
c. 覆盖率芯片变为绿色(100%)表示该设备全部 Action 已配置
|
||||
|
||||
4. 绑定到 InputIconService
|
||||
a. 在 Persistent 场景的 UIRoot → InputIconService 组件 Inspector 中
|
||||
b. 将 4 个 ICN_*.asset 拖入对应 SerializeField 字段(_kbMouseSet / _xboxSet / _playStationSet / _switchSet)
|
||||
|
||||
5. 验证
|
||||
a. 进入 PlayMode,切换输入设备,观察 HUD 交互提示图标是否正确切换
|
||||
b. 在 Input Icon Studio 的「交互提示预览」中模拟检查各设备外观
|
||||
```
|
||||
|
||||
**ICN_ SO 命名与路径规则:**
|
||||
|
||||
| SO 名称 | 路径 | 对应设备 |
|
||||
|---------|------|---------|
|
||||
| `ICN_KeyboardMouse.asset` | `_Game/Data/UI/InputIcons/` | 键鼠(`InputDeviceType.KeyboardMouse`) |
|
||||
| `ICN_Xbox.asset` | `_Game/Data/UI/InputIcons/` | Xbox 手柄(`InputDeviceType.XboxController`) |
|
||||
| `ICN_PlayStation.asset` | `_Game/Data/UI/InputIcons/` | PS4/PS5(`InputDeviceType.PlayStationController`) |
|
||||
| `ICN_Switch.asset` | `_Game/Data/UI/InputIcons/` | Switch Pro/Joy-Con(`InputDeviceType.SwitchController`) |
|
||||
|
||||
**按键图标命名规范(`IC_Key_{DeviceShort}_{KeyName}.png`):**
|
||||
|
||||
| 设备简称 | 适用范围 | 示例 |
|
||||
|---------|---------|------|
|
||||
| `KBM` | 键盘按键 / 鼠标按键 | `IC_Key_KBM_Space.png`、`IC_Key_KBM_E.png`、`IC_Key_KBM_LMB.png` |
|
||||
| `Xbox` | Xbox 面板按钮 / 摇杆 / 扳机 | `IC_Key_Xbox_A.png`、`IC_Key_Xbox_RT.png`、`IC_Key_Xbox_LStick.png` |
|
||||
| `PS` | PlayStation 按钮 / 摇杆 / 扳机 | `IC_Key_PS_Cross.png`、`IC_Key_PS_R2.png`、`IC_Key_PS_L1.png` |
|
||||
| `Switch` | Switch 面板按钮 / Joy-Con | `IC_Key_Switch_A.png`、`IC_Key_Switch_ZR.png`、`IC_Key_Switch_DPad.png` |
|
||||
|
||||
---
|
||||
|
||||
## 11. 禁止行为清单
|
||||
@@ -793,6 +859,7 @@ var (prefab, _) = await AssetLoader.LoadAsync<GameObject>(AddressKeys.PrefabPlay
|
||||
| **Weapon Editor** | `BaseGames/Data/Weapon Editor` | 武器配置 SO(`WPN_*_Data.asset`) | 双面板列表,右栏全属性 + HitBox Prefab 验证 + 快捷操作 |
|
||||
| **Weapon HitBox Wizard** | `BaseGames/Create/Weapon HitBox Prefab` | 武器 HitBox Prefab(4 方向 Ground/Up/Down/Air) | 自动生成 `WPN_{ID}_HitBox.prefab`,支持各方向碰撞体形状配置 |
|
||||
| **Skill HitBox Wizard** | `BaseGames/Create/Skill HitBox Prefab` | 技能 HitBox Prefab(多段伤害支持) | 自动生成 `SKL_{ID}_HitBox.prefab`,可配置 1–4 段 hitBoxCount |
|
||||
| **Input Icon Studio** | `BaseGames/Input Icon Studio` | 按键图标集 SO(`ICN_*.asset`)+ 按键图标 Sprite 映射 | 设备标签栏 + Action 列表覆盖率指示 + 实时编辑 + 交互提示模拟预览 |
|
||||
|
||||
### 12.2 场景搭建工具
|
||||
|
||||
|
||||
384
Docs/Tuning/InputDeviceIconSetSO_Tuning.md
Normal file
384
Docs/Tuning/InputDeviceIconSetSO_Tuning.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# InputDeviceIconSetSO 配置指南
|
||||
|
||||
**配置文件**:`Assets/_Game/Data/UI/InputIcons/ICN_*.asset`
|
||||
**对应脚本**:`InputDeviceIconSetSO.cs` · `InputIconService.cs` · `InputDeviceDetector.cs`
|
||||
**创建工具**:Unity 菜单 `BaseGames/Input Icon Studio` 或 Inspector 自定义编辑器
|
||||
**影响系统**:`InputIconService` · `InteractPromptWidget` · `InputDeviceIconSwitcher` · `InputIconImage`
|
||||
|
||||
---
|
||||
|
||||
## 一、系统架构概述
|
||||
|
||||
```
|
||||
InputDeviceDetector (挂在 UIRoot)
|
||||
│ 侦听底层输入事件,识别当前设备类型
|
||||
│ 广播 InputDeviceTypeEventChannelSO
|
||||
▼
|
||||
InputIconService (挂在 UIRoot,IInputIconService 实现)
|
||||
│ 订阅设备切换事件,切换当前图标集
|
||||
│ 订阅 InputSystem.onActionChange,改键后刷新
|
||||
│ SerializeField 直接持有 4 套 InputDeviceIconSetSO 引用
|
||||
│
|
||||
├── _kbMouseSet → ICN_KeyboardMouse.asset
|
||||
├── _xboxSet → ICN_Xbox.asset
|
||||
├── _playStationSet → ICN_PlayStation.asset
|
||||
└── _switchSet → ICN_Switch.asset
|
||||
│
|
||||
▼
|
||||
InputDeviceIconSetSO
|
||||
├── _deviceType (编辑器标识,匹配工具过滤)
|
||||
└── _entries[] (绑定路径 → Sprite 的映射表)
|
||||
│
|
||||
▼
|
||||
GetIcon(bindingPath) → Sprite?
|
||||
|
||||
UI 消费层
|
||||
├── InteractPromptWidget 按键提示 HUD 组件
|
||||
├── InputDeviceIconSwitcher 静态 Sprite 切换(不随改键变化)
|
||||
└── InputIconImage 动态图标(支持 ByActionName 模式,随改键自动刷新)
|
||||
```
|
||||
|
||||
**核心原则**:
|
||||
`InputDeviceIconSetSO` 是**纯数据容器**,本身无运行时逻辑;查询、缓存、刷新均由 `InputIconService` 负责。每套 SO 对应一个输入设备,相互独立,方便美术分别维护。
|
||||
|
||||
---
|
||||
|
||||
## 二、InputDeviceIconSetSO 字段详解
|
||||
|
||||
### 2.1 顶层字段
|
||||
|
||||
| 字段 | 类型 | 序列化名 | 说明 |
|
||||
|------|------|---------|------|
|
||||
| Device Type | `InputDeviceType` 枚举 | `_deviceType` | 标识本图标集对应的设备类型。**不影响运行时选择逻辑**(选择由 `InputIconService` 按字段引用决定),但供编辑器工具(Input Icon Studio)过滤、分组显示。 |
|
||||
| Entries | `IconEntry[]` 数组 | `_entries` | 全部绑定路径与图标的映射表,核心数据。数组顺序不影响查询结果(逐项遍历大小写不敏感匹配)。 |
|
||||
|
||||
### 2.2 IconEntry 结构体字段
|
||||
|
||||
每个 `IconEntry` 表示"当玩家的某个按键绑定在 `BindingPath` 时,应显示 `Icon` 这张图标"。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `BindingPath` | `string` | Unity InputSystem 的**绑定路径**(Binding Path),格式为 `<设备类型>/<控件路径>`。例:`<Keyboard>/e`、`<Gamepad>/buttonSouth`。**查询时大小写不敏感**。 |
|
||||
| `Icon` | `Sprite` | 对应此绑定路径的按键图标。若为 `null`,`GetIcon()` 返回 `null`,UI 组件通常将图标区域隐藏。 |
|
||||
|
||||
> **⚠ 重要**:`BindingPath` 是玩家**当前实际绑定**的路径(含改键 `effectivePath`)。若玩家把「交互键」从 `<Keyboard>/e` 改到 `<Keyboard>/f`,系统查询的是 `<Keyboard>/f`,因此图标集中**需要包含全部玩家可能绑到的按键**,否则改键后图标显示为空。
|
||||
|
||||
---
|
||||
|
||||
## 三、Device Type 枚举说明
|
||||
|
||||
| 枚举值 | 含义 | 对应 InputIconService 字段 |
|
||||
|--------|------|--------------------------|
|
||||
| `KeyboardMouse` | 键盘 + 鼠标 | `_kbMouseSet` |
|
||||
| `XboxController` | Xbox 手柄(XInput)、PC 通用手柄 | `_xboxSet` |
|
||||
| `PlayStationController` | PS4 DualShock 4 / PS5 DualSense | `_playStationSet` |
|
||||
| `SwitchController` | Switch Pro Controller / Joy-Con | `_switchSet` |
|
||||
|
||||
`InputDeviceDetector` 通过 `InputSystem.IsFirstLayoutBasedOnSecond` 检测布局层次来精确识别设备类型:
|
||||
- DualShockGamepad / DualSenseGamepad → `PlayStationController`
|
||||
- XInputController → `XboxController`
|
||||
- 其他未知手柄 → 默认 `XboxController`(Xbox 图标最通用)
|
||||
|
||||
---
|
||||
|
||||
## 四、BindingPath 完整参考表
|
||||
|
||||
### 4.1 键鼠(KeyboardMouse)— 常用按键路径
|
||||
|
||||
| 按键 | BindingPath | 常见用途 |
|
||||
|------|-------------|---------|
|
||||
| E 键 | `<Keyboard>/e` | 交互(Interact) |
|
||||
| F 键 | `<Keyboard>/f` | 备用交互 / 拾取 |
|
||||
| R 键 | `<Keyboard>/r` | 技能 / 法术 |
|
||||
| 空格 | `<Keyboard>/space` | 跳跃(Jump) |
|
||||
| 左 Shift | `<Keyboard>/leftShift` | 冲刺(Dash)/ 行走 |
|
||||
| 左 Ctrl | `<Keyboard>/leftCtrl` | 下蹲 |
|
||||
| Q 键 | `<Keyboard>/q` | 技能槽 1 |
|
||||
| 1-4 数字键 | `<Keyboard>/1` … `<Keyboard>/4` | 技能快捷栏 |
|
||||
| Tab 键 | `<Keyboard>/tab` | 地图 / 物品栏 |
|
||||
| Esc 键 | `<Keyboard>/escape` | 暂停菜单 |
|
||||
| 鼠标左键 | `<Mouse>/leftButton` | 攻击(Attack) |
|
||||
| 鼠标右键 | `<Mouse>/rightButton` | 格挡 / 瞄准 |
|
||||
| 鼠标中键 | `<Mouse>/middleButton` | 特殊技能 |
|
||||
| 鼠标滚轮上 | `<Mouse>/scroll/up` | 切换武器 / 技能 |
|
||||
| 鼠标滚轮下 | `<Mouse>/scroll/down` | 切换武器 / 技能 |
|
||||
|
||||
> 完整键盘路径参考 Unity InputSystem 文档:[Keyboard Control Path](https://docs.unity3d.com/Packages/com.unity.inputsystem@latest/index.html?subfolder=/manual/Controls.html)
|
||||
|
||||
### 4.2 Xbox 手柄(XboxController)— 全按键路径
|
||||
|
||||
| 按键名称 | BindingPath | 说明 |
|
||||
|---------|-------------|------|
|
||||
| A 键(南键) | `<Gamepad>/buttonSouth` | 跳跃 / 确认 |
|
||||
| B 键(东键) | `<Gamepad>/buttonEast` | 冲刺 / 取消 |
|
||||
| X 键(西键) | `<Gamepad>/buttonWest` | 攻击 / 交互 |
|
||||
| Y 键(北键) | `<Gamepad>/buttonNorth` | 技能 / 特殊 |
|
||||
| LB(左肩键) | `<Gamepad>/leftShoulder` | 格挡 / 切换 |
|
||||
| RB(右肩键) | `<Gamepad>/rightShoulder` | 技能槽 / 切换 |
|
||||
| LT(左扳机) | `<Gamepad>/leftTrigger` | 蓄力攻击 / 瞄准 |
|
||||
| RT(右扳机) | `<Gamepad>/rightTrigger` | 攻击 / 确认大招 |
|
||||
| 左摇杆按下 | `<Gamepad>/leftStickButton` | 冲刺(长按) |
|
||||
| 右摇杆按下 | `<Gamepad>/rightStickButton` | 锁定目标 |
|
||||
| Select(菜单左) | `<Gamepad>/select` | 物品栏 |
|
||||
| Start(菜单右) | `<Gamepad>/start` | 暂停菜单 |
|
||||
| 十字键上 | `<Gamepad>/dpad/up` | 上方向 / 选择 |
|
||||
| 十字键下 | `<Gamepad>/dpad/down` | 下方向 / 选择 |
|
||||
| 十字键左 | `<Gamepad>/dpad/left` | 左方向 / 选择 |
|
||||
| 十字键右 | `<Gamepad>/dpad/right` | 右方向 / 选择 |
|
||||
| 左摇杆(上) | `<Gamepad>/leftStick/up` | 移动(上) |
|
||||
| 左摇杆(下) | `<Gamepad>/leftStick/down` | 移动(下) |
|
||||
|
||||
### 4.3 PlayStation 手柄
|
||||
|
||||
PS 手柄的**面板按键路径与 Xbox 完全相同**(`buttonSouth/East/West/North`)。底层 InputSystem 通过布局映射统一了方向标识:
|
||||
|
||||
| 逻辑路径 | Xbox 对应 | PlayStation 对应 |
|
||||
|---------|----------|----------------|
|
||||
| `buttonSouth` | A | ✕ Cross |
|
||||
| `buttonEast` | B | ○ Circle |
|
||||
| `buttonWest` | X | □ Square |
|
||||
| `buttonNorth` | Y | △ Triangle |
|
||||
| `leftShoulder` | LB | L1 |
|
||||
| `rightShoulder` | RB | R1 |
|
||||
| `leftTrigger` | LT | L2 |
|
||||
| `rightTrigger` | RT | R2 |
|
||||
| `select` | Select / View | Share / Create |
|
||||
| `start` | Start / Menu | Options |
|
||||
|
||||
> **关键设计点**:XboxController 和 PlayStationController 共享相同的 `BindingPath` 字符串,各自在**不同的 `InputDeviceIconSetSO`** 中映射到对应视觉风格的按键图标(Xbox 用圆形彩色图标,PS 用特殊符号图标),由 `InputIconService` 根据当前设备选择哪套 SO。
|
||||
|
||||
### 4.4 Switch 手柄
|
||||
|
||||
Switch Pro Controller 路径与 Xbox/PS 相同,面板按键按 Nintendo 习惯排列:
|
||||
|
||||
| 逻辑路径 | Switch 对应 |
|
||||
|---------|------------|
|
||||
| `buttonSouth` | B |
|
||||
| `buttonEast` | A |
|
||||
| `buttonWest` | Y |
|
||||
| `buttonNorth` | X |
|
||||
| `leftShoulder` | L |
|
||||
| `rightShoulder` | R |
|
||||
| `leftTrigger` | ZL |
|
||||
| `rightTrigger` | ZR |
|
||||
| `select` | - |
|
||||
| `start` | + |
|
||||
|
||||
> ⚠ **Switch 按键位置与 Xbox/PS 习惯相反**(南键是 B 不是 A),美术制作图标时需注意:
|
||||
> `buttonSouth` 图标应画 **B**(而非 A),`buttonEast` 图标应画 **A**(而非 B)。
|
||||
|
||||
---
|
||||
|
||||
## 五、图片(Sprite)规格要求
|
||||
|
||||
### 5.1 推荐尺寸
|
||||
|
||||
| 用途 | 推荐尺寸 | 说明 |
|
||||
|------|---------|------|
|
||||
| 标准按键图标(HUD/提示 UI) | **32×32 px** | InteractPromptWidget 默认显示尺寸,32px 在像素风格下清晰无锯齿 |
|
||||
| 高分辨率版本(设置界面、教程) | **64×64 px** | 用于放大显示的场合,需额外维护一套 |
|
||||
| 复合按键图标(LT+RT、双摇杆) | **48×24 px** 或 **64×32 px** | 横向组合按键,宽高比 2:1 |
|
||||
|
||||
### 5.2 Import Settings(Unity Inspector)
|
||||
|
||||
| 设置项 | 推荐值 | 说明 |
|
||||
|-------|-------|------|
|
||||
| Texture Type | `Sprite (2D and UI)` | 必须设置为 Sprite 类型,否则无法拖入 ObjectField |
|
||||
| Sprite Mode | `Single` | 每个按键一张独立图片,避免拆分 |
|
||||
| Pixels Per Unit | `32` | 与项目统一 PPU,1 像素 = 1/32 unit |
|
||||
| Filter Mode | `Point (no filter)` | 像素风格固定值,避免双线性插值导致模糊 |
|
||||
| Compression | `None`(开发),发布时 `ASTC 6x6`(移动端)| UI 图标质量要求高,开发期不压缩 |
|
||||
| Generate Mip Maps | `关闭` | 2D UI 不需要 Mip Maps |
|
||||
| Read/Write Enabled | `关闭` | 不需要像素读写 |
|
||||
| Max Size | `128` | 32px 图标不需要超过 128 |
|
||||
| Wrap Mode | `Clamp` | 避免边缘溢出 |
|
||||
|
||||
### 5.3 美术风格规范
|
||||
|
||||
| 规范项 | 说明 |
|
||||
|-------|------|
|
||||
| **背景透明** | 必须使用 PNG 格式,透明通道保存完整,不要有白色/黑色底边 |
|
||||
| **视觉安全区** | 图标主体保留 2px 内边距,避免内容贴近边缘被裁剪 |
|
||||
| **明度/对比度** | 图标应在深色(`rgba(0,0,0,0.85)`)背景上清晰可见;避免使用纯黑色线条(改用深灰 `#222`) |
|
||||
| **键鼠图标风格** | 偏向拟物,键盘按键绘制带圆角的方形按键轮廓,鼠标按键绘制鼠标轮廓 |
|
||||
| **手柄图标风格** | 手柄按键使用品牌标准配色:Xbox(A=绿、B=红、X=蓝、Y=黄),PS(✕=蓝、○=红、□=粉、△=绿),Switch 使用黑底圆形字母 |
|
||||
| **一致性** | 同一设备下所有图标使用相同的线条粗细、阴影效果、发光效果 |
|
||||
|
||||
### 5.4 文件命名规范
|
||||
|
||||
格式:`IC_Key_{DeviceShort}_{KeyName}.png`
|
||||
|
||||
| 示例路径 | 对应按键 |
|
||||
|---------|---------|
|
||||
| `_Game/Art/UI/Icons/InputKeys/IC_Key_KBM_E.png` | 键盘 E 键 |
|
||||
| `_Game/Art/UI/Icons/InputKeys/IC_Key_KBM_Space.png` | 键盘空格键 |
|
||||
| `_Game/Art/UI/Icons/InputKeys/IC_Key_KBM_LMB.png` | 鼠标左键 |
|
||||
| `_Game/Art/UI/Icons/InputKeys/IC_Key_KBM_RMB.png` | 鼠标右键 |
|
||||
| `_Game/Art/UI/Icons/InputKeys/IC_Key_Xbox_A.png` | Xbox A 键 |
|
||||
| `_Game/Art/UI/Icons/InputKeys/IC_Key_Xbox_RT.png` | Xbox RT 扳机 |
|
||||
| `_Game/Art/UI/Icons/InputKeys/IC_Key_PS_Cross.png` | PS ✕ 键 |
|
||||
| `_Game/Art/UI/Icons/InputKeys/IC_Key_PS_R2.png` | PS R2 扳机 |
|
||||
| `_Game/Art/UI/Icons/InputKeys/IC_Key_Switch_B.png` | Switch B 键(南键) |
|
||||
| `_Game/Art/UI/Icons/InputKeys/IC_Key_Switch_ZR.png` | Switch ZR 扳机 |
|
||||
|
||||
---
|
||||
|
||||
## 六、InputIconService 绑定配置
|
||||
|
||||
`InputIconService` 是图标集的运行时持有者,挂载在 Persistent 场景的 UIRoot 上。其 Inspector 字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `_inputReader` | `InputReaderSO` | 用于查询 Action 的绑定路径(`GetActionEffectivePath`) |
|
||||
| `_kbMouseSet` | `InputDeviceIconSetSO` | **键鼠图标集**,当检测到键盘/鼠标输入时激活 |
|
||||
| `_xboxSet` | `InputDeviceIconSetSO` | **Xbox 图标集**,检测到 XInput 手柄时激活;未配置时自动 fallback 至 `_kbMouseSet` |
|
||||
| `_playStationSet` | `InputDeviceIconSetSO` | **PlayStation 图标集**,检测到 DualShock/DualSense 时激活;未配置时 fallback 至 `_kbMouseSet` |
|
||||
| `_switchSet` | `InputDeviceIconSetSO` | **Switch 图标集**,检测到 Switch Pro/Joy-Con 时激活;未配置时 fallback 至 `_kbMouseSet` |
|
||||
| `_onDeviceChanged` | `InputDeviceTypeEventChannelSO` | 设备切换事件频道(`EVT_InputDeviceChanged.asset`),与 `InputDeviceDetector` 共用 |
|
||||
|
||||
> **Fallback 策略**:若对应设备图标集为 `null`,自动 fallback 至键鼠集。这意味着开发初期只配置 `_kbMouseSet` 即可正常运行,手柄图标集可后续补充,不影响功能。
|
||||
|
||||
---
|
||||
|
||||
## 七、UI 消费组件说明
|
||||
|
||||
### 7.1 InteractPromptWidget(按键提示 HUD)
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|-------|------|
|
||||
| `_promptChannel` | 引用 `InteractPromptEventChannelSO`,订阅 `InteractableDetector` 的广播 |
|
||||
| 显示逻辑 | 收到事件后查询 `IInputIconService.GetActionIcon(actionName)`,设置图标 Sprite;若图标为 `null` 则隐藏图标容器,仅显示文字 |
|
||||
| 刷新时机 | 设备切换(`OnIconSetChanged`)、改键(`BoundControlsChanged`)时自动重绘 |
|
||||
|
||||
### 7.2 InputIconImage(动态图标 Image 组件)
|
||||
|
||||
`InputIconImage` 是挂载在任意 `Image` 组件旁的辅助脚本,通过 `ServiceLocator` 获取 `IInputIconService`,支持两种工作模式:
|
||||
|
||||
| LookupMode | 说明 |
|
||||
|-----------|------|
|
||||
| `ByActionName` | **推荐**。指定 Action 名称(如 `"Interact"`、`"Jump"`),自动查询当前设备+改键后的有效图标 |
|
||||
| `ByBindingPath` | 固定路径,不随改键变化。适用于「始终显示空格键图标」等特定场景 |
|
||||
|
||||
### 7.3 InputDeviceIconSwitcher(静态图标切换)
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `_kbmSprite` | 键鼠时显示的 Sprite |
|
||||
| `_xboxSprite` | Xbox 手柄时显示的 Sprite |
|
||||
| `_psSprite` | PS 手柄时显示的 Sprite |
|
||||
| `_switchSprite` | Switch 手柄时显示的 Sprite |
|
||||
|
||||
适用于「图标固定、仅需在设备间切换整套外观」的场景(如教程截图、设置界面设备标识)。
|
||||
**不支持改键响应**,如需改键后自动更新请用 `InputIconImage`(ByActionName 模式)。
|
||||
|
||||
---
|
||||
|
||||
## 八、完整配置工作流
|
||||
|
||||
### 8.1 快速开始(最小可用配置)
|
||||
|
||||
```
|
||||
1. 准备图片
|
||||
放入 _Game/Art/UI/Icons/InputKeys/
|
||||
至少准备以下按键(根据实际绑定按需增删):
|
||||
键鼠:IC_Key_KBM_E.png(交互)、IC_Key_KBM_Space.png(跳跃)、IC_Key_KBM_LMB.png(攻击)
|
||||
Xbox:IC_Key_Xbox_X.png(交互)、IC_Key_Xbox_A.png(跳跃)、IC_Key_Xbox_RT.png(攻击)
|
||||
|
||||
2. 创建图标集 SO
|
||||
菜单 BaseGames/Input Icon Studio → 选择键鼠标签页 → 点击「指定图标集」→「新建...」
|
||||
保存至 _Game/Data/UI/InputIcons/ICN_KeyboardMouse.asset
|
||||
重复创建 ICN_Xbox.asset
|
||||
|
||||
3. 填充映射
|
||||
在 Input Icon Studio 中:
|
||||
a. 左侧选中 Action(如 Interact)
|
||||
b. 右侧确认 BindingPath(如 <Keyboard>/e)
|
||||
c. 将 IC_Key_KBM_E.png 拖入 Sprite 字段
|
||||
d. 切换到 Xbox 标签,重复上述步骤
|
||||
|
||||
4. 绑定到 InputIconService
|
||||
在 Persistent 场景 UIRoot → InputIconService Inspector 中:
|
||||
将 ICN_KeyboardMouse.asset 拖入 _kbMouseSet 字段
|
||||
将 ICN_Xbox.asset 拖入 _xboxSet 字段
|
||||
|
||||
5. 验证
|
||||
进入 PlayMode → 按 E 键 → 观察 HUD 交互提示出现键盘图标
|
||||
连接手柄 → 观察图标自动切换为 Xbox 图标
|
||||
```
|
||||
|
||||
### 8.2 批量填充工作流(大量按键)
|
||||
|
||||
```
|
||||
1. 在 ICN_KeyboardMouse.asset 的 Inspector 中:
|
||||
点击「从 Action Asset 填充路径」
|
||||
→ 自动为 Gameplay ActionMap 中所有 Keyboard&Mouse 绑定生成 Entry(路径已填入,Icon 为空)
|
||||
|
||||
2. 将对应 Sprite 批量拖入各 Entry 的 Icon 字段
|
||||
(可用 Input Icon Studio 左列的覆盖率指示点确认进度)
|
||||
|
||||
3. 在 Input Icon Studio 查看总覆盖率:
|
||||
标签栏显示 "🖱 键鼠 N/N" — N/N 表示全部配置完成
|
||||
```
|
||||
|
||||
### 8.3 改键适配说明
|
||||
|
||||
**图标集应包含玩家可能绑定到的所有按键**,而非仅包含默认绑定。
|
||||
|
||||
例如交互键默认为 `<Keyboard>/e`,若玩家将其改为 `<Keyboard>/f`,系统查询 `<Keyboard>/f`,图标集中需有该条目才能正确显示。
|
||||
|
||||
**推荐策略**:为键盘图标集预填充全部 26 个字母键 + 常用功能键,为手柄图标集预填充所有面板按键和扳机键。
|
||||
|
||||
---
|
||||
|
||||
## 九、调试与常见问题排查
|
||||
|
||||
| 现象 | 原因 | 解决方案 |
|
||||
|------|------|---------|
|
||||
| 图标区域不显示(空白/隐藏) | `BindingPath` 未在图标集中找到匹配条目 | 在 Input Icon Studio 确认该 Action 当前绑定路径,并在图标集中添加对应条目 |
|
||||
| 切换设备后图标没有变化 | `InputDeviceDetector` 或 `InputIconService` 未在场景中激活 | 确认 Persistent 场景 UIRoot 上挂载了两个组件,且 `_onDeviceChanged` 字段引用正确 |
|
||||
| 改键后图标仍显示旧按键 | 使用的是 `InputDeviceIconSwitcher` 而非 `InputIconImage(ByActionName)` | 改用 `InputIconImage` 组件并设置 `LookupMode = ByActionName` |
|
||||
| 所有设备均显示键鼠图标 | 手柄图标集字段为 null,触发了 fallback 逻辑 | 在 InputIconService Inspector 中为手柄字段指定对应 SO |
|
||||
| 图标显示模糊 | Import Settings 的 Filter Mode 不正确 | 将所有按键图标的 Filter Mode 改为 `Point (no filter)` |
|
||||
| 图标有白色边缘 | 导出时 PNG 透明通道处理不正确 | 用 Photoshop/Aseprite 重新导出,确保「直接 Alpha」(Straight Alpha)而非预乘 Alpha |
|
||||
| 手柄图标显示 Xbox 风格,但玩家用的是 PS 手柄 | `InputDeviceDetector` 未能识别 DualShock 布局 | 确保 Input System 包含 PS 设备支持(`com.unity.inputsystem` ≥ 1.4),并在 Player Settings 中勾选 DualShock 支持 |
|
||||
| Input Icon Studio 左列 Action 为空 | `InputReaderSO` 未填入,或 `_inputActions` 字段名不匹配 | 在工具栏 InputReaderSO 字段指定资产;若字段名有变更,同步更新 `InputIconStudioWindow.ReloadActionAsset()` 中的 `FindProperty("_inputActions")` |
|
||||
|
||||
---
|
||||
|
||||
## 十、推荐图标集最小条目清单
|
||||
|
||||
以下是按项目实际 Gameplay ActionMap 中的 Actions 推荐配置的最小图标集。
|
||||
|
||||
### 10.1 键鼠图标集 — ICN_KeyboardMouse
|
||||
|
||||
| Action 名称 | 默认 BindingPath | 建议图标 |
|
||||
|------------|-----------------|---------|
|
||||
| Move(上) | `<Keyboard>/w` | W键图标 |
|
||||
| Move(下) | `<Keyboard>/s` | S键图标 |
|
||||
| Move(左) | `<Keyboard>/a` | A键图标 |
|
||||
| Move(右) | `<Keyboard>/d` | D键图标 |
|
||||
| Jump | `<Keyboard>/space` | 空格键图标 |
|
||||
| Dash | `<Keyboard>/leftShift` | Shift键图标 |
|
||||
| Attack | `<Mouse>/leftButton` | 鼠标左键图标 |
|
||||
| Interact | `<Keyboard>/e` | E键图标 |
|
||||
| Map | `<Keyboard>/m` | M键图标 |
|
||||
| Pause | `<Keyboard>/escape` | Esc键图标 |
|
||||
|
||||
### 10.2 Xbox 手柄图标集 — ICN_Xbox
|
||||
|
||||
| Action 名称 | BindingPath | 建议图标 |
|
||||
|------------|-------------|---------|
|
||||
| Jump | `<Gamepad>/buttonSouth` | A键图标(绿) |
|
||||
| Dash | `<Gamepad>/buttonEast` | B键图标(红) |
|
||||
| Attack | `<Gamepad>/rightTrigger` | RT图标 |
|
||||
| Interact | `<Gamepad>/buttonWest` | X键图标(蓝) |
|
||||
| Map | `<Gamepad>/select` | View/Select图标 |
|
||||
| Pause | `<Gamepad>/start` | Menu/Start图标 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、修改历史
|
||||
|
||||
| 日期 | 修改内容 |
|
||||
|------|---------|
|
||||
| 2026-05-22 | 初版:完整字段说明、BindingPath 参考表、图片规格要求、工作流及排查指南 |
|
||||
549
Docs/Tuning/InputDeviceIconSet_Tuning.md
Normal file
549
Docs/Tuning/InputDeviceIconSet_Tuning.md
Normal file
@@ -0,0 +1,549 @@
|
||||
# InputDeviceIconSetSO 配置指南
|
||||
|
||||
**配置文件**:`Assets/_Game/Data/UI/InputIcons/ICN_*.asset`
|
||||
**对应脚本**:`InputDeviceIconSetSO.cs` · `InputIconService.cs` · `InputDeviceDetector.cs`
|
||||
**创建工具**:Unity 菜单 `BaseGames/Input Icon Studio`(推荐)或 Inspector 右键 `Create > BaseGames/UI/Input Device Icon Set`
|
||||
**影响系统**:`InputIconService` · `InteractPromptWidget` · `InputIconImage` · `InputDeviceIconSwitcher`
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [系统架构概述](#1-系统架构概述)
|
||||
2. [InputDeviceIconSetSO 字段详解](#2-inputdeviceiconsetsoso-字段详解)
|
||||
3. [IconEntry 绑定路径参考表](#3-iconentry-绑定路径参考表)
|
||||
4. [图片规格要求](#4-图片规格要求)
|
||||
5. [InputIconService 配置字段](#5-inputiconservice-配置字段)
|
||||
6. [设备自动识别逻辑](#6-设备自动识别逻辑)
|
||||
7. [InteractPromptWidget 配置](#7-interactpromptwidget-配置)
|
||||
8. [InputIconImage 配置](#8-inputiconimage-配置)
|
||||
9. [完整工作流:从零到运行](#9-完整工作流从零到运行)
|
||||
10. [常见问题与排查](#10-常见问题与排查)
|
||||
|
||||
---
|
||||
|
||||
## 1. 系统架构概述
|
||||
|
||||
```
|
||||
InputSystem (底层事件)
|
||||
│
|
||||
▼
|
||||
InputDeviceDetector ← 监听所有输入流,精确识别设备类型
|
||||
│ 广播 InputDeviceTypeEventChannelSO
|
||||
▼
|
||||
InputIconService ← 根据设备切换活跃图标集 + 监听改键事件
|
||||
│ implements IInputIconService
|
||||
├── _kbMouseSet (ICN_KeyboardMouse.asset)
|
||||
├── _xboxSet (ICN_Xbox.asset)
|
||||
├── _playStationSet (ICN_PlayStation.asset)
|
||||
└── _switchSet (ICN_Switch.asset)
|
||||
│
|
||||
│ GetActionIcon(actionName)
|
||||
│ → GetActionEffectivePath() ← 读取 InputActionAsset + 改键覆盖路径
|
||||
│ → InputDeviceIconSetSO.GetIcon(path)
|
||||
▼
|
||||
UI 消费者:
|
||||
├── InteractPromptWidget → Image (按键图标) + TMP_Text (动作名称)
|
||||
└── InputIconImage → Image (任意场景内按键提示)
|
||||
```
|
||||
|
||||
### 核心设计要点
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **改键自动更新** | `InputIconService.GetActionEffectivePath()` 使用 `binding.effectivePath`,该值已内置玩家改键后的覆盖路径,UI 无需额外处理 |
|
||||
| **多设备无缝切换** | 玩家随时插拔手柄,`InputDeviceDetector` 实时检测最后一次输入来自哪个设备,图标自动切换,无卡顿 |
|
||||
| **回退机制** | 若某设备图标集未配置(null),`InputIconService` 自动回退到键鼠集 |
|
||||
| **不走 Addressables** | 4 套图标集 SO 通过 `SerializeField` 直接挂载在 Persistent 场景的 `InputIconService` 组件上,随常驻场景加载,零加载延迟 |
|
||||
|
||||
---
|
||||
|
||||
## 2. InputDeviceIconSetSO 字段详解
|
||||
|
||||
> 路径:`Assets/_Game/Data/UI/InputIcons/ICN_{设备}.asset`
|
||||
> 菜单:`Create > BaseGames/UI/Input Device Icon Set`
|
||||
|
||||
### 2.1 顶层字段
|
||||
|
||||
#### `_deviceType`(InputDeviceType 枚举)
|
||||
|
||||
| 枚举值 | 对应设备 | 说明 |
|
||||
|--------|---------|------|
|
||||
| `KeyboardMouse` | 键盘 + 鼠标 | 默认设备,未识别到手柄时回退到此 |
|
||||
| `XboxController` | Xbox 手柄 / 其他 XInput 手柄 | 包含 Xbox One、Xbox Series、大多数第三方 PC 手柄 |
|
||||
| `PlayStationController` | DualShock 4 / DualSense(PS5)| 通过 Sony 布局层次或 manufacturer 字段识别 |
|
||||
| `SwitchController` | Switch Pro Controller / Joy-Con | 通过 Nintendo 厂商字段或布局识别 |
|
||||
|
||||
**用途**:
|
||||
- 编辑器工具(Input Icon Studio)用此字段将 SO 自动归类到对应设备标签栏
|
||||
- `AddressableRules` 工具可依此字段批量校验命名一致性
|
||||
- **不影响运行时逻辑**:运行时由 `InputIconService` 按字段顺序(_kbMouseSet / _xboxSet / _playStationSet / _switchSet)直接选择,不读取此字段
|
||||
|
||||
> ⚠️ 务必与文件命名对应:`ICN_KeyboardMouse.asset` → `KeyboardMouse`
|
||||
|
||||
---
|
||||
|
||||
#### `_entries`(IconEntry 数组)
|
||||
|
||||
每个元素代表「一个按键路径 → 一张 Sprite」的映射关系。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `BindingPath` | string | InputSystem 绑定路径(见 §3 参考表)。**大小写不敏感**,匹配时使用 `OrdinalIgnoreCase` |
|
||||
| `Icon` | Sprite | 该按键对应的图标 Sprite(规格要求见 §4)|
|
||||
|
||||
**注意事项**:
|
||||
|
||||
1. **路径必须是 InputSystem 的标准路径格式**,不是自定义字符串。路径来源:
|
||||
- 运行时从 `binding.effectivePath` 获取(含改键)
|
||||
- 编辑时从 `InputActionAsset` 的 Binding 列表获取
|
||||
- 可在 Input Icon Studio 的 Action 列表中查看当前绑定路径预览
|
||||
|
||||
2. **改键后路径会变化**:例如玩家将「跳跃」从 `<Keyboard>/space` 改为 `<Keyboard>/leftShift`,系统会查询 `<Keyboard>/leftShift`。因此图标集中需要覆盖所有玩家可能绑定的物理按键,而非仅默认按键。
|
||||
|
||||
3. **同一按键路径在多个 Action 中可复用**:图标集是路径 → Sprite 的扁平映射,一张 Sprite 可对应多个 Action 的查询结果(只要路径相同)。
|
||||
|
||||
4. **路径不存在时行为**:`GetIcon()` 返回 `null`,UI 消费方(`InteractPromptWidget` / `InputIconImage`)会将 `Image.enabled = false`,隐藏图标区域,不显示错误占位图。
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Inspector 快捷操作(InputDeviceIconSetSOEditor)
|
||||
|
||||
在 Inspector 选中 `ICN_*.asset` 时,自定义编辑器提供以下操作:
|
||||
|
||||
| 按钮 | 功能 |
|
||||
|------|------|
|
||||
| **从 Action Asset 填充路径** | 读取 `InputReaderSO` 中的 `InputActionAsset`,自动提取当前设备方案的所有绑定路径,批量创建 `_entries` 条目(Icon 留空,待手动填入)|
|
||||
| **在 Input Icon Studio 打开** | 跳转到 Input Icon Studio 窗口并定位此 SO |
|
||||
| **复制路径** | 右键单个条目的路径字段,复制路径字符串到剪贴板 |
|
||||
|
||||
---
|
||||
|
||||
## 3. IconEntry 绑定路径参考表
|
||||
|
||||
### 3.1 键盘常用路径(`ICN_KeyboardMouse.asset`)
|
||||
|
||||
| 描述 | BindingPath |
|
||||
|------|-------------|
|
||||
| 空格键 | `<Keyboard>/space` |
|
||||
| E 键(交互默认) | `<Keyboard>/e` |
|
||||
| F 键 | `<Keyboard>/f` |
|
||||
| R 键 | `<Keyboard>/r` |
|
||||
| Shift 键(左) | `<Keyboard>/leftShift` |
|
||||
| Ctrl 键(左) | `<Keyboard>/leftCtrl` |
|
||||
| Tab 键 | `<Keyboard>/tab` |
|
||||
| Escape 键 | `<Keyboard>/escape` |
|
||||
| Enter 键 | `<Keyboard>/enter` |
|
||||
| 方向键上 | `<Keyboard>/upArrow` |
|
||||
| 方向键下 | `<Keyboard>/downArrow` |
|
||||
| 方向键左 | `<Keyboard>/leftArrow` |
|
||||
| 方向键右 | `<Keyboard>/rightArrow` |
|
||||
| W / A / S / D | `<Keyboard>/w` · `<Keyboard>/a` · `<Keyboard>/s` · `<Keyboard>/d` |
|
||||
| 数字键 1~4 | `<Keyboard>/1` ~ `<Keyboard>/4` |
|
||||
| 鼠标左键 | `<Mouse>/leftButton` |
|
||||
| 鼠标右键 | `<Mouse>/rightButton` |
|
||||
| 鼠标中键 | `<Mouse>/middleButton` |
|
||||
| 滚轮向上 | `<Mouse>/scroll/up` |
|
||||
| 滚轮向下 | `<Mouse>/scroll/down` |
|
||||
|
||||
### 3.2 Xbox / XInput 手柄路径(`ICN_Xbox.asset`)
|
||||
|
||||
| 描述 | BindingPath |
|
||||
|------|-------------|
|
||||
| A 键(南方键,交互默认) | `<Gamepad>/buttonSouth` |
|
||||
| B 键(东方键) | `<Gamepad>/buttonEast` |
|
||||
| X 键(西方键) | `<Gamepad>/buttonWest` |
|
||||
| Y 键(北方键) | `<Gamepad>/buttonNorth` |
|
||||
| 左摇杆按下 | `<Gamepad>/leftStickButton` |
|
||||
| 右摇杆按下 | `<Gamepad>/rightStickButton` |
|
||||
| 左肩键 LB | `<Gamepad>/leftShoulder` |
|
||||
| 右肩键 RB | `<Gamepad>/rightShoulder` |
|
||||
| 左扳机 LT | `<Gamepad>/leftTrigger` |
|
||||
| 右扳机 RT | `<Gamepad>/rightTrigger` |
|
||||
| 十字键上 | `<Gamepad>/dpad/up` |
|
||||
| 十字键下 | `<Gamepad>/dpad/down` |
|
||||
| 十字键左 | `<Gamepad>/dpad/left` |
|
||||
| 十字键右 | `<Gamepad>/dpad/right` |
|
||||
| 开始键 Start / Menu | `<Gamepad>/start` |
|
||||
| 选择键 Select / View | `<Gamepad>/select` |
|
||||
| 左摇杆方向 | `<Gamepad>/leftStick` |
|
||||
| 右摇杆方向 | `<Gamepad>/rightStick` |
|
||||
|
||||
### 3.3 PlayStation 手柄路径(`ICN_PlayStation.asset`)
|
||||
|
||||
> **路径与 Xbox 完全相同**(均使用通用 `<Gamepad>` 路径),只有图标 Sprite 不同。
|
||||
> PlayStation 的按键物理布局与 Xbox 一致(南/东/西/北方键),区别在于图标(✕ / ○ / □ / △ vs A/B/X/Y)。
|
||||
|
||||
| 描述 | PS 图标 | BindingPath(与 Xbox 相同) |
|
||||
|------|---------|--------------------------|
|
||||
| 南方键 Cross(✕) | ✕ 图标 | `<Gamepad>/buttonSouth` |
|
||||
| 东方键 Circle(○) | ○ 图标 | `<Gamepad>/buttonEast` |
|
||||
| 西方键 Square(□) | □ 图标 | `<Gamepad>/buttonWest` |
|
||||
| 北方键 Triangle(△) | △ 图标 | `<Gamepad>/buttonNorth` |
|
||||
| L1 | L1 图标 | `<Gamepad>/leftShoulder` |
|
||||
| R1 | R1 图标 | `<Gamepad>/rightShoulder` |
|
||||
| L2 | L2 图标 | `<Gamepad>/leftTrigger` |
|
||||
| R2 | R2 图标 | `<Gamepad>/rightTrigger` |
|
||||
| L3(左摇杆按下)| L3 图标 | `<Gamepad>/leftStickButton` |
|
||||
| R3(右摇杆按下)| R3 图标 | `<Gamepad>/rightStickButton` |
|
||||
| Options 键 | Options 图标 | `<Gamepad>/start` |
|
||||
| TouchPad / Share | TouchPad 图标 | `<Gamepad>/select` |
|
||||
|
||||
### 3.4 Nintendo Switch 手柄路径(`ICN_Switch.asset`)
|
||||
|
||||
> **路径同样与 Xbox/PS 共用 `<Gamepad>` 路径**,仅图标不同。
|
||||
> Switch 的南方键是 B,东方键是 A,与 Xbox 方向相反,务必使用正确的 Switch 图标。
|
||||
|
||||
| 描述 | Switch 图标 | BindingPath |
|
||||
|------|------------|-------------|
|
||||
| 南方键 B | B 图标 | `<Gamepad>/buttonSouth` |
|
||||
| 东方键 A | A 图标 | `<Gamepad>/buttonEast` |
|
||||
| 西方键 Y | Y 图标 | `<Gamepad>/buttonWest` |
|
||||
| 北方键 X | X 图标 | `<Gamepad>/buttonNorth` |
|
||||
| L 键 | L 图标 | `<Gamepad>/leftShoulder` |
|
||||
| R 键 | R 图标 | `<Gamepad>/rightShoulder` |
|
||||
| ZL 键 | ZL 图标 | `<Gamepad>/leftTrigger` |
|
||||
| ZR 键 | ZR 图标 | `<Gamepad>/rightTrigger` |
|
||||
| + 键 | + 图标 | `<Gamepad>/start` |
|
||||
| - 键 | - 图标 | `<Gamepad>/select` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 图片规格要求
|
||||
|
||||
### 4.1 分辨率与尺寸
|
||||
|
||||
| 场景 | 建议尺寸 | 说明 |
|
||||
|------|---------|------|
|
||||
| **HUD 交互提示**(InteractPromptWidget 主图标)| **64×64 px** | 运行时显示在 32×32 的 Image 控件中,2× 超采样保证清晰度 |
|
||||
| **InputIconImage**(通用场景内提示)| **64×64 px** | 与上同 |
|
||||
| Input Icon Studio 大图预览 | 64×64 px | 编辑器预览用,与运行时同图 |
|
||||
|
||||
> 最小可用尺寸 **32×32 px**,但在 Retina / 高 DPI 屏幕上会模糊。
|
||||
> 不建议超过 **128×128 px**(Atlas 占用增大但视觉收益有限)。
|
||||
|
||||
### 4.2 格式
|
||||
|
||||
| 项目 | 规范 |
|
||||
|------|------|
|
||||
| **文件格式** | PNG(推荐,支持透明度)|
|
||||
| **色彩模式** | RGBA(需要透明背景)|
|
||||
| **背景** | **透明**,图标主体不超过 90% 画布区域,四周留 5% 安全边距 |
|
||||
| **颜色风格** | 与游戏 UI 风格一致:像素风选用扁平配色;高清风可使用渐变 |
|
||||
|
||||
### 4.3 Unity Import Settings
|
||||
|
||||
在 Inspector 选中图标 PNG 后,设置以下 Import Settings:
|
||||
|
||||
| 设置项 | 推荐值 | 原因 |
|
||||
|-------|-------|------|
|
||||
| **Texture Type** | `Sprite (2D and UI)` | 用于 UI Image 组件 |
|
||||
| **Sprite Mode** | `Single`(单图)或 `Multiple`(Sprite Sheet) | 单张图标用 Single;若多个图标合并到一张 Atlas 用 Multiple |
|
||||
| **Pixels Per Unit** | `32`(像素风项目)或 `64`(高清项目,与 Canvas 缩放匹配) | 与项目全局 PPU 保持一致 |
|
||||
| **Filter Mode** | `Point (no filter)`(像素风)或 `Bilinear`(高清风) | 像素风必须用 Point,否则会模糊 |
|
||||
| **Compression** | `None`(开发阶段)/ `ASTC 4x4`(移动端发布)| 图标资产小,压缩感知不明显,开发期优先无损 |
|
||||
| **Generate Mip Maps** | **关闭** | 2D UI 不需要 Mip Maps |
|
||||
| **Read/Write Enabled** | **关闭**(除非代码读像素)| 减少内存占用 |
|
||||
| **Max Size** | `128` | 图标不需要超过 128px |
|
||||
| **Alpha Is Transparency** | **开启** | 正确处理透明边缘抗锯齿 |
|
||||
|
||||
### 4.4 Sprite Atlas 策略
|
||||
|
||||
将同一设备的所有按键图标合并到一张 Atlas,可减少 DrawCall:
|
||||
|
||||
| Atlas 文件 | 覆盖内容 | 存放位置 |
|
||||
|-----------|---------|---------|
|
||||
| `Atlas_UI_Keys_KBM.spriteatlas` | 键鼠全部按键图标 | `_Game/Art/UI/Icons/InputKeys/` |
|
||||
| `Atlas_UI_Keys_Xbox.spriteatlas` | Xbox 全部按键图标 | `_Game/Art/UI/Icons/InputKeys/` |
|
||||
| `Atlas_UI_Keys_PS.spriteatlas` | PlayStation 全部按键图标 | `_Game/Art/UI/Icons/InputKeys/` |
|
||||
| `Atlas_UI_Keys_Switch.spriteatlas` | Switch 全部按键图标 | `_Game/Art/UI/Icons/InputKeys/` |
|
||||
|
||||
> **Atlas 本身不注册 Addressable**,由引用它的 SO 间接带入包体。
|
||||
|
||||
### 4.5 命名规范
|
||||
|
||||
```
|
||||
IC_Key_{DeviceShort}_{KeyName}.png
|
||||
|
||||
DeviceShort:
|
||||
KBM → 键盘鼠标
|
||||
Xbox → Xbox / XInput
|
||||
PS → PlayStation (DualShock/DualSense)
|
||||
Switch → Nintendo Switch
|
||||
|
||||
示例:
|
||||
IC_Key_KBM_Space.png ← <Keyboard>/space
|
||||
IC_Key_KBM_E.png ← <Keyboard>/e
|
||||
IC_Key_KBM_LMB.png ← <Mouse>/leftButton
|
||||
IC_Key_Xbox_South.png ← <Gamepad>/buttonSouth (A键)
|
||||
IC_Key_Xbox_RT.png ← <Gamepad>/rightTrigger
|
||||
IC_Key_PS_Cross.png ← <Gamepad>/buttonSouth (✕键)
|
||||
IC_Key_PS_R2.png ← <Gamepad>/rightTrigger
|
||||
IC_Key_Switch_South.png ← <Gamepad>/buttonSouth (B键)
|
||||
IC_Key_Switch_ZR.png ← <Gamepad>/rightTrigger
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. InputIconService 配置字段
|
||||
|
||||
> 组件挂载位置:`Persistent 场景` → `UIRoot` → `InputIconService`
|
||||
|
||||
| 字段(Header) | 字段名 | 类型 | 说明 |
|
||||
|--------------|--------|------|------|
|
||||
| **Input** | `_inputReader` | InputReaderSO | 游戏全局 InputReaderSO,用于查询 Action 的绑定路径(含改键覆盖)|
|
||||
| **Icon Sets** | `_kbMouseSet` | InputDeviceIconSetSO | 键鼠图标集(`ICN_KeyboardMouse.asset`)。**默认激活集,任何未识别设备均回退到此** |
|
||||
| **Icon Sets** | `_xboxSet` | InputDeviceIconSetSO | Xbox 图标集(`ICN_Xbox.asset`)。可为 null,null 时回退到 `_kbMouseSet` |
|
||||
| **Icon Sets** | `_playStationSet` | InputDeviceIconSetSO | PlayStation 图标集(`ICN_PlayStation.asset`)。可为 null,null 时回退到 `_kbMouseSet` |
|
||||
| **Icon Sets** | `_switchSet` | InputDeviceIconSetSO | Switch 图标集(`ICN_Switch.asset`)。可为 null,null 时回退到 `_kbMouseSet` |
|
||||
| **Event Channels** | `_onDeviceChanged` | InputDeviceTypeEventChannelSO | 订阅 InputDeviceDetector 广播的设备切换事件(`EVT_InputDeviceChanged.asset`)|
|
||||
|
||||
### 5.1 回退规则
|
||||
|
||||
```
|
||||
设备切换到 XboxController
|
||||
_xboxSet != null → 使用 _xboxSet
|
||||
_xboxSet == null → 回退 _kbMouseSet
|
||||
|
||||
设备切换到 PlayStationController
|
||||
_playStationSet != null → 使用 _playStationSet
|
||||
_playStationSet == null → 回退 _kbMouseSet
|
||||
|
||||
设备切换到 SwitchController
|
||||
_switchSet != null → 使用 _switchSet
|
||||
_switchSet == null → 回退 _kbMouseSet
|
||||
```
|
||||
|
||||
> 回退到 `_kbMouseSet` 时,手柄路径(如 `<Gamepad>/buttonSouth`)查询 `_kbMouseSet` 会返回 null(因为键鼠集里没有 Gamepad 路径),最终 `Image.enabled = false`,图标区域隐藏。
|
||||
> **建议**:四套图标集均配置完整,避免出现图标消失的情况。
|
||||
|
||||
---
|
||||
|
||||
## 6. 设备自动识别逻辑
|
||||
|
||||
> 组件:`InputDeviceDetector`,挂在 `UIRoot` 上,与 `InputIconService` 同节点
|
||||
|
||||
### 6.1 识别优先级
|
||||
|
||||
```
|
||||
有输入事件到来时(StateEvent / DeltaStateEvent):
|
||||
1. device is Keyboard or Mouse → KeyboardMouse
|
||||
2. device is Gamepad
|
||||
2a. 布局继承自 DualShockGamepad
|
||||
或 product 含 "DualShock"/"DualSense"
|
||||
或 manufacturer 含 "Sony" → PlayStationController
|
||||
2b. 布局继承自 SwitchProControllerHID
|
||||
或 product 含 "Switch"/"Joy-Con"
|
||||
或 manufacturer 含 "Nintendo" → SwitchController
|
||||
2c. 布局继承自 XInputController
|
||||
或 product 含 "Xbox"
|
||||
或 interfaceName == "XInput" → XboxController
|
||||
2d. 未匹配(第三方 HID 手柄) → XboxController(默认)
|
||||
3. 其他设备类型(触摸屏等) → KeyboardMouse(默认)
|
||||
```
|
||||
|
||||
### 6.2 热插拔行为
|
||||
|
||||
- 手柄**拔出**:`InputDeviceChange.Removed` 不触发设备切换,保持当前图标集,防止玩家无意拔线导致图标乱跳
|
||||
- 手柄**重连**:`InputDeviceChange.Reconnected/Added` 同样不自动切换,等待下一次实际输入事件再切换
|
||||
- 玩家**实际操作**键鼠或手柄时,才触发切换 —— 这是最符合用户体验的设计
|
||||
|
||||
---
|
||||
|
||||
## 7. InteractPromptWidget 配置
|
||||
|
||||
> 组件挂载位置:`Persistent 场景` → `HUD Canvas` → `InteractPromptWidget_GO`
|
||||
|
||||
### 7.1 字段说明
|
||||
|
||||
| Header | 字段 | 类型 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| **UI 引用** | `_keyIcon` | Image | 显示按键图标的 UI Image 组件。宽高建议 **32×32**(Canvas 坐标空间),Preserve Aspect = true |
|
||||
| **UI 引用** | `_labelText` | TMP_Text | 显示动作说明文字(如"交互"、"拾取"),对应 `InteractPromptEvent.LabelText` |
|
||||
| **UI 引用** | `_root` | GameObject | 整个提示 UI 的根节点(含图标+文字),`SetActive(false)` 时完整隐藏。若为 null,则控制 Widget 所在 GameObject 本身 |
|
||||
| **Event Channels** | `_onShowPrompt` | InteractPromptEventChannelSO | 订阅此频道接收显示指令,负载为 `InteractPromptEvent { ActionName, LabelText }` |
|
||||
| **Event Channels** | `_onHidePrompt` | VoidEventChannelSO | 订阅此频道接收隐藏指令(玩家离开交互范围时触发)|
|
||||
|
||||
### 7.2 InteractPromptEvent 负载
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `ActionName` | string | InputSystem Action 名称,如 `"Interact"`。用于在 InputIconService 查询对应图标 |
|
||||
| `LabelText` | string | 显示的文字,如 `"交互"` / `"拾取"` / `"打开"`。由交互物件在广播时指定(来自 `Interactable.InteractPrompt`)|
|
||||
|
||||
### 7.3 延迟绑定机制
|
||||
|
||||
`InteractPromptWidget` 在 `OnEnable` 时**不**立即获取 `IInputIconService`,而是在首次 `ShowPrompt()` 时才获取。这避免了执行顺序问题(`InputIconService.Awake()` 可能晚于 Widget 的 `OnEnable`)。
|
||||
|
||||
---
|
||||
|
||||
## 8. InputIconImage 配置
|
||||
|
||||
> 适用场景:教程界面、暂停菜单、任何需要显示单个按键图标的 UI Image
|
||||
> 组件:`InputIconImage`,挂在带有 `Image` 组件的 UI GameObject 上
|
||||
|
||||
### 8.1 字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `_mode` | LookupMode 枚举 | 查询模式(见下表)|
|
||||
| `_actionName` | string | **ByActionName 模式**:填写 Action 名称(如 `"Jump"`),自动跟随改键和设备切换 |
|
||||
| `_bindingPath` | string | **ByBindingPath 模式**:填写固定路径(如 `<Keyboard>/space`),不跟随改键 |
|
||||
|
||||
### 8.2 LookupMode 枚举
|
||||
|
||||
| 枚举值 | 适用场景 | 改键响应 | 设备切换响应 |
|
||||
|--------|---------|---------|------------|
|
||||
| `ByActionName`(推荐)| 所有正常游戏内 UI 提示 | ✅ 自动更新 | ✅ 自动切换 |
|
||||
| `ByBindingPath` | 教程截图说明、固定按键展示(如「按 Space 确认」这类静态文案)| ❌ 不更新 | ❌ 不切换 |
|
||||
|
||||
> `ByBindingPath` 模式目前图标查询功能为低优先级,实际返回 null(`Image.enabled = false`)。如需支持,可在 `InputIconService` 中通过 `GetOrDefault<IInputIconService>()` + `GetActiveSet().GetIcon(path)` 手动实现。
|
||||
|
||||
---
|
||||
|
||||
## 9. 完整工作流:从零到运行
|
||||
|
||||
### Step 1 · 导入图标素材
|
||||
|
||||
```
|
||||
1. 在 _Game/Art/UI/Icons/InputKeys/ 下创建4个子文件夹:KBM / Xbox / PS / Switch
|
||||
2. 将各设备按键图标 PNG 放入对应子文件夹
|
||||
3. 在 Unity 中选中所有图标,统一设置 Import Settings:
|
||||
- Texture Type: Sprite (2D and UI)
|
||||
- Sprite Mode: Single
|
||||
- Pixels Per Unit: 32(像素风)
|
||||
- Filter Mode: Point (no filter)
|
||||
- Max Size: 128
|
||||
- Generate Mip Maps: 关闭
|
||||
- Alpha Is Transparency: 开启
|
||||
4. 创建4个 Sprite Atlas(见 §4.4),将图标拖入对应 Atlas
|
||||
```
|
||||
|
||||
### Step 2 · 创建图标集 SO
|
||||
|
||||
方式 A(推荐)— 使用 Input Icon Studio:
|
||||
```
|
||||
1. 菜单 BaseGames/Input Icon Studio
|
||||
2. 在「图标集资产状态」区域,点击对应设备的「+ 新建」
|
||||
3. 保存路径:_Game/Data/UI/InputIcons/ICN_{设备}.asset
|
||||
4. 确认 _deviceType 字段与文件名一致
|
||||
```
|
||||
|
||||
方式 B — 手动创建:
|
||||
```
|
||||
1. Project 窗口右键 → Create → BaseGames/UI/Input Device Icon Set
|
||||
2. 重命名为 ICN_{设备}.asset,移至 _Game/Data/UI/InputIcons/
|
||||
3. 在 Inspector 中设置 _deviceType
|
||||
```
|
||||
|
||||
### Step 3 · 填充绑定路径与图标
|
||||
|
||||
```
|
||||
在 Input Icon Studio 中:
|
||||
1. 顶部选择 InputReaderSO(自动加载唯一实例)
|
||||
2. 点击设备标签(键鼠 / Xbox / PS / Switch)
|
||||
3. 左列选择 Action(如 Interact)
|
||||
4. 右列拖入对应 Sprite
|
||||
5. 覆盖率圆点变绿(●)= 已配置
|
||||
|
||||
或在 Inspector 中直接编辑 _entries 数组:
|
||||
1. 展开 _entries
|
||||
2. 点击「从 Action Asset 填充路径」批量生成所有条目(Icon 留空)
|
||||
3. 逐条拖入 Sprite
|
||||
```
|
||||
|
||||
### Step 4 · 绑定到 InputIconService
|
||||
|
||||
```
|
||||
1. 打开 Persistent 场景
|
||||
2. 选中 UIRoot GameObject(或单独的 InputIconService GameObject)
|
||||
3. 找到 InputIconService 组件
|
||||
4. 拖入对应字段:
|
||||
_inputReader → InputReaderSO(通常为 PLY_InputReader.asset)
|
||||
_kbMouseSet → ICN_KeyboardMouse.asset
|
||||
_xboxSet → ICN_Xbox.asset
|
||||
_playStationSet → ICN_PlayStation.asset
|
||||
_switchSet → ICN_Switch.asset
|
||||
_onDeviceChanged → EVT_InputDeviceChanged.asset
|
||||
```
|
||||
|
||||
### Step 5 · 配置 InteractPromptWidget
|
||||
|
||||
```
|
||||
1. 在 HUD Canvas 下找到(或创建)InteractPromptWidget GameObject
|
||||
2. 挂上 InteractPromptWidget 组件
|
||||
3. 配置字段:
|
||||
_keyIcon → 按键图标 Image 组件(32×32 UI Image)
|
||||
_labelText → 动作说明 TMP_Text 组件
|
||||
_root → 整个提示 UI 根节点(控制显隐)
|
||||
_onShowPrompt → EVT_ShowInteractPrompt.asset
|
||||
_onHidePrompt → EVT_HideInteractPrompt.asset
|
||||
```
|
||||
|
||||
### Step 6 · 验证
|
||||
|
||||
```
|
||||
进入 PlayMode:
|
||||
✅ 走近 NPC / 宝箱 → 右下角显示按键图标 + "交互"文字
|
||||
✅ 插入 Xbox 手柄 → 图标自动切换为 Xbox A 键图标
|
||||
✅ 插入 DualShock → 图标切换为 PS ✕ 图标
|
||||
✅ 改键(在游戏内按键重映射菜单)→ 图标同步更新
|
||||
✅ 拔出手柄 → 恢复键鼠图标(下次键盘/鼠标操作后切换)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 常见问题与排查
|
||||
|
||||
### 图标不显示(Image 被隐藏)
|
||||
|
||||
1. 检查 `InputIconService` 的4个图标集字段是否已赋值
|
||||
2. 检查 `_entries` 中是否有该 Action 对应设备的绑定路径条目
|
||||
3. 在 Input Icon Studio 左列查看该 Action 的绑定路径预览,与 `_entries` 中的 BindingPath 对比
|
||||
4. 检查 `InputReaderSO` 是否正确赋值给 `InputIconService._inputReader`
|
||||
5. 运行时打开 Event Bus Monitor(`BaseGames/Events/Event Bus Monitor`)确认 `EVT_InputDeviceChanged` 是否正常广播
|
||||
|
||||
### 改键后图标不更新
|
||||
|
||||
1. 确认 `InputIconService.OnEnable()` 已订阅 `InputSystem.onActionChange`(Inspector 中确保组件处于 active 状态)
|
||||
2. 确认 `_entries` 中包含改键后目标按键的路径(如玩家改到 `<Keyboard>/leftShift`,需有对应条目)
|
||||
3. 改键后图标集必须覆盖所有可能被绑定的物理按键;对于玩家自定义改键的游戏,建议将整个键盘的常用键全部配置进 `_kbMouseSet._entries`
|
||||
|
||||
### 设备切换后图标不更新
|
||||
|
||||
1. 确认 `InputDeviceDetector` 组件已挂载在场景中(UIRoot 上)
|
||||
2. 确认 `InputDeviceDetector._onDeviceChanged` 与 `InputIconService._onDeviceChanged` 引用的是**同一个** `EVT_InputDeviceChanged.asset`
|
||||
3. 插入手柄后需要实际按下任意按键才会触发识别(纯插入不切换)
|
||||
|
||||
### PlayStation 手柄显示 Xbox 图标
|
||||
|
||||
1. 在 Unity 菜单 `Window > Analysis > Input Debugger` 查看手柄的 `layout` 字段
|
||||
2. 若 layout 不继承自 `DualShockGamepad`(部分 USB 适配器会出现此情况),系统会回退到 XboxController
|
||||
3. 可在 `InputDeviceDetector.ClassifyDevice()` 中添加额外的 product 字符串匹配规则
|
||||
|
||||
### 编辑器工具显示覆盖率为 0/0
|
||||
|
||||
- 说明 `InputReaderSO` 未在 Input Icon Studio 工具栏加载,或 `InputActionAsset` 的 Gameplay ActionMap 为空
|
||||
- 在工具栏 ObjectField 手动指定 InputReaderSO,然后点击「⟳ 刷新」
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文件速查
|
||||
|
||||
| 用途 | 文件路径 |
|
||||
|------|---------|
|
||||
| 图标集 SO(键鼠)| `_Game/Data/UI/InputIcons/ICN_KeyboardMouse.asset` |
|
||||
| 图标集 SO(Xbox)| `_Game/Data/UI/InputIcons/ICN_Xbox.asset` |
|
||||
| 图标集 SO(PS)| `_Game/Data/UI/InputIcons/ICN_PlayStation.asset` |
|
||||
| 图标集 SO(Switch)| `_Game/Data/UI/InputIcons/ICN_Switch.asset` |
|
||||
| 按键图标图片 | `_Game/Art/UI/Icons/InputKeys/` |
|
||||
| 核心 SO 脚本 | `Scripts/UI/InputDeviceIconSetSO.cs` |
|
||||
| 服务接口 | `Scripts/UI/IInputIconService.cs` |
|
||||
| 服务实现 | `Scripts/UI/InputIconService.cs` |
|
||||
| 设备检测器 | `Scripts/UI/InputDeviceDetector.cs` |
|
||||
| 交互提示 Widget | `Scripts/UI/HUD/InteractPromptWidget.cs` |
|
||||
| 通用图标 Image | `Scripts/UI/InputDeviceIconSwitcher.cs`(含 InputIconImage 类)|
|
||||
| 编辑器窗口 | `Scripts/Editor/Input/InputIconStudioWindow.cs` |
|
||||
| 编辑器 Inspector | `Scripts/Editor/Input/InputDeviceIconSetSOEditor.cs` |
|
||||
| 事件频道 | `Data/Events/UI/EVT_InputDeviceChanged.asset` |
|
||||
| 交互提示事件 | `Data/Events/World/EVT_ShowInteractPrompt.asset` |
|
||||
@@ -11,6 +11,8 @@
|
||||
| 文件 | 覆盖系统 |
|
||||
|---|---|
|
||||
| [PlayerJumpDash_Tuning.md](PlayerJumpDash_Tuning.md) | 玩家跳跃 / 冲刺手感参数 |
|
||||
| [WeaponSO_Tuning.md](WeaponSO_Tuning.md) | 武器连击 / 伤害 / HitBox 参数 |
|
||||
| [InputDeviceIconSet_Tuning.md](InputDeviceIconSet_Tuning.md) | 按键图标集 SO 配置、BindingPath 参考表、图片规格、完整工作流 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user