Add WeaponFeedback component and AddressableManagerWindow meta file
- Implemented WeaponFeedback class for handling weapon-related feedbacks such as hit effects and attack sounds. - Added meta file for AddressableManagerWindow to manage addressable assets. - Included a new jump.data file for profiler data.
This commit is contained in:
@@ -15,7 +15,7 @@ MonoBehaviour:
|
|||||||
m_DefaultGroup: 9ce5c865a2d3a0840aabdd8ccb3fd4b1
|
m_DefaultGroup: 9ce5c865a2d3a0840aabdd8ccb3fd4b1
|
||||||
m_currentHash:
|
m_currentHash:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
Hash: 00000000000000000000000000000000
|
Hash: 3b5a6592fec2f53c65ab132b7f731fb2
|
||||||
m_OptimizeCatalogSize: 0
|
m_OptimizeCatalogSize: 0
|
||||||
m_BuildRemoteCatalog: 0
|
m_BuildRemoteCatalog: 0
|
||||||
m_BundleLocalCatalog: 0
|
m_BundleLocalCatalog: 0
|
||||||
@@ -88,6 +88,14 @@ MonoBehaviour:
|
|||||||
m_LabelTable:
|
m_LabelTable:
|
||||||
m_LabelNames:
|
m_LabelNames:
|
||||||
- default
|
- default
|
||||||
|
- Preload
|
||||||
|
- Poolable
|
||||||
|
- Enemy
|
||||||
|
- BGM
|
||||||
|
- SFX
|
||||||
|
- Charms
|
||||||
|
- Config
|
||||||
|
- Weapon
|
||||||
m_SchemaTemplates: []
|
m_SchemaTemplates: []
|
||||||
m_GroupTemplateObjects:
|
m_GroupTemplateObjects:
|
||||||
- {fileID: 11400000, guid: f9701da6026b3a54f9b4d6eb144ee443, type: 2}
|
- {fileID: 11400000, guid: f9701da6026b3a54f9b4d6eb144ee443, type: 2}
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ MonoBehaviour:
|
|||||||
weaponTrailPrefab: {fileID: 0}
|
weaponTrailPrefab: {fileID: 0}
|
||||||
trailColor: {r: 1, g: 1, b: 1, a: 1}
|
trailColor: {r: 1, g: 1, b: 1, a: 1}
|
||||||
soulPowerGain: 10
|
soulPowerGain: 10
|
||||||
|
hitWeight: 1
|
||||||
references:
|
references:
|
||||||
version: 2
|
version: 2
|
||||||
RefIds: []
|
RefIds: []
|
||||||
|
|||||||
@@ -12,23 +12,25 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 81da55e0fcf99d34693cbc5a348225c3, type: 3}
|
m_Script: {fileID: 11500000, guid: 81da55e0fcf99d34693cbc5a348225c3, type: 3}
|
||||||
m_Name: PLY_PlayerMovementConfig
|
m_Name: PLY_PlayerMovementConfig
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
RunSpeed: 8
|
RunSpeed: 6
|
||||||
AirDragFactor: 1
|
AirDragFactor: 1
|
||||||
JumpForce: 24
|
JumpForce: 20
|
||||||
CoyoteTime: 0.12
|
CoyoteTime: 0.12
|
||||||
FallGravityMult: 2.5
|
FallGravityMult: 2.5
|
||||||
MaxFallSpeed: 28
|
MaxFallSpeed: 28
|
||||||
JumpCutMultiplier: 0.321
|
JumpCutMultiplier: 0.321
|
||||||
ApexThreshold: 3
|
ApexThreshold: 3
|
||||||
ApexGravityMultiplier: 0.3
|
ApexGravityMultiplier: 0.3
|
||||||
MaxAirJumps: 1
|
MaxAirJumps: 5
|
||||||
DoubleJumpForce: 19
|
DoubleJumpForce: 15
|
||||||
DashSpeed: 20
|
DashSpeed: 20
|
||||||
DashDuration: 0.25
|
DashDuration: 0.25
|
||||||
DashCooldown: 0.4
|
DashCooldown: 0.4
|
||||||
DashInvincibilityDuration: 0.2
|
DashInvincibilityDuration: 0.2
|
||||||
DashInvincibilityCooldown: 0.9
|
DashInvincibilityCooldown: 0.9
|
||||||
WallSlideSpeed: 2
|
DownDashSpeed: 22
|
||||||
|
DownDashDuration: 0.25
|
||||||
|
WallSlideSpeed: 3
|
||||||
WallHangSpeed: 1
|
WallHangSpeed: 1
|
||||||
WallRayLength: 0.37
|
WallRayLength: 0.37
|
||||||
WallRayOffsetY: 0.2
|
WallRayOffsetY: 0.2
|
||||||
|
|||||||
@@ -1,101 +1,5 @@
|
|||||||
%YAML 1.1
|
%YAML 1.1
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
%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
|
--- !u!1 &1932889250901504761
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -123,7 +27,7 @@ Transform:
|
|||||||
m_GameObject: {fileID: 1932889250901504761}
|
m_GameObject: {fileID: 1932889250901504761}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 0, y: -0.562, z: 0}
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
@@ -161,7 +65,7 @@ BoxCollider2D:
|
|||||||
m_IsTrigger: 1
|
m_IsTrigger: 1
|
||||||
m_UsedByEffector: 0
|
m_UsedByEffector: 0
|
||||||
m_UsedByComposite: 0
|
m_UsedByComposite: 0
|
||||||
m_Offset: {x: -0.027121663, y: -0.15051937}
|
m_Offset: {x: 0, y: 0}
|
||||||
m_SpriteTilingProperty:
|
m_SpriteTilingProperty:
|
||||||
border: {x: 0, y: 0, z: 0, w: 0}
|
border: {x: 0, y: 0, z: 0, w: 0}
|
||||||
pivot: {x: 0, y: 0}
|
pivot: {x: 0, y: 0}
|
||||||
@@ -172,7 +76,7 @@ BoxCollider2D:
|
|||||||
adaptiveTiling: 0
|
adaptiveTiling: 0
|
||||||
m_AutoTiling: 0
|
m_AutoTiling: 0
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Size: {x: 1.189852, y: 0.80103874}
|
m_Size: {x: 1, y: 0.5}
|
||||||
m_EdgeRadius: 0
|
m_EdgeRadius: 0
|
||||||
--- !u!114 &6478051166999031478
|
--- !u!114 &6478051166999031478
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
@@ -188,11 +92,11 @@ MonoBehaviour:
|
|||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
_defaultSource: {fileID: 0}
|
_defaultSource: {fileID: 0}
|
||||||
_hitCooldown: 0.1
|
_hitCooldown: 0.1
|
||||||
_id: ATK_Down
|
_id:
|
||||||
_rivalHitBoxMask:
|
_rivalHitBoxMask:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Bits: 134217792
|
m_Bits: 0
|
||||||
--- !u!1 &2584603199706918030
|
--- !u!1 &3989564331693126876
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
@@ -200,38 +104,38 @@ GameObject:
|
|||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
serializedVersion: 6
|
serializedVersion: 6
|
||||||
m_Component:
|
m_Component:
|
||||||
- component: {fileID: 1660186156348129284}
|
- component: {fileID: 8294071144630811572}
|
||||||
- component: {fileID: 1152578598430080845}
|
- component: {fileID: 4949779957213724475}
|
||||||
- component: {fileID: 3007294148525084107}
|
- component: {fileID: 4757677899241504248}
|
||||||
m_Layer: 14
|
m_Layer: 14
|
||||||
m_Name: HitBox_Ground _2
|
m_Name: HitBox_Ground
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
m_NavMeshLayer: 0
|
m_NavMeshLayer: 0
|
||||||
m_StaticEditorFlags: 0
|
m_StaticEditorFlags: 0
|
||||||
m_IsActive: 1
|
m_IsActive: 1
|
||||||
--- !u!4 &1660186156348129284
|
--- !u!4 &8294071144630811572
|
||||||
Transform:
|
Transform:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 2584603199706918030}
|
m_GameObject: {fileID: 3989564331693126876}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 0.798, y: 0, z: 0}
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 8975424752584779179}
|
m_Father: {fileID: 8975424752584779179}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!61 &1152578598430080845
|
--- !u!61 &4949779957213724475
|
||||||
BoxCollider2D:
|
BoxCollider2D:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 2584603199706918030}
|
m_GameObject: {fileID: 3989564331693126876}
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
m_Density: 1
|
m_Density: 1
|
||||||
m_Material: {fileID: 0}
|
m_Material: {fileID: 0}
|
||||||
@@ -257,7 +161,7 @@ BoxCollider2D:
|
|||||||
m_IsTrigger: 1
|
m_IsTrigger: 1
|
||||||
m_UsedByEffector: 0
|
m_UsedByEffector: 0
|
||||||
m_UsedByComposite: 0
|
m_UsedByComposite: 0
|
||||||
m_Offset: {x: -0.117884755, y: 0.01309824}
|
m_Offset: {x: 0, y: 0}
|
||||||
m_SpriteTilingProperty:
|
m_SpriteTilingProperty:
|
||||||
border: {x: 0, y: 0, z: 0, w: 0}
|
border: {x: 0, y: 0, z: 0, w: 0}
|
||||||
pivot: {x: 0, y: 0}
|
pivot: {x: 0, y: 0}
|
||||||
@@ -268,15 +172,15 @@ BoxCollider2D:
|
|||||||
adaptiveTiling: 0
|
adaptiveTiling: 0
|
||||||
m_AutoTiling: 0
|
m_AutoTiling: 0
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Size: {x: 0.7642305, y: 1.1956644}
|
m_Size: {x: 1, y: 0.5}
|
||||||
m_EdgeRadius: 0
|
m_EdgeRadius: 0
|
||||||
--- !u!114 &3007294148525084107
|
--- !u!114 &4757677899241504248
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 2584603199706918030}
|
m_GameObject: {fileID: 3989564331693126876}
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
m_EditorHideFlags: 0
|
m_EditorHideFlags: 0
|
||||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||||
@@ -284,106 +188,10 @@ MonoBehaviour:
|
|||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
_defaultSource: {fileID: 0}
|
_defaultSource: {fileID: 0}
|
||||||
_hitCooldown: 0.1
|
_hitCooldown: 0.1
|
||||||
_id: ATK_Ground_2
|
_id:
|
||||||
_rivalHitBoxMask:
|
_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
|
serializedVersion: 2
|
||||||
m_Bits: 0
|
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
|
--- !u!1 &4335406389674002762
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -411,7 +219,7 @@ Transform:
|
|||||||
m_GameObject: {fileID: 4335406389674002762}
|
m_GameObject: {fileID: 4335406389674002762}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 0, y: 0.918, z: 0}
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
@@ -449,7 +257,7 @@ BoxCollider2D:
|
|||||||
m_IsTrigger: 1
|
m_IsTrigger: 1
|
||||||
m_UsedByEffector: 0
|
m_UsedByEffector: 0
|
||||||
m_UsedByComposite: 0
|
m_UsedByComposite: 0
|
||||||
m_Offset: {x: 0.072324514, y: 0}
|
m_Offset: {x: 0, y: 0}
|
||||||
m_SpriteTilingProperty:
|
m_SpriteTilingProperty:
|
||||||
border: {x: 0, y: 0, z: 0, w: 0}
|
border: {x: 0, y: 0, z: 0, w: 0}
|
||||||
pivot: {x: 0, y: 0}
|
pivot: {x: 0, y: 0}
|
||||||
@@ -460,7 +268,7 @@ BoxCollider2D:
|
|||||||
adaptiveTiling: 0
|
adaptiveTiling: 0
|
||||||
m_AutoTiling: 0
|
m_AutoTiling: 0
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Size: {x: 1.1599612, y: 1}
|
m_Size: {x: 0.5, y: 1}
|
||||||
m_EdgeRadius: 0
|
m_EdgeRadius: 0
|
||||||
--- !u!114 &1392799324577637263
|
--- !u!114 &1392799324577637263
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
@@ -476,10 +284,10 @@ MonoBehaviour:
|
|||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
_defaultSource: {fileID: 0}
|
_defaultSource: {fileID: 0}
|
||||||
_hitCooldown: 0.1
|
_hitCooldown: 0.1
|
||||||
_id: ATK_Up
|
_id:
|
||||||
_rivalHitBoxMask:
|
_rivalHitBoxMask:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Bits: 134217792
|
m_Bits: 0
|
||||||
--- !u!1 &4821376343125962025
|
--- !u!1 &4821376343125962025
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -510,12 +318,10 @@ Transform:
|
|||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 7119158475861943178}
|
- {fileID: 8294071144630811572}
|
||||||
- {fileID: 1660186156348129284}
|
|
||||||
- {fileID: 7468586589501741901}
|
- {fileID: 7468586589501741901}
|
||||||
- {fileID: 6088225995420515986}
|
- {fileID: 6088225995420515986}
|
||||||
- {fileID: 4362395311111627733}
|
- {fileID: 6913225169405126738}
|
||||||
- {fileID: 4405470499151834857}
|
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!114 &3691925044832415471
|
--- !u!114 &3691925044832415471
|
||||||
@@ -530,11 +336,11 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: ec12dacf2519f58429dd3c59da8f93b0, type: 3}
|
m_Script: {fileID: 11500000, guid: ec12dacf2519f58429dd3c59da8f93b0, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
_hitBoxGround: {fileID: 4639356286286040131}
|
_hitBoxGround: {fileID: 4757677899241504248}
|
||||||
_hitBoxUp: {fileID: 1392799324577637263}
|
_hitBoxUp: {fileID: 1392799324577637263}
|
||||||
_hitBoxDown: {fileID: 6478051166999031478}
|
_hitBoxDown: {fileID: 6478051166999031478}
|
||||||
_hitBoxAir: {fileID: 9014207169512774676}
|
_hitBoxAir: {fileID: 1382006829078153708}
|
||||||
--- !u!1 &8582289489283119946
|
--- !u!1 &6434981771063321190
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
@@ -542,38 +348,38 @@ GameObject:
|
|||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
serializedVersion: 6
|
serializedVersion: 6
|
||||||
m_Component:
|
m_Component:
|
||||||
- component: {fileID: 4362395311111627733}
|
- component: {fileID: 6913225169405126738}
|
||||||
- component: {fileID: 922051492914393482}
|
- component: {fileID: 6843760498109474434}
|
||||||
- component: {fileID: 9014207169512774676}
|
- component: {fileID: 1382006829078153708}
|
||||||
m_Layer: 14
|
m_Layer: 14
|
||||||
m_Name: HitBox_Air_1
|
m_Name: HitBox_Air
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
m_NavMeshLayer: 0
|
m_NavMeshLayer: 0
|
||||||
m_StaticEditorFlags: 0
|
m_StaticEditorFlags: 0
|
||||||
m_IsActive: 1
|
m_IsActive: 1
|
||||||
--- !u!4 &4362395311111627733
|
--- !u!4 &6913225169405126738
|
||||||
Transform:
|
Transform:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 8582289489283119946}
|
m_GameObject: {fileID: 6434981771063321190}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 0.553, y: 0, z: 0}
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 8975424752584779179}
|
m_Father: {fileID: 8975424752584779179}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!61 &922051492914393482
|
--- !u!61 &6843760498109474434
|
||||||
BoxCollider2D:
|
BoxCollider2D:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 8582289489283119946}
|
m_GameObject: {fileID: 6434981771063321190}
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
m_Density: 1
|
m_Density: 1
|
||||||
m_Material: {fileID: 0}
|
m_Material: {fileID: 0}
|
||||||
@@ -599,7 +405,7 @@ BoxCollider2D:
|
|||||||
m_IsTrigger: 1
|
m_IsTrigger: 1
|
||||||
m_UsedByEffector: 0
|
m_UsedByEffector: 0
|
||||||
m_UsedByComposite: 0
|
m_UsedByComposite: 0
|
||||||
m_Offset: {x: 0.46717286, y: 0}
|
m_Offset: {x: 0, y: 0}
|
||||||
m_SpriteTilingProperty:
|
m_SpriteTilingProperty:
|
||||||
border: {x: 0, y: 0, z: 0, w: 0}
|
border: {x: 0, y: 0, z: 0, w: 0}
|
||||||
pivot: {x: 0, y: 0}
|
pivot: {x: 0, y: 0}
|
||||||
@@ -610,15 +416,15 @@ BoxCollider2D:
|
|||||||
adaptiveTiling: 0
|
adaptiveTiling: 0
|
||||||
m_AutoTiling: 0
|
m_AutoTiling: 0
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Size: {x: 1.4343457, y: 1}
|
m_Size: {x: 0.5, y: 1}
|
||||||
m_EdgeRadius: 0
|
m_EdgeRadius: 0
|
||||||
--- !u!114 &9014207169512774676
|
--- !u!114 &1382006829078153708
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 8582289489283119946}
|
m_GameObject: {fileID: 6434981771063321190}
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
m_EditorHideFlags: 0
|
m_EditorHideFlags: 0
|
||||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||||
@@ -626,7 +432,7 @@ MonoBehaviour:
|
|||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
_defaultSource: {fileID: 0}
|
_defaultSource: {fileID: 0}
|
||||||
_hitCooldown: 0.1
|
_hitCooldown: 0.1
|
||||||
_id: ATK_Air_1
|
_id:
|
||||||
_rivalHitBoxMask:
|
_rivalHitBoxMask:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Bits: 134217792
|
m_Bits: 0
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ namespace BaseGames.Camera
|
|||||||
[SerializeField] private CinemachineBrain _brain;
|
[SerializeField] private CinemachineBrain _brain;
|
||||||
|
|
||||||
private UnityEngine.Camera _camera;
|
private UnityEngine.Camera _camera;
|
||||||
|
private CinemachineCamera _cachedVCam;
|
||||||
|
private CinemachineConfiner3D _cachedConfiner;
|
||||||
|
|
||||||
private void Reset()
|
private void Reset()
|
||||||
{
|
{
|
||||||
@@ -77,7 +79,8 @@ namespace BaseGames.Camera
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前活跃 VCam 的 CinemachineConfiner3D 边界盒(世界空间 AABB)。
|
/// 获取当前活跃 VCam 的 CinemachineConfiner3D 边界盒(世界空间 AABB)。
|
||||||
/// 用于在像素取整后将相机钳制回限位区域内。
|
/// 缓存上次查询的 VCam 实例;仅在活跃 VCam 发生切换时重新调用 GetComponent,
|
||||||
|
/// 避免每帧 GetComponent 开销。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool TryGetActiveConfinerBounds(out Bounds bounds)
|
private bool TryGetActiveConfinerBounds(out Bounds bounds)
|
||||||
{
|
{
|
||||||
@@ -85,9 +88,14 @@ namespace BaseGames.Camera
|
|||||||
if (_brain == null) return false;
|
if (_brain == null) return false;
|
||||||
var vcam = _brain.ActiveVirtualCamera as CinemachineCamera;
|
var vcam = _brain.ActiveVirtualCamera as CinemachineCamera;
|
||||||
if (vcam == null) return false;
|
if (vcam == null) return false;
|
||||||
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
|
// 只在活跃 VCam 切换时刷新缓存
|
||||||
if (confiner == null || !confiner.IsValid) return false;
|
if (!ReferenceEquals(vcam, _cachedVCam))
|
||||||
bounds = confiner.BoundingVolume.bounds;
|
{
|
||||||
|
_cachedVCam = vcam;
|
||||||
|
_cachedConfiner = vcam.GetComponent<CinemachineConfiner3D>();
|
||||||
|
}
|
||||||
|
if (_cachedConfiner == null || !_cachedConfiner.IsValid) return false;
|
||||||
|
bounds = _cachedConfiner.BoundingVolume.bounds;
|
||||||
return true;
|
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));
|
_activeZones.Add((area, priority));
|
||||||
|
|
||||||
// 仅当此区域是当前最优且尚未激活时才切换
|
// 仅当此区域是当前最优且尚未激活时才切换
|
||||||
@@ -132,7 +133,9 @@ namespace BaseGames.Camera
|
|||||||
if (releasedArea == null) return;
|
if (releasedArea == null) return;
|
||||||
|
|
||||||
bool wasActive = releasedArea == _currentArea;
|
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;
|
if (removed == 0 && !wasActive) return;
|
||||||
|
|
||||||
|
|||||||
@@ -57,9 +57,14 @@ namespace BaseGames.Camera
|
|||||||
// ── 静态:跨实例共享触发状态 ──────────────────────────────────────────
|
// ── 静态:跨实例共享触发状态 ──────────────────────────────────────────
|
||||||
// 玩家当前物理上所在的所有触发区域(按进入顺序排列)
|
// 玩家当前物理上所在的所有触发区域(按进入顺序排列)
|
||||||
private static readonly List<CameraTriggerZone> s_InsideZones = new();
|
private static readonly List<CameraTriggerZone> s_InsideZones = new();
|
||||||
|
// 场景内所有已启用的触发区域,供 RoomController 等查询(替代 FindObjectsOfType)
|
||||||
|
private static readonly List<CameraTriggerZone> s_AllZones = new();
|
||||||
// 当前已向 ICameraService 发出 SwitchArea 请求的触发区域
|
// 当前已向 ICameraService 发出 SwitchArea 请求的触发区域
|
||||||
private static CameraTriggerZone s_ActiveZone;
|
private static CameraTriggerZone s_ActiveZone;
|
||||||
|
|
||||||
|
/// <summary>场景内当前所有已启用的触发区域(只读)。</summary>
|
||||||
|
public static IReadOnlyList<CameraTriggerZone> AllZones => s_AllZones;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 在每次进入 Play Mode 前(或禁用 Domain Reload 时的跨会话)重置静态状态,
|
/// 在每次进入 Play Mode 前(或禁用 Domain Reload 时的跨会话)重置静态状态,
|
||||||
/// 防止上一次游戏会话残留的区域引用导致触发逻辑错误。
|
/// 防止上一次游戏会话残留的区域引用导致触发逻辑错误。
|
||||||
@@ -68,6 +73,7 @@ namespace BaseGames.Camera
|
|||||||
private static void ResetStaticState()
|
private static void ResetStaticState()
|
||||||
{
|
{
|
||||||
s_InsideZones.Clear();
|
s_InsideZones.Clear();
|
||||||
|
s_AllZones.Clear();
|
||||||
s_ActiveZone = null;
|
s_ActiveZone = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,12 +83,22 @@ namespace BaseGames.Camera
|
|||||||
_collider.isTrigger = true;
|
_collider.isTrigger = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
if (Application.isPlaying)
|
||||||
|
s_AllZones.Add(this);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDisable()
|
private void OnDisable()
|
||||||
{
|
{
|
||||||
if (!Application.isPlaying) return;
|
if (!Application.isPlaying) return;
|
||||||
|
s_AllZones.Remove(this);
|
||||||
HandlePlayerExit();
|
HandlePlayerExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>判断世界坐标点是否在本触发区域多边形内(供 RoomController 等无需 GetComponent 直接查询)。</summary>
|
||||||
|
public bool ContainsPoint(Vector2 worldPoint) => _collider != null && _collider.OverlapPoint(worldPoint);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 若玩家出生时已在触发区域内,OnTriggerEnter2D 不会触发。
|
/// 若玩家出生时已在触发区域内,OnTriggerEnter2D 不会触发。
|
||||||
/// 延迟一帧(确保 RoomController.Start 先完成基准区域设置)后主动检测。
|
/// 延迟一帧(确保 RoomController.Start 先完成基准区域设置)后主动检测。
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ namespace BaseGames.Combat
|
|||||||
bool isRivalHitBoxLayer = (_rivalHitBoxMask.value & (1 << otherLayer)) != 0;
|
bool isRivalHitBoxLayer = (_rivalHitBoxMask.value & (1 << otherLayer)) != 0;
|
||||||
if (isRivalHitBoxLayer && CanClash)
|
if (isRivalHitBoxLayer && CanClash)
|
||||||
{
|
{
|
||||||
var rivalHitBox = other.GetComponent<HitBox>();
|
if (other.TryGetComponent<HitBox>(out var rivalHitBox) &&
|
||||||
if (rivalHitBox != null && rivalHitBox.IsActive && rivalHitBox.CanClash)
|
rivalHitBox.IsActive && rivalHitBox.CanClash)
|
||||||
{
|
{
|
||||||
_clashService?.ResolveClash(this, rivalHitBox);
|
_clashService?.ResolveClash(this, rivalHitBox);
|
||||||
return; // 拼刀,中止伤害流水线
|
return; // 拼刀,中止伤害流水线
|
||||||
@@ -151,8 +151,7 @@ namespace BaseGames.Combat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ② 命中 HurtBox
|
// ② 命中 HurtBox
|
||||||
var hurtBox = other.GetComponent<HurtBox>();
|
if (other.TryGetComponent<HurtBox>(out var hurtBox))
|
||||||
if (hurtBox != null)
|
|
||||||
{
|
{
|
||||||
// 用 HitBox 自身碰撞盒中心在 HurtBox 表面上的最近点作为受击位置。
|
// 用 HitBox 自身碰撞盒中心在 HurtBox 表面上的最近点作为受击位置。
|
||||||
// 对大体积/长条形受击体(如地刺),此点远比 HurtBox 节点中心更准确。
|
// 对大体积/长条形受击体(如地刺),此点远比 HurtBox 节点中心更准确。
|
||||||
@@ -163,7 +162,8 @@ namespace BaseGames.Combat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ③ 命中 IBreakable(机关/障碍物)
|
// ③ 命中 IBreakable(机关/障碍物)
|
||||||
other.GetComponent<IBreakable>()?.TryInteract(info);
|
if (other.TryGetComponent<IBreakable>(out var breakable))
|
||||||
|
breakable.TryInteract(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 当前激活期已命中目标集合(防止复合子 Collider 导致同帧多次命中)────────────
|
// ── 当前激活期已命中目标集合(防止复合子 Collider 导致同帧多次命中)────────────
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ namespace BaseGames.Combat
|
|||||||
protected Rigidbody2D _rb;
|
protected Rigidbody2D _rb;
|
||||||
protected HitBox _hitBox;
|
protected HitBox _hitBox;
|
||||||
protected float _aliveTimer;
|
protected float _aliveTimer;
|
||||||
|
// Lifetime 在 Initialize 时缓存,避免 Update 每帧访问 SO 成员并做 null check
|
||||||
|
private float _lifetime = float.MaxValue;
|
||||||
|
|
||||||
private PooledObject _pooledObject;
|
private PooledObject _pooledObject;
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ namespace BaseGames.Combat
|
|||||||
public virtual void Initialize(ProjectileConfigSO config, DamageInfo damageInfo, Vector2 direction, int ownerLayer = 0)
|
public virtual void Initialize(ProjectileConfigSO config, DamageInfo damageInfo, Vector2 direction, int ownerLayer = 0)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_lifetime = config.Lifetime;
|
||||||
DamageInfo = damageInfo;
|
DamageInfo = damageInfo;
|
||||||
Direction = direction.normalized;
|
Direction = direction.normalized;
|
||||||
_aliveTimer = 0f;
|
_aliveTimer = 0f;
|
||||||
@@ -75,7 +78,7 @@ namespace BaseGames.Combat
|
|||||||
protected virtual void Update()
|
protected virtual void Update()
|
||||||
{
|
{
|
||||||
_aliveTimer += Time.deltaTime;
|
_aliveTimer += Time.deltaTime;
|
||||||
if (_config != null && _aliveTimer >= _config.Lifetime)
|
if (_aliveTimer >= _lifetime)
|
||||||
ReturnToPool();
|
ReturnToPool();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +94,7 @@ namespace BaseGames.Combat
|
|||||||
protected virtual void OnDisable()
|
protected virtual void OnDisable()
|
||||||
{
|
{
|
||||||
_aliveTimer = 0f;
|
_aliveTimer = 0f;
|
||||||
|
_lifetime = float.MaxValue; // 归还对象池后重置,防止未初始化时自毁
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ namespace BaseGames.Combat
|
|||||||
|
|
||||||
public event System.Action<DamageInfo> OnHitConfirmed;
|
public event System.Action<DamageInfo> OnHitConfirmed;
|
||||||
|
|
||||||
|
private Coroutine _returnCoroutine;
|
||||||
|
// 按 duration 缓存 WaitForSeconds,同一技能复用无 GC 分配
|
||||||
|
private WaitForSeconds _cachedWait;
|
||||||
|
private float _cachedWaitDuration = float.NaN;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
foreach (var hb in _hitBoxes)
|
foreach (var hb in _hitBoxes)
|
||||||
@@ -35,7 +40,31 @@ namespace BaseGames.Combat
|
|||||||
hb?.Activate(source, attacker);
|
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)
|
public void AutoDestroyAfter(float duration)
|
||||||
=> Destroy(gameObject, Mathf.Max(0f, duration));
|
=> Destroy(gameObject, Mathf.Max(0f, duration));
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ namespace BaseGames.Combat.StatusEffects
|
|||||||
|
|
||||||
public override StatusEffectType EffectType => StatusEffectType.Fire;
|
public override StatusEffectType EffectType => StatusEffectType.Fire;
|
||||||
public override int MaxStacks => 1;
|
public override int MaxStacks => 1;
|
||||||
|
private static readonly StatusEffectType[] s_MutualExclusions = { StatusEffectType.Freeze };
|
||||||
/// <summary>施加燃烧时移除冻结(火冰互斥)。</summary>
|
/// <summary>施加燃烧时移除冻结(火冰互斥)。</summary>
|
||||||
public override StatusEffectType[] MutualExclusions => new[] { StatusEffectType.Freeze };
|
public override StatusEffectType[] MutualExclusions => s_MutualExclusions;
|
||||||
|
|
||||||
public FireEffect()
|
public FireEffect()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ namespace BaseGames.Combat.StatusEffects
|
|||||||
// ── Shader 渲染(MaterialPropertyBlock,不修改共享材质)─────────
|
// ── Shader 渲染(MaterialPropertyBlock,不修改共享材质)─────────
|
||||||
private SpriteRenderer _renderer;
|
private SpriteRenderer _renderer;
|
||||||
private MaterialPropertyBlock _propBlock;
|
private MaterialPropertyBlock _propBlock;
|
||||||
|
// 缓存 Shader 属性 ID,避免每次调用 SetShaderParam 都做字符串哈希查找
|
||||||
|
private readonly Dictionary<string, int> _shaderPropIds = new();
|
||||||
|
|
||||||
// ── DoT 伤害代理(由 StatusEffect.OnTick 通过 Owner 调用)──────────
|
// ── DoT 伤害代理(由 StatusEffect.OnTick 通过 Owner 调用)──────────
|
||||||
private IDamageable _damageable;
|
private IDamageable _damageable;
|
||||||
@@ -135,8 +137,13 @@ namespace BaseGames.Combat.StatusEffects
|
|||||||
public void SetShaderParam(string param, float value)
|
public void SetShaderParam(string param, float value)
|
||||||
{
|
{
|
||||||
if (_renderer == null) return;
|
if (_renderer == null) return;
|
||||||
|
if (!_shaderPropIds.TryGetValue(param, out int propId))
|
||||||
|
{
|
||||||
|
propId = Shader.PropertyToID(param);
|
||||||
|
_shaderPropIds[param] = propId;
|
||||||
|
}
|
||||||
_renderer.GetPropertyBlock(_propBlock);
|
_renderer.GetPropertyBlock(_propBlock);
|
||||||
_propBlock.SetFloat(param, value);
|
_propBlock.SetFloat(propId, value);
|
||||||
_renderer.SetPropertyBlock(_propBlock);
|
_renderer.SetPropertyBlock(_propBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ namespace BaseGames.Core
|
|||||||
/// <item><b>Room</b>:极短淡出(<see cref="_roomFadeDuration"/>),无加载画面。</item>
|
/// <item><b>Room</b>:极短淡出(<see cref="_roomFadeDuration"/>),无加载画面。</item>
|
||||||
/// <item><b>Scene</b>:完整淡出(<see cref="_sceneFadeDuration"/>),显示加载画面。</item>
|
/// <item><b>Scene</b>:完整淡出(<see cref="_sceneFadeDuration"/>),显示加载画面。</item>
|
||||||
/// </list>
|
/// </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>
|
/// </summary>
|
||||||
[DefaultExecutionOrder(-900)]
|
[DefaultExecutionOrder(-900)]
|
||||||
public class SceneService : MonoBehaviour, ISceneService
|
public class SceneService : MonoBehaviour, ISceneService
|
||||||
@@ -35,6 +45,12 @@ namespace BaseGames.Core
|
|||||||
[SerializeField] private VoidEventChannelSO _onFadeInRequest;
|
[SerializeField] private VoidEventChannelSO _onFadeInRequest;
|
||||||
[SerializeField] private VoidEventChannelSO _onFadeOutRequest;
|
[SerializeField] private VoidEventChannelSO _onFadeOutRequest;
|
||||||
|
|
||||||
|
[Tooltip("场景加载完成、WorldStateRegistry 已就绪后触发。\n" +
|
||||||
|
"场景内物体应订阅此事件,从 WorldStateRegistry 读取存档状态并应用(替代在 Start() 中读取)。\n" +
|
||||||
|
"触发后会等待一帧,确保所有处理器执行完毕,再执行淡入显示场景。\n" +
|
||||||
|
"对应 SO:EVT_SceneWorldStateRestored")]
|
||||||
|
[SerializeField] private VoidEventChannelSO _onSceneWorldStateRestored;
|
||||||
|
|
||||||
[SerializeField] private SceneLoader _sceneLoader;
|
[SerializeField] private SceneLoader _sceneLoader;
|
||||||
|
|
||||||
[Header("淡出时长")]
|
[Header("淡出时长")]
|
||||||
@@ -71,6 +87,14 @@ namespace BaseGames.Core
|
|||||||
else
|
else
|
||||||
Debug.LogError("[SceneService] _sceneLoader 未赋值,场景加载中断。请在 Inspector 中绑定 SceneLoader 组件。");
|
Debug.LogError("[SceneService] _sceneLoader 未赋值,场景加载中断。请在 Inspector 中绑定 SceneLoader 组件。");
|
||||||
|
|
||||||
|
// 通知:WorldStateRegistry 已就绪,场景物体应在此帧内从中读取存档状态并应用初始状态。
|
||||||
|
// 订阅者(WorldStateRegistrySaver、各场景 StateApplier 等)会在同一帧同步执行。
|
||||||
|
_onSceneWorldStateRestored?.Raise();
|
||||||
|
|
||||||
|
// 等待一帧:确保所有场景物体的 Start() 和事件处理器都已执行完毕,
|
||||||
|
// 场景物体处于正确的初始状态后再揭开黑幕,避免出现一帧状态错误的画面闪烁。
|
||||||
|
yield return null;
|
||||||
|
|
||||||
_onFadeInRequest?.Raise();
|
_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;
|
// 此文件已被 AddressableManagerWindow 取代。
|
||||||
using System.Collections.Generic;
|
// 原有功能已整合到统一工具中,请使用:
|
||||||
using System.IO;
|
// BaseGames → Addressables → Addressables Manager(总入口)
|
||||||
using System.Linq;
|
// BaseGames → Addressables → Rule Sync(直达规则校验 Tab)
|
||||||
using System.Text;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.AddressableAssets;
|
|
||||||
using UnityEditor.AddressableAssets.Settings;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Editor
|
namespace BaseGames.Editor
|
||||||
{
|
{
|
||||||
/// <summary>
|
// 保留空类以避免 .meta 文件孤立。
|
||||||
/// Addressable 规则同步窗口。
|
internal static class AddressableRuleSyncWindowStub { }
|
||||||
///
|
|
||||||
/// 功能:
|
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ namespace BaseGames.Enemies.AI
|
|||||||
|
|
||||||
if (distance > 0.01f)
|
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),则视线畅通
|
// 若无遮挡物(hit.collider == null),则视线畅通
|
||||||
hasLOS = hit.collider == null;
|
hasLOS = hit.collider == null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,18 @@ namespace BaseGames.Enemies
|
|||||||
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。</summary>
|
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。</summary>
|
||||||
public void MoveHorizontal(float dir)
|
public void MoveHorizontal(float dir)
|
||||||
{
|
{
|
||||||
float speed = _config.WalkSpeed;
|
var vel = _rb.velocity;
|
||||||
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
|
vel.x = dir * _config.WalkSpeed;
|
||||||
|
_rb.velocity = vel;
|
||||||
UpdateFacing(dir);
|
UpdateFacing(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>显式指定速度(BD 追击任务调用)。</summary>
|
/// <summary>显式指定速度(BD 追击任务调用)。</summary>
|
||||||
public void MoveWithSpeed(float dir, float speed)
|
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);
|
UpdateFacing(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +55,9 @@ namespace BaseGames.Enemies
|
|||||||
|
|
||||||
public void StopHorizontal()
|
public void StopHorizontal()
|
||||||
{
|
{
|
||||||
_rb.velocity = new Vector2(0f, _rb.velocity.y);
|
var vel = _rb.velocity;
|
||||||
|
vel.x = 0f;
|
||||||
|
_rb.velocity = vel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ namespace BaseGames.Enemies
|
|||||||
private readonly HashSet<EnemyBase> _registeredSet = new();
|
private readonly HashSet<EnemyBase> _registeredSet = new();
|
||||||
private readonly List<EnemyBase> _registered = new();
|
private readonly List<EnemyBase> _registered = new();
|
||||||
private readonly Dictionary<EnemyBase, int> _indexMap = 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
|
// 缓存玩家 Transform
|
||||||
private Transform _playerTransform;
|
private Transform _playerTransform;
|
||||||
private readonly CompositeDisposable _subs = new();
|
private readonly CompositeDisposable _subs = new();
|
||||||
@@ -72,22 +74,35 @@ namespace BaseGames.Enemies
|
|||||||
// ── 内部 ──────────────────────────────────────────────────────────
|
// ── 内部 ──────────────────────────────────────────────────────────
|
||||||
private void Rebalance()
|
private void Rebalance()
|
||||||
{
|
{
|
||||||
if (_registered.Count == 0) return;
|
int n = _registered.Count;
|
||||||
|
if (n == 0) return;
|
||||||
|
|
||||||
var playerPos = _playerTransform != null ? _playerTransform.position : Vector3.zero;
|
var playerPos = _playerTransform != null ? _playerTransform.position : Vector3.zero;
|
||||||
|
|
||||||
// 按距离平方升序排序(避免开方,性能更好)
|
// ① 预计算距离(O(n) Vector3 运算,而非在比较器内重复执行 O(n logn) 次)
|
||||||
_registered.Sort((a, b) =>
|
_sortTemp.Clear();
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
{
|
{
|
||||||
if (a == null) return 1;
|
var e = _registered[i];
|
||||||
if (b == null) return -1;
|
float sqd = e != null
|
||||||
float sqA = (a.transform.position - playerPos).sqrMagnitude;
|
? (e.transform.position - playerPos).sqrMagnitude
|
||||||
float sqB = (b.transform.position - playerPos).sqrMagnitude;
|
: float.MaxValue;
|
||||||
return sqA.CompareTo(sqB);
|
_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
|
#if GRAPH_DESIGNER
|
||||||
for (int i = _registered.Count - 1; i >= 0; i--)
|
for (int i = n - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
var enemy = _registered[i];
|
var enemy = _registered[i];
|
||||||
if (enemy == null) { _registered.RemoveAt(i); continue; }
|
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 ParryPhase _phase = ParryPhase.Inactive;
|
||||||
private float _phaseTimer;
|
private float _phaseTimer;
|
||||||
private float _cooldownTimer;
|
private float _cooldownTimer;
|
||||||
|
// 缓存 WaitForSecondsRealtime,避免每次完美弹反都触发 GC 分配
|
||||||
|
private WaitForSecondsRealtime _bulletTimeWait;
|
||||||
|
|
||||||
public ParryPhase CurrentPhase => _phase;
|
public ParryPhase CurrentPhase => _phase;
|
||||||
/// <summary>是否处于弹反有效窗口(供外部检测)。</summary>
|
/// <summary>是否处于弹反有效窗口(供外部检测)。</summary>
|
||||||
@@ -74,6 +76,13 @@ namespace BaseGames.Parry
|
|||||||
Debug.Assert(_config != null, "[ParrySystem] _config 未赋值,请在 Inspector 中指定 ParryConfigSO。", this);
|
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>
|
/// <summary>由 PlayerController 在 Awake 中注入 InputReader,无需在 Inspector 单独指定。</summary>
|
||||||
public void SetInputReader(InputReaderSO reader)
|
public void SetInputReader(InputReaderSO reader)
|
||||||
{
|
{
|
||||||
@@ -206,7 +215,7 @@ namespace BaseGames.Parry
|
|||||||
private IEnumerator ApplyBulletTime()
|
private IEnumerator ApplyBulletTime()
|
||||||
{
|
{
|
||||||
Time.timeScale = _config.BulletTimeScale;
|
Time.timeScale = _config.BulletTimeScale;
|
||||||
yield return new WaitForSecondsRealtime(_config.BulletTimeDuration);
|
yield return _bulletTimeWait;
|
||||||
Time.timeScale = 1f;
|
Time.timeScale = 1f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using BaseGames.Combat;
|
using BaseGames.Combat;
|
||||||
|
using BaseGames.Feedback;
|
||||||
|
|
||||||
namespace BaseGames.Player
|
namespace BaseGames.Player
|
||||||
{
|
{
|
||||||
@@ -14,6 +15,7 @@ namespace BaseGames.Player
|
|||||||
private PlayerStats _stats;
|
private PlayerStats _stats;
|
||||||
private PlayerMovement _movement;
|
private PlayerMovement _movement;
|
||||||
private WeaponHitBoxInstance _currentHitBoxInstance;
|
private WeaponHitBoxInstance _currentHitBoxInstance;
|
||||||
|
private IFeedbackPlayer _feedback;
|
||||||
|
|
||||||
/// <summary>下劈 HitBox 命中确认事件(供 DownAttackState 订阅 pogo 弹跳逻辑)。</summary>
|
/// <summary>下劈 HitBox 命中确认事件(供 DownAttackState 订阅 pogo 弹跳逻辑)。</summary>
|
||||||
public event System.Action<DamageInfo> OnDownHitConfirmed;
|
public event System.Action<DamageInfo> OnDownHitConfirmed;
|
||||||
@@ -22,6 +24,9 @@ namespace BaseGames.Player
|
|||||||
{
|
{
|
||||||
_stats = GetComponentInParent<PlayerStats>();
|
_stats = GetComponentInParent<PlayerStats>();
|
||||||
_movement = GetComponentInParent<PlayerMovement>();
|
_movement = GetComponentInParent<PlayerMovement>();
|
||||||
|
_feedback = GetComponentInParent<IFeedbackPlayer>()
|
||||||
|
?? GetComponentInChildren<IFeedbackPlayer>()
|
||||||
|
?? NullFeedbackPlayer.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
@@ -83,6 +88,12 @@ namespace BaseGames.Player
|
|||||||
int gain = _weaponManager?.ActiveWeapon?.soulPowerGain ?? 10;
|
int gain = _weaponManager?.ActiveWeapon?.soulPowerGain ?? 10;
|
||||||
_stats?.AddSoulPower(gain);
|
_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)
|
if (_movement?.Rb != null && info.KnockbackDirection.x != 0f)
|
||||||
_movement.Rb.AddForce(
|
_movement.Rb.AddForce(
|
||||||
|
|||||||
@@ -46,8 +46,12 @@ namespace BaseGames.Player
|
|||||||
private bool _facingLocked; // 为 true 时 UpdateFacing() 不覆盖朝向
|
private bool _facingLocked; // 为 true 时 UpdateFacing() 不覆盖朝向
|
||||||
private bool _cancelWindowOpen;
|
private bool _cancelWindowOpen;
|
||||||
private SurfaceType _currentSurface = SurfaceType.Ground;
|
private SurfaceType _currentSurface = SurfaceType.Ground;
|
||||||
|
private bool _wasGrounded;
|
||||||
|
// 跳跃/二段跳期间禁用斜坡吸附,防止把起跳判定成斜坡而立即下压
|
||||||
|
private bool _slopeSnapDisabled;
|
||||||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
||||||
private int _groundHitCount;
|
private int _groundHitCount;
|
||||||
|
private readonly ContactPoint2D[] _slopeContactBuffer = new ContactPoint2D[8];
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
// ── 运行时调试(Inspector 中可见)───────────────────────────────
|
// ── 运行时调试(Inspector 中可见)───────────────────────────────
|
||||||
@@ -118,6 +122,7 @@ namespace BaseGames.Player
|
|||||||
_wallCoyoteTimer = Mathf.Max(0f, _wallCoyoteTimer - Time.fixedDeltaTime);
|
_wallCoyoteTimer = Mathf.Max(0f, _wallCoyoteTimer - Time.fixedDeltaTime);
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
|
// 值类型字段每帧同步(无分配)
|
||||||
_dbg_VelocityX = _rb.velocity.x;
|
_dbg_VelocityX = _rb.velocity.x;
|
||||||
_dbg_VelocityY = _rb.velocity.y;
|
_dbg_VelocityY = _rb.velocity.y;
|
||||||
_dbg_IsGrounded = _isGrounded;
|
_dbg_IsGrounded = _isGrounded;
|
||||||
@@ -128,6 +133,8 @@ namespace BaseGames.Player
|
|||||||
_dbg_IsWallRight = _isWallRight;
|
_dbg_IsWallRight = _isWallRight;
|
||||||
_dbg_CancelWindowOpen = _cancelWindowOpen;
|
_dbg_CancelWindowOpen = _cancelWindowOpen;
|
||||||
_dbg_FacingDirection = _facingDirection;
|
_dbg_FacingDirection = _facingDirection;
|
||||||
|
// 字符串格式化限速到 ~10 Hz,避免每帧分配
|
||||||
|
if (Time.frameCount % 6 == 0)
|
||||||
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
|
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -175,6 +182,7 @@ namespace BaseGames.Player
|
|||||||
{
|
{
|
||||||
_rb.velocity = new Vector2(_rb.velocity.x, _config.JumpForce);
|
_rb.velocity = new Vector2(_rb.velocity.x, _config.JumpForce);
|
||||||
_coyoteTimer = 0f;
|
_coyoteTimer = 0f;
|
||||||
|
_slopeSnapDisabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CutJump()
|
public void CutJump()
|
||||||
@@ -191,6 +199,7 @@ namespace BaseGames.Player
|
|||||||
{
|
{
|
||||||
_rb.velocity = new Vector2(_rb.velocity.x, _config.DoubleJumpForce);
|
_rb.velocity = new Vector2(_rb.velocity.x, _config.DoubleJumpForce);
|
||||||
_coyoteTimer = 0f;
|
_coyoteTimer = 0f;
|
||||||
|
_slopeSnapDisabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 重力 ──────────────────────────────────────────────────────────────
|
// ── 重力 ──────────────────────────────────────────────────────────────
|
||||||
@@ -379,10 +388,38 @@ namespace BaseGames.Player
|
|||||||
{
|
{
|
||||||
if (_groundCheck == null) return;
|
if (_groundCheck == null) return;
|
||||||
|
|
||||||
|
_wasGrounded = _isGrounded;
|
||||||
|
|
||||||
_groundHitCount = Physics2D.OverlapBoxNonAlloc(
|
_groundHitCount = Physics2D.OverlapBoxNonAlloc(
|
||||||
_groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer);
|
_groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer);
|
||||||
_isGrounded = _groundHitCount > 0;
|
_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 组件的碰撞体)
|
// 检测是否站在单向平台(含 IDropThrough 组件的碰撞体)
|
||||||
_onOneWayPlatform = false;
|
_onOneWayPlatform = false;
|
||||||
for (int i = 0; i < _groundHitCount; i++)
|
for (int i = 0; i < _groundHitCount; i++)
|
||||||
|
|||||||
@@ -127,14 +127,19 @@ namespace BaseGames.Player
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
|
// 非字符串字段每帧同步(拷贝值,无分配)
|
||||||
|
_dbg_IsInvincible = IsInvincible;
|
||||||
|
_dbg_InvincibleTimer = _invincibleTimer;
|
||||||
|
_dbg_GodMode = _isGodMode;
|
||||||
|
// 字符串插值限速到 ~10 Hz,避免每帧分配(GC)
|
||||||
|
if (Time.frameCount % 6 == 0)
|
||||||
|
{
|
||||||
_dbg_HP = $"{CurrentHP} / {MaxHP}";
|
_dbg_HP = $"{CurrentHP} / {MaxHP}";
|
||||||
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
|
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
|
||||||
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
|
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
|
||||||
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
|
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
|
||||||
_dbg_IsInvincible = IsInvincible;
|
|
||||||
_dbg_InvincibleTimer = _invincibleTimer;
|
|
||||||
_dbg_GodMode = _isGodMode;
|
|
||||||
_dbg_Abilities = _unlockedAbilities == AbilityType.None ? "\u65e0" : _unlockedAbilities.ToString();
|
_dbg_Abilities = _unlockedAbilities == AbilityType.None ? "\u65e0" : _unlockedAbilities.ToString();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
// ── 护符修改器 API ─────────────────────────────────────────────────────
|
// ── 护符修改器 API ─────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ namespace BaseGames.Player
|
|||||||
// 物理接触点缓冲区(避免每帧 GC)
|
// 物理接触点缓冲区(避免每帧 GC)
|
||||||
private Rigidbody2D _rb;
|
private Rigidbody2D _rb;
|
||||||
private static readonly ContactPoint2D[] _contactBuffer = new ContactPoint2D[8];
|
private static readonly ContactPoint2D[] _contactBuffer = new ContactPoint2D[8];
|
||||||
|
// LayerMask 在 Awake 解析一次,避免 FixedUpdate(50Hz)每帧字符串查找
|
||||||
|
private LayerMask _resolvedWallMask;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 指定方向上是否存在墙壁接触(射线命中 OR 物理接触点命中,任一为 true)。
|
/// 指定方向上是否存在墙壁接触(射线命中 OR 物理接触点命中,任一为 true)。
|
||||||
@@ -43,6 +45,7 @@ namespace BaseGames.Player
|
|||||||
{
|
{
|
||||||
Debug.Assert(_config != null, "[PlayerWallDetector] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
Debug.Assert(_config != null, "[PlayerWallDetector] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||||||
_rb = GetComponent<Rigidbody2D>();
|
_rb = GetComponent<Rigidbody2D>();
|
||||||
|
_resolvedWallMask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Platform");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FixedUpdate()
|
private void FixedUpdate()
|
||||||
@@ -69,9 +72,8 @@ namespace BaseGames.Player
|
|||||||
private bool CheckPhysicalContact(int direction)
|
private bool CheckPhysicalContact(int direction)
|
||||||
{
|
{
|
||||||
if (_rb == null) return false;
|
if (_rb == null) return false;
|
||||||
LayerMask mask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Platform");
|
|
||||||
var filter = new ContactFilter2D();
|
var filter = new ContactFilter2D();
|
||||||
filter.SetLayerMask(mask);
|
filter.SetLayerMask(_resolvedWallMask);
|
||||||
filter.useTriggers = false;
|
filter.useTriggers = false;
|
||||||
|
|
||||||
int count = _rb.GetContacts(filter, _contactBuffer);
|
int count = _rb.GetContacts(filter, _contactBuffer);
|
||||||
@@ -94,7 +96,7 @@ namespace BaseGames.Player
|
|||||||
Vector2 center = transform.position;
|
Vector2 center = transform.position;
|
||||||
float len = _config.WallRayLength;
|
float len = _config.WallRayLength;
|
||||||
float oy = _config.WallRayOffsetY;
|
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 top = Physics2D.Raycast(center + Vector2.up * oy, dir, len, layer);
|
||||||
bool bot = Physics2D.Raycast(center + Vector2.down * 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.DashInvincible
|
||||||
: AnimCfg?.Dash;
|
: AnimCfg?.Dash;
|
||||||
if (dashClip != null) Anim?.Play(dashClip);
|
if (dashClip != null) Anim?.Play(dashClip);
|
||||||
|
|
||||||
|
Feedback.TriggerPreset("dash");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnStateUpdate()
|
public override void OnStateUpdate()
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ namespace BaseGames.Player.States
|
|||||||
if (Owner.HurtBox != null)
|
if (Owner.HurtBox != null)
|
||||||
Owner.HurtBox.SetActive(false);
|
Owner.HurtBox.SetActive(false);
|
||||||
|
|
||||||
|
Feedback.PlayDeath();
|
||||||
|
|
||||||
// 播放死亡动画
|
// 播放死亡动画
|
||||||
if (AnimCfg?.Dead != null)
|
if (AnimCfg?.Dead != null)
|
||||||
Anim?.Play(AnimCfg.Dead);
|
Anim?.Play(AnimCfg.Dead);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace BaseGames.Player.States
|
|||||||
_timer = Owner.AnimConfig?.HurtDuration ?? 0.4f;
|
_timer = Owner.AnimConfig?.HurtDuration ?? 0.4f;
|
||||||
_ended = false;
|
_ended = false;
|
||||||
Stats?.BeginInvincibility();
|
Stats?.BeginInvincibility();
|
||||||
|
Feedback.PlayTakeHit();
|
||||||
|
|
||||||
if (AnimCfg?.Hurt != null)
|
if (AnimCfg?.Hurt != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Animancer;
|
|||||||
using BaseGames.Core.Events;
|
using BaseGames.Core.Events;
|
||||||
using BaseGames.Input;
|
using BaseGames.Input;
|
||||||
using BaseGames.Combat;
|
using BaseGames.Combat;
|
||||||
|
using BaseGames.Feedback;
|
||||||
using BaseGames.Parry;
|
using BaseGames.Parry;
|
||||||
using BaseGames.Skills;
|
using BaseGames.Skills;
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ namespace BaseGames.Player.States
|
|||||||
|
|
||||||
// ── 战斗组件 ──────────────────────────────────────────────────────────
|
// ── 战斗组件 ──────────────────────────────────────────────────────────
|
||||||
[Header("战斗")]
|
[Header("战斗")]
|
||||||
|
[SerializeField] private PlayerFeedback _feedback;
|
||||||
[SerializeField] private PlayerCombat _combat;
|
[SerializeField] private PlayerCombat _combat;
|
||||||
[SerializeField] private FormController _formController;
|
[SerializeField] private FormController _formController;
|
||||||
[SerializeField] private WeaponManager _weaponManager;
|
[SerializeField] private WeaponManager _weaponManager;
|
||||||
@@ -57,6 +59,8 @@ namespace BaseGames.Player.States
|
|||||||
private InputBuffer _inputBuffer;
|
private InputBuffer _inputBuffer;
|
||||||
private bool _missingDependencyLogged;
|
private bool _missingDependencyLogged;
|
||||||
private bool _dependenciesReady;
|
private bool _dependenciesReady;
|
||||||
|
// DashState 在 Update 每帧访问(TickCooldown + CanDash),提前缓存避免重复 Dictionary 查找
|
||||||
|
private DashState _dashState;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 当前腾空可用的额外跳跃次数(二段跳)。
|
/// 当前腾空可用的额外跳跃次数(二段跳)。
|
||||||
/// 由 IdleState/RunState.OnStateEnter 落地时通过 ResetAirJumps() 重置;
|
/// 由 IdleState/RunState.OnStateEnter 落地时通过 ResetAirJumps() 重置;
|
||||||
@@ -128,6 +132,7 @@ namespace BaseGames.Player.States
|
|||||||
public InputReaderSO Input => _inputReader;
|
public InputReaderSO Input => _inputReader;
|
||||||
public InputBuffer Buffer => _inputBuffer;
|
public InputBuffer Buffer => _inputBuffer;
|
||||||
|
|
||||||
|
public IFeedbackPlayer Feedback => _feedback != null ? (IFeedbackPlayer)_feedback : NullFeedbackPlayer.Instance;
|
||||||
public PlayerCombat Combat => _combat;
|
public PlayerCombat Combat => _combat;
|
||||||
public FormController Form => _formController;
|
public FormController Form => _formController;
|
||||||
public WeaponManager Weapon => _weaponManager;
|
public WeaponManager Weapon => _weaponManager;
|
||||||
@@ -272,6 +277,7 @@ namespace BaseGames.Player.States
|
|||||||
{
|
{
|
||||||
_stats?.AddSoul(info.SoulGained);
|
_stats?.AddSoul(info.SoulGained);
|
||||||
_shield?.OnParrySuccess();
|
_shield?.OnParrySuccess();
|
||||||
|
Feedback.PlayParrySuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>灵泉输入:地面且有剩余充能时转入 SpringState 使用一次。</summary>
|
/// <summary>灵泉输入:地面且有剩余充能时转入 SpringState 使用一次。</summary>
|
||||||
@@ -301,7 +307,7 @@ namespace BaseGames.Player.States
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// 冲刺冷却计时
|
// 冲刺冷却计时
|
||||||
GetState<DashState>()?.TickCooldown(Time.deltaTime);
|
_dashState?.TickCooldown(Time.deltaTime);
|
||||||
|
|
||||||
_currentState?.OnStateUpdate();
|
_currentState?.OnStateUpdate();
|
||||||
|
|
||||||
@@ -309,7 +315,7 @@ namespace BaseGames.Player.States
|
|||||||
_dbg_CurrentState = _currentState?.GetType().Name ?? "None";
|
_dbg_CurrentState = _currentState?.GetType().Name ?? "None";
|
||||||
_dbg_IsGrounded = _movement != null && _movement.IsGrounded;
|
_dbg_IsGrounded = _movement != null && _movement.IsGrounded;
|
||||||
_dbg_AirJumpsLeft = _airJumpsLeft;
|
_dbg_AirJumpsLeft = _airJumpsLeft;
|
||||||
_dbg_CanDash = GetState<DashState>()?.CanDash ?? false;
|
_dbg_CanDash = _dashState?.CanDash ?? false;
|
||||||
_dbg_IsInvincible = _stats != null && _stats.IsInvincible;
|
_dbg_IsInvincible = _stats != null && _stats.IsInvincible;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -361,6 +367,7 @@ namespace BaseGames.Player.States
|
|||||||
_states[typeof(SpringState)] = new SpringState(this);
|
_states[typeof(SpringState)] = new SpringState(this);
|
||||||
_states[typeof(ParryState)] = new ParryState(this);
|
_states[typeof(ParryState)] = new ParryState(this);
|
||||||
_states[typeof(SwimState)] = new SwimState(this);
|
_states[typeof(SwimState)] = new SwimState(this);
|
||||||
|
_dashState = (DashState)_states[typeof(DashState)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Animancer;
|
using Animancer;
|
||||||
|
using BaseGames.Feedback;
|
||||||
using BaseGames.Input;
|
using BaseGames.Input;
|
||||||
using BaseGames.Player;
|
using BaseGames.Player;
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ namespace BaseGames.Player.States
|
|||||||
protected InputBuffer Buffer => _owner.Buffer;
|
protected InputBuffer Buffer => _owner.Buffer;
|
||||||
protected PlayerMovement Move => _owner.Movement;
|
protected PlayerMovement Move => _owner.Movement;
|
||||||
protected PlayerStats Stats => _owner.Stats;
|
protected PlayerStats Stats => _owner.Stats;
|
||||||
|
protected IFeedbackPlayer Feedback => _owner.Feedback;
|
||||||
protected AnimancerComponent Anim => _owner.Animancer;
|
protected AnimancerComponent Anim => _owner.Animancer;
|
||||||
protected PlayerMovementConfigSO Cfg => _owner.MovConfig;
|
protected PlayerMovementConfigSO Cfg => _owner.MovConfig;
|
||||||
protected PlayerAnimationConfigSO AnimCfg => _owner.AnimConfig;
|
protected PlayerAnimationConfigSO AnimCfg => _owner.AnimConfig;
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ namespace BaseGames.Player.States
|
|||||||
|
|
||||||
private void OnSpringEnd()
|
private void OnSpringEnd()
|
||||||
{
|
{
|
||||||
// 前摇正常结束 → 执行回血
|
// 前摇正常结束 → 执行回血 + 反馈
|
||||||
Stats?.ApplySpringHeal();
|
Stats?.ApplySpringHeal();
|
||||||
|
Feedback.PlayHeal();
|
||||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using BaseGames.Combat;
|
using BaseGames.Combat;
|
||||||
|
using BaseGames.Feedback;
|
||||||
|
|
||||||
namespace BaseGames.Player
|
namespace BaseGames.Player
|
||||||
{
|
{
|
||||||
@@ -28,6 +29,7 @@ namespace BaseGames.Player
|
|||||||
|
|
||||||
private HitBox[] _allHitBoxes;
|
private HitBox[] _allHitBoxes;
|
||||||
private AttackDirection _activeDir;
|
private AttackDirection _activeDir;
|
||||||
|
private IFeedbackPlayer _feedback;
|
||||||
|
|
||||||
/// <summary>下劈命中确认事件(供 DownAttackState Pogo 逻辑)。</summary>
|
/// <summary>下劈命中确认事件(供 DownAttackState Pogo 逻辑)。</summary>
|
||||||
public event System.Action<DamageInfo> OnDownHitConfirmed;
|
public event System.Action<DamageInfo> OnDownHitConfirmed;
|
||||||
@@ -40,10 +42,16 @@ namespace BaseGames.Player
|
|||||||
_allHitBoxes = GetComponentsInChildren<HitBox>(true);
|
_allHitBoxes = GetComponentsInChildren<HitBox>(true);
|
||||||
foreach (var hb in _allHitBoxes)
|
foreach (var hb in _allHitBoxes)
|
||||||
hb.OnHitConfirmed += OnAnyHitConfirmed;
|
hb.OnHitConfirmed += OnAnyHitConfirmed;
|
||||||
|
_feedback = GetComponentInChildren<IFeedbackPlayer>() ?? NullFeedbackPlayer.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAnyHitConfirmed(DamageInfo info)
|
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);
|
OnHitConfirmed?.Invoke(info);
|
||||||
if (_activeDir == AttackDirection.Down)
|
if (_activeDir == AttackDirection.Down)
|
||||||
OnDownHitConfirmed?.Invoke(info);
|
OnDownHitConfirmed?.Invoke(info);
|
||||||
@@ -59,6 +67,7 @@ namespace BaseGames.Player
|
|||||||
string hitBoxId = "")
|
string hitBoxId = "")
|
||||||
{
|
{
|
||||||
_activeDir = dir;
|
_activeDir = dir;
|
||||||
|
_feedback.PlayAttackWhoosh();
|
||||||
var hitBox = string.IsNullOrEmpty(hitBoxId)
|
var hitBox = string.IsNullOrEmpty(hitBoxId)
|
||||||
? GetHitBox(dir)
|
? GetHitBox(dir)
|
||||||
: (GetHitBoxById(hitBoxId) ?? GetHitBox(dir));
|
: (GetHitBoxById(hitBoxId) ?? GetHitBox(dir));
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ namespace BaseGames.Player
|
|||||||
[Min(0)]
|
[Min(0)]
|
||||||
public int soulPowerGain = 10;
|
public int soulPowerGain = 10;
|
||||||
|
|
||||||
|
[Tooltip("命中敌人时的打击力度反馈档位(影响摄像机震屏和控制器振动强度)。")]
|
||||||
|
public HitWeight hitWeight = HitWeight.Medium;
|
||||||
|
|
||||||
// ── 查询 API ──────────────────────────────────────────────────────────
|
// ── 查询 API ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>取指定方向、指定段的完整配置,越界自动取最后一个。</summary>
|
/// <summary>取指定方向、指定段的完整配置,越界自动取最后一个。</summary>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"BaseGames.Player",
|
"BaseGames.Player",
|
||||||
"BaseGames.Input",
|
"BaseGames.Input",
|
||||||
"BaseGames.Combat",
|
"BaseGames.Combat",
|
||||||
|
"BaseGames.Feedback",
|
||||||
"Kybernetik.Animancer"
|
"Kybernetik.Animancer"
|
||||||
],
|
],
|
||||||
"autoReferenced": true,
|
"autoReferenced": true,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||||||
using BaseGames.Player;
|
using BaseGames.Player;
|
||||||
using BaseGames.Input;
|
using BaseGames.Input;
|
||||||
using BaseGames.Combat;
|
using BaseGames.Combat;
|
||||||
|
using BaseGames.Feedback;
|
||||||
|
|
||||||
namespace BaseGames.Skills
|
namespace BaseGames.Skills
|
||||||
{
|
{
|
||||||
@@ -41,13 +42,23 @@ namespace BaseGames.Skills
|
|||||||
private FormSkillSO _soulSkill;
|
private FormSkillSO _soulSkill;
|
||||||
private FormSkillSO _spirit1;
|
private FormSkillSO _spirit1;
|
||||||
private FormSkillSO _spirit2;
|
private FormSkillSO _spirit2;
|
||||||
|
private IFeedbackPlayer _feedback;
|
||||||
|
|
||||||
// 冷却字典(FormSkillSO → 剩余冷却秒数),UpdateSkillSet 时重建
|
// 冷却字典(FormSkillSO → 剩余冷却秒数),UpdateSkillSet 时重建
|
||||||
private readonly Dictionary<FormSkillSO, float> _cooldowns = new(3);
|
private readonly Dictionary<FormSkillSO, float> _cooldowns = new(3);
|
||||||
// 无分配 Update 遍历用的快照数组
|
// 无分配 Update 遍历用的快照数组
|
||||||
private FormSkillSO[] _activeSkills = System.Array.Empty<FormSkillSO>();
|
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()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
if (_input != null)
|
if (_input != null)
|
||||||
@@ -147,6 +158,9 @@ namespace BaseGames.Skills
|
|||||||
|
|
||||||
_cooldowns[skill] = p.effectiveCooldown;
|
_cooldowns[skill] = p.effectiveCooldown;
|
||||||
|
|
||||||
|
// 施放反馈
|
||||||
|
_feedback.TriggerPreset("skill_cast");
|
||||||
|
|
||||||
// 播放动画(优先修改器动画,回退技能默认动画)
|
// 播放动画(优先修改器动画,回退技能默认动画)
|
||||||
var clip = p.effectiveAnimation.Clip != null
|
var clip = p.effectiveAnimation.Clip != null
|
||||||
? p.effectiveAnimation
|
? p.effectiveAnimation
|
||||||
@@ -158,14 +172,40 @@ namespace BaseGames.Skills
|
|||||||
if (skill.SkillHitBoxPrefab != null)
|
if (skill.SkillHitBoxPrefab != null)
|
||||||
{
|
{
|
||||||
var socket = _skillSocket != null ? _skillSocket : transform;
|
var socket = _skillSocket != null ? _skillSocket : transform;
|
||||||
var go = Object.Instantiate(skill.SkillHitBoxPrefab, socket.position,
|
var inst = GetOrCreateHitBox(skill.SkillHitBoxPrefab, socket);
|
||||||
socket.rotation, socket);
|
|
||||||
var inst = go.GetComponent<SkillHitBoxInstance>();
|
|
||||||
inst?.Activate(skill.damageSource, transform);
|
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 SoulSkill => _soulSkill;
|
||||||
public FormSkillSO Spirit1 => _spirit1;
|
public FormSkillSO Spirit1 => _spirit1;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"BaseGames.Input",
|
"BaseGames.Input",
|
||||||
"BaseGames.Player",
|
"BaseGames.Player",
|
||||||
"BaseGames.Combat",
|
"BaseGames.Combat",
|
||||||
|
"BaseGames.Feedback",
|
||||||
"BaseGames.Skills",
|
"BaseGames.Skills",
|
||||||
"Kybernetik.Animancer"
|
"Kybernetik.Animancer"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using BaseGames.Player;
|
using BaseGames.Player;
|
||||||
using BaseGames.Input;
|
using BaseGames.Input;
|
||||||
|
using BaseGames.Feedback;
|
||||||
|
|
||||||
namespace BaseGames.Spells
|
namespace BaseGames.Spells
|
||||||
{
|
{
|
||||||
@@ -24,9 +25,17 @@ namespace BaseGames.Spells
|
|||||||
// 当前装备的法术(单槽;如需多槽可扩展为数组)
|
// 当前装备的法术(单槽;如需多槽可扩展为数组)
|
||||||
private SpellSO _equippedSpell;
|
private SpellSO _equippedSpell;
|
||||||
private float _cooldownRemaining;
|
private float _cooldownRemaining;
|
||||||
|
private IFeedbackPlayer _feedback;
|
||||||
|
|
||||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
_feedback = GetComponentInChildren<IFeedbackPlayer>()
|
||||||
|
?? GetComponentInParent<IFeedbackPlayer>()
|
||||||
|
?? NullFeedbackPlayer.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
if (_input != null)
|
if (_input != null)
|
||||||
@@ -86,6 +95,9 @@ namespace BaseGames.Spells
|
|||||||
|
|
||||||
_cooldownRemaining = _equippedSpell.cooldown;
|
_cooldownRemaining = _equippedSpell.cooldown;
|
||||||
|
|
||||||
|
// 施放反馈
|
||||||
|
_feedback.TriggerPreset("spell_cast");
|
||||||
|
|
||||||
ExecuteSpellEffect(_equippedSpell);
|
ExecuteSpellEffect(_equippedSpell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ namespace BaseGames.UI
|
|||||||
|
|
||||||
private RectTransform _rectTransform;
|
private RectTransform _rectTransform;
|
||||||
private Coroutine _animCoroutine;
|
private Coroutine _animCoroutine;
|
||||||
|
// 每次 Show() 解析一次,协程期间(< 1s)复用,避免每帧走 FindObjectByTag
|
||||||
|
private Camera _cachedCamera;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
_rectTransform = (RectTransform)transform;
|
_rectTransform = (RectTransform)transform;
|
||||||
// 不在 Awake 缓存 Camera.main,避免 Boss 过场切换主摄像机后引用过期
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -42,6 +43,12 @@ namespace BaseGames.UI
|
|||||||
{
|
{
|
||||||
if (_animCoroutine != null) StopCoroutine(_animCoroutine);
|
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.text = damage.ToString();
|
||||||
_text.color = GetColorForType(type);
|
_text.color = GetColorForType(type);
|
||||||
|
|
||||||
@@ -51,9 +58,7 @@ namespace BaseGames.UI
|
|||||||
|
|
||||||
private void SetAnchoredPosition(Vector2 worldPosition)
|
private void SetAnchoredPosition(Vector2 worldPosition)
|
||||||
{
|
{
|
||||||
var cam = (_parentCanvas != null && _parentCanvas.renderMode == RenderMode.ScreenSpaceCamera)
|
var cam = _cachedCamera;
|
||||||
? _parentCanvas.worldCamera
|
|
||||||
: UnityEngine.Camera.main;
|
|
||||||
|
|
||||||
var screenPoint = cam != null
|
var screenPoint = cam != null
|
||||||
? (Vector2)cam.WorldToScreenPoint(worldPosition)
|
? (Vector2)cam.WorldToScreenPoint(worldPosition)
|
||||||
@@ -122,7 +127,7 @@ namespace BaseGames.UI
|
|||||||
[Header("预制体(对象池 key = AddressKeys.PrefabUIFloatingDmgText)")]
|
[Header("预制体(对象池 key = AddressKeys.PrefabUIFloatingDmgText)")]
|
||||||
[SerializeField] private GameObject _floatingDmgPrefab; // Fallback:Inspector 直接拖入
|
[SerializeField] private GameObject _floatingDmgPrefab; // Fallback:Inspector 直接拖入
|
||||||
|
|
||||||
private readonly Queue<FloatingDamageText> _pool = new();
|
private readonly List<FloatingDamageText> _pool = new();
|
||||||
private readonly CompositeDisposable _subs = new();
|
private readonly CompositeDisposable _subs = new();
|
||||||
|
|
||||||
private void OnEnable() => _onDamageDealt?.Subscribe(OnDamageDealt).AddTo(_subs);
|
private void OnEnable() => _onDamageDealt?.Subscribe(OnDamageDealt).AddTo(_subs);
|
||||||
@@ -138,25 +143,22 @@ namespace BaseGames.UI
|
|||||||
|
|
||||||
private FloatingDamageText GetOrCreate()
|
private FloatingDamageText GetOrCreate()
|
||||||
{
|
{
|
||||||
// 从池中找到已停用的实例
|
// 线性扫描全部已创建实例,找首个未激活的复用
|
||||||
while (_pool.Count > 0)
|
for (int i = 0; i < _pool.Count; i++)
|
||||||
{
|
{
|
||||||
var pooled = _pool.Dequeue();
|
var pooled = _pool[i];
|
||||||
if (pooled == null) continue;
|
if (pooled != null && !pooled.gameObject.activeSelf)
|
||||||
if (!pooled.gameObject.activeSelf)
|
|
||||||
{
|
{
|
||||||
pooled.gameObject.SetActive(true);
|
pooled.gameObject.SetActive(true);
|
||||||
return pooled;
|
return pooled;
|
||||||
}
|
}
|
||||||
_pool.Enqueue(pooled); // 仍在使用,放回
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有可用实例则实例化
|
// 没有可用实例则实例化新的,加入列表供下次复用
|
||||||
if (_floatingDmgPrefab == null) return null;
|
if (_floatingDmgPrefab == null) return null;
|
||||||
var go = Instantiate(_floatingDmgPrefab, transform);
|
var go = Instantiate(_floatingDmgPrefab, transform);
|
||||||
var comp = go.GetComponent<FloatingDamageText>();
|
var comp = go.GetComponent<FloatingDamageText>();
|
||||||
if (comp != null) _pool.Enqueue(comp);
|
if (comp != null) _pool.Add(comp);
|
||||||
return comp;
|
return comp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,11 @@ namespace BaseGames.UI
|
|||||||
_maxHP = max;
|
_maxHP = max;
|
||||||
// 重建阶段标记(每次 BossHPMax 改变时清空并按已存阈值重建,此处简化为清空)
|
// 重建阶段标记(每次 BossHPMax 改变时清空并按已存阈值重建,此处简化为清空)
|
||||||
if (_phaseMarkersRoot != null)
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 动画协程 ──────────────────────────────────────────────────────────
|
// ── 动画协程 ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ namespace BaseGames.UI.HUD
|
|||||||
private readonly List<GameObject> _hpCells = new();
|
private readonly List<GameObject> _hpCells = new();
|
||||||
private readonly List<GameObject> _springIcons = new();
|
private readonly List<GameObject> _springIcons = new();
|
||||||
private readonly CompositeDisposable _subs = new();
|
private readonly CompositeDisposable _subs = new();
|
||||||
|
private int _lastLingZhu = int.MinValue;
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
@@ -91,7 +92,9 @@ namespace BaseGames.UI.HUD
|
|||||||
|
|
||||||
private void UpdateLingZhu(int val)
|
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)
|
private void RebuildSpringIcons(int charges)
|
||||||
|
|||||||
@@ -46,19 +46,17 @@ namespace BaseGames.World
|
|||||||
{
|
{
|
||||||
Vector2 playerPos = player.transform.position;
|
Vector2 playerPos = player.transform.position;
|
||||||
|
|
||||||
#if UNITY_6000_0_OR_NEWER
|
// 使用 CameraTriggerZone 静态注册表,避免 FindObjectsOfType 全场景扫描
|
||||||
var zones = Object.FindObjectsByType<CameraTriggerZone>(FindObjectsSortMode.None);
|
var zones = CameraTriggerZone.AllZones;
|
||||||
#else
|
|
||||||
var zones = Object.FindObjectsOfType<CameraTriggerZone>();
|
|
||||||
#endif
|
|
||||||
// 选取优先级最高的匹配区域(避免多区域重叠时选错基线)
|
// 选取优先级最高的匹配区域(避免多区域重叠时选错基线)
|
||||||
CameraArea bestArea = null;
|
CameraArea bestArea = null;
|
||||||
int bestPriority = int.MinValue;
|
int bestPriority = int.MinValue;
|
||||||
|
|
||||||
foreach (var zone in zones)
|
foreach (var zone in zones)
|
||||||
{
|
{
|
||||||
var poly = zone.GetComponent<PolygonCollider2D>();
|
if (zone == null) continue;
|
||||||
if (poly != null && poly.OverlapPoint(playerPos))
|
if (zone.ContainsPoint(playerPos))
|
||||||
{
|
{
|
||||||
var area = zone.GetComponentInParent<CameraArea>();
|
var area = zone.GetComponentInParent<CameraArea>();
|
||||||
if (area != null && zone.Priority > bestPriority)
|
if (area != null && zone.Priority > bestPriority)
|
||||||
|
|||||||
BIN
Docs/Profiler/jump.data
Normal file
BIN
Docs/Profiler/jump.data
Normal file
Binary file not shown.
Reference in New Issue
Block a user