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_currentHash:
|
||||
serializedVersion: 2
|
||||
Hash: 00000000000000000000000000000000
|
||||
Hash: 3b5a6592fec2f53c65ab132b7f731fb2
|
||||
m_OptimizeCatalogSize: 0
|
||||
m_BuildRemoteCatalog: 0
|
||||
m_BundleLocalCatalog: 0
|
||||
@@ -88,6 +88,14 @@ MonoBehaviour:
|
||||
m_LabelTable:
|
||||
m_LabelNames:
|
||||
- default
|
||||
- Preload
|
||||
- Poolable
|
||||
- Enemy
|
||||
- BGM
|
||||
- SFX
|
||||
- Charms
|
||||
- Config
|
||||
- Weapon
|
||||
m_SchemaTemplates: []
|
||||
m_GroupTemplateObjects:
|
||||
- {fileID: 11400000, guid: f9701da6026b3a54f9b4d6eb144ee443, type: 2}
|
||||
|
||||
@@ -134,6 +134,7 @@ MonoBehaviour:
|
||||
weaponTrailPrefab: {fileID: 0}
|
||||
trailColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
soulPowerGain: 10
|
||||
hitWeight: 1
|
||||
references:
|
||||
version: 2
|
||||
RefIds: []
|
||||
|
||||
@@ -12,23 +12,25 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 81da55e0fcf99d34693cbc5a348225c3, type: 3}
|
||||
m_Name: PLY_PlayerMovementConfig
|
||||
m_EditorClassIdentifier:
|
||||
RunSpeed: 8
|
||||
RunSpeed: 6
|
||||
AirDragFactor: 1
|
||||
JumpForce: 24
|
||||
JumpForce: 20
|
||||
CoyoteTime: 0.12
|
||||
FallGravityMult: 2.5
|
||||
MaxFallSpeed: 28
|
||||
JumpCutMultiplier: 0.321
|
||||
ApexThreshold: 3
|
||||
ApexGravityMultiplier: 0.3
|
||||
MaxAirJumps: 1
|
||||
DoubleJumpForce: 19
|
||||
MaxAirJumps: 5
|
||||
DoubleJumpForce: 15
|
||||
DashSpeed: 20
|
||||
DashDuration: 0.25
|
||||
DashCooldown: 0.4
|
||||
DashInvincibilityDuration: 0.2
|
||||
DashInvincibilityCooldown: 0.9
|
||||
WallSlideSpeed: 2
|
||||
DownDashSpeed: 22
|
||||
DownDashDuration: 0.25
|
||||
WallSlideSpeed: 3
|
||||
WallHangSpeed: 1
|
||||
WallRayLength: 0.37
|
||||
WallRayOffsetY: 0.2
|
||||
|
||||
@@ -1,101 +1,5 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &467203328547477162
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7119158475861943178}
|
||||
- component: {fileID: 7882116945389632025}
|
||||
- component: {fileID: 4639356286286040131}
|
||||
m_Layer: 14
|
||||
m_Name: HitBox_Ground_1
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &7119158475861943178
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 467203328547477162}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.798, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &7882116945389632025
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 467203328547477162}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_ForceSendLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ForceReceiveLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ContactCaptureLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_CallbackLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0, y: -0.16736698}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
oldSize: {x: 0, y: 0}
|
||||
newSize: {x: 0, y: 0}
|
||||
adaptiveTilingThreshold: 0
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1, y: 0.83473396}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &4639356286286040131
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 467203328547477162}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Ground_1
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
--- !u!1 &1932889250901504761
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -123,7 +27,7 @@ Transform:
|
||||
m_GameObject: {fileID: 1932889250901504761}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: -0.562, z: 0}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
@@ -161,7 +65,7 @@ BoxCollider2D:
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: -0.027121663, y: -0.15051937}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
@@ -172,7 +76,7 @@ BoxCollider2D:
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.189852, y: 0.80103874}
|
||||
m_Size: {x: 1, y: 0.5}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &6478051166999031478
|
||||
MonoBehaviour:
|
||||
@@ -188,11 +92,11 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Down
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
--- !u!1 &2584603199706918030
|
||||
m_Bits: 0
|
||||
--- !u!1 &3989564331693126876
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
@@ -200,38 +104,38 @@ GameObject:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1660186156348129284}
|
||||
- component: {fileID: 1152578598430080845}
|
||||
- component: {fileID: 3007294148525084107}
|
||||
- component: {fileID: 8294071144630811572}
|
||||
- component: {fileID: 4949779957213724475}
|
||||
- component: {fileID: 4757677899241504248}
|
||||
m_Layer: 14
|
||||
m_Name: HitBox_Ground _2
|
||||
m_Name: HitBox_Ground
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1660186156348129284
|
||||
--- !u!4 &8294071144630811572
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2584603199706918030}
|
||||
m_GameObject: {fileID: 3989564331693126876}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.798, y: 0, z: 0}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &1152578598430080845
|
||||
--- !u!61 &4949779957213724475
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2584603199706918030}
|
||||
m_GameObject: {fileID: 3989564331693126876}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
@@ -257,7 +161,7 @@ BoxCollider2D:
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: -0.117884755, y: 0.01309824}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
@@ -268,15 +172,15 @@ BoxCollider2D:
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 0.7642305, y: 1.1956644}
|
||||
m_Size: {x: 1, y: 0.5}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &3007294148525084107
|
||||
--- !u!114 &4757677899241504248
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2584603199706918030}
|
||||
m_GameObject: {fileID: 3989564331693126876}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
@@ -284,106 +188,10 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Ground_2
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
--- !u!1 &4050057806632877121
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4405470499151834857}
|
||||
- component: {fileID: 8597809172682257212}
|
||||
- component: {fileID: 1610035618021234136}
|
||||
m_Layer: 14
|
||||
m_Name: HitBox_Air_2
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &4405470499151834857
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4050057806632877121}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.553, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &8597809172682257212
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4050057806632877121}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_ForceSendLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ForceReceiveLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ContactCaptureLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_CallbackLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0.27943045, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
oldSize: {x: 0, y: 0}
|
||||
newSize: {x: 0, y: 0}
|
||||
adaptiveTilingThreshold: 0
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.0588609, y: 1}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &1610035618021234136
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4050057806632877121}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Air_2
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
--- !u!1 &4335406389674002762
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -411,7 +219,7 @@ Transform:
|
||||
m_GameObject: {fileID: 4335406389674002762}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0.918, z: 0}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
@@ -449,7 +257,7 @@ BoxCollider2D:
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0.072324514, y: 0}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
@@ -460,7 +268,7 @@ BoxCollider2D:
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.1599612, y: 1}
|
||||
m_Size: {x: 0.5, y: 1}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &1392799324577637263
|
||||
MonoBehaviour:
|
||||
@@ -476,10 +284,10 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Up
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
m_Bits: 0
|
||||
--- !u!1 &4821376343125962025
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -510,12 +318,10 @@ Transform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 7119158475861943178}
|
||||
- {fileID: 1660186156348129284}
|
||||
- {fileID: 8294071144630811572}
|
||||
- {fileID: 7468586589501741901}
|
||||
- {fileID: 6088225995420515986}
|
||||
- {fileID: 4362395311111627733}
|
||||
- {fileID: 4405470499151834857}
|
||||
- {fileID: 6913225169405126738}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &3691925044832415471
|
||||
@@ -530,11 +336,11 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: ec12dacf2519f58429dd3c59da8f93b0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_hitBoxGround: {fileID: 4639356286286040131}
|
||||
_hitBoxGround: {fileID: 4757677899241504248}
|
||||
_hitBoxUp: {fileID: 1392799324577637263}
|
||||
_hitBoxDown: {fileID: 6478051166999031478}
|
||||
_hitBoxAir: {fileID: 9014207169512774676}
|
||||
--- !u!1 &8582289489283119946
|
||||
_hitBoxAir: {fileID: 1382006829078153708}
|
||||
--- !u!1 &6434981771063321190
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
@@ -542,38 +348,38 @@ GameObject:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4362395311111627733}
|
||||
- component: {fileID: 922051492914393482}
|
||||
- component: {fileID: 9014207169512774676}
|
||||
- component: {fileID: 6913225169405126738}
|
||||
- component: {fileID: 6843760498109474434}
|
||||
- component: {fileID: 1382006829078153708}
|
||||
m_Layer: 14
|
||||
m_Name: HitBox_Air_1
|
||||
m_Name: HitBox_Air
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &4362395311111627733
|
||||
--- !u!4 &6913225169405126738
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8582289489283119946}
|
||||
m_GameObject: {fileID: 6434981771063321190}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0.553, y: 0, z: 0}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8975424752584779179}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!61 &922051492914393482
|
||||
--- !u!61 &6843760498109474434
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8582289489283119946}
|
||||
m_GameObject: {fileID: 6434981771063321190}
|
||||
m_Enabled: 1
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
@@ -599,7 +405,7 @@ BoxCollider2D:
|
||||
m_IsTrigger: 1
|
||||
m_UsedByEffector: 0
|
||||
m_UsedByComposite: 0
|
||||
m_Offset: {x: 0.46717286, y: 0}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
@@ -610,15 +416,15 @@ BoxCollider2D:
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 1.4343457, y: 1}
|
||||
m_Size: {x: 0.5, y: 1}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &9014207169512774676
|
||||
--- !u!114 &1382006829078153708
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8582289489283119946}
|
||||
m_GameObject: {fileID: 6434981771063321190}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a655e2461396a8348a32a13144438e8e, type: 3}
|
||||
@@ -626,7 +432,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
_defaultSource: {fileID: 0}
|
||||
_hitCooldown: 0.1
|
||||
_id: ATK_Air_1
|
||||
_id:
|
||||
_rivalHitBoxMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 134217792
|
||||
m_Bits: 0
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace BaseGames.Camera
|
||||
[SerializeField] private CinemachineBrain _brain;
|
||||
|
||||
private UnityEngine.Camera _camera;
|
||||
private CinemachineCamera _cachedVCam;
|
||||
private CinemachineConfiner3D _cachedConfiner;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
@@ -77,7 +79,8 @@ namespace BaseGames.Camera
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前活跃 VCam 的 CinemachineConfiner3D 边界盒(世界空间 AABB)。
|
||||
/// 用于在像素取整后将相机钳制回限位区域内。
|
||||
/// 缓存上次查询的 VCam 实例;仅在活跃 VCam 发生切换时重新调用 GetComponent,
|
||||
/// 避免每帧 GetComponent 开销。
|
||||
/// </summary>
|
||||
private bool TryGetActiveConfinerBounds(out Bounds bounds)
|
||||
{
|
||||
@@ -85,9 +88,14 @@ namespace BaseGames.Camera
|
||||
if (_brain == null) return false;
|
||||
var vcam = _brain.ActiveVirtualCamera as CinemachineCamera;
|
||||
if (vcam == null) return false;
|
||||
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
|
||||
if (confiner == null || !confiner.IsValid) return false;
|
||||
bounds = confiner.BoundingVolume.bounds;
|
||||
// 只在活跃 VCam 切换时刷新缓存
|
||||
if (!ReferenceEquals(vcam, _cachedVCam))
|
||||
{
|
||||
_cachedVCam = vcam;
|
||||
_cachedConfiner = vcam.GetComponent<CinemachineConfiner3D>();
|
||||
}
|
||||
if (_cachedConfiner == null || !_cachedConfiner.IsValid) return false;
|
||||
bounds = _cachedConfiner.BoundingVolume.bounds;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,8 @@ namespace BaseGames.Camera
|
||||
}
|
||||
|
||||
// 触发区域进入:更新集合(同一区域去重后重新加入,保证最新优先级)
|
||||
_activeZones.RemoveAll(e => e.area == area);
|
||||
for (int i = _activeZones.Count - 1; i >= 0; i--)
|
||||
if (_activeZones[i].area == area) _activeZones.RemoveAt(i);
|
||||
_activeZones.Add((area, priority));
|
||||
|
||||
// 仅当此区域是当前最优且尚未激活时才切换
|
||||
@@ -132,7 +133,9 @@ namespace BaseGames.Camera
|
||||
if (releasedArea == null) return;
|
||||
|
||||
bool wasActive = releasedArea == _currentArea;
|
||||
int removed = _activeZones.RemoveAll(e => e.area == releasedArea);
|
||||
int removed = 0;
|
||||
for (int i = _activeZones.Count - 1; i >= 0; i--)
|
||||
if (_activeZones[i].area == releasedArea) { _activeZones.RemoveAt(i); removed++; }
|
||||
// 若区域本就不在栈中,且又不是当前激活区,则无需任何操作
|
||||
if (removed == 0 && !wasActive) return;
|
||||
|
||||
|
||||
@@ -57,9 +57,14 @@ namespace BaseGames.Camera
|
||||
// ── 静态:跨实例共享触发状态 ──────────────────────────────────────────
|
||||
// 玩家当前物理上所在的所有触发区域(按进入顺序排列)
|
||||
private static readonly List<CameraTriggerZone> s_InsideZones = new();
|
||||
// 场景内所有已启用的触发区域,供 RoomController 等查询(替代 FindObjectsOfType)
|
||||
private static readonly List<CameraTriggerZone> s_AllZones = new();
|
||||
// 当前已向 ICameraService 发出 SwitchArea 请求的触发区域
|
||||
private static CameraTriggerZone s_ActiveZone;
|
||||
|
||||
/// <summary>场景内当前所有已启用的触发区域(只读)。</summary>
|
||||
public static IReadOnlyList<CameraTriggerZone> AllZones => s_AllZones;
|
||||
|
||||
/// <summary>
|
||||
/// 在每次进入 Play Mode 前(或禁用 Domain Reload 时的跨会话)重置静态状态,
|
||||
/// 防止上一次游戏会话残留的区域引用导致触发逻辑错误。
|
||||
@@ -68,6 +73,7 @@ namespace BaseGames.Camera
|
||||
private static void ResetStaticState()
|
||||
{
|
||||
s_InsideZones.Clear();
|
||||
s_AllZones.Clear();
|
||||
s_ActiveZone = null;
|
||||
}
|
||||
|
||||
@@ -77,12 +83,22 @@ namespace BaseGames.Camera
|
||||
_collider.isTrigger = true;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
s_AllZones.Add(this);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
s_AllZones.Remove(this);
|
||||
HandlePlayerExit();
|
||||
}
|
||||
|
||||
/// <summary>判断世界坐标点是否在本触发区域多边形内(供 RoomController 等无需 GetComponent 直接查询)。</summary>
|
||||
public bool ContainsPoint(Vector2 worldPoint) => _collider != null && _collider.OverlapPoint(worldPoint);
|
||||
|
||||
/// <summary>
|
||||
/// 若玩家出生时已在触发区域内,OnTriggerEnter2D 不会触发。
|
||||
/// 延迟一帧(确保 RoomController.Start 先完成基准区域设置)后主动检测。
|
||||
|
||||
@@ -142,8 +142,8 @@ namespace BaseGames.Combat
|
||||
bool isRivalHitBoxLayer = (_rivalHitBoxMask.value & (1 << otherLayer)) != 0;
|
||||
if (isRivalHitBoxLayer && CanClash)
|
||||
{
|
||||
var rivalHitBox = other.GetComponent<HitBox>();
|
||||
if (rivalHitBox != null && rivalHitBox.IsActive && rivalHitBox.CanClash)
|
||||
if (other.TryGetComponent<HitBox>(out var rivalHitBox) &&
|
||||
rivalHitBox.IsActive && rivalHitBox.CanClash)
|
||||
{
|
||||
_clashService?.ResolveClash(this, rivalHitBox);
|
||||
return; // 拼刀,中止伤害流水线
|
||||
@@ -151,8 +151,7 @@ namespace BaseGames.Combat
|
||||
}
|
||||
|
||||
// ② 命中 HurtBox
|
||||
var hurtBox = other.GetComponent<HurtBox>();
|
||||
if (hurtBox != null)
|
||||
if (other.TryGetComponent<HurtBox>(out var hurtBox))
|
||||
{
|
||||
// 用 HitBox 自身碰撞盒中心在 HurtBox 表面上的最近点作为受击位置。
|
||||
// 对大体积/长条形受击体(如地刺),此点远比 HurtBox 节点中心更准确。
|
||||
@@ -163,7 +162,8 @@ namespace BaseGames.Combat
|
||||
}
|
||||
|
||||
// ③ 命中 IBreakable(机关/障碍物)
|
||||
other.GetComponent<IBreakable>()?.TryInteract(info);
|
||||
if (other.TryGetComponent<IBreakable>(out var breakable))
|
||||
breakable.TryInteract(info);
|
||||
}
|
||||
|
||||
// ── 当前激活期已命中目标集合(防止复合子 Collider 导致同帧多次命中)────────────
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace BaseGames.Combat
|
||||
protected Rigidbody2D _rb;
|
||||
protected HitBox _hitBox;
|
||||
protected float _aliveTimer;
|
||||
// Lifetime 在 Initialize 时缓存,避免 Update 每帧访问 SO 成员并做 null check
|
||||
private float _lifetime = float.MaxValue;
|
||||
|
||||
private PooledObject _pooledObject;
|
||||
|
||||
@@ -32,6 +34,7 @@ namespace BaseGames.Combat
|
||||
public virtual void Initialize(ProjectileConfigSO config, DamageInfo damageInfo, Vector2 direction, int ownerLayer = 0)
|
||||
{
|
||||
_config = config;
|
||||
_lifetime = config.Lifetime;
|
||||
DamageInfo = damageInfo;
|
||||
Direction = direction.normalized;
|
||||
_aliveTimer = 0f;
|
||||
@@ -75,7 +78,7 @@ namespace BaseGames.Combat
|
||||
protected virtual void Update()
|
||||
{
|
||||
_aliveTimer += Time.deltaTime;
|
||||
if (_config != null && _aliveTimer >= _config.Lifetime)
|
||||
if (_aliveTimer >= _lifetime)
|
||||
ReturnToPool();
|
||||
}
|
||||
|
||||
@@ -91,6 +94,7 @@ namespace BaseGames.Combat
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
_aliveTimer = 0f;
|
||||
_lifetime = float.MaxValue; // 归还对象池后重置,防止未初始化时自毁
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ namespace BaseGames.Combat
|
||||
|
||||
public event System.Action<DamageInfo> OnHitConfirmed;
|
||||
|
||||
private Coroutine _returnCoroutine;
|
||||
// 按 duration 缓存 WaitForSeconds,同一技能复用无 GC 分配
|
||||
private WaitForSeconds _cachedWait;
|
||||
private float _cachedWaitDuration = float.NaN;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
foreach (var hb in _hitBoxes)
|
||||
@@ -35,7 +40,31 @@ namespace BaseGames.Combat
|
||||
hb?.Activate(source, attacker);
|
||||
}
|
||||
|
||||
/// <summary>duration 秒后自动销毁此 GameObject。</summary>
|
||||
/// <summary>
|
||||
/// duration 秒后归还对象池(SetActive false)。
|
||||
/// 由 SkillManager 对象池调用;替代旧版 Destroy 流程。
|
||||
/// </summary>
|
||||
public void AutoReturnAfter(float duration)
|
||||
{
|
||||
if (!Mathf.Approximately(_cachedWaitDuration, duration))
|
||||
{
|
||||
_cachedWaitDuration = duration;
|
||||
_cachedWait = new WaitForSeconds(duration);
|
||||
}
|
||||
if (_returnCoroutine != null) StopCoroutine(_returnCoroutine);
|
||||
_returnCoroutine = StartCoroutine(ReturnCoroutine());
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator ReturnCoroutine()
|
||||
{
|
||||
yield return _cachedWait;
|
||||
foreach (var hb in _hitBoxes)
|
||||
hb?.Deactivate();
|
||||
_returnCoroutine = null;
|
||||
gameObject.SetActive(false); // 触发对象池回收
|
||||
}
|
||||
|
||||
/// <summary>duration 秒后销毁(非池化路径,保留向后兼容)。</summary>
|
||||
public void AutoDestroyAfter(float duration)
|
||||
=> Destroy(gameObject, Mathf.Max(0f, duration));
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ namespace BaseGames.Combat.StatusEffects
|
||||
|
||||
public override StatusEffectType EffectType => StatusEffectType.Fire;
|
||||
public override int MaxStacks => 1;
|
||||
private static readonly StatusEffectType[] s_MutualExclusions = { StatusEffectType.Freeze };
|
||||
/// <summary>施加燃烧时移除冻结(火冰互斥)。</summary>
|
||||
public override StatusEffectType[] MutualExclusions => new[] { StatusEffectType.Freeze };
|
||||
public override StatusEffectType[] MutualExclusions => s_MutualExclusions;
|
||||
|
||||
public FireEffect()
|
||||
{
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace BaseGames.Combat.StatusEffects
|
||||
// ── Shader 渲染(MaterialPropertyBlock,不修改共享材质)─────────
|
||||
private SpriteRenderer _renderer;
|
||||
private MaterialPropertyBlock _propBlock;
|
||||
// 缓存 Shader 属性 ID,避免每次调用 SetShaderParam 都做字符串哈希查找
|
||||
private readonly Dictionary<string, int> _shaderPropIds = new();
|
||||
|
||||
// ── DoT 伤害代理(由 StatusEffect.OnTick 通过 Owner 调用)──────────
|
||||
private IDamageable _damageable;
|
||||
@@ -135,8 +137,13 @@ namespace BaseGames.Combat.StatusEffects
|
||||
public void SetShaderParam(string param, float value)
|
||||
{
|
||||
if (_renderer == null) return;
|
||||
if (!_shaderPropIds.TryGetValue(param, out int propId))
|
||||
{
|
||||
propId = Shader.PropertyToID(param);
|
||||
_shaderPropIds[param] = propId;
|
||||
}
|
||||
_renderer.GetPropertyBlock(_propBlock);
|
||||
_propBlock.SetFloat(param, value);
|
||||
_propBlock.SetFloat(propId, value);
|
||||
_renderer.SetPropertyBlock(_propBlock);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,16 @@ namespace BaseGames.Core
|
||||
/// <item><b>Room</b>:极短淡出(<see cref="_roomFadeDuration"/>),无加载画面。</item>
|
||||
/// <item><b>Scene</b>:完整淡出(<see cref="_sceneFadeDuration"/>),显示加载画面。</item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// 完整加载时序(保证场景物体在显示前完成状态初始化):
|
||||
/// <list type="number">
|
||||
/// <item>淡出(黑幕遮挡)</item>
|
||||
/// <item>Addressable 异步加载场景(场景物体 Awake / OnEnable 同步执行)</item>
|
||||
/// <item>触发 <see cref="_onSceneWorldStateRestored"/>:通知场景物体从 WorldStateRegistry 恢复初始状态</item>
|
||||
/// <item>等待一帧(保证所有场景物体 Start() 和事件处理器执行完毕)</item>
|
||||
/// <item>淡入(显示已完成初始化的场景)</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-900)]
|
||||
public class SceneService : MonoBehaviour, ISceneService
|
||||
@@ -35,6 +45,12 @@ namespace BaseGames.Core
|
||||
[SerializeField] private VoidEventChannelSO _onFadeInRequest;
|
||||
[SerializeField] private VoidEventChannelSO _onFadeOutRequest;
|
||||
|
||||
[Tooltip("场景加载完成、WorldStateRegistry 已就绪后触发。\n" +
|
||||
"场景内物体应订阅此事件,从 WorldStateRegistry 读取存档状态并应用(替代在 Start() 中读取)。\n" +
|
||||
"触发后会等待一帧,确保所有处理器执行完毕,再执行淡入显示场景。\n" +
|
||||
"对应 SO:EVT_SceneWorldStateRestored")]
|
||||
[SerializeField] private VoidEventChannelSO _onSceneWorldStateRestored;
|
||||
|
||||
[SerializeField] private SceneLoader _sceneLoader;
|
||||
|
||||
[Header("淡出时长")]
|
||||
@@ -71,6 +87,14 @@ namespace BaseGames.Core
|
||||
else
|
||||
Debug.LogError("[SceneService] _sceneLoader 未赋值,场景加载中断。请在 Inspector 中绑定 SceneLoader 组件。");
|
||||
|
||||
// 通知:WorldStateRegistry 已就绪,场景物体应在此帧内从中读取存档状态并应用初始状态。
|
||||
// 订阅者(WorldStateRegistrySaver、各场景 StateApplier 等)会在同一帧同步执行。
|
||||
_onSceneWorldStateRestored?.Raise();
|
||||
|
||||
// 等待一帧:确保所有场景物体的 Start() 和事件处理器都已执行完毕,
|
||||
// 场景物体处于正确的初始状态后再揭开黑幕,避免出现一帧状态错误的画面闪烁。
|
||||
yield return null;
|
||||
|
||||
_onFadeInRequest?.Raise();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1274
Assets/_Game/Scripts/Editor/Addressables/AddressableManagerWindow.cs
Normal file
1274
Assets/_Game/Scripts/Editor/Addressables/AddressableManagerWindow.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abd3b31b261435e4786f53b937c71742
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,527 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEngine;
|
||||
// 此文件已被 AddressableManagerWindow 取代。
|
||||
// 原有功能已整合到统一工具中,请使用:
|
||||
// BaseGames → Addressables → Addressables Manager(总入口)
|
||||
// BaseGames → Addressables → Rule Sync(直达规则校验 Tab)
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Addressable 规则同步窗口。
|
||||
///
|
||||
/// 功能:
|
||||
/// 1. 扫描所有已注册的 Addressable 资产
|
||||
/// 2. 根据 <see cref="AddressableRules"/> 中的规则计算期望分组与期望标签
|
||||
/// 3. 对比实际值,显示所有不符合规范的条目(分组错误 / 标签缺失 / 标签多余)
|
||||
/// 4. 一键自动修复全部问题
|
||||
/// 5. 导出 CSV 报告供存档或 Code Review
|
||||
///
|
||||
/// 菜单:BaseGames → Addressables → Rule Sync
|
||||
/// </summary>
|
||||
public class AddressableRuleSyncWindow : EditorWindow
|
||||
{
|
||||
// ── 内部数据结构 ───────────────────────────────────────────────────────
|
||||
|
||||
private enum IssueKind { None, WrongGroup, MissingLabel, ExtraLabel }
|
||||
|
||||
private class EntryReport
|
||||
{
|
||||
public string Address;
|
||||
public string AssetPath;
|
||||
public string CurrentGroup;
|
||||
public string ExpectedGroup; // null = 规则未覆盖,维持现状
|
||||
public string[] CurrentLabels;
|
||||
public string[] ExpectedLabels;
|
||||
public string[] MissingLabels; // 应有但没有(规则要求),红色错误
|
||||
public string[] ExtraLabels; // 规则不要求且在 KnownLabels 中(多余规则标签),红色错误
|
||||
public string[] UnknownLabels; // 规则不要求且不在 KnownLabels 中(自定义标签),黄色警告,不自动删除
|
||||
public bool GroupOk => ExpectedGroup == null || CurrentGroup == ExpectedGroup;
|
||||
public bool LabelsOk => MissingLabels.Length == 0 && ExtraLabels.Length == 0;
|
||||
public bool IsOk => GroupOk && LabelsOk;
|
||||
public bool HasWarnings => UnknownLabels.Length > 0;
|
||||
}
|
||||
|
||||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private List<EntryReport> _reports = new();
|
||||
private Vector2 _scrollPos;
|
||||
private bool _showOk = false;
|
||||
private bool _scanned = false;
|
||||
private string _searchFilter = "";
|
||||
|
||||
// ── 样式(惰性初始化)────────────────────────────────────────────────
|
||||
|
||||
private GUIStyle _okStyle;
|
||||
private GUIStyle _warnStyle;
|
||||
private GUIStyle _errorStyle;
|
||||
private GUIStyle _boldStyle;
|
||||
private GUIStyle _rowEven;
|
||||
private GUIStyle _rowOdd;
|
||||
private bool _stylesReady;
|
||||
|
||||
// ── 颜色 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static readonly Color ColOk = new(0.20f, 0.78f, 0.35f, 1f);
|
||||
private static readonly Color ColWarn = new(0.95f, 0.75f, 0.10f, 1f);
|
||||
private static readonly Color ColError = new(0.90f, 0.25f, 0.20f, 1f);
|
||||
private static readonly Color ColRowEven = new(0.22f, 0.22f, 0.22f, 0.4f);
|
||||
|
||||
// ── 菜单入口 ──────────────────────────────────────────────────────────
|
||||
|
||||
[MenuItem("BaseGames/Addressables/Rule Sync", priority = 110)]
|
||||
public static void OpenWindow()
|
||||
{
|
||||
var win = GetWindow<AddressableRuleSyncWindow>("Addressable Rule Sync");
|
||||
win.minSize = new Vector2(1040, 540);
|
||||
win.Show();
|
||||
}
|
||||
|
||||
// ── GUI ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EnsureStyles();
|
||||
|
||||
if (AddressableAssetSettingsDefaultObject.Settings == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"Addressable Settings 未初始化。\n" +
|
||||
"请先执行 Window → Asset Management → Addressables → Groups → Create Addressables Settings。",
|
||||
MessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawToolbar();
|
||||
DrawStats();
|
||||
DrawTable();
|
||||
DrawFooter();
|
||||
}
|
||||
|
||||
// ── 工具栏 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||||
{
|
||||
if (GUILayout.Button("扫描", EditorStyles.toolbarButton, GUILayout.Width(80)))
|
||||
Scan();
|
||||
|
||||
if (GUILayout.Button("🔄 刷新", EditorStyles.toolbarButton, GUILayout.Width(60)))
|
||||
Scan();
|
||||
|
||||
GUILayout.Space(8);
|
||||
_showOk = GUILayout.Toggle(_showOk, "显示正常项", EditorStyles.toolbarButton, GUILayout.Width(80));
|
||||
GUILayout.Space(8);
|
||||
|
||||
EditorGUILayout.LabelField("搜索:", GUILayout.Width(42));
|
||||
_searchFilter = EditorGUILayout.TextField(_searchFilter, EditorStyles.toolbarSearchField,
|
||||
GUILayout.Width(200));
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
GUI.enabled = _scanned && _reports.Any(r => !r.IsOk);
|
||||
if (GUILayout.Button("✦ 修复所有问题", EditorStyles.toolbarButton, GUILayout.Width(120)))
|
||||
FixAll();
|
||||
GUI.enabled = _scanned;
|
||||
if (GUILayout.Button("导出 CSV", EditorStyles.toolbarButton, GUILayout.Width(80)))
|
||||
ExportCsv();
|
||||
GUI.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 统计行 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawStats()
|
||||
{
|
||||
if (!_scanned) return;
|
||||
|
||||
int total = _reports.Count;
|
||||
int ok = _reports.Count(r => r.IsOk);
|
||||
int issues = _reports.Count(r => !r.IsOk);
|
||||
int warnings = _reports.Count(r => r.IsOk && r.HasWarnings);
|
||||
int wrongGrp = _reports.Count(r => !r.GroupOk);
|
||||
int misLabel = _reports.Count(r => r.MissingLabels.Length > 0);
|
||||
int extLabel = _reports.Count(r => r.ExtraLabels.Length > 0);
|
||||
int unkLabel = _reports.Count(r => r.UnknownLabels.Length > 0);
|
||||
|
||||
EditorGUILayout.Space(2);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label($"共 {total} 条目", EditorStyles.miniLabel);
|
||||
GUILayout.Space(12);
|
||||
DrawColoredLabel($"✅ 正常 {ok}", ColOk);
|
||||
GUILayout.Space(12);
|
||||
DrawColoredLabel($"❌ 问题 {issues}", issues > 0 ? ColError : ColOk);
|
||||
GUILayout.Space(8);
|
||||
DrawColoredLabel($"⚠ 自定义标签 {unkLabel}", unkLabel > 0 ? ColWarn : ColOk);
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label($"分组错误 {wrongGrp} | 标签缺失 {misLabel} | 多余规则标签 {extLabel}",
|
||||
EditorStyles.miniLabel);
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
EditorGUILayout.Space(2);
|
||||
}
|
||||
|
||||
// ── 主表格 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawTable()
|
||||
{
|
||||
// 表头
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||||
{
|
||||
GUILayout.Label("Address", _boldStyle, GUILayout.Width(200));
|
||||
GUILayout.Label("当前分组", _boldStyle, GUILayout.Width(120));
|
||||
GUILayout.Label("期望分组", _boldStyle, GUILayout.Width(120));
|
||||
GUILayout.Label("缺失标签", _boldStyle, GUILayout.Width(130));
|
||||
GUILayout.Label("多余规则标签", _boldStyle, GUILayout.Width(110));
|
||||
GUILayout.Label("自定义标签", _boldStyle, GUILayout.Width(110));
|
||||
GUILayout.Label("状态", _boldStyle, GUILayout.Width(80));
|
||||
}
|
||||
|
||||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.ExpandHeight(true));
|
||||
|
||||
if (!_scanned)
|
||||
{
|
||||
EditorGUILayout.HelpBox("点击「扫描」按钮开始分析已注册的 Addressable 资产。", MessageType.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = _reports
|
||||
.Where(r => _showOk || !r.IsOk)
|
||||
.Where(r => string.IsNullOrEmpty(_searchFilter)
|
||||
|| r.Address.IndexOf(_searchFilter, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
.ToList();
|
||||
|
||||
if (display.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
_showOk ? "没有匹配搜索条件的条目。" : "✅ 所有资产均符合规范!",
|
||||
MessageType.Info);
|
||||
}
|
||||
|
||||
for (int i = 0; i < display.Count; i++)
|
||||
DrawRow(display[i], i);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawRow(EntryReport r, int idx)
|
||||
{
|
||||
var bg = idx % 2 == 0 ? _rowEven : GUIStyle.none;
|
||||
using (new EditorGUILayout.HorizontalScope(bg, GUILayout.Height(20)))
|
||||
{
|
||||
// Address(点击可 Ping)
|
||||
if (GUILayout.Button(r.Address, EditorStyles.linkLabel, GUILayout.Width(200)))
|
||||
PingAsset(r.AssetPath);
|
||||
|
||||
// 当前分组
|
||||
var grpColor = r.GroupOk ? ColOk : ColError;
|
||||
DrawColoredLabel(r.CurrentGroup ?? "—", grpColor, GUILayout.Width(120));
|
||||
|
||||
// 期望分组
|
||||
var expGrpText = r.ExpectedGroup ?? "(规则未覆盖)";
|
||||
var expGrpColor = r.GroupOk ? ColOk : ColWarn;
|
||||
DrawColoredLabel(expGrpText, expGrpColor, GUILayout.Width(120));
|
||||
|
||||
// 缺失标签(红色,须补齐)
|
||||
var missingText = r.MissingLabels.Length > 0 ? string.Join(", ", r.MissingLabels) : "—";
|
||||
DrawColoredLabel(missingText, r.MissingLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(130));
|
||||
|
||||
// 多余规则标签(红色,将被 FixEntry 移除)
|
||||
var extraText = r.ExtraLabels.Length > 0 ? string.Join(", ", r.ExtraLabels) : "—";
|
||||
DrawColoredLabel(extraText, r.ExtraLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(110));
|
||||
|
||||
// 自定义标签(黄色警告,不会被自动删除,建议写入规范)
|
||||
var unknownText = r.UnknownLabels.Length > 0 ? string.Join(", ", r.UnknownLabels) : "—";
|
||||
DrawColoredLabel(unknownText, r.UnknownLabels.Length > 0 ? ColWarn : ColOk, GUILayout.Width(110));
|
||||
|
||||
// 状态 + 单条修复按钮
|
||||
if (r.IsOk)
|
||||
{
|
||||
var statusColor = r.HasWarnings ? ColWarn : ColOk;
|
||||
var statusText = r.HasWarnings ? "⚠ 自定义标签" : "✅ 正常";
|
||||
DrawColoredLabel(statusText, statusColor, GUILayout.Width(80));
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawColoredLabel("❌ 需修复", ColError, GUILayout.Width(60));
|
||||
if (GUILayout.Button("修复", EditorStyles.miniButton, GUILayout.Width(40)))
|
||||
FixEntry(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 底栏 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawFooter()
|
||||
{
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.HelpBox(
|
||||
"规则来源:Docs/Standards/AddressablesLabelSpec.md §3 分组规则:AssetFolderSpec.md §8.1\n" +
|
||||
"「修复所有问题」仅修改已注册资产的分组/标签,不注册新资产,不删除自定义标签(黄色警告项)。\n" +
|
||||
"新增资产工作流:① Addressable Batch Tool → ⚡ 全量扫描 _Game/ → 注册所有 ② 返回此窗口 → 扫描 → 修复所有问题",
|
||||
MessageType.None);
|
||||
}
|
||||
|
||||
// ── 扫描逻辑 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void Scan()
|
||||
{
|
||||
_reports.Clear();
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null) return;
|
||||
|
||||
foreach (var group in settings.groups)
|
||||
{
|
||||
if (group == null) continue;
|
||||
foreach (var entry in group.entries)
|
||||
{
|
||||
if (entry == null) continue;
|
||||
|
||||
var address = entry.address;
|
||||
var expectedGroup = AddressableRules.GetExpectedGroup(address);
|
||||
var expectedLbls = AddressableRules.GetExpectedLabels(address);
|
||||
var currentLbls = entry.labels.ToArray();
|
||||
|
||||
var missing = expectedLbls.Except(currentLbls, StringComparer.Ordinal).ToArray();
|
||||
|
||||
// 区分两类"多余标签":
|
||||
// extra = 规则已知标签(KnownLabels)中规则不要求的 → 红色,FixEntry 会移除
|
||||
// unknown = 不在 KnownLabels 中的自定义标签 → 黄色警告,FixEntry 保留,建议写入规范
|
||||
var notExpected = currentLbls.Except(expectedLbls, StringComparer.Ordinal);
|
||||
var extra = notExpected.Where(l => AddressableRules.KnownLabels.Contains(l)).ToArray();
|
||||
var unknown = notExpected.Where(l => !AddressableRules.KnownLabels.Contains(l)).ToArray();
|
||||
|
||||
_reports.Add(new EntryReport
|
||||
{
|
||||
Address = address,
|
||||
AssetPath = entry.AssetPath,
|
||||
CurrentGroup = group.name,
|
||||
ExpectedGroup = expectedGroup,
|
||||
CurrentLabels = currentLbls,
|
||||
ExpectedLabels = expectedLbls,
|
||||
MissingLabels = missing,
|
||||
ExtraLabels = extra,
|
||||
UnknownLabels = unknown,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 问题项排前面,仅有警告的次之,正常项排最后;同类按 Address 字母序
|
||||
_reports = _reports
|
||||
.OrderBy(r => r.IsOk ? (r.HasWarnings ? 1 : 2) : 0)
|
||||
.ThenBy(r => r.Address, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
_scanned = true;
|
||||
Repaint();
|
||||
|
||||
int issues = _reports.Count(r => !r.IsOk);
|
||||
int warnings = _reports.Count(r => r.IsOk && r.HasWarnings);
|
||||
Debug.Log($"[AddressableRuleSync] 扫描完成:{_reports.Count} 个条目," +
|
||||
$"{issues} 个需要修复,{warnings} 个含自定义标签警告。");
|
||||
}
|
||||
|
||||
// ── 修复逻辑 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void FixAll()
|
||||
{
|
||||
var issues = _reports.Where(r => !r.IsOk).ToList();
|
||||
if (issues.Count == 0) return;
|
||||
|
||||
int moveCount = issues.Count(r => !r.GroupOk);
|
||||
int addCount = issues.Sum(r => r.MissingLabels.Length);
|
||||
int removeCount = issues.Sum(r => r.ExtraLabels.Length);
|
||||
|
||||
// 干跑预览对话框
|
||||
bool confirmed = EditorUtility.DisplayDialog(
|
||||
"确认修复所有问题",
|
||||
$"将对 {issues.Count} 个条目执行以下操作:\n\n" +
|
||||
$" • 移动分组:{moveCount} 个\n" +
|
||||
$" • 添加标签:{addCount} 个\n" +
|
||||
$" • 移除多余规则标签:{removeCount} 个\n\n" +
|
||||
"⚠ 自定义标签(黄色警告项)不会被删除。\n" +
|
||||
"此操作不可撤销,请确认后继续。",
|
||||
"确认修复", "取消");
|
||||
if (!confirmed) return;
|
||||
|
||||
int fixedCount = 0;
|
||||
foreach (var r in issues)
|
||||
{
|
||||
if (FixEntry(r)) fixedCount++;
|
||||
}
|
||||
|
||||
SaveSettings();
|
||||
Scan(); // 修复后重新扫描以更新结果
|
||||
Debug.Log($"[AddressableRuleSync] 修复完成:共处理 {fixedCount} 个条目。");
|
||||
}
|
||||
|
||||
private bool FixEntry(EntryReport r)
|
||||
{
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null) return false;
|
||||
|
||||
var entry = FindEntry(settings, r.Address);
|
||||
if (entry == null)
|
||||
{
|
||||
Debug.LogWarning($"[AddressableRuleSync] 找不到条目:{r.Address}");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
|
||||
// 修复分组
|
||||
if (!r.GroupOk && r.ExpectedGroup != null)
|
||||
{
|
||||
var targetGroup = GetOrCreateGroup(settings, r.ExpectedGroup);
|
||||
if (targetGroup != null && entry.parentGroup != targetGroup)
|
||||
{
|
||||
settings.MoveEntry(entry, targetGroup, false, false);
|
||||
r.CurrentGroup = r.ExpectedGroup;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加缺失标签
|
||||
foreach (var lbl in r.MissingLabels)
|
||||
{
|
||||
EnsureLabelExists(settings, lbl);
|
||||
entry.SetLabel(lbl, true, true);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 移除多余规则标签(ExtraLabels 只包含 KnownLabels 中规则不要求的标签;
|
||||
// UnknownLabels 是用户自定义标签,刻意保留,不做删除)
|
||||
foreach (var lbl in r.ExtraLabels)
|
||||
{
|
||||
entry.SetLabel(lbl, false, true);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ── 导出 CSV ──────────────────────────────────────────────────────────
|
||||
|
||||
private void ExportCsv()
|
||||
{
|
||||
if (_reports.Count == 0) return;
|
||||
|
||||
var path = EditorUtility.SaveFilePanel(
|
||||
"导出 Addressable Rule 报告", "", "AddressableRuleReport.csv", "csv");
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Address,CurrentGroup,ExpectedGroup,GroupOk,MissingLabels,ExtraLabels,Status");
|
||||
|
||||
foreach (var r in _reports)
|
||||
{
|
||||
var status = r.IsOk ? "OK" : "ISSUE";
|
||||
sb.AppendLine(
|
||||
$"\"{r.Address}\"," +
|
||||
$"\"{r.CurrentGroup}\"," +
|
||||
$"\"{r.ExpectedGroup ?? "(uncovered)"}\"," +
|
||||
$"{r.GroupOk}," +
|
||||
$"\"{string.Join(";", r.MissingLabels)}\"," +
|
||||
$"\"{string.Join(";", r.ExtraLabels)}\"," +
|
||||
$"{status}");
|
||||
}
|
||||
|
||||
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
|
||||
Debug.Log($"[AddressableRuleSync] CSV 报告已导出:{path}");
|
||||
}
|
||||
|
||||
// ── 辅助方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static AddressableAssetEntry FindEntry(AddressableAssetSettings settings, string address)
|
||||
{
|
||||
foreach (var group in settings.groups)
|
||||
{
|
||||
if (group == null) continue;
|
||||
foreach (var e in group.entries)
|
||||
if (e != null && e.address == address) return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName)
|
||||
{
|
||||
var existing = settings.groups.FirstOrDefault(g => g != null && g.name == groupName);
|
||||
if (existing != null) return existing;
|
||||
|
||||
var template = settings.GroupTemplateObjects.FirstOrDefault()
|
||||
as AddressableAssetGroupTemplate;
|
||||
var newGroup = settings.CreateGroup(groupName, false, false, true,
|
||||
template != null
|
||||
? new List<AddressableAssetGroupSchema>(template.SchemaObjects)
|
||||
: null);
|
||||
|
||||
if (newGroup != null)
|
||||
Debug.Log($"[AddressableRuleSync] 已自动创建分组:{groupName}");
|
||||
|
||||
return newGroup ?? settings.DefaultGroup;
|
||||
}
|
||||
|
||||
private static void EnsureLabelExists(AddressableAssetSettings settings, string label)
|
||||
{
|
||||
var labels = settings.GetLabels();
|
||||
if (!labels.Contains(label))
|
||||
{
|
||||
settings.AddLabel(label, true);
|
||||
Debug.Log($"[AddressableRuleSync] 已创建标签:{label}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveSettings()
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
AddressableAssetSettingsDefaultObject.Settings?.SetDirty(
|
||||
AddressableAssetSettings.ModificationEvent.EntryModified, null, true);
|
||||
}
|
||||
|
||||
private static void PingAsset(string assetPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetPath)) return;
|
||||
var obj = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
||||
if (obj != null) EditorGUIUtility.PingObject(obj);
|
||||
}
|
||||
|
||||
// ── 样式初始化 ────────────────────────────────────────────────────────
|
||||
|
||||
private void EnsureStyles()
|
||||
{
|
||||
if (_stylesReady) return;
|
||||
|
||||
_boldStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 11 };
|
||||
_okStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColOk } };
|
||||
_warnStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColWarn } };
|
||||
_errorStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColError } };
|
||||
|
||||
_rowEven = new GUIStyle();
|
||||
_rowEven.normal.background = MakeTexture(1, 1, ColRowEven);
|
||||
|
||||
_stylesReady = true;
|
||||
}
|
||||
|
||||
private void DrawColoredLabel(string text, Color color, params GUILayoutOption[] options)
|
||||
{
|
||||
var prev = GUI.color;
|
||||
GUI.color = color;
|
||||
GUILayout.Label(text, EditorStyles.miniLabel, options);
|
||||
GUI.color = prev;
|
||||
}
|
||||
|
||||
private static Texture2D MakeTexture(int width, int height, Color color)
|
||||
{
|
||||
var tex = new Texture2D(width, height);
|
||||
tex.SetPixel(0, 0, color);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
}
|
||||
// 保留空类以避免 .meta 文件孤立。
|
||||
internal static class AddressableRuleSyncWindowStub { }
|
||||
}
|
||||
|
||||
@@ -81,7 +81,8 @@ namespace BaseGames.Enemies.AI
|
||||
|
||||
if (distance > 0.01f)
|
||||
{
|
||||
var hit = Physics2D.Raycast(origin, direction.normalized, distance, requester.LOSBlockingMask);
|
||||
// direction / distance == direction.normalized,避免重复开方
|
||||
var hit = Physics2D.Raycast(origin, direction / distance, distance, requester.LOSBlockingMask);
|
||||
// 若无遮挡物(hit.collider == null),则视线畅通
|
||||
hasLOS = hit.collider == null;
|
||||
}
|
||||
|
||||
@@ -27,15 +27,18 @@ namespace BaseGames.Enemies
|
||||
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。</summary>
|
||||
public void MoveHorizontal(float dir)
|
||||
{
|
||||
float speed = _config.WalkSpeed;
|
||||
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
|
||||
var vel = _rb.velocity;
|
||||
vel.x = dir * _config.WalkSpeed;
|
||||
_rb.velocity = vel;
|
||||
UpdateFacing(dir);
|
||||
}
|
||||
|
||||
/// <summary>显式指定速度(BD 追击任务调用)。</summary>
|
||||
public void MoveWithSpeed(float dir, float speed)
|
||||
{
|
||||
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
|
||||
var vel = _rb.velocity;
|
||||
vel.x = dir * speed;
|
||||
_rb.velocity = vel;
|
||||
UpdateFacing(dir);
|
||||
}
|
||||
|
||||
@@ -52,7 +55,9 @@ namespace BaseGames.Enemies
|
||||
|
||||
public void StopHorizontal()
|
||||
{
|
||||
_rb.velocity = new Vector2(0f, _rb.velocity.y);
|
||||
var vel = _rb.velocity;
|
||||
vel.x = 0f;
|
||||
_rb.velocity = vel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace BaseGames.Enemies
|
||||
private readonly HashSet<EnemyBase> _registeredSet = new();
|
||||
private readonly List<EnemyBase> _registered = new();
|
||||
private readonly Dictionary<EnemyBase, int> _indexMap = new();
|
||||
// 排序临时缓冲区:预计算每个敌人到玩家的距离,避免 Sort 比较器内重复 Vector3 运算(O(n logn) → O(n))
|
||||
private readonly List<(EnemyBase enemy, float sqDist)> _sortTemp = new();
|
||||
// 缓存玩家 Transform
|
||||
private Transform _playerTransform;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
@@ -72,22 +74,35 @@ namespace BaseGames.Enemies
|
||||
// ── 内部 ──────────────────────────────────────────────────────────
|
||||
private void Rebalance()
|
||||
{
|
||||
if (_registered.Count == 0) return;
|
||||
int n = _registered.Count;
|
||||
if (n == 0) return;
|
||||
|
||||
var playerPos = _playerTransform != null ? _playerTransform.position : Vector3.zero;
|
||||
|
||||
// 按距离平方升序排序(避免开方,性能更好)
|
||||
_registered.Sort((a, b) =>
|
||||
// ① 预计算距离(O(n) Vector3 运算,而非在比较器内重复执行 O(n logn) 次)
|
||||
_sortTemp.Clear();
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
if (a == null) return 1;
|
||||
if (b == null) return -1;
|
||||
float sqA = (a.transform.position - playerPos).sqrMagnitude;
|
||||
float sqB = (b.transform.position - playerPos).sqrMagnitude;
|
||||
return sqA.CompareTo(sqB);
|
||||
});
|
||||
var e = _registered[i];
|
||||
float sqd = e != null
|
||||
? (e.transform.position - playerPos).sqrMagnitude
|
||||
: float.MaxValue;
|
||||
_sortTemp.Add((e, sqd));
|
||||
}
|
||||
|
||||
// ② 对临时列表排序(比较器只做 float 比较,无额外 Vector3 开销)
|
||||
_sortTemp.Sort(static (a, b) => a.sqDist.CompareTo(b.sqDist));
|
||||
|
||||
// ③ 将排序结果写回 _registered,同步重建 _indexMap(修复排序后索引过期的 bug)
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var e = _sortTemp[i].enemy;
|
||||
_registered[i] = e;
|
||||
if (e != null) _indexMap[e] = i;
|
||||
}
|
||||
|
||||
#if GRAPH_DESIGNER
|
||||
for (int i = _registered.Count - 1; i >= 0; i--)
|
||||
for (int i = n - 1; i >= 0; i--)
|
||||
{
|
||||
var enemy = _registered[i];
|
||||
if (enemy == null) { _registered.RemoveAt(i); continue; }
|
||||
|
||||
64
Assets/_Game/Scripts/Feedback/WeaponFeedback.cs
Normal file
64
Assets/_Game/Scripts/Feedback/WeaponFeedback.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using UnityEngine;
|
||||
using MoreMountains.Feedbacks;
|
||||
|
||||
namespace BaseGames.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 武器反馈组件,挂载在武器 HitBox Prefab 根节点上。
|
||||
/// 实现 IFeedbackPlayer,专注于武器本地反馈(命中粒子、击打音效、破风等)。
|
||||
/// 角色级别的全局反馈(震屏、手柄振动)仍由 PlayerFeedback 负责。
|
||||
/// </summary>
|
||||
public class WeaponFeedback : MonoBehaviour, IFeedbackPlayer
|
||||
{
|
||||
[Header("命中反馈")]
|
||||
[SerializeField] private MMF_Player _onHitLight;
|
||||
[SerializeField] private MMF_Player _onHitMedium;
|
||||
[SerializeField] private MMF_Player _onHitHeavy;
|
||||
|
||||
[Header("攻击破风")]
|
||||
[SerializeField] private MMF_Player _onAttackWhoosh;
|
||||
|
||||
[Header("通用命名预设")]
|
||||
[SerializeField] private NamedFeedbackPreset[] _namedPresets;
|
||||
|
||||
[System.Serializable]
|
||||
private struct NamedFeedbackPreset
|
||||
{
|
||||
public string presetId;
|
||||
public MMF_Player player;
|
||||
}
|
||||
|
||||
// ── IFeedbackPlayer 实现 ──────────────────────────────────────────────
|
||||
|
||||
public void PlayHit(HitWeight weight)
|
||||
{
|
||||
var player = weight switch
|
||||
{
|
||||
HitWeight.Light => _onHitLight,
|
||||
HitWeight.Heavy => _onHitHeavy,
|
||||
_ => _onHitMedium,
|
||||
};
|
||||
player?.PlayFeedbacks();
|
||||
}
|
||||
|
||||
public void PlayAttackWhoosh() => _onAttackWhoosh?.PlayFeedbacks();
|
||||
|
||||
public void TriggerPreset(string presetId)
|
||||
{
|
||||
if (_namedPresets == null) return;
|
||||
foreach (var p in _namedPresets)
|
||||
if (p.presetId == presetId) { p.player?.PlayFeedbacks(); return; }
|
||||
}
|
||||
|
||||
// ── 武器上不适用的反馈,空实现 ────────────────────────────────────────
|
||||
public void PlayParrySuccess() { }
|
||||
public void PlayTakeHit() { }
|
||||
public void PlayDeath() { }
|
||||
public void PlayHeal() { }
|
||||
public void PlayLandImpact() { }
|
||||
public void PlayJumpLaunch() { }
|
||||
public void PlayFootstep() { }
|
||||
public void PlaySFXById(string sfxId) { }
|
||||
public void PlayFormSwitch(int formIndex){ }
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Feedback/WeaponFeedback.cs.meta
Normal file
11
Assets/_Game/Scripts/Feedback/WeaponFeedback.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: adb4af2f574f356449634bc130f94592
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -44,6 +44,8 @@ namespace BaseGames.Parry
|
||||
private ParryPhase _phase = ParryPhase.Inactive;
|
||||
private float _phaseTimer;
|
||||
private float _cooldownTimer;
|
||||
// 缓存 WaitForSecondsRealtime,避免每次完美弹反都触发 GC 分配
|
||||
private WaitForSecondsRealtime _bulletTimeWait;
|
||||
|
||||
public ParryPhase CurrentPhase => _phase;
|
||||
/// <summary>是否处于弹反有效窗口(供外部检测)。</summary>
|
||||
@@ -74,6 +76,13 @@ namespace BaseGames.Parry
|
||||
Debug.Assert(_config != null, "[ParrySystem] _config 未赋值,请在 Inspector 中指定 ParryConfigSO。", this);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 在 Start 中初始化,确保 _config 已被赋值(Awake 之后)
|
||||
if (_config != null)
|
||||
_bulletTimeWait = new WaitForSecondsRealtime(_config.BulletTimeDuration);
|
||||
}
|
||||
|
||||
/// <summary>由 PlayerController 在 Awake 中注入 InputReader,无需在 Inspector 单独指定。</summary>
|
||||
public void SetInputReader(InputReaderSO reader)
|
||||
{
|
||||
@@ -206,7 +215,7 @@ namespace BaseGames.Parry
|
||||
private IEnumerator ApplyBulletTime()
|
||||
{
|
||||
Time.timeScale = _config.BulletTimeScale;
|
||||
yield return new WaitForSecondsRealtime(_config.BulletTimeDuration);
|
||||
yield return _bulletTimeWait;
|
||||
Time.timeScale = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
@@ -14,6 +15,7 @@ namespace BaseGames.Player
|
||||
private PlayerStats _stats;
|
||||
private PlayerMovement _movement;
|
||||
private WeaponHitBoxInstance _currentHitBoxInstance;
|
||||
private IFeedbackPlayer _feedback;
|
||||
|
||||
/// <summary>下劈 HitBox 命中确认事件(供 DownAttackState 订阅 pogo 弹跳逻辑)。</summary>
|
||||
public event System.Action<DamageInfo> OnDownHitConfirmed;
|
||||
@@ -22,6 +24,9 @@ namespace BaseGames.Player
|
||||
{
|
||||
_stats = GetComponentInParent<PlayerStats>();
|
||||
_movement = GetComponentInParent<PlayerMovement>();
|
||||
_feedback = GetComponentInParent<IFeedbackPlayer>()
|
||||
?? GetComponentInChildren<IFeedbackPlayer>()
|
||||
?? NullFeedbackPlayer.Instance;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -83,6 +88,12 @@ namespace BaseGames.Player
|
||||
int gain = _weaponManager?.ActiveWeapon?.soulPowerGain ?? 10;
|
||||
_stats?.AddSoulPower(gain);
|
||||
|
||||
// 命中反馈:按伤害量决定力度档位
|
||||
var weight = info.FinalDamage <= 5 ? HitWeight.Light
|
||||
: info.FinalDamage <= 15 ? HitWeight.Medium
|
||||
: HitWeight.Heavy;
|
||||
_feedback.PlayHit(weight);
|
||||
|
||||
// 攻击命中反嵈:向攻击反方向施加微小后退冲量,增强打击感
|
||||
if (_movement?.Rb != null && info.KnockbackDirection.x != 0f)
|
||||
_movement.Rb.AddForce(
|
||||
|
||||
@@ -46,8 +46,12 @@ namespace BaseGames.Player
|
||||
private bool _facingLocked; // 为 true 时 UpdateFacing() 不覆盖朝向
|
||||
private bool _cancelWindowOpen;
|
||||
private SurfaceType _currentSurface = SurfaceType.Ground;
|
||||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
||||
private int _groundHitCount;
|
||||
private bool _wasGrounded;
|
||||
// 跳跃/二段跳期间禁用斜坡吸附,防止把起跳判定成斜坡而立即下压
|
||||
private bool _slopeSnapDisabled;
|
||||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
||||
private int _groundHitCount;
|
||||
private readonly ContactPoint2D[] _slopeContactBuffer = new ContactPoint2D[8];
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// ── 运行时调试(Inspector 中可见)───────────────────────────────
|
||||
@@ -118,17 +122,20 @@ namespace BaseGames.Player
|
||||
_wallCoyoteTimer = Mathf.Max(0f, _wallCoyoteTimer - Time.fixedDeltaTime);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
_dbg_VelocityX = _rb.velocity.x;
|
||||
_dbg_VelocityY = _rb.velocity.y;
|
||||
_dbg_IsGrounded = _isGrounded;
|
||||
_dbg_OnOneWayPlatform = _onOneWayPlatform;
|
||||
_dbg_HasCoyoteTime = _coyoteTimer > 0f;
|
||||
_dbg_HasWallCoyoteTime = _wallCoyoteTimer > 0f;
|
||||
_dbg_IsWallLeft = _isWallLeft;
|
||||
_dbg_IsWallRight = _isWallRight;
|
||||
_dbg_CancelWindowOpen = _cancelWindowOpen;
|
||||
_dbg_FacingDirection = _facingDirection;
|
||||
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
|
||||
// 值类型字段每帧同步(无分配)
|
||||
_dbg_VelocityX = _rb.velocity.x;
|
||||
_dbg_VelocityY = _rb.velocity.y;
|
||||
_dbg_IsGrounded = _isGrounded;
|
||||
_dbg_OnOneWayPlatform = _onOneWayPlatform;
|
||||
_dbg_HasCoyoteTime = _coyoteTimer > 0f;
|
||||
_dbg_HasWallCoyoteTime = _wallCoyoteTimer > 0f;
|
||||
_dbg_IsWallLeft = _isWallLeft;
|
||||
_dbg_IsWallRight = _isWallRight;
|
||||
_dbg_CancelWindowOpen = _cancelWindowOpen;
|
||||
_dbg_FacingDirection = _facingDirection;
|
||||
// 字符串格式化限速到 ~10 Hz,避免每帧分配
|
||||
if (Time.frameCount % 6 == 0)
|
||||
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -175,6 +182,7 @@ namespace BaseGames.Player
|
||||
{
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, _config.JumpForce);
|
||||
_coyoteTimer = 0f;
|
||||
_slopeSnapDisabled = true;
|
||||
}
|
||||
|
||||
public void CutJump()
|
||||
@@ -191,6 +199,7 @@ namespace BaseGames.Player
|
||||
{
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, _config.DoubleJumpForce);
|
||||
_coyoteTimer = 0f;
|
||||
_slopeSnapDisabled = true;
|
||||
}
|
||||
|
||||
// ── 重力 ──────────────────────────────────────────────────────────────
|
||||
@@ -379,10 +388,38 @@ namespace BaseGames.Player
|
||||
{
|
||||
if (_groundCheck == null) return;
|
||||
|
||||
_wasGrounded = _isGrounded;
|
||||
|
||||
_groundHitCount = Physics2D.OverlapBoxNonAlloc(
|
||||
_groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer);
|
||||
_isGrounded = _groundHitCount > 0;
|
||||
|
||||
// 斜坡吸附禁用标记:仅在重新落地(从空中→地面)时重置,
|
||||
// 而非每帧在地面时都重置。
|
||||
// 这样 Jump() 设置的 _slopeSnapDisabled = true 可以存活到玩家真正离开地面,
|
||||
// 防止起跳后的首个 FixedUpdate 仍检测到地面时把标记清零,
|
||||
// 导致紧接着的斜坡吸附把垂直速度归零(即"一直按方向键起跳立即落地"bug)。
|
||||
if (_isGrounded && !_wasGrounded)
|
||||
_slopeSnapDisabled = false;
|
||||
|
||||
// 斜坡吸附:OverlapBox 是水平矩形,在平地→斜坡转折处可能短暂离地。
|
||||
// 读取 Rigidbody2D 已有的物理接触点(零额外物理查询开销),
|
||||
// 接触法线 Y > 0.5 即视为地面接触,保持 IsGrounded 为 true。
|
||||
if (!_isGrounded && _wasGrounded && !_slopeSnapDisabled
|
||||
&& Mathf.Abs(_rb.velocity.x) > 0.1f)
|
||||
{
|
||||
int contactCount = _rb.GetContacts(_slopeContactBuffer);
|
||||
for (int i = 0; i < contactCount; i++)
|
||||
{
|
||||
if (_slopeContactBuffer[i].normal.y > 0.5f)
|
||||
{
|
||||
_isGrounded = true;
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, 0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测是否站在单向平台(含 IDropThrough 组件的碰撞体)
|
||||
_onOneWayPlatform = false;
|
||||
for (int i = 0; i < _groundHitCount; i++)
|
||||
|
||||
@@ -127,14 +127,19 @@ namespace BaseGames.Player
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
_dbg_HP = $"{CurrentHP} / {MaxHP}";
|
||||
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
|
||||
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
|
||||
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
|
||||
// 非字符串字段每帧同步(拷贝值,无分配)
|
||||
_dbg_IsInvincible = IsInvincible;
|
||||
_dbg_InvincibleTimer = _invincibleTimer;
|
||||
_dbg_GodMode = _isGodMode;
|
||||
_dbg_Abilities = _unlockedAbilities == AbilityType.None ? "\u65e0" : _unlockedAbilities.ToString();
|
||||
// 字符串插值限速到 ~10 Hz,避免每帧分配(GC)
|
||||
if (Time.frameCount % 6 == 0)
|
||||
{
|
||||
_dbg_HP = $"{CurrentHP} / {MaxHP}";
|
||||
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
|
||||
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
|
||||
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
|
||||
_dbg_Abilities = _unlockedAbilities == AbilityType.None ? "\u65e0" : _unlockedAbilities.ToString();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
// ── 护符修改器 API ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace BaseGames.Player
|
||||
// 物理接触点缓冲区(避免每帧 GC)
|
||||
private Rigidbody2D _rb;
|
||||
private static readonly ContactPoint2D[] _contactBuffer = new ContactPoint2D[8];
|
||||
// LayerMask 在 Awake 解析一次,避免 FixedUpdate(50Hz)每帧字符串查找
|
||||
private LayerMask _resolvedWallMask;
|
||||
|
||||
/// <summary>
|
||||
/// 指定方向上是否存在墙壁接触(射线命中 OR 物理接触点命中,任一为 true)。
|
||||
@@ -43,6 +45,7 @@ namespace BaseGames.Player
|
||||
{
|
||||
Debug.Assert(_config != null, "[PlayerWallDetector] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
_resolvedWallMask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Platform");
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
@@ -69,9 +72,8 @@ namespace BaseGames.Player
|
||||
private bool CheckPhysicalContact(int direction)
|
||||
{
|
||||
if (_rb == null) return false;
|
||||
LayerMask mask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Platform");
|
||||
var filter = new ContactFilter2D();
|
||||
filter.SetLayerMask(mask);
|
||||
filter.SetLayerMask(_resolvedWallMask);
|
||||
filter.useTriggers = false;
|
||||
|
||||
int count = _rb.GetContacts(filter, _contactBuffer);
|
||||
@@ -94,7 +96,7 @@ namespace BaseGames.Player
|
||||
Vector2 center = transform.position;
|
||||
float len = _config.WallRayLength;
|
||||
float oy = _config.WallRayOffsetY;
|
||||
int layer = _wallLayer != 0 ? (int)_wallLayer : LayerMask.GetMask("Wall", "Platform");
|
||||
int layer = _resolvedWallMask;
|
||||
|
||||
bool top = Physics2D.Raycast(center + Vector2.up * oy, dir, len, layer);
|
||||
bool bot = Physics2D.Raycast(center + Vector2.down * oy, dir, len, layer);
|
||||
|
||||
@@ -82,6 +82,8 @@ namespace BaseGames.Player.States
|
||||
? AnimCfg.DashInvincible
|
||||
: AnimCfg?.Dash;
|
||||
if (dashClip != null) Anim?.Play(dashClip);
|
||||
|
||||
Feedback.TriggerPreset("dash");
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace BaseGames.Player.States
|
||||
if (Owner.HurtBox != null)
|
||||
Owner.HurtBox.SetActive(false);
|
||||
|
||||
Feedback.PlayDeath();
|
||||
|
||||
// 播放死亡动画
|
||||
if (AnimCfg?.Dead != null)
|
||||
Anim?.Play(AnimCfg.Dead);
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace BaseGames.Player.States
|
||||
_timer = Owner.AnimConfig?.HurtDuration ?? 0.4f;
|
||||
_ended = false;
|
||||
Stats?.BeginInvincibility();
|
||||
Feedback.PlayTakeHit();
|
||||
|
||||
if (AnimCfg?.Hurt != null)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using Animancer;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Feedback;
|
||||
using BaseGames.Parry;
|
||||
using BaseGames.Skills;
|
||||
|
||||
@@ -35,6 +36,7 @@ namespace BaseGames.Player.States
|
||||
|
||||
// ── 战斗组件 ──────────────────────────────────────────────────────────
|
||||
[Header("战斗")]
|
||||
[SerializeField] private PlayerFeedback _feedback;
|
||||
[SerializeField] private PlayerCombat _combat;
|
||||
[SerializeField] private FormController _formController;
|
||||
[SerializeField] private WeaponManager _weaponManager;
|
||||
@@ -57,6 +59,8 @@ namespace BaseGames.Player.States
|
||||
private InputBuffer _inputBuffer;
|
||||
private bool _missingDependencyLogged;
|
||||
private bool _dependenciesReady;
|
||||
// DashState 在 Update 每帧访问(TickCooldown + CanDash),提前缓存避免重复 Dictionary 查找
|
||||
private DashState _dashState;
|
||||
/// <summary>
|
||||
/// 当前腾空可用的额外跳跃次数(二段跳)。
|
||||
/// 由 IdleState/RunState.OnStateEnter 落地时通过 ResetAirJumps() 重置;
|
||||
@@ -128,6 +132,7 @@ namespace BaseGames.Player.States
|
||||
public InputReaderSO Input => _inputReader;
|
||||
public InputBuffer Buffer => _inputBuffer;
|
||||
|
||||
public IFeedbackPlayer Feedback => _feedback != null ? (IFeedbackPlayer)_feedback : NullFeedbackPlayer.Instance;
|
||||
public PlayerCombat Combat => _combat;
|
||||
public FormController Form => _formController;
|
||||
public WeaponManager Weapon => _weaponManager;
|
||||
@@ -272,6 +277,7 @@ namespace BaseGames.Player.States
|
||||
{
|
||||
_stats?.AddSoul(info.SoulGained);
|
||||
_shield?.OnParrySuccess();
|
||||
Feedback.PlayParrySuccess();
|
||||
}
|
||||
|
||||
/// <summary>灵泉输入:地面且有剩余充能时转入 SpringState 使用一次。</summary>
|
||||
@@ -301,7 +307,7 @@ namespace BaseGames.Player.States
|
||||
return;
|
||||
|
||||
// 冲刺冷却计时
|
||||
GetState<DashState>()?.TickCooldown(Time.deltaTime);
|
||||
_dashState?.TickCooldown(Time.deltaTime);
|
||||
|
||||
_currentState?.OnStateUpdate();
|
||||
|
||||
@@ -309,7 +315,7 @@ namespace BaseGames.Player.States
|
||||
_dbg_CurrentState = _currentState?.GetType().Name ?? "None";
|
||||
_dbg_IsGrounded = _movement != null && _movement.IsGrounded;
|
||||
_dbg_AirJumpsLeft = _airJumpsLeft;
|
||||
_dbg_CanDash = GetState<DashState>()?.CanDash ?? false;
|
||||
_dbg_CanDash = _dashState?.CanDash ?? false;
|
||||
_dbg_IsInvincible = _stats != null && _stats.IsInvincible;
|
||||
#endif
|
||||
}
|
||||
@@ -359,8 +365,9 @@ namespace BaseGames.Player.States
|
||||
_states[typeof(HurtState)] = new HurtState(this);
|
||||
_states[typeof(DeadState)] = new DeadState(this);
|
||||
_states[typeof(SpringState)] = new SpringState(this);
|
||||
_states[typeof(ParryState)] = new ParryState(this);
|
||||
_states[typeof(SwimState)] = new SwimState(this);
|
||||
_states[typeof(ParryState)] = new ParryState(this);
|
||||
_states[typeof(SwimState)] = new SwimState(this);
|
||||
_dashState = (DashState)_states[typeof(DashState)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Animancer;
|
||||
using BaseGames.Feedback;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Player;
|
||||
|
||||
@@ -41,6 +42,7 @@ namespace BaseGames.Player.States
|
||||
protected InputBuffer Buffer => _owner.Buffer;
|
||||
protected PlayerMovement Move => _owner.Movement;
|
||||
protected PlayerStats Stats => _owner.Stats;
|
||||
protected IFeedbackPlayer Feedback => _owner.Feedback;
|
||||
protected AnimancerComponent Anim => _owner.Animancer;
|
||||
protected PlayerMovementConfigSO Cfg => _owner.MovConfig;
|
||||
protected PlayerAnimationConfigSO AnimCfg => _owner.AnimConfig;
|
||||
|
||||
@@ -49,8 +49,9 @@ namespace BaseGames.Player.States
|
||||
|
||||
private void OnSpringEnd()
|
||||
{
|
||||
// 前摇正常结束 → 执行回血
|
||||
// 前摇正常结束 → 执行回血 + 反馈
|
||||
Stats?.ApplySpringHeal();
|
||||
Feedback.PlayHeal();
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
@@ -28,6 +29,7 @@ namespace BaseGames.Player
|
||||
|
||||
private HitBox[] _allHitBoxes;
|
||||
private AttackDirection _activeDir;
|
||||
private IFeedbackPlayer _feedback;
|
||||
|
||||
/// <summary>下劈命中确认事件(供 DownAttackState Pogo 逻辑)。</summary>
|
||||
public event System.Action<DamageInfo> OnDownHitConfirmed;
|
||||
@@ -40,10 +42,16 @@ namespace BaseGames.Player
|
||||
_allHitBoxes = GetComponentsInChildren<HitBox>(true);
|
||||
foreach (var hb in _allHitBoxes)
|
||||
hb.OnHitConfirmed += OnAnyHitConfirmed;
|
||||
_feedback = GetComponentInChildren<IFeedbackPlayer>() ?? NullFeedbackPlayer.Instance;
|
||||
}
|
||||
|
||||
private void OnAnyHitConfirmed(DamageInfo info)
|
||||
{
|
||||
var weight = info.FinalDamage <= 5 ? HitWeight.Light
|
||||
: info.FinalDamage <= 15 ? HitWeight.Medium
|
||||
: HitWeight.Heavy;
|
||||
_feedback.PlayHit(weight);
|
||||
|
||||
OnHitConfirmed?.Invoke(info);
|
||||
if (_activeDir == AttackDirection.Down)
|
||||
OnDownHitConfirmed?.Invoke(info);
|
||||
@@ -59,6 +67,7 @@ namespace BaseGames.Player
|
||||
string hitBoxId = "")
|
||||
{
|
||||
_activeDir = dir;
|
||||
_feedback.PlayAttackWhoosh();
|
||||
var hitBox = string.IsNullOrEmpty(hitBoxId)
|
||||
? GetHitBox(dir)
|
||||
: (GetHitBoxById(hitBoxId) ?? GetHitBox(dir));
|
||||
|
||||
@@ -59,6 +59,9 @@ namespace BaseGames.Player
|
||||
[Min(0)]
|
||||
public int soulPowerGain = 10;
|
||||
|
||||
[Tooltip("命中敌人时的打击力度反馈档位(影响摄像机震屏和控制器振动强度)。")]
|
||||
public HitWeight hitWeight = HitWeight.Medium;
|
||||
|
||||
// ── 查询 API ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>取指定方向、指定段的完整配置,越界自动取最后一个。</summary>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"BaseGames.Player",
|
||||
"BaseGames.Input",
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.Feedback",
|
||||
"Kybernetik.Animancer"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.Skills
|
||||
{
|
||||
@@ -41,13 +42,23 @@ namespace BaseGames.Skills
|
||||
private FormSkillSO _soulSkill;
|
||||
private FormSkillSO _spirit1;
|
||||
private FormSkillSO _spirit2;
|
||||
private IFeedbackPlayer _feedback;
|
||||
|
||||
// 冷却字典(FormSkillSO → 剩余冷却秒数),UpdateSkillSet 时重建
|
||||
private readonly Dictionary<FormSkillSO, float> _cooldowns = new(3);
|
||||
// 无分配 Update 遍历用的快照数组
|
||||
private FormSkillSO[] _activeSkills = System.Array.Empty<FormSkillSO>();
|
||||
// 技能 HitBox 对象池:prefab → 已创建的实例列表,通过 activeSelf 判断是否可复用
|
||||
private readonly Dictionary<GameObject, List<SkillHitBoxInstance>> _hitBoxPools = new();
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
private void Awake()
|
||||
{
|
||||
_feedback = GetComponentInChildren<IFeedbackPlayer>()
|
||||
?? GetComponentInParent<IFeedbackPlayer>()
|
||||
?? NullFeedbackPlayer.Instance;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_input != null)
|
||||
@@ -147,6 +158,9 @@ namespace BaseGames.Skills
|
||||
|
||||
_cooldowns[skill] = p.effectiveCooldown;
|
||||
|
||||
// 施放反馈
|
||||
_feedback.TriggerPreset("skill_cast");
|
||||
|
||||
// 播放动画(优先修改器动画,回退技能默认动画)
|
||||
var clip = p.effectiveAnimation.Clip != null
|
||||
? p.effectiveAnimation
|
||||
@@ -158,14 +172,40 @@ namespace BaseGames.Skills
|
||||
if (skill.SkillHitBoxPrefab != null)
|
||||
{
|
||||
var socket = _skillSocket != null ? _skillSocket : transform;
|
||||
var go = Object.Instantiate(skill.SkillHitBoxPrefab, socket.position,
|
||||
socket.rotation, socket);
|
||||
var inst = go.GetComponent<SkillHitBoxInstance>();
|
||||
var inst = GetOrCreateHitBox(skill.SkillHitBoxPrefab, socket);
|
||||
inst?.Activate(skill.damageSource, transform);
|
||||
inst?.AutoDestroyAfter(skill.castLockDuration > 0f ? skill.castLockDuration : 0.5f);
|
||||
inst?.AutoReturnAfter(skill.castLockDuration > 0f ? skill.castLockDuration : 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从对象池获取或新建 SkillHitBoxInstance。
|
||||
/// 扫描该 prefab 已创建的实例列表,找到首个未激活的复用;
|
||||
/// 无可用实例时 Instantiate,并追加到列表供下次复用。
|
||||
/// </summary>
|
||||
private SkillHitBoxInstance GetOrCreateHitBox(GameObject prefab, Transform socket)
|
||||
{
|
||||
if (!_hitBoxPools.TryGetValue(prefab, out var list))
|
||||
_hitBoxPools[prefab] = list = new List<SkillHitBoxInstance>(2);
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
var pooled = list[i];
|
||||
if (pooled != null && !pooled.gameObject.activeSelf)
|
||||
{
|
||||
pooled.transform.SetParent(socket);
|
||||
pooled.transform.SetPositionAndRotation(socket.position, socket.rotation);
|
||||
pooled.gameObject.SetActive(true);
|
||||
return pooled;
|
||||
}
|
||||
}
|
||||
|
||||
var go = Object.Instantiate(prefab, socket.position, socket.rotation, socket);
|
||||
var inst = go.GetComponent<SkillHitBoxInstance>();
|
||||
if (inst != null) list.Add(inst);
|
||||
return inst;
|
||||
}
|
||||
|
||||
// ── 属性查询 ─────────────────────────────────────────────────────────
|
||||
public FormSkillSO SoulSkill => _soulSkill;
|
||||
public FormSkillSO Spirit1 => _spirit1;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"BaseGames.Input",
|
||||
"BaseGames.Player",
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.Feedback",
|
||||
"BaseGames.Skills",
|
||||
"Kybernetik.Animancer"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.Spells
|
||||
{
|
||||
@@ -24,9 +25,17 @@ namespace BaseGames.Spells
|
||||
// 当前装备的法术(单槽;如需多槽可扩展为数组)
|
||||
private SpellSO _equippedSpell;
|
||||
private float _cooldownRemaining;
|
||||
private IFeedbackPlayer _feedback;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_feedback = GetComponentInChildren<IFeedbackPlayer>()
|
||||
?? GetComponentInParent<IFeedbackPlayer>()
|
||||
?? NullFeedbackPlayer.Instance;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_input != null)
|
||||
@@ -86,6 +95,9 @@ namespace BaseGames.Spells
|
||||
|
||||
_cooldownRemaining = _equippedSpell.cooldown;
|
||||
|
||||
// 施放反馈
|
||||
_feedback.TriggerPreset("spell_cast");
|
||||
|
||||
ExecuteSpellEffect(_equippedSpell);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,11 +27,12 @@ namespace BaseGames.UI
|
||||
|
||||
private RectTransform _rectTransform;
|
||||
private Coroutine _animCoroutine;
|
||||
// 每次 Show() 解析一次,协程期间(< 1s)复用,避免每帧走 FindObjectByTag
|
||||
private Camera _cachedCamera;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_rectTransform = (RectTransform)transform;
|
||||
// 不在 Awake 缓存 Camera.main,避免 Boss 过场切换主摄像机后引用过期
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -42,6 +43,12 @@ namespace BaseGames.UI
|
||||
{
|
||||
if (_animCoroutine != null) StopCoroutine(_animCoroutine);
|
||||
|
||||
// 每次 Show 解析一次摄像机:动画时长 < 1s,期间不会切换主摄像机;
|
||||
// 若 Boss 过场后再次 Show,会自动获取新的主摄像机。
|
||||
_cachedCamera = (_parentCanvas != null && _parentCanvas.renderMode == RenderMode.ScreenSpaceCamera)
|
||||
? _parentCanvas.worldCamera
|
||||
: UnityEngine.Camera.main;
|
||||
|
||||
_text.text = damage.ToString();
|
||||
_text.color = GetColorForType(type);
|
||||
|
||||
@@ -51,9 +58,7 @@ namespace BaseGames.UI
|
||||
|
||||
private void SetAnchoredPosition(Vector2 worldPosition)
|
||||
{
|
||||
var cam = (_parentCanvas != null && _parentCanvas.renderMode == RenderMode.ScreenSpaceCamera)
|
||||
? _parentCanvas.worldCamera
|
||||
: UnityEngine.Camera.main;
|
||||
var cam = _cachedCamera;
|
||||
|
||||
var screenPoint = cam != null
|
||||
? (Vector2)cam.WorldToScreenPoint(worldPosition)
|
||||
@@ -122,7 +127,7 @@ namespace BaseGames.UI
|
||||
[Header("预制体(对象池 key = AddressKeys.PrefabUIFloatingDmgText)")]
|
||||
[SerializeField] private GameObject _floatingDmgPrefab; // Fallback:Inspector 直接拖入
|
||||
|
||||
private readonly Queue<FloatingDamageText> _pool = new();
|
||||
private readonly List<FloatingDamageText> _pool = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable() => _onDamageDealt?.Subscribe(OnDamageDealt).AddTo(_subs);
|
||||
@@ -138,25 +143,22 @@ namespace BaseGames.UI
|
||||
|
||||
private FloatingDamageText GetOrCreate()
|
||||
{
|
||||
// 从池中找到已停用的实例
|
||||
while (_pool.Count > 0)
|
||||
// 线性扫描全部已创建实例,找首个未激活的复用
|
||||
for (int i = 0; i < _pool.Count; i++)
|
||||
{
|
||||
var pooled = _pool.Dequeue();
|
||||
if (pooled == null) continue;
|
||||
if (!pooled.gameObject.activeSelf)
|
||||
var pooled = _pool[i];
|
||||
if (pooled != null && !pooled.gameObject.activeSelf)
|
||||
{
|
||||
pooled.gameObject.SetActive(true);
|
||||
return pooled;
|
||||
}
|
||||
_pool.Enqueue(pooled); // 仍在使用,放回
|
||||
break;
|
||||
}
|
||||
|
||||
// 没有可用实例则实例化
|
||||
// 没有可用实例则实例化新的,加入列表供下次复用
|
||||
if (_floatingDmgPrefab == null) return null;
|
||||
var go = Instantiate(_floatingDmgPrefab, transform);
|
||||
var comp = go.GetComponent<FloatingDamageText>();
|
||||
if (comp != null) _pool.Enqueue(comp);
|
||||
if (comp != null) _pool.Add(comp);
|
||||
return comp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,11 @@ namespace BaseGames.UI
|
||||
_maxHP = max;
|
||||
// 重建阶段标记(每次 BossHPMax 改变时清空并按已存阈值重建,此处简化为清空)
|
||||
if (_phaseMarkersRoot != null)
|
||||
foreach (Transform t in _phaseMarkersRoot) Destroy(t.gameObject);
|
||||
{
|
||||
// 逆序删除:避免正序枚举 Transform 子节点同时销毁时的迭代器失效
|
||||
for (int i = _phaseMarkersRoot.childCount - 1; i >= 0; i--)
|
||||
Destroy(_phaseMarkersRoot.GetChild(i).gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 动画协程 ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace BaseGames.UI.HUD
|
||||
private readonly List<GameObject> _hpCells = new();
|
||||
private readonly List<GameObject> _springIcons = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
private int _lastLingZhu = int.MinValue;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
@@ -91,7 +92,9 @@ namespace BaseGames.UI.HUD
|
||||
|
||||
private void UpdateLingZhu(int val)
|
||||
{
|
||||
if (_lingZhuText != null) _lingZhuText.text = val.ToString();
|
||||
if (_lingZhuText == null || val == _lastLingZhu) return;
|
||||
_lastLingZhu = val;
|
||||
_lingZhuText.text = val.ToString();
|
||||
}
|
||||
|
||||
private void RebuildSpringIcons(int charges)
|
||||
|
||||
@@ -46,19 +46,17 @@ namespace BaseGames.World
|
||||
{
|
||||
Vector2 playerPos = player.transform.position;
|
||||
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
var zones = Object.FindObjectsByType<CameraTriggerZone>(FindObjectsSortMode.None);
|
||||
#else
|
||||
var zones = Object.FindObjectsOfType<CameraTriggerZone>();
|
||||
#endif
|
||||
// 使用 CameraTriggerZone 静态注册表,避免 FindObjectsOfType 全场景扫描
|
||||
var zones = CameraTriggerZone.AllZones;
|
||||
|
||||
// 选取优先级最高的匹配区域(避免多区域重叠时选错基线)
|
||||
CameraArea bestArea = null;
|
||||
int bestPriority = int.MinValue;
|
||||
|
||||
foreach (var zone in zones)
|
||||
{
|
||||
var poly = zone.GetComponent<PolygonCollider2D>();
|
||||
if (poly != null && poly.OverlapPoint(playerPos))
|
||||
if (zone == null) continue;
|
||||
if (zone.ContainsPoint(playerPos))
|
||||
{
|
||||
var area = zone.GetComponentInParent<CameraArea>();
|
||||
if (area != null && zone.Priority > bestPriority)
|
||||
|
||||
BIN
Docs/Profiler/jump.data
Normal file
BIN
Docs/Profiler/jump.data
Normal file
Binary file not shown.
Reference in New Issue
Block a user