feat: Add HurtBoxOwnerGuard to prevent multiple damage registrations from the same HitBox activation
- Implemented HurtBoxOwnerGuard to ensure that multiple HurtBoxes on the same character do not register damage multiple times during a single HitBox activation. - Added custom editor for HitBox to facilitate the creation of shape colliders with HitBoxColliderProxy. - Introduced PhysicsPerceptionSystem for enemy perception, supporting multiple detection modes including RangeCircle, BatchLOS, FanCast, and BoxCast. - Created EnemyPatrolZone to define patrol and chase areas for enemies, allowing for shared zones among multiple enemies. - Added BD_IsOutsideZone conditional task for Behavior Designer to check if an enemy or player is outside a defined patrol zone.
This commit is contained in:
@@ -61,7 +61,7 @@ AnimationClip:
|
|||||||
m_Level: 0
|
m_Level: 0
|
||||||
m_CycleOffset: 0
|
m_CycleOffset: 0
|
||||||
m_HasAdditiveReferencePose: 0
|
m_HasAdditiveReferencePose: 0
|
||||||
m_LoopTime: 1
|
m_LoopTime: 0
|
||||||
m_LoopBlend: 0
|
m_LoopBlend: 0
|
||||||
m_LoopBlendOrientation: 0
|
m_LoopBlendOrientation: 0
|
||||||
m_LoopBlendPositionY: 0
|
m_LoopBlendPositionY: 0
|
||||||
|
|||||||
@@ -7766,75 +7766,6 @@ Transform:
|
|||||||
- {fileID: 2120252236}
|
- {fileID: 2120252236}
|
||||||
m_Father: {fileID: 495476726}
|
m_Father: {fileID: 495476726}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &435966522
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 435966523}
|
|
||||||
- component: {fileID: 435966524}
|
|
||||||
m_Layer: 0
|
|
||||||
m_Name: ContactChaseAbility_Chase
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!4 &435966523
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 435966522}
|
|
||||||
serializedVersion: 2
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children: []
|
|
||||||
m_Father: {fileID: 1340413309}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!114 &435966524
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 435966522}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 7bfd6f44ebdb5bf489ab6703b1ee429b, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
_config: {fileID: 11400000, guid: 0adeaa8a8508fbd40986dbb71cc85acd, type: 2}
|
|
||||||
_loopClip:
|
|
||||||
_FadeDuration: 0.25
|
|
||||||
_Speed: 1
|
|
||||||
_Events:
|
|
||||||
_NormalizedTimes: []
|
|
||||||
_Callbacks: []
|
|
||||||
_Names: []
|
|
||||||
_Clip: {fileID: 0}
|
|
||||||
_NormalizedStartTime: NaN
|
|
||||||
_endClip:
|
|
||||||
_FadeDuration: 0.25
|
|
||||||
_Speed: 1
|
|
||||||
_Events:
|
|
||||||
_NormalizedTimes: []
|
|
||||||
_Callbacks: []
|
|
||||||
_Names: []
|
|
||||||
_Clip: {fileID: 0}
|
|
||||||
_NormalizedStartTime: NaN
|
|
||||||
_contactDamage: {fileID: 1347552443}
|
|
||||||
_sensorHub: {fileID: 1584752068}
|
|
||||||
_aggroSlotName: aggro
|
|
||||||
references:
|
|
||||||
version: 2
|
|
||||||
RefIds: []
|
|
||||||
--- !u!1 &442873750
|
--- !u!1 &442873750
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -10679,63 +10610,6 @@ Transform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 52454202}
|
m_Father: {fileID: 52454202}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &637286414
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 637286415}
|
|
||||||
- component: {fileID: 637286416}
|
|
||||||
m_Layer: 0
|
|
||||||
m_Name: PlayClipAbility_Alert
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!4 &637286415
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 637286414}
|
|
||||||
serializedVersion: 2
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children: []
|
|
||||||
m_Father: {fileID: 1340413309}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!114 &637286416
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 637286414}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: a26fca0fa72894a4da1a5a58ee023154, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
_config: {fileID: 11400000, guid: 157dc45e6b444c64ea1a80a5886a8b92, type: 2}
|
|
||||||
_clip:
|
|
||||||
_FadeDuration: 0.25
|
|
||||||
_Speed: 1
|
|
||||||
_Events:
|
|
||||||
_NormalizedTimes: []
|
|
||||||
_Callbacks: []
|
|
||||||
_Names: []
|
|
||||||
_Clip: {fileID: 0}
|
|
||||||
_NormalizedStartTime: NaN
|
|
||||||
references:
|
|
||||||
version: 2
|
|
||||||
RefIds: []
|
|
||||||
--- !u!1 &645364058
|
--- !u!1 &645364058
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -23507,39 +23381,6 @@ MonoBehaviour:
|
|||||||
_noiseFrequency: 1
|
_noiseFrequency: 1
|
||||||
_dedicatedCamera: {fileID: 2064386419}
|
_dedicatedCamera: {fileID: 2064386419}
|
||||||
_dedicatedPriority: 20
|
_dedicatedPriority: 20
|
||||||
--- !u!1 &1340413308
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 1340413309}
|
|
||||||
m_Layer: 0
|
|
||||||
m_Name: Abilities
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!4 &1340413309
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1340413308}
|
|
||||||
serializedVersion: 2
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 637286415}
|
|
||||||
- {fileID: 435966523}
|
|
||||||
m_Father: {fileID: 1584752080}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!1 &1341564411
|
--- !u!1 &1341564411
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -23919,106 +23760,6 @@ MonoBehaviour:
|
|||||||
_noiseFrequency: 1
|
_noiseFrequency: 1
|
||||||
_dedicatedCamera: {fileID: 1901040639}
|
_dedicatedCamera: {fileID: 1901040639}
|
||||||
_dedicatedPriority: 20
|
_dedicatedPriority: 20
|
||||||
--- !u!1 &1347552442
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 1347552446}
|
|
||||||
- component: {fileID: 1347552445}
|
|
||||||
- component: {fileID: 1347552444}
|
|
||||||
- component: {fileID: 1347552443}
|
|
||||||
m_Layer: 25
|
|
||||||
m_Name: ContactDamageZone
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!114 &1347552443
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1347552442}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 6def12af0589a9545b80eb5accf61bb6, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
_repeatInterval: 0.5
|
|
||||||
--- !u!114 &1347552444
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1347552442}
|
|
||||||
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:
|
|
||||||
_rivalHitBoxMask:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 0
|
|
||||||
--- !u!58 &1347552445
|
|
||||||
CircleCollider2D:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1347552442}
|
|
||||||
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}
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Radius: 0.4
|
|
||||||
--- !u!4 &1347552446
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1347552442}
|
|
||||||
serializedVersion: 2
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0.409, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children: []
|
|
||||||
m_Father: {fileID: 1584752080}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!1 &1347749803
|
--- !u!1 &1347749803
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -26704,88 +26445,6 @@ Transform:
|
|||||||
- {fileID: 212756508}
|
- {fileID: 212756508}
|
||||||
m_Father: {fileID: 1295644710}
|
m_Father: {fileID: 1295644710}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &1432871575
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 1432871578}
|
|
||||||
- component: {fileID: 1432871577}
|
|
||||||
- component: {fileID: 1432871576}
|
|
||||||
m_Layer: 27
|
|
||||||
m_Name: HurtBox
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!114 &1432871576
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1432871575}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: d7b7a233d7f70aa4f86b473412b826de, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
_onDamageDealt: {fileID: 0}
|
|
||||||
_onHitConfirmed: {fileID: 11400000, guid: a67d56f5124e0db4f98f326c74be8091, type: 2}
|
|
||||||
--- !u!70 &1432871577
|
|
||||||
CapsuleCollider2D:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1432871575}
|
|
||||||
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}
|
|
||||||
m_Size: {x: 0.55, y: 0.75}
|
|
||||||
m_Direction: 0
|
|
||||||
--- !u!4 &1432871578
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1432871575}
|
|
||||||
serializedVersion: 2
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0.373, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children: []
|
|
||||||
m_Father: {fileID: 1584752080}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!1 &1434802717
|
--- !u!1 &1434802717
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -207623,433 +207282,6 @@ Transform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 1411393553}
|
m_Father: {fileID: 1411393553}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &1584752067
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 1584752080}
|
|
||||||
- component: {fileID: 1584752079}
|
|
||||||
- component: {fileID: 1584752078}
|
|
||||||
- component: {fileID: 1584752077}
|
|
||||||
- component: {fileID: 1584752076}
|
|
||||||
- component: {fileID: 1584752075}
|
|
||||||
- component: {fileID: 1584752074}
|
|
||||||
- component: {fileID: 1584752073}
|
|
||||||
- component: {fileID: 1584752072}
|
|
||||||
- component: {fileID: 1584752071}
|
|
||||||
- component: {fileID: 1584752070}
|
|
||||||
- component: {fileID: 1584752069}
|
|
||||||
- component: {fileID: 1584752068}
|
|
||||||
- component: {fileID: 1584752081}
|
|
||||||
m_Layer: 13
|
|
||||||
m_Name: ENM_CaoZhi
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!114 &1584752068
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 58cb3ac0e49c151429cad39d3e164a3d, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
_slots:
|
|
||||||
- slotName: aggro
|
|
||||||
sensor: {fileID: 0}
|
|
||||||
- slotName: wall_ahead
|
|
||||||
sensor: {fileID: 0}
|
|
||||||
- slotName: ledge
|
|
||||||
sensor: {fileID: 0}
|
|
||||||
--- !u!114 &1584752069
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 44871319d7318de40b9ac21757b69c78, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
_edgeCheckFwdOffset: 0.3
|
|
||||||
_edgeCheckDownLen: 0.6
|
|
||||||
_groundMask:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 256
|
|
||||||
--- !u!114 &1584752070
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 77030faff3812a7429edeaca91e9c873, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
movementSpeed: 5
|
|
||||||
cornerSpeed: 100
|
|
||||||
jumpSpeed: 5
|
|
||||||
fallSpeed: 5
|
|
||||||
climbSpeed: 5
|
|
||||||
enableAgentRotation: 1
|
|
||||||
enabledFeatures: -1
|
|
||||||
--- !u!114 &1584752071
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 3864fd1487d130847b11b82f276d11b6, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
height: 0.80000114
|
|
||||||
maxSlopeAngle: 13.9
|
|
||||||
autoRepathIntervall: 1
|
|
||||||
maximumDistanceToPathStart: 0.7
|
|
||||||
linkTraversalCostMultipliers:
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
allowCloseEnoughPath: 0
|
|
||||||
movementSpeed: 5
|
|
||||||
cornerSpeed: 100
|
|
||||||
jumpSpeed: 5
|
|
||||||
fallSpeed: 5
|
|
||||||
climbSpeed: 5
|
|
||||||
enableDebugMessages: 0
|
|
||||||
navTagTraversalCostMultipliers:
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
status: 0
|
|
||||||
navTagMask: -1
|
|
||||||
--- !u!114 &1584752072
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 20bd45717dc17a94581eee24814fe60c, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
_config: {fileID: 11400000, guid: 508afd17a0cf2fe47935c78097c3b093, type: 2}
|
|
||||||
_spriteRenderer: {fileID: 1584752075}
|
|
||||||
_enableTurnAnimation: 0
|
|
||||||
_animancer: {fileID: 1584752076}
|
|
||||||
_animConfig: {fileID: 11400000, guid: 06936c5bc3358904cb269abdfa60ed14, type: 2}
|
|
||||||
_navJumpMaxHeight: 6
|
|
||||||
_navJumpMaxDist: 10
|
|
||||||
_groundCheckDist: 0.35
|
|
||||||
_groundMask:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 0
|
|
||||||
_dbg_FacingDirection: 0
|
|
||||||
_dbg_VelocityX: 0
|
|
||||||
_dbg_VelocityY: 0
|
|
||||||
_dbg_IsGrounded: 0
|
|
||||||
_dbg_IsTurning: 0
|
|
||||||
--- !u!114 &1584752073
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 48bc7c82cd2c1df4ba7103160db48a11, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
_onDifficultyChanged: {fileID: 11400000, guid: 156874a2ffc17694e91e949abbf97fee, type: 2}
|
|
||||||
--- !u!114 &1584752074
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 1a2dbfbcc31a4c34cbd3ac893f02e07d, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
_enemyId:
|
|
||||||
_statsSO: {fileID: 11400000, guid: 508afd17a0cf2fe47935c78097c3b093, type: 2}
|
|
||||||
_animConfig: {fileID: 11400000, guid: 06936c5bc3358904cb269abdfa60ed14, type: 2}
|
|
||||||
_stats: {fileID: 1584752073}
|
|
||||||
_movement: {fileID: 1584752072}
|
|
||||||
_combat: {fileID: 0}
|
|
||||||
_animancer: {fileID: 1584752076}
|
|
||||||
_feedback: {fileID: 0}
|
|
||||||
_hurtBox: {fileID: 1432871576}
|
|
||||||
_onEnemyDied: {fileID: 11400000, guid: def849e2c5ec8204eae6b083b02307aa, type: 2}
|
|
||||||
_onPlayerSpawned: {fileID: 11400000, guid: 7e2c7e614f6627b449a244ab44443adf, type: 2}
|
|
||||||
_btIdleTickInterval: 0.3
|
|
||||||
_btPatrolTickInterval: 0.15
|
|
||||||
_btAlertTickInterval: 0.08
|
|
||||||
_btChaseTickInterval: 0.05
|
|
||||||
_btCombatTickInterval: 0
|
|
||||||
_dbg_CurrentState: 0
|
|
||||||
_dbg_AiPhase: 0
|
|
||||||
_dbg_HasPlayer: 0
|
|
||||||
_dbg_LastKnownPos: {x: 0, y: 0}
|
|
||||||
_dbg_BtTickInterval: 0
|
|
||||||
_autoPlayPhaseAnimation: 1
|
|
||||||
--- !u!212 &1584752075
|
|
||||||
SpriteRenderer:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_CastShadows: 0
|
|
||||||
m_ReceiveShadows: 0
|
|
||||||
m_DynamicOccludee: 1
|
|
||||||
m_StaticShadowCaster: 0
|
|
||||||
m_MotionVectors: 1
|
|
||||||
m_LightProbeUsage: 1
|
|
||||||
m_ReflectionProbeUsage: 1
|
|
||||||
m_RayTracingMode: 0
|
|
||||||
m_RayTraceProcedural: 0
|
|
||||||
m_RenderingLayerMask: 1
|
|
||||||
m_RendererPriority: 0
|
|
||||||
m_Materials:
|
|
||||||
- {fileID: 2100000, guid: 6c1a7b756ba1d4646a405f7f6e0833ad, type: 2}
|
|
||||||
m_StaticBatchInfo:
|
|
||||||
firstSubMesh: 0
|
|
||||||
subMeshCount: 0
|
|
||||||
m_StaticBatchRoot: {fileID: 0}
|
|
||||||
m_ProbeAnchor: {fileID: 0}
|
|
||||||
m_LightProbeVolumeOverride: {fileID: 0}
|
|
||||||
m_ScaleInLightmap: 1
|
|
||||||
m_ReceiveGI: 1
|
|
||||||
m_PreserveUVs: 0
|
|
||||||
m_IgnoreNormalsForChartDetection: 0
|
|
||||||
m_ImportantGI: 0
|
|
||||||
m_StitchLightmapSeams: 1
|
|
||||||
m_SelectedEditorRenderState: 0
|
|
||||||
m_MinimumChartSize: 4
|
|
||||||
m_AutoUVMaxDistance: 0.5
|
|
||||||
m_AutoUVMaxAngle: 89
|
|
||||||
m_LightmapParameters: {fileID: 0}
|
|
||||||
m_SortingLayerID: 0
|
|
||||||
m_SortingLayer: 0
|
|
||||||
m_SortingOrder: 0
|
|
||||||
m_Sprite: {fileID: 7482667652216324306, guid: 311925a002f4447b3a28927169b83ea6, type: 3}
|
|
||||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
|
||||||
m_FlipX: 0
|
|
||||||
m_FlipY: 0
|
|
||||||
m_DrawMode: 0
|
|
||||||
m_Size: {x: 1, y: 1}
|
|
||||||
m_AdaptiveModeThreshold: 0.5
|
|
||||||
m_SpriteTileMode: 0
|
|
||||||
m_WasSpriteAssigned: 1
|
|
||||||
m_MaskInteraction: 0
|
|
||||||
m_SpriteSortPoint: 0
|
|
||||||
--- !u!114 &1584752076
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 0ad50f81b1d25c441943c37a89ba23f6, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
_Animator: {fileID: 1584752077}
|
|
||||||
_Transitions: {fileID: 0}
|
|
||||||
_ActionOnDisable: 0
|
|
||||||
--- !u!95 &1584752077
|
|
||||||
Animator:
|
|
||||||
serializedVersion: 5
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_Avatar: {fileID: 0}
|
|
||||||
m_Controller: {fileID: 0}
|
|
||||||
m_CullingMode: 0
|
|
||||||
m_UpdateMode: 0
|
|
||||||
m_ApplyRootMotion: 0
|
|
||||||
m_LinearVelocityBlending: 0
|
|
||||||
m_StabilizeFeet: 0
|
|
||||||
m_WarningMessage:
|
|
||||||
m_HasTransformHierarchy: 1
|
|
||||||
m_AllowConstantClipSamplingOptimization: 1
|
|
||||||
m_KeepAnimatorStateOnDisable: 0
|
|
||||||
m_WriteDefaultValuesOnDisable: 0
|
|
||||||
--- !u!70 &1584752078
|
|
||||||
CapsuleCollider2D:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
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: 0
|
|
||||||
m_UsedByEffector: 0
|
|
||||||
m_UsedByComposite: 0
|
|
||||||
m_Offset: {x: 0, y: 0.5}
|
|
||||||
m_Size: {x: 1, y: 1}
|
|
||||||
m_Direction: 0
|
|
||||||
--- !u!50 &1584752079
|
|
||||||
Rigidbody2D:
|
|
||||||
serializedVersion: 4
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_BodyType: 0
|
|
||||||
m_Simulated: 1
|
|
||||||
m_UseFullKinematicContacts: 0
|
|
||||||
m_UseAutoMass: 0
|
|
||||||
m_Mass: 1
|
|
||||||
m_LinearDrag: 0
|
|
||||||
m_AngularDrag: 0.05
|
|
||||||
m_GravityScale: 2
|
|
||||||
m_Material: {fileID: 0}
|
|
||||||
m_IncludeLayers:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 0
|
|
||||||
m_ExcludeLayers:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 0
|
|
||||||
m_Interpolate: 0
|
|
||||||
m_SleepingMode: 1
|
|
||||||
m_CollisionDetection: 1
|
|
||||||
m_Constraints: 4
|
|
||||||
--- !u!4 &1584752080
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
serializedVersion: 2
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: -5.2, y: 8.1, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 1432871578}
|
|
||||||
- {fileID: 1347552446}
|
|
||||||
- {fileID: 1340413309}
|
|
||||||
m_Father: {fileID: 0}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!114 &1584752081
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1584752067}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 0cdaa3305fa954c45a80c9662aa6f425, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_GraphName: Behavior Tree
|
|
||||||
m_Index: 0
|
|
||||||
m_Data:
|
|
||||||
m_TaskData:
|
|
||||||
- m_ObjectType: Opsive.BehaviorDesigner.Runtime.Tasks.Actions.StackedAction
|
|
||||||
m_ValueHashes:
|
|
||||||
m_LongValueHashes: 30b77c28f48d1b2966cb3cf98c4bd96480f6501333363a66622a4da7e5588b5847627e5c3afba34af262c60c48f0bccad0ed2caea3f34ac163eb2caea3f34ac1a8724a1098fa102319cb31fdb78bb19fc8fad5b29007ef3a396f2e031315f24fe4a06bb554569d8946fff10c773614c9
|
|
||||||
m_ValuePositions: 000000000200000004000000060000000a0000003100000035000000390000003d0000004100000045000000460000004a0000004a000000
|
|
||||||
m_Values: 0000ffffffff010000004261736547616d65732e456e656d6965732e41492e42445f506174726f6c576179706f696e7473020000000000000001000000000000009a99993e010000000000000000
|
|
||||||
m_UnityObjects:
|
|
||||||
- {fileID: 5705765}
|
|
||||||
- {fileID: 1398696825}
|
|
||||||
m_Version: 3.4
|
|
||||||
m_EventTaskData:
|
|
||||||
- m_ObjectType: Opsive.BehaviorDesigner.Runtime.Tasks.Events.Start
|
|
||||||
m_ValueHashes:
|
|
||||||
m_LongValueHashes: 59405171878141b1
|
|
||||||
m_ValuePositions: 00000000
|
|
||||||
m_Values: 0000
|
|
||||||
m_UnityObjects: []
|
|
||||||
m_Version: 3.4
|
|
||||||
m_SharedVariableData: []
|
|
||||||
m_DisabledEventNodesData: []
|
|
||||||
m_DisabledLogicNodesData: []
|
|
||||||
m_UniqueID: -1404223673
|
|
||||||
m_LogicNodePropertiesData:
|
|
||||||
- m_ObjectType: Opsive.GraphDesigner.Runtime.LogicNodeProperties
|
|
||||||
m_ValueHashes:
|
|
||||||
m_LongValueHashes: bc124df8ef5e104cf36ca30dee0de9958dd19827f48d1b29bd8814239a1bb7642f6406e2580d1e294f869cc9196b0c27aea3bc2e18d5b803a034c7c2b541f015557e981535906112a98ff48b9e8a66b37f542abda0a249c23a7f01b1f88d1a7b
|
|
||||||
m_ValuePositions: 00000000240000002c000000300000003000000030000000400000004100000045000000450000004700000049000000
|
|
||||||
m_Values: 37383637303330652d633337332d346433612d623831662d643362383132353230343464008001440000de4300000043000000000000000000000000000000000000000000ffffffff00
|
|
||||||
m_UnityObjects: []
|
|
||||||
m_Version: 3.4
|
|
||||||
m_EventNodePropertiesData:
|
|
||||||
- m_ObjectType: Opsive.GraphDesigner.Runtime.NodeProperties
|
|
||||||
m_ValueHashes:
|
|
||||||
m_LongValueHashes: bc124df8ef5e104cf36ca30dee0de9958dd19827f48d1b29bd8814239a1bb7642f6406e2580d1e294f869cc9196b0c27aea3bc2e18d5b803
|
|
||||||
m_ValuePositions: 00000000240000002c00000030000000300000003000000040000000
|
|
||||||
m_Values: 63663532303038392d646165382d343566632d393539372d3833613734336635316638300000044400007e430000dc420000000000000000000000000000000000
|
|
||||||
m_UnityObjects: []
|
|
||||||
m_Version: 3.4
|
|
||||||
m_GroupPropertiesData: []
|
|
||||||
m_SharedVariableGroupsData: []
|
|
||||||
m_StartWhenEnabled: 1
|
|
||||||
m_PauseWhenDisabled: 0
|
|
||||||
m_UpdateMode: 0
|
|
||||||
m_EvaluationType: 0
|
|
||||||
m_MaxEvaluationCount: 1
|
|
||||||
m_Subtree: {fileID: 0}
|
|
||||||
--- !u!1 &1584945498
|
--- !u!1 &1584945498
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -217134,4 +216366,3 @@ SceneRoots:
|
|||||||
- {fileID: 1865796631}
|
- {fileID: 1865796631}
|
||||||
- {fileID: 1354690328}
|
- {fileID: 1354690328}
|
||||||
- {fileID: 783576435}
|
- {fileID: 783576435}
|
||||||
- {fileID: 1584752080}
|
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ namespace BaseGames.Combat
|
|||||||
public string SourceId;
|
public string SourceId;
|
||||||
public string SkillId;
|
public string SkillId;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// HitBox 激活实例 ID(由 HitBox.Activate() 自动生成并写入,0 = 无追踪路径)。
|
||||||
|
/// HurtBoxOwnerGuard 用此字段做同激活去重,防止多 HurtBox 节点被同一次攻击重复扣血。
|
||||||
|
/// [NonSerialized]:每次激活动态生成,不需要序列化。
|
||||||
|
/// </summary>
|
||||||
|
[System.NonSerialized] public uint HitActivationId;
|
||||||
|
/// <summary>
|
||||||
/// 攻击来源投射物(仅当攻击方是 Projectile 时非 null)。
|
/// 攻击来源投射物(仅当攻击方是 Projectile 时非 null)。
|
||||||
/// 用于弹反成功时调用 ReflectAsPlayerProjectile() 翻转阵营。
|
/// 用于弹反成功时调用 ReflectAsPlayerProjectile() 翻转阵营。
|
||||||
/// [NonSerialized]:MonoBehaviour 引用不参与 Unity 资产序列化。
|
/// [NonSerialized]:MonoBehaviour 引用不参与 Unity 资产序列化。
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ namespace BaseGames.Combat
|
|||||||
/// 攻击判定盒。挂载在武器 Prefab 或技能 HitBox Prefab 的子节点上。
|
/// 攻击判定盒。挂载在武器 Prefab 或技能 HitBox Prefab 的子节点上。
|
||||||
/// 直接挂在 Player Prefab 子节点 [HitBoxGround/Up/Down/Air]。
|
/// 直接挂在 Player Prefab 子节点 [HitBoxGround/Up/Down/Air]。
|
||||||
/// Collider2D 需设 IsTrigger = true,Layer = PlayerHitBox 或 EnemyHitBox。
|
/// Collider2D 需设 IsTrigger = true,Layer = PlayerHitBox 或 EnemyHitBox。
|
||||||
|
///
|
||||||
|
/// 多碰撞体模式:在子节点上挂载 HitBoxColliderProxy + Collider2D 即可组合任意形状。
|
||||||
|
/// HitBox 本身可不带 Collider2D(仅代理子节点)或同时拥有直属 Collider2D。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RequireComponent(typeof(Collider2D))]
|
|
||||||
public class HitBox : MonoBehaviour
|
public class HitBox : MonoBehaviour
|
||||||
{
|
{
|
||||||
[SerializeField] private DamageSourceSO _defaultSource;
|
[SerializeField] private DamageSourceSO _defaultSource;
|
||||||
@@ -33,7 +35,16 @@ namespace BaseGames.Combat
|
|||||||
private Rigidbody2D _ownerRigidbody;
|
private Rigidbody2D _ownerRigidbody;
|
||||||
private bool _isActive;
|
private bool _isActive;
|
||||||
private IClashService _clashService;
|
private IClashService _clashService;
|
||||||
private Collider2D _collider;
|
|
||||||
|
// 直属碰撞体(本 GameObject 上)
|
||||||
|
private Collider2D[] _directColliders = System.Array.Empty<Collider2D>();
|
||||||
|
// 子节点代理碰撞体
|
||||||
|
private HitBoxColliderProxy[] _proxies = System.Array.Empty<HitBoxColliderProxy>();
|
||||||
|
|
||||||
|
// 激活 ID:每次 Activate() 递增,写入 DamageInfo.HitActivationId,
|
||||||
|
// HurtBoxOwnerGuard 据此防止多 HurtBox 节点被同一次攻击重复扣血。
|
||||||
|
private static uint _nextActivationId = 1;
|
||||||
|
private uint _currentActivationId;
|
||||||
|
|
||||||
/// <summary>HitBox 当前是否激活(供 ClashResolver 查询)。</summary>
|
/// <summary>HitBox 当前是否激活(供 ClashResolver 查询)。</summary>
|
||||||
public bool IsActive => _isActive;
|
public bool IsActive => _isActive;
|
||||||
@@ -58,12 +69,13 @@ namespace BaseGames.Combat
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Activate(DamageSourceSO source = null, Transform attacker = null)
|
public void Activate(DamageSourceSO source = null, Transform attacker = null)
|
||||||
{
|
{
|
||||||
|
_currentActivationId = _nextActivationId++;
|
||||||
_currentSource = source ?? _defaultSource;
|
_currentSource = source ?? _defaultSource;
|
||||||
_attackerTransform = attacker ?? transform;
|
_attackerTransform = attacker ?? transform;
|
||||||
_isActive = true;
|
_isActive = true;
|
||||||
_collider.enabled = true;
|
|
||||||
// 缓存宿主 Rigidbody2D(沿父层级向上查找)
|
|
||||||
_ownerRigidbody = _attackerTransform.GetComponentInParent<Rigidbody2D>();
|
_ownerRigidbody = _attackerTransform.GetComponentInParent<Rigidbody2D>();
|
||||||
|
foreach (var col in _directColliders) col.enabled = true;
|
||||||
|
foreach (var proxy in _proxies) proxy.SetEnabled(true);
|
||||||
// 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标)
|
// 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标)
|
||||||
_hitThisActivation.Clear();
|
_hitThisActivation.Clear();
|
||||||
_hitCooldownTimers.Clear();
|
_hitCooldownTimers.Clear();
|
||||||
@@ -72,7 +84,8 @@ namespace BaseGames.Combat
|
|||||||
public void Deactivate()
|
public void Deactivate()
|
||||||
{
|
{
|
||||||
_isActive = false;
|
_isActive = false;
|
||||||
_collider.enabled = false;
|
foreach (var col in _directColliders) col.enabled = false;
|
||||||
|
foreach (var proxy in _proxies) proxy.SetEnabled(false);
|
||||||
_hitThisActivation.Clear();
|
_hitThisActivation.Clear();
|
||||||
_hitCooldownTimers.Clear();
|
_hitCooldownTimers.Clear();
|
||||||
}
|
}
|
||||||
@@ -85,12 +98,18 @@ namespace BaseGames.Combat
|
|||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
// 确保 Collider2D 是 Trigger
|
// 收集本节点上所有直属 Collider2D,并验证 isTrigger
|
||||||
_collider = GetComponent<Collider2D>();
|
_directColliders = GetComponents<Collider2D>();
|
||||||
if (!_collider.isTrigger)
|
foreach (var col in _directColliders)
|
||||||
Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this);
|
{
|
||||||
// 初始状态关闭碰撞体,防止未激活时产生物理检测
|
if (!col.isTrigger)
|
||||||
_collider.enabled = false;
|
Debug.LogWarning($"[HitBox] {name}: Collider2D ({col.GetType().Name}) isTrigger 应为 true。", this);
|
||||||
|
col.enabled = false;
|
||||||
|
}
|
||||||
|
// 注册所有子代 HitBoxColliderProxy(子节点多形状模式)
|
||||||
|
_proxies = GetComponentsInChildren<HitBoxColliderProxy>(true);
|
||||||
|
foreach (var proxy in _proxies)
|
||||||
|
proxy.Init(this);
|
||||||
// 缓存 IClashService:OnTriggerEnter2D 为物理热路径,避免每次调用 Dictionary 查找
|
// 缓存 IClashService:OnTriggerEnter2D 为物理热路径,避免每次调用 Dictionary 查找
|
||||||
_clashService = ServiceLocator.GetOrDefault<IClashService>();
|
_clashService = ServiceLocator.GetOrDefault<IClashService>();
|
||||||
// 缓存宿主投射物(仅 Projectile GameObject 上挂载的 HitBox 非 null)
|
// 缓存宿主投射物(仅 Projectile GameObject 上挂载的 HitBox 非 null)
|
||||||
@@ -100,20 +119,18 @@ namespace BaseGames.Combat
|
|||||||
private void OnDisable()
|
private void OnDisable()
|
||||||
{
|
{
|
||||||
_isActive = false;
|
_isActive = false;
|
||||||
if (_collider != null) _collider.enabled = false;
|
foreach (var col in _directColliders) if (col != null) col.enabled = false;
|
||||||
|
foreach (var proxy in _proxies) proxy.SetEnabled(false);
|
||||||
_hitThisActivation.Clear();
|
_hitThisActivation.Clear();
|
||||||
_hitCooldownTimers.Clear();
|
_hitCooldownTimers.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTriggerExit2D(Collider2D other)
|
private void OnTriggerEnter2D(Collider2D other) => HandleTriggerEnter(other, null);
|
||||||
{
|
private void OnTriggerExit2D(Collider2D other) => HandleTriggerExit(other);
|
||||||
// 目标离开判定区域时清除其冷却记录,防止持续激活的 HitBox(环境危险等)
|
|
||||||
// 因有效目标持续流动而无限积累已离场对象。
|
|
||||||
// 注意:_hitThisActivation 刻意保留,确保同一激活期内不重复命中。
|
|
||||||
_hitCooldownTimers.Remove(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTriggerEnter2D(Collider2D other) {
|
/// <summary>代理入口:由 HitBoxColliderProxy 或本节点 OnTriggerEnter2D 转发。</summary>
|
||||||
|
internal void HandleTriggerEnter(Collider2D other, Collider2D sourceCollider)
|
||||||
|
{
|
||||||
if (!_isActive) return;
|
if (!_isActive) return;
|
||||||
if (_currentSource == null)
|
if (_currentSource == null)
|
||||||
{
|
{
|
||||||
@@ -136,6 +153,7 @@ namespace BaseGames.Combat
|
|||||||
_attackerTransform.position,
|
_attackerTransform.position,
|
||||||
_attackerTransform.gameObject.layer,
|
_attackerTransform.gameObject.layer,
|
||||||
_ownerProjectile);
|
_ownerProjectile);
|
||||||
|
info.HitActivationId = _currentActivationId;
|
||||||
|
|
||||||
// ① 拼刀检测:当前 HitBox 携带 CanClash 标记,且碰到对立阵营的 HitBox 层
|
// ① 拼刀检测:当前 HitBox 携带 CanClash 标记,且碰到对立阵营的 HitBox 层
|
||||||
int otherLayer = other.gameObject.layer;
|
int otherLayer = other.gameObject.layer;
|
||||||
@@ -153,9 +171,12 @@ namespace BaseGames.Combat
|
|||||||
// ② 命中 HurtBox
|
// ② 命中 HurtBox
|
||||||
if (other.TryGetComponent<HurtBox>(out var hurtBox))
|
if (other.TryGetComponent<HurtBox>(out var hurtBox))
|
||||||
{
|
{
|
||||||
// 用 HitBox 自身碰撞盒中心在 HurtBox 表面上的最近点作为受击位置。
|
// hitPoint:优先使用触发命中的碰撞体中心在 HurtBox 表面的最近点;
|
||||||
// 对大体积/长条形受击体(如地刺),此点远比 HurtBox 节点中心更准确。
|
// 无 sourceCollider(直属碰撞体)时回退到 HitBox 节点坐标。
|
||||||
Vector3 hitPoint = other.ClosestPoint(_collider.bounds.center);
|
Vector2 hitOrigin = sourceCollider != null
|
||||||
|
? (Vector2)sourceCollider.bounds.center
|
||||||
|
: (Vector2)transform.position;
|
||||||
|
Vector3 hitPoint = other.ClosestPoint(hitOrigin);
|
||||||
hurtBox.ReceiveDamage(info, hitPoint);
|
hurtBox.ReceiveDamage(info, hitPoint);
|
||||||
OnHitConfirmed?.Invoke(info);
|
OnHitConfirmed?.Invoke(info);
|
||||||
return;
|
return;
|
||||||
@@ -171,6 +192,15 @@ namespace BaseGames.Combat
|
|||||||
// ── 同目标多帧命中冷却(防止 Trigger 处于重叠状态时重入等殊情导致的连击)──
|
// ── 同目标多帧命中冷却(防止 Trigger 处于重叠状态时重入等殊情导致的连击)──
|
||||||
private readonly Dictionary<Collider2D, float> _hitCooldownTimers = new(8);
|
private readonly Dictionary<Collider2D, float> _hitCooldownTimers = new(8);
|
||||||
|
|
||||||
|
/// <summary>代理出口:由 HitBoxColliderProxy 或本节点 OnTriggerExit2D 转发。</summary>
|
||||||
|
internal void HandleTriggerExit(Collider2D other)
|
||||||
|
{
|
||||||
|
// 目标离开判定区域时清除其冷却记录,防止持续激活的 HitBox(环境危险等)
|
||||||
|
// 因有效目标持续流动而无限积累已离场对象。
|
||||||
|
// 注意:_hitThisActivation 刻意保留,确保同一激活期内不重复命中。
|
||||||
|
_hitCooldownTimers.Remove(other);
|
||||||
|
}
|
||||||
|
|
||||||
private bool CheckCooldown(Collider2D other)
|
private bool CheckCooldown(Collider2D other)
|
||||||
{
|
{
|
||||||
float now = Time.time;
|
float now = Time.time;
|
||||||
@@ -183,11 +213,17 @@ namespace BaseGames.Combat
|
|||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
private void OnDrawGizmos()
|
private void OnDrawGizmos()
|
||||||
{
|
{
|
||||||
var col = GetComponent<Collider2D>();
|
|
||||||
if (col == null) return;
|
|
||||||
Color fill = _isActive ? new Color(1f, 0.15f, 0.15f, 0.25f) : new Color(1f, 0.15f, 0.15f, 0.05f);
|
Color fill = _isActive ? new Color(1f, 0.15f, 0.15f, 0.25f) : new Color(1f, 0.15f, 0.15f, 0.05f);
|
||||||
Color outline = _isActive ? new Color(1f, 0.15f, 0.15f, 0.90f) : new Color(1f, 0.15f, 0.15f, 0.25f);
|
Color outline = _isActive ? new Color(1f, 0.15f, 0.15f, 0.90f) : new Color(1f, 0.15f, 0.15f, 0.25f);
|
||||||
|
// 直属碰撞体
|
||||||
|
foreach (var col in GetComponents<Collider2D>())
|
||||||
DrawCollider2DFilled(col, fill, outline);
|
DrawCollider2DFilled(col, fill, outline);
|
||||||
|
// 子代代理碰撞体
|
||||||
|
foreach (var proxy in GetComponentsInChildren<HitBoxColliderProxy>(true))
|
||||||
|
{
|
||||||
|
var proxyCol = proxy.GetComponent<Collider2D>();
|
||||||
|
if (proxyCol != null) DrawCollider2DFilled(proxyCol, fill, outline);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Gizmo 辅助(填充 + 轮廓,不依赖外部工具类)──────────────────────────
|
// ── Gizmo 辅助(填充 + 轮廓,不依赖外部工具类)──────────────────────────
|
||||||
|
|||||||
41
Assets/_Game/Scripts/Combat/HitBoxColliderProxy.cs
Normal file
41
Assets/_Game/Scripts/Combat/HitBoxColliderProxy.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Combat
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 子碰撞体代理。挂载在 HitBox 子节点上,将 Trigger 事件转发给父级 HitBox,
|
||||||
|
/// 实现"单一 HitBox 组件 + 多个异形 Collider2D"的组合判定盒。
|
||||||
|
///
|
||||||
|
/// 配置说明(子节点多形状模式):
|
||||||
|
/// [AttackNode] ← HitBox 组件(本身可不带 Collider2D)
|
||||||
|
/// ├── [Shape_Box] ← BoxCollider2D + HitBoxColliderProxy
|
||||||
|
/// └── [Shape_Circle] ← CircleCollider2D + HitBoxColliderProxy
|
||||||
|
///
|
||||||
|
/// ⚠️ 子节点 Collider2D 须设 IsTrigger = true,Layer 与父 HitBox 一致。
|
||||||
|
/// ⚠️ 无需手动调用 Init();HitBox.Awake() 自动完成注册。
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(Collider2D))]
|
||||||
|
public sealed class HitBoxColliderProxy : MonoBehaviour
|
||||||
|
{
|
||||||
|
private HitBox _owner;
|
||||||
|
private Collider2D _col;
|
||||||
|
|
||||||
|
/// <summary>由父 HitBox.Awake() 调用,完成双向注册。</summary>
|
||||||
|
internal void Init(HitBox owner)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_col = GetComponent<Collider2D>();
|
||||||
|
if (!_col.isTrigger)
|
||||||
|
Debug.LogWarning($"[HitBoxColliderProxy] {name}: Collider2D.isTrigger 应为 true。", this);
|
||||||
|
_col.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetEnabled(bool value)
|
||||||
|
{
|
||||||
|
if (_col != null) _col.enabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerEnter2D(Collider2D other) => _owner?.HandleTriggerEnter(other, _col);
|
||||||
|
private void OnTriggerExit2D(Collider2D other) => _owner?.HandleTriggerExit(other);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 58cb3ac0e49c151429cad39d3e164a3d
|
guid: e11b931e351246344aec20aa35489592
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
@@ -20,6 +20,8 @@ namespace BaseGames.Combat
|
|||||||
|
|
||||||
private bool _isHurtBoxInvincible;
|
private bool _isHurtBoxInvincible;
|
||||||
private bool _isActive = true;
|
private bool _isActive = true;
|
||||||
|
// 所有者级去重保护:防止同一角色的多个 HurtBox 子节点在同一次 HitBox 激活中被重复伤害
|
||||||
|
private HurtBoxOwnerGuard _ownerGuard;
|
||||||
|
|
||||||
// ── 事件频道 ──────────────────────────────────────────────────────────
|
// ── 事件频道 ──────────────────────────────────────────────────────────
|
||||||
[SerializeField] private DamageInfoEventChannelSO _onDamageDealt;
|
[SerializeField] private DamageInfoEventChannelSO _onDamageDealt;
|
||||||
@@ -45,6 +47,10 @@ namespace BaseGames.Combat
|
|||||||
_statusEffectable = GetComponentInParent<IStatusEffectable>();
|
_statusEffectable = GetComponentInParent<IStatusEffectable>();
|
||||||
if (_owner == null)
|
if (_owner == null)
|
||||||
Debug.LogWarning($"[HurtBox] {name}: 父节点中未找到 IDamageable 实现。", this);
|
Debug.LogWarning($"[HurtBox] {name}: 父节点中未找到 IDamageable 实现。", this);
|
||||||
|
// 在角色根节点查找或自动创建 HurtBoxOwnerGuard(多 HurtBox 共享所有者时只有一个 Guard)
|
||||||
|
_ownerGuard = transform.root.GetComponent<HurtBoxOwnerGuard>();
|
||||||
|
if (_ownerGuard == null && _owner != null)
|
||||||
|
_ownerGuard = transform.root.gameObject.AddComponent<HurtBoxOwnerGuard>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,6 +62,8 @@ namespace BaseGames.Combat
|
|||||||
{
|
{
|
||||||
Vector3 resolvedHitPoint = hitPoint ?? transform.position;
|
Vector3 resolvedHitPoint = hitPoint ?? transform.position;
|
||||||
if (!_isActive || _owner == null) return;
|
if (!_isActive || _owner == null) return;
|
||||||
|
// 所有者级去重:同一 HitBox 激活期内多个 HurtBox 子节点只处理首次命中(共享 HP)
|
||||||
|
if (_ownerGuard != null && !_ownerGuard.TryRegisterHit(info.HitActivationId)) return;
|
||||||
|
|
||||||
// 1. 无敌帧检查
|
// 1. 无敌帧检查
|
||||||
if ((_owner.IsInvincible || _isHurtBoxInvincible)
|
if ((_owner.IsInvincible || _isHurtBoxInvincible)
|
||||||
|
|||||||
45
Assets/_Game/Scripts/Combat/HurtBoxOwnerGuard.cs
Normal file
45
Assets/_Game/Scripts/Combat/HurtBoxOwnerGuard.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Combat
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// HurtBox 所有者保护组件。防止同一角色身上的多个 HurtBox 在同一次 HitBox 激活中被重复伤害。
|
||||||
|
///
|
||||||
|
/// 工作原理:每次 HitBox 激活携带唯一 ActivationId(由 HitBox.Activate() 生成递增值)。
|
||||||
|
/// HurtBox.ReceiveDamage 调用 TryRegisterHit(id) 时,同一 id 只有首次调用返回 true;
|
||||||
|
/// 后续同 id 调用(来自同一角色的其他 HurtBox 子节点)返回 false 并跳过伤害流水线,
|
||||||
|
/// 从而保证"多个 HurtBox 共享同一 HP 池时,一次攻击只扣一次血"。
|
||||||
|
///
|
||||||
|
/// 生命周期:由 HurtBox.Awake() 在角色根节点自动添加(如不存在则创建)。无需手动挂载。
|
||||||
|
/// 逐帧通过 frameCount 差异懒清空处理集,零 GC 开销。
|
||||||
|
/// </summary>
|
||||||
|
[AddComponentMenu("")] // 隐藏菜单:由 HurtBox 自动管理
|
||||||
|
public sealed class HurtBoxOwnerGuard : MonoBehaviour
|
||||||
|
{
|
||||||
|
private readonly HashSet<uint> _processedIds = new(4);
|
||||||
|
private int _lastClearFrame = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试注册一次命中。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="activationId">HitBox 激活 ID(0 = 无追踪路径,始终允许通过)。</param>
|
||||||
|
/// <returns>true = 首次注册,应继续处理伤害;false = 同 id 已被处理,跳过。</returns>
|
||||||
|
public bool TryRegisterHit(uint activationId)
|
||||||
|
{
|
||||||
|
// activationId == 0:LethalTrap / BodyContactDamage 等旁路路径,不做去重
|
||||||
|
if (activationId == 0) return true;
|
||||||
|
|
||||||
|
EnsureClearedThisFrame();
|
||||||
|
return _processedIds.Add(activationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureClearedThisFrame()
|
||||||
|
{
|
||||||
|
int frame = Time.frameCount;
|
||||||
|
if (frame == _lastClearFrame) return;
|
||||||
|
_processedIds.Clear();
|
||||||
|
_lastClearFrame = frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Assets/_Game/Scripts/Combat/HurtBoxOwnerGuard.cs.meta
Normal file
11
Assets/_Game/Scripts/Combat/HurtBoxOwnerGuard.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d9d0dceef43f3534c9bcc84af2134c53
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -2,7 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.UIElements;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UIElements;
|
using UnityEngine.UIElements;
|
||||||
using BaseGames.Boss;
|
using BaseGames.Boss;
|
||||||
@@ -15,6 +15,8 @@ using BaseGames.Parry;
|
|||||||
using BaseGames.Player;
|
using BaseGames.Player;
|
||||||
using BaseGames.Player.States;
|
using BaseGames.Player.States;
|
||||||
using BaseGames.Skills;
|
using BaseGames.Skills;
|
||||||
|
using BaseGames.Editor.Combat;
|
||||||
|
using BaseGames.Editor.Skills;
|
||||||
|
|
||||||
namespace BaseGames.Editor
|
namespace BaseGames.Editor
|
||||||
{
|
{
|
||||||
@@ -59,6 +61,7 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
// 小怪类型选择 — 具体敌人类型
|
// 小怪类型选择 — 具体敌人类型
|
||||||
private int _enemyTypeIndex = 0;
|
private int _enemyTypeIndex = 0;
|
||||||
|
private SceneObjectPlacerTool.EnemyBodyColliderType _enemyBodyCollider = SceneObjectPlacerTool.EnemyBodyColliderType.Box;
|
||||||
private static readonly (string id, string name)[] EnemyTypes =
|
private static readonly (string id, string name)[] EnemyTypes =
|
||||||
{
|
{
|
||||||
("E001", "草蛭"),
|
("E001", "草蛭"),
|
||||||
@@ -195,6 +198,8 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
var jumpGroup = MakeActionGroup();
|
var jumpGroup = MakeActionGroup();
|
||||||
jumpGroup.Add(MakeJumpButton("Data Hub(武器/技能/形态)", DataHubWindow.Open));
|
jumpGroup.Add(MakeJumpButton("Data Hub(武器/技能/形态)", DataHubWindow.Open));
|
||||||
|
jumpGroup.Add(MakeJumpButton("武器 HitBox 向导", WeaponHitBoxWizard.Open));
|
||||||
|
jumpGroup.Add(MakeJumpButton("技能 HitBox 向导", SkillHitBoxWizard.Open));
|
||||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||||
root.Add(jumpGroup);
|
root.Add(jumpGroup);
|
||||||
|
|
||||||
@@ -249,6 +254,7 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
var jumpGroup = MakeActionGroup();
|
var jumpGroup = MakeActionGroup();
|
||||||
jumpGroup.Add(MakeJumpButton("Data Hub(敌人数据)", DataHubWindow.Open));
|
jumpGroup.Add(MakeJumpButton("Data Hub(敌人数据)", DataHubWindow.Open));
|
||||||
|
jumpGroup.Add(MakeJumpButton("武器 HitBox 向导", WeaponHitBoxWizard.Open));
|
||||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||||
root.Add(jumpGroup);
|
root.Add(jumpGroup);
|
||||||
|
|
||||||
@@ -286,11 +292,16 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
container.Add(MakeSeparator());
|
container.Add(MakeSeparator());
|
||||||
container.Add(MakeSectionHeader("▶ 场景搭建"));
|
container.Add(MakeSectionHeader("▶ 场景搭建"));
|
||||||
container.Add(MakeHelpBox("在当前活动场景中放置完整组件树并自动绑定已有 SO。"));
|
container.Add(MakeHelpBox("在当前活动场景中放置完整组件树(含 Visual 子节点对齐 Collider)并自动绑定已有 SO。"));
|
||||||
|
|
||||||
|
var colliderField = new UnityEngine.UIElements.EnumField("主体碰撞器类型", _enemyBodyCollider);
|
||||||
|
colliderField.RegisterValueChangedCallback(evt => _enemyBodyCollider = (SceneObjectPlacerTool.EnemyBodyColliderType)evt.newValue);
|
||||||
|
container.Add(colliderField);
|
||||||
|
|
||||||
var sceneGroup = MakeActionGroup();
|
var sceneGroup = MakeActionGroup();
|
||||||
string sceneLabel = $"放置 {id} {name} 到场景";
|
string sceneLabel = $"放置 {id} {name} 到场景";
|
||||||
sceneGroup.Add(MakeSceneButton(sceneLabel, () => PlaceSpecificEnemy(id)));
|
sceneGroup.Add(MakeSceneButton(sceneLabel, () => PlaceSpecificEnemy(id, _enemyBodyCollider)));
|
||||||
|
sceneGroup.Add(MakeSceneButton("迁移选中对象 Visual 节点", MigrateSelectedEnemyVisualNode));
|
||||||
container.Add(sceneGroup);
|
container.Add(sceneGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +341,7 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
var sceneGroup = MakeActionGroup();
|
var sceneGroup = MakeActionGroup();
|
||||||
sceneGroup.Add(MakeSceneButton("放置嘲风到场景并绑定 SO", SceneObjectPlacerTool.PlaceChaoFeng));
|
sceneGroup.Add(MakeSceneButton("放置嘲风到场景并绑定 SO", SceneObjectPlacerTool.PlaceChaoFeng));
|
||||||
|
sceneGroup.Add(MakeSceneButton("迁移选中对象 Visual 节点", MigrateSelectedEnemyVisualNode));
|
||||||
root.Add(sceneGroup);
|
root.Add(sceneGroup);
|
||||||
|
|
||||||
root.Add(MakeSeparator());
|
root.Add(MakeSeparator());
|
||||||
@@ -337,6 +349,7 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
var jumpGroup = MakeActionGroup();
|
var jumpGroup = MakeActionGroup();
|
||||||
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
|
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
|
||||||
|
jumpGroup.Add(MakeJumpButton("武器 HitBox 向导", WeaponHitBoxWizard.Open));
|
||||||
jumpGroup.Add(MakeJumpButton("Data Hub(Boss技能)", DataHubWindow.Open));
|
jumpGroup.Add(MakeJumpButton("Data Hub(Boss技能)", DataHubWindow.Open));
|
||||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||||
root.Add(jumpGroup);
|
root.Add(jumpGroup);
|
||||||
@@ -603,23 +616,45 @@ namespace BaseGames.Editor
|
|||||||
$"全部 {id} SO 已创建(已存在的跳过)。\n请放置到场景后检查组件绑定。", "确定");
|
$"全部 {id} SO 已创建(已存在的跳过)。\n请放置到场景后检查组件绑定。", "确定");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PlaceSpecificEnemy(string id)
|
private static void PlaceSpecificEnemy(string id, SceneObjectPlacerTool.EnemyBodyColliderType bodyCollider)
|
||||||
{
|
{
|
||||||
switch (id)
|
switch (id)
|
||||||
{
|
{
|
||||||
case "E001": SceneObjectPlacerTool.PlaceE001_CaoZhi(); break;
|
case "E001": SceneObjectPlacerTool.PlaceE001_CaoZhi(bodyCollider); break;
|
||||||
case "E002": SceneObjectPlacerTool.PlaceE002_HuangZhi(); break;
|
case "E002": SceneObjectPlacerTool.PlaceE002_HuangZhi(bodyCollider); break;
|
||||||
case "E003": SceneObjectPlacerTool.PlaceE003_YouZhi_Enemy(); break;
|
case "E003": SceneObjectPlacerTool.PlaceE003_YouZhi_Enemy(bodyCollider); break;
|
||||||
case "E004": SceneObjectPlacerTool.PlaceE004_ZhiMu_Enemy(); break;
|
case "E004": SceneObjectPlacerTool.PlaceE004_ZhiMu_Enemy(bodyCollider); break;
|
||||||
case "E005": SceneObjectPlacerTool.PlaceE005_FeiZhi_Enemy(); break;
|
case "E005": SceneObjectPlacerTool.PlaceE005_FeiZhi_Enemy(bodyCollider); break;
|
||||||
case "E006": SceneObjectPlacerTool.PlaceE006_Huan(); break;
|
case "E006": SceneObjectPlacerTool.PlaceE006_Huan(bodyCollider); break;
|
||||||
default:
|
default:
|
||||||
Debug.LogError($"[CharacterWizardWindow] 未注册的敌人 id '{id}',请在 SceneObjectPlacerTool 中实现对应 PlaceE...() 方法并注册。");
|
Debug.LogError($"[CharacterWizardWindow] 未注册的敌人 id '{id}',请在 SceneObjectPlacerTool 中实现对应 PlaceE...() 方法并注册。");
|
||||||
SceneObjectPlacerTool.PlaceEnemy();
|
SceneObjectPlacerTool.PlaceEnemy(bodyCollider);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void MigrateSelectedEnemyVisualNode()
|
||||||
|
{
|
||||||
|
var targets = Selection.gameObjects;
|
||||||
|
if (targets == null || targets.Length == 0)
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("迁移 Visual 节点",
|
||||||
|
"请先在 Hierarchy 中选中一个或多个 Enemy 对象。", "确定");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int count = 0;
|
||||||
|
foreach (var go in targets)
|
||||||
|
{
|
||||||
|
var movement = go.GetComponent<EnemyMovement>();
|
||||||
|
if (movement != null) { movement.SetupVisualNode(); count++; }
|
||||||
|
}
|
||||||
|
if (count == 0)
|
||||||
|
EditorUtility.DisplayDialog("迁移 Visual 节点",
|
||||||
|
"选中的对象中没有找到 EnemyMovement 组件。", "确定");
|
||||||
|
else
|
||||||
|
Debug.Log($"[CharacterWizardWindow] 已对 {count} 个对象完成 Visual 节点迁移。");
|
||||||
|
}
|
||||||
|
|
||||||
// ── SO 资产工厂:嘲风 Boss ─────────────────────────────────────────────
|
// ── SO 资产工厂:嘲风 Boss ─────────────────────────────────────────────
|
||||||
|
|
||||||
private static void CreateChaoFengStatsSO()
|
private static void CreateChaoFengStatsSO()
|
||||||
|
|||||||
115
Assets/_Game/Scripts/Editor/Combat/HitBoxEditor.cs
Normal file
115
Assets/_Game/Scripts/Editor/Combat/HitBoxEditor.cs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using BaseGames.Combat;
|
||||||
|
|
||||||
|
namespace BaseGames.Editor.Combat
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// HitBox 自定义 Inspector。
|
||||||
|
/// 在"多形状碰撞体"面板中提供快捷按钮,一键创建带 HitBoxColliderProxy 的子形状节点。
|
||||||
|
///
|
||||||
|
/// 生成子节点结构:
|
||||||
|
/// [HitBoxParent] ← HitBox 组件(可无 Collider2D)
|
||||||
|
/// ├── [Shape_Box] ← BoxCollider2D(isTrigger) + HitBoxColliderProxy
|
||||||
|
/// ├── [Shape_Circle] ← CircleCollider2D(isTrigger) + HitBoxColliderProxy
|
||||||
|
/// ├── [Shape_Capsule] ← CapsuleCollider2D(isTrigger) + HitBoxColliderProxy
|
||||||
|
/// └── [Shape_Polygon] ← PolygonCollider2D(isTrigger) + HitBoxColliderProxy
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(HitBox))]
|
||||||
|
public class HitBoxEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
DrawDefaultInspector();
|
||||||
|
|
||||||
|
EditorGUILayout.Space(6);
|
||||||
|
EditorGUILayout.LabelField("── 多形状碰撞体 ──", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
"在子节点添加 HitBoxColliderProxy + Collider2D 以组合多形状判定盒。\n" +
|
||||||
|
"子节点 Layer 自动继承本节点;Collider2D.IsTrigger 自动设为 true。",
|
||||||
|
MessageType.Info);
|
||||||
|
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
if (GUILayout.Button("+ Box"))
|
||||||
|
AddShapeChild((HitBox)target, ShapeKind.Box);
|
||||||
|
if (GUILayout.Button("+ Circle"))
|
||||||
|
AddShapeChild((HitBox)target, ShapeKind.Circle);
|
||||||
|
if (GUILayout.Button("+ Capsule"))
|
||||||
|
AddShapeChild((HitBox)target, ShapeKind.Capsule);
|
||||||
|
if (GUILayout.Button("+ Polygon"))
|
||||||
|
AddShapeChild((HitBox)target, ShapeKind.Polygon);
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
if (Application.isPlaying)
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(2);
|
||||||
|
int proxyCount = ((HitBox)target).GetComponentsInChildren<HitBoxColliderProxy>(true).Length;
|
||||||
|
EditorGUILayout.LabelField($"子代 HitBoxColliderProxy 数:{proxyCount}", EditorStyles.miniLabel);
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 内部工具 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private enum ShapeKind { Box, Circle, Capsule, Polygon }
|
||||||
|
|
||||||
|
private static void AddShapeChild(HitBox hitBox, ShapeKind kind)
|
||||||
|
{
|
||||||
|
var child = new GameObject($"[Shape_{kind}]");
|
||||||
|
Undo.RegisterCreatedObjectUndo(child, $"Add HitBox {kind} Shape");
|
||||||
|
child.transform.SetParent(hitBox.transform, false);
|
||||||
|
child.layer = hitBox.gameObject.layer;
|
||||||
|
|
||||||
|
Collider2D col = kind switch
|
||||||
|
{
|
||||||
|
ShapeKind.Box => CreateBox(child),
|
||||||
|
ShapeKind.Circle => CreateCircle(child),
|
||||||
|
ShapeKind.Capsule => CreateCapsule(child),
|
||||||
|
ShapeKind.Polygon => CreatePolygon(child),
|
||||||
|
_ => CreateBox(child),
|
||||||
|
};
|
||||||
|
col.isTrigger = true;
|
||||||
|
|
||||||
|
Undo.AddComponent<HitBoxColliderProxy>(child);
|
||||||
|
|
||||||
|
Selection.activeGameObject = child;
|
||||||
|
EditorGUIUtility.PingObject(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoxCollider2D CreateBox(GameObject go)
|
||||||
|
{
|
||||||
|
var c = Undo.AddComponent<BoxCollider2D>(go);
|
||||||
|
c.size = new Vector2(1f, 0.5f);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CircleCollider2D CreateCircle(GameObject go)
|
||||||
|
{
|
||||||
|
var c = Undo.AddComponent<CircleCollider2D>(go);
|
||||||
|
c.radius = 0.4f;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CapsuleCollider2D CreateCapsule(GameObject go)
|
||||||
|
{
|
||||||
|
var c = Undo.AddComponent<CapsuleCollider2D>(go);
|
||||||
|
c.size = new Vector2(0.5f, 1f);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PolygonCollider2D CreatePolygon(GameObject go)
|
||||||
|
{
|
||||||
|
var c = Undo.AddComponent<PolygonCollider2D>(go);
|
||||||
|
c.SetPath(0, new Vector2[]
|
||||||
|
{
|
||||||
|
new( 0f, 0.3f),
|
||||||
|
new( 0.5f, 0f ),
|
||||||
|
new( 0f, -0.3f),
|
||||||
|
new(-0.5f, 0f ),
|
||||||
|
});
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
11
Assets/_Game/Scripts/Editor/Combat/HitBoxEditor.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Combat/HitBoxEditor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 414d2528016107148929f4d65d5507e7
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -6,9 +6,14 @@ using BaseGames.Combat;
|
|||||||
namespace BaseGames.Editor
|
namespace BaseGames.Editor
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// HurtBox 运行时注入状态可视化面板。
|
/// HurtBox 自定义 Inspector。
|
||||||
/// 通过 HurtBox 上的 Editor* 属性读取注入状态,以颜色区分是否注入成功。
|
/// ① 提供"多形状受击区域"快捷按钮:一键创建带 HurtBox 的子形状节点(共享同一 HP 池)。
|
||||||
/// 绿色 = 注入完成;橙色 = 未注入(该能力静默不生效);灰色 = 非 PlayMode。
|
/// ② 运行时注入状态可视化面板:绿色 = 注入完成;橙色 = 未注入;灰色 = 非 PlayMode。
|
||||||
|
///
|
||||||
|
/// 生成子节点结构:
|
||||||
|
/// [HurtBoxParent] ← HurtBox + 任意 Collider2D(主受击区)
|
||||||
|
/// ├── [HurtShape_Box] ← BoxCollider2D(isTrigger) + HurtBox(共享 HP)
|
||||||
|
/// └── [HurtShape_Circle] ← CircleCollider2D(isTrigger) + HurtBox(共享 HP)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[CustomEditor(typeof(HurtBox))]
|
[CustomEditor(typeof(HurtBox))]
|
||||||
public class HurtBoxEditor : UnityEditor.Editor
|
public class HurtBoxEditor : UnityEditor.Editor
|
||||||
@@ -27,6 +32,24 @@ namespace BaseGames.Editor
|
|||||||
{
|
{
|
||||||
DrawDefaultInspector();
|
DrawDefaultInspector();
|
||||||
|
|
||||||
|
// ── 多形状受击区域 ──────────────────────────────────────────────
|
||||||
|
EditorGUILayout.Space(6);
|
||||||
|
EditorGUILayout.LabelField("── 多形状受击区域 ──", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
"在子节点添加 HurtBox + Collider2D 以组合多形状受击区域,各子节点共享同一 HP 池。\n" +
|
||||||
|
"子节点 Layer 自动继承本节点;HurtBoxOwnerGuard 防止同一次攻击重复扣血。",
|
||||||
|
MessageType.Info);
|
||||||
|
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
if (GUILayout.Button("+ Box"))
|
||||||
|
AddShapeChild((HurtBox)target, ShapeKind.Box);
|
||||||
|
if (GUILayout.Button("+ Circle"))
|
||||||
|
AddShapeChild((HurtBox)target, ShapeKind.Circle);
|
||||||
|
if (GUILayout.Button("+ Capsule"))
|
||||||
|
AddShapeChild((HurtBox)target, ShapeKind.Capsule);
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
// ── 运行时注入状态 ──────────────────────────────────────────────
|
||||||
EditorGUILayout.Space(4);
|
EditorGUILayout.Space(4);
|
||||||
EditorGUILayout.LabelField("── 运行时注入状态 ──", EditorStyles.boldLabel);
|
EditorGUILayout.LabelField("── 运行时注入状态 ──", EditorStyles.boldLabel);
|
||||||
|
|
||||||
@@ -59,6 +82,34 @@ namespace BaseGames.Editor
|
|||||||
// 持续刷新(避免只显示初始状态)
|
// 持续刷新(避免只显示初始状态)
|
||||||
if (Application.isPlaying) Repaint();
|
if (Application.isPlaying) Repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 子形状创建工具 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private enum ShapeKind { Box, Circle, Capsule }
|
||||||
|
|
||||||
|
private static void AddShapeChild(HurtBox hurtBox, ShapeKind kind)
|
||||||
|
{
|
||||||
|
var child = new GameObject($"[HurtShape_{kind}]");
|
||||||
|
Undo.RegisterCreatedObjectUndo(child, $"Add HurtBox {kind} Shape");
|
||||||
|
child.transform.SetParent(hurtBox.transform, false);
|
||||||
|
child.layer = hurtBox.gameObject.layer;
|
||||||
|
|
||||||
|
// 先加 Collider2D 以满足 HurtBox 的 [RequireComponent(typeof(Collider2D))],
|
||||||
|
// 再 AddComponent<HurtBox>() 则不会重复创建 Collider2D。
|
||||||
|
Collider2D col = kind switch
|
||||||
|
{
|
||||||
|
ShapeKind.Box => Undo.AddComponent<BoxCollider2D>(child),
|
||||||
|
ShapeKind.Circle => Undo.AddComponent<CircleCollider2D>(child),
|
||||||
|
ShapeKind.Capsule => Undo.AddComponent<CapsuleCollider2D>(child),
|
||||||
|
_ => Undo.AddComponent<BoxCollider2D>(child),
|
||||||
|
};
|
||||||
|
col.isTrigger = true;
|
||||||
|
|
||||||
|
Undo.AddComponent<HurtBox>(child);
|
||||||
|
|
||||||
|
Selection.activeGameObject = child;
|
||||||
|
EditorGUIUtility.PingObject(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using BaseGames.Enemies.Perception;
|
||||||
|
|
||||||
|
namespace BaseGames.Editor
|
||||||
|
{
|
||||||
|
[CustomEditor(typeof(PhysicsPerceptionSystem))]
|
||||||
|
public sealed class PhysicsPerceptionSystemEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
// ── Scene 视图 Gizmo ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected | GizmoType.InSelectionHierarchy)]
|
||||||
|
static void DrawGizmos(PhysicsPerceptionSystem system, GizmoType gizmoType)
|
||||||
|
{
|
||||||
|
var slots = system.EditorSlots;
|
||||||
|
if (slots == null) return;
|
||||||
|
|
||||||
|
bool isSelected = (gizmoType & (GizmoType.Selected | GizmoType.InSelectionHierarchy)) != 0;
|
||||||
|
Vector3 rootPos = system.transform.position;
|
||||||
|
float facingSign = system.transform.localScale.x < 0f ? -1f : 1f;
|
||||||
|
|
||||||
|
foreach (var slot in slots)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(slot.slotName)) continue;
|
||||||
|
|
||||||
|
Color baseColor = ResolveGizmoColor(slot.gizmoColor);
|
||||||
|
Color fill = baseColor; fill.a = isSelected ? 0.12f : 0.04f;
|
||||||
|
Color outline = baseColor; outline.a = isSelected ? 0.90f : 0.40f;
|
||||||
|
|
||||||
|
// 每个 Slot 独立检测原点(X 随朝向翻转)
|
||||||
|
Vector3 slotCenter = rootPos + new Vector3(slot.offset.x * facingSign, slot.offset.y, 0f);
|
||||||
|
|
||||||
|
switch (slot.type)
|
||||||
|
{
|
||||||
|
case PhysicsPerceptionSystem.SlotType.RangeCircle:
|
||||||
|
if (slot.radius <= 0f) break;
|
||||||
|
Handles.color = fill;
|
||||||
|
Handles.DrawSolidDisc(slotCenter, Vector3.forward, slot.radius);
|
||||||
|
Handles.color = outline;
|
||||||
|
Handles.DrawWireDisc(slotCenter, Vector3.forward, slot.radius);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PhysicsPerceptionSystem.SlotType.FanCast:
|
||||||
|
if (slot.radius > 0f && slot.fanAngle > 0f)
|
||||||
|
{
|
||||||
|
DrawFanGizmo(slotCenter, facingSign, slot, fill, outline);
|
||||||
|
if (isSelected) DrawOriginDot(slotCenter, outline);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PhysicsPerceptionSystem.SlotType.BoxCast:
|
||||||
|
if (slot.boxSize.x > 0f && slot.boxSize.y > 0f)
|
||||||
|
{
|
||||||
|
DrawBoxGizmo(slotCenter, facingSign, slot, fill, outline);
|
||||||
|
if (isSelected) DrawOriginDot(slotCenter, outline);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// BatchLOS: eye-position marker + optional range disc + runtime ray
|
||||||
|
case PhysicsPerceptionSystem.SlotType.BatchLOS:
|
||||||
|
DrawBatchLOSGizmo(system, slotCenter, slot, outline, isSelected);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Handles.color = Color.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>在 Slot 原点处画一个小十字点,帮助可视化 offset 配置。</summary>
|
||||||
|
static void DrawOriginDot(Vector3 pos, Color color)
|
||||||
|
{
|
||||||
|
Handles.color = color;
|
||||||
|
Handles.DrawSolidDisc(pos, Vector3.forward, 0.04f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 仅当颜色字段为 Unity 默认值 Color(0,0,0,0)(全零 = 未配置)时,
|
||||||
|
/// 返回易辨识的紫色回退(alpha=1);用户明确设置的任何颜色(包括近黑色)均原样保留。
|
||||||
|
/// </summary>
|
||||||
|
static Color ResolveGizmoColor(Color c)
|
||||||
|
{
|
||||||
|
bool isDefault = c.r + c.g + c.b + c.a < 0.01f;
|
||||||
|
return isDefault ? new Color(0.85f, 0.3f, 1.0f, 1.0f) : c;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrawBatchLOSGizmo(PhysicsPerceptionSystem system, Vector3 slotCenter,
|
||||||
|
PhysicsPerceptionSystem.PerceptionSlot slot, Color slotColor, bool isSelected)
|
||||||
|
{
|
||||||
|
// slotColor 已经过 ResolveGizmoColor 处理,alpha 已由调用方设为 outline alpha。
|
||||||
|
|
||||||
|
// 所有 gizmo 元素统一画在 slotCenter(由 slot.offset 控制),
|
||||||
|
// 与 LOSOrigin(实际射线起点)解耦——gizmo 跟 offset 走。
|
||||||
|
float facingSign = system.transform.localScale.x < 0f ? -1f : 1f;
|
||||||
|
|
||||||
|
// ── 最大检测范围圆(slot.radius > 0 时)──
|
||||||
|
if (slot.radius > 0f)
|
||||||
|
{
|
||||||
|
Color fill = slotColor; fill.a = isSelected ? 0.08f : 0.03f;
|
||||||
|
Handles.color = fill;
|
||||||
|
Handles.DrawSolidDisc(slotCenter, Vector3.forward, slot.radius);
|
||||||
|
Color rim = slotColor; rim.a = isSelected ? 0.70f : 0.30f;
|
||||||
|
Handles.color = rim;
|
||||||
|
Handles.DrawWireDisc(slotCenter, Vector3.forward, slot.radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 眼睛中心圆点 ──
|
||||||
|
Handles.color = slotColor;
|
||||||
|
Handles.DrawSolidDisc(slotCenter, Vector3.forward, 0.08f);
|
||||||
|
|
||||||
|
// ── 朝向指示箭头(沿 facingSign 方向,明确"方向性"视线感知)──
|
||||||
|
float arrowLen = slot.radius > 0f ? Mathf.Min(slot.radius * 0.6f, 0.6f) : 0.4f;
|
||||||
|
Vector3 fwdArrow = new Vector3(facingSign * arrowLen, 0f, 0f);
|
||||||
|
Handles.DrawLine(slotCenter, slotCenter + fwdArrow);
|
||||||
|
|
||||||
|
Vector3 arrowTip = slotCenter + fwdArrow;
|
||||||
|
float headLen = 0.08f;
|
||||||
|
Handles.DrawLine(arrowTip, arrowTip + new Vector3(-facingSign * headLen, headLen, 0f));
|
||||||
|
Handles.DrawLine(arrowTip, arrowTip + new Vector3(-facingSign * headLen, -headLen, 0f));
|
||||||
|
|
||||||
|
// ── 放射线(仅选中时显示,避免杂乱)──
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
float innerR = 0.12f;
|
||||||
|
float outerR = slot.radius > 0f ? Mathf.Min(slot.radius, 0.30f) : 0.26f;
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
float deg = i * 45f;
|
||||||
|
float rad = deg * Mathf.Deg2Rad;
|
||||||
|
Vector3 dir = new Vector3(Mathf.Cos(rad), Mathf.Sin(rad), 0f);
|
||||||
|
Handles.DrawLine(slotCenter + dir * innerR, slotCenter + dir * outerR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 运行时:视线连线;绿 = 可见 / 红 = 遮挡 ──
|
||||||
|
if (!Application.isPlaying) return;
|
||||||
|
var owner = system.EditorOwner ?? system.GetComponentInParent<BaseGames.Enemies.EnemyBase>();
|
||||||
|
if (owner == null || owner.PlayerTransform == null) return;
|
||||||
|
|
||||||
|
bool visible = owner.IsPlayerVisible();
|
||||||
|
Color rayCol = visible
|
||||||
|
? new Color(0.2f, 1.0f, 0.3f, 0.9f)
|
||||||
|
: new Color(1.0f, 0.3f, 0.3f, 0.45f);
|
||||||
|
|
||||||
|
Handles.color = rayCol;
|
||||||
|
Handles.DrawDottedLine(slotCenter, owner.PlayerTransform.position, 3f);
|
||||||
|
|
||||||
|
if (visible)
|
||||||
|
Handles.DrawSolidDisc(owner.PlayerTransform.position, Vector3.forward, 0.09f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrawFanGizmo(Vector3 center, float facingSign,
|
||||||
|
PhysicsPerceptionSystem.PerceptionSlot slot, Color fill, Color outline)
|
||||||
|
{
|
||||||
|
float halfAngle = slot.fanAngle * 0.5f;
|
||||||
|
Vector3 fromDir = RotateVec3(new Vector3(facingSign, 0f, 0f), -halfAngle);
|
||||||
|
|
||||||
|
Handles.color = fill;
|
||||||
|
Handles.DrawSolidArc(center, Vector3.forward, fromDir, slot.fanAngle, slot.radius);
|
||||||
|
Handles.color = outline;
|
||||||
|
Handles.DrawWireArc(center, Vector3.forward, fromDir, slot.fanAngle, slot.radius);
|
||||||
|
|
||||||
|
// Edge rays for clarity
|
||||||
|
Vector3 edgeL = RotateVec3(new Vector3(facingSign, 0f, 0f), -halfAngle) * slot.radius;
|
||||||
|
Vector3 edgeR = RotateVec3(new Vector3(facingSign, 0f, 0f), halfAngle) * slot.radius;
|
||||||
|
Handles.DrawLine(center, center + edgeL);
|
||||||
|
Handles.DrawLine(center, center + edgeR);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrawBoxGizmo(Vector3 center, float facingSign,
|
||||||
|
PhysicsPerceptionSystem.PerceptionSlot slot, Color fill, Color outline)
|
||||||
|
{
|
||||||
|
Vector3 boxCenter = center + new Vector3(slot.boxOffset.x * facingSign, slot.boxOffset.y, 0f);
|
||||||
|
float hw = slot.boxSize.x * 0.5f;
|
||||||
|
float hh = slot.boxSize.y * 0.5f;
|
||||||
|
|
||||||
|
Vector3[] corners =
|
||||||
|
{
|
||||||
|
boxCenter + new Vector3(-hw, -hh, 0f),
|
||||||
|
boxCenter + new Vector3( hw, -hh, 0f),
|
||||||
|
boxCenter + new Vector3( hw, hh, 0f),
|
||||||
|
boxCenter + new Vector3(-hw, hh, 0f),
|
||||||
|
};
|
||||||
|
|
||||||
|
Handles.DrawSolidRectangleWithOutline(corners, fill, outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vector3 RotateVec3(Vector3 v, float angleDeg)
|
||||||
|
{
|
||||||
|
float rad = angleDeg * Mathf.Deg2Rad;
|
||||||
|
float cos = Mathf.Cos(rad);
|
||||||
|
float sin = Mathf.Sin(rad);
|
||||||
|
return new Vector3(cos * v.x - sin * v.y, sin * v.x + cos * v.y, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Inspector ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
DrawDefaultInspector();
|
||||||
|
|
||||||
|
if (!Application.isPlaying) return;
|
||||||
|
|
||||||
|
var system = (PhysicsPerceptionSystem)target;
|
||||||
|
var detected = system.EditorDetected;
|
||||||
|
if (detected == null || detected.Count == 0) return;
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
EditorGUILayout.LabelField("── 实时检测结果 ──", EditorStyles.boldLabel);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var kvp in detected)
|
||||||
|
{
|
||||||
|
sb.Clear();
|
||||||
|
if (kvp.Value.Count > 0)
|
||||||
|
{
|
||||||
|
sb.Append("✓");
|
||||||
|
foreach (var go in kvp.Value)
|
||||||
|
sb.Append(' ').Append(go != null ? go.name : "null");
|
||||||
|
}
|
||||||
|
EditorGUILayout.LabelField(kvp.Key, kvp.Value.Count > 0 ? sb.ToString() : "—");
|
||||||
|
}
|
||||||
|
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2cd6fa1109af6d04fafb6c1556eea81d
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Animancer;
|
using Animancer;
|
||||||
using BaseGames.Boss;
|
using BaseGames.Boss;
|
||||||
@@ -36,6 +36,9 @@ namespace BaseGames.Editor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SceneObjectPlacerTool
|
public static class SceneObjectPlacerTool
|
||||||
{
|
{
|
||||||
|
// ── 碰撞器类型 ────────────────────────────────────────────────────────
|
||||||
|
public enum EnemyBodyColliderType { Box, Capsule, Circle }
|
||||||
|
|
||||||
// ══ 菜单入口 ══════════════════════════════════════════════════════════
|
// ══ 菜单入口 ══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
[MenuItem("BaseGames/Scene/Place/Player", priority = 100)]
|
[MenuItem("BaseGames/Scene/Place/Player", priority = 100)]
|
||||||
@@ -246,7 +249,9 @@ namespace BaseGames.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("BaseGames/Scene/Place/Enemy (Basic)", priority = 110)]
|
[MenuItem("BaseGames/Scene/Place/Enemy (Basic)", priority = 110)]
|
||||||
public static void PlaceEnemy()
|
public static void PlaceEnemy() => PlaceEnemy(EnemyBodyColliderType.Box);
|
||||||
|
|
||||||
|
public static void PlaceEnemy(EnemyBodyColliderType bodyCollider)
|
||||||
{
|
{
|
||||||
var report = new List<string>();
|
var report = new List<string>();
|
||||||
int undoGroup = Undo.GetCurrentGroup();
|
int undoGroup = Undo.GetCurrentGroup();
|
||||||
@@ -264,10 +269,12 @@ namespace BaseGames.Editor
|
|||||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||||
|
|
||||||
GetOrAddComponent<CapsuleCollider2D>(go);
|
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.7f, 0.9f));
|
||||||
GetOrAddComponent<Animator>(go);
|
Transform visual = GetOrCreateChild(go.transform, "Visual");
|
||||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
visual.localPosition = (Vector3)(Vector2)body.offset;
|
||||||
SpriteRenderer sr = SetupSpriteRenderer(go);
|
GetOrAddComponent<Animator>(visual.gameObject);
|
||||||
|
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
|
||||||
|
SpriteRenderer sr = SetupSpriteRenderer(visual.gameObject);
|
||||||
|
|
||||||
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
||||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||||
@@ -275,7 +282,7 @@ namespace BaseGames.Editor
|
|||||||
GetOrAddComponent<EnemyNavAgent>(go);
|
GetOrAddComponent<EnemyNavAgent>(go);
|
||||||
GetOrAddComponent<NavAgent>(go);
|
GetOrAddComponent<NavAgent>(go);
|
||||||
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
||||||
GetOrAddComponent<EnemySensorHub>(go);
|
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||||
|
|
||||||
// HurtBox child
|
// HurtBox child
|
||||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||||
@@ -306,6 +313,7 @@ namespace BaseGames.Editor
|
|||||||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||||||
|
|
||||||
// Wire EnemyMovement
|
// Wire EnemyMovement
|
||||||
|
AssignReference(movement, "_visualRoot", visual, report);
|
||||||
AssignReference(movement, "_animancer", animancer, report);
|
AssignReference(movement, "_animancer", animancer, report);
|
||||||
AssignReference(movement, "_spriteRenderer", sr, report);
|
AssignReference(movement, "_spriteRenderer", sr, report);
|
||||||
AssignLayerMask(movement, "_groundMask",
|
AssignLayerMask(movement, "_groundMask",
|
||||||
@@ -319,6 +327,7 @@ namespace BaseGames.Editor
|
|||||||
else
|
else
|
||||||
report.Add("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。请创建 CMB_DS_EnemyBody.asset。");
|
report.Add("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。请创建 CMB_DS_EnemyBody.asset。");
|
||||||
|
|
||||||
|
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
|
||||||
report.Add("★ 指定 EnemyBase._statsSO、_animConfig 资产(按所创建的敌人类型命名)。");
|
report.Add("★ 指定 EnemyBase._statsSO、_animConfig 资产(按所创建的敌人类型命名)。");
|
||||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定对应 .asset。");
|
report.Add("★ 挂载行为树 BehaviorTree 组件,指定对应 .asset。");
|
||||||
|
|
||||||
@@ -329,7 +338,9 @@ namespace BaseGames.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("BaseGames/Scene/Place/Boss Enemy", priority = 115)]
|
[MenuItem("BaseGames/Scene/Place/Boss Enemy", priority = 115)]
|
||||||
public static void PlaceBossEnemy()
|
public static void PlaceBossEnemy() => PlaceBossEnemy(EnemyBodyColliderType.Box);
|
||||||
|
|
||||||
|
public static void PlaceBossEnemy(EnemyBodyColliderType bodyCollider)
|
||||||
{
|
{
|
||||||
var report = new List<string>();
|
var report = new List<string>();
|
||||||
|
|
||||||
@@ -345,12 +356,13 @@ namespace BaseGames.Editor
|
|||||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||||
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||||||
|
|
||||||
GetOrAddComponent<CapsuleCollider2D>(go);
|
CreateBodyCollider(go, bodyCollider, new Vector2(1.5f, 2.5f));
|
||||||
GetOrAddComponent<Animator>(go);
|
GetOrAddComponent<Animator>(go);
|
||||||
SetupSpriteRenderer(go);
|
SetupSpriteRenderer(go);
|
||||||
|
|
||||||
BossBase bossBase = GetOrAddComponent<BossBase>(go);
|
BossBase bossBase = GetOrAddComponent<BossBase>(go);
|
||||||
EnemyStats bossStats = GetOrAddComponent<EnemyStats>(go);
|
EnemyStats bossStats = GetOrAddComponent<EnemyStats>(go);
|
||||||
|
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||||
|
|
||||||
// HurtBox child
|
// HurtBox child
|
||||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||||
@@ -388,6 +400,7 @@ namespace BaseGames.Editor
|
|||||||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||||||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||||||
|
|
||||||
|
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
|
||||||
report.Add("填写 _bossId。");
|
report.Add("填写 _bossId。");
|
||||||
report.Add("挂载 BossSkillSequencer 组件并指定技能序列 SO;行为树、NavAgent 需手工添加。");
|
report.Add("挂载 BossSkillSequencer 组件并指定技能序列 SO;行为树、NavAgent 需手工添加。");
|
||||||
report.Add("多阶段 Boss 可在此 GameObject 上继续 AddComponent 阶段切换控制器。");
|
report.Add("多阶段 Boss 可在此 GameObject 上继续 AddComponent 阶段切换控制器。");
|
||||||
@@ -399,7 +412,9 @@ namespace BaseGames.Editor
|
|||||||
// ══ 具体敌人快速放置 ════════════════════════════════════════════════════
|
// ══ 具体敌人快速放置 ════════════════════════════════════════════════════
|
||||||
|
|
||||||
[MenuItem("BaseGames/Scene/Place/Enemy E001 (草蛭)", priority = 111)]
|
[MenuItem("BaseGames/Scene/Place/Enemy E001 (草蛭)", priority = 111)]
|
||||||
public static void PlaceE001_CaoZhi()
|
public static void PlaceE001_CaoZhi() => PlaceE001_CaoZhi(EnemyBodyColliderType.Box);
|
||||||
|
|
||||||
|
public static void PlaceE001_CaoZhi(EnemyBodyColliderType bodyCollider)
|
||||||
{
|
{
|
||||||
var report = new List<string>();
|
var report = new List<string>();
|
||||||
int undoGroup = Undo.GetCurrentGroup();
|
int undoGroup = Undo.GetCurrentGroup();
|
||||||
@@ -416,12 +431,12 @@ namespace BaseGames.Editor
|
|||||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||||
|
|
||||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.6f, 0.8f));
|
||||||
body.size = new Vector2(0.6f, 0.8f);
|
Transform visual = GetOrCreateChild(go.transform, "Visual");
|
||||||
|
visual.localPosition = (Vector3)(Vector2)body.offset;
|
||||||
GetOrAddComponent<Animator>(go);
|
GetOrAddComponent<Animator>(visual.gameObject);
|
||||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
|
||||||
SpriteRenderer sr1 = SetupSpriteRenderer(go);
|
SpriteRenderer sr1 = SetupSpriteRenderer(visual.gameObject);
|
||||||
|
|
||||||
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
||||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||||
@@ -429,7 +444,7 @@ namespace BaseGames.Editor
|
|||||||
GetOrAddComponent<EnemyNavAgent>(go);
|
GetOrAddComponent<EnemyNavAgent>(go);
|
||||||
GetOrAddComponent<NavAgent>(go);
|
GetOrAddComponent<NavAgent>(go);
|
||||||
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
||||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||||
|
|
||||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||||
@@ -469,6 +484,7 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
AssignAsset(movement, "_config", report, false, "ENM_E001_Stats");
|
AssignAsset(movement, "_config", report, false, "ENM_E001_Stats");
|
||||||
AssignAsset(movement, "_animConfig", report, false, "ENM_E001_AnimConfig");
|
AssignAsset(movement, "_animConfig", report, false, "ENM_E001_AnimConfig");
|
||||||
|
AssignReference(movement, "_visualRoot", visual, report);
|
||||||
AssignReference(movement, "_animancer", animancer, report);
|
AssignReference(movement, "_animancer", animancer, report);
|
||||||
AssignReference(movement, "_spriteRenderer", sr1, report);
|
AssignReference(movement, "_spriteRenderer", sr1, report);
|
||||||
AssignLayerMask(movement, "_groundMask",
|
AssignLayerMask(movement, "_groundMask",
|
||||||
@@ -478,13 +494,11 @@ namespace BaseGames.Editor
|
|||||||
AssignAsset(alertAbility, "_config", report, false, "ABL_E001_Alert");
|
AssignAsset(alertAbility, "_config", report, false, "ABL_E001_Alert");
|
||||||
AssignAsset(chaseAbility, "_config", report, false, "ABL_E001_Chase");
|
AssignAsset(chaseAbility, "_config", report, false, "ABL_E001_Chase");
|
||||||
AssignReference(chaseAbility, "_contactDamage", bodyContact, report);
|
AssignReference(chaseAbility, "_contactDamage", bodyContact, report);
|
||||||
AssignReference(chaseAbility, "_sensorHub", sensorHub, report);
|
|
||||||
|
|
||||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||||
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
|
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
|
||||||
|
|
||||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "wall_ahead", "ledge" }, report);
|
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
|
||||||
report.Add("★ 在 EnemySensorHub Inspector 中绑定 Sensor 子节点(aggro/wall_ahead/ledge)。");
|
|
||||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E001_CaoZhi.asset。");
|
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E001_CaoZhi.asset。");
|
||||||
|
|
||||||
Undo.CollapseUndoOperations(undoGroup);
|
Undo.CollapseUndoOperations(undoGroup);
|
||||||
@@ -494,7 +508,9 @@ namespace BaseGames.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("BaseGames/Scene/Place/Enemy E002 (簧蛭)", priority = 112)]
|
[MenuItem("BaseGames/Scene/Place/Enemy E002 (簧蛭)", priority = 112)]
|
||||||
public static void PlaceE002_HuangZhi()
|
public static void PlaceE002_HuangZhi() => PlaceE002_HuangZhi(EnemyBodyColliderType.Box);
|
||||||
|
|
||||||
|
public static void PlaceE002_HuangZhi(EnemyBodyColliderType bodyCollider)
|
||||||
{
|
{
|
||||||
var report = new List<string>();
|
var report = new List<string>();
|
||||||
int undoGroup = Undo.GetCurrentGroup();
|
int undoGroup = Undo.GetCurrentGroup();
|
||||||
@@ -509,18 +525,24 @@ namespace BaseGames.Editor
|
|||||||
rb.bodyType = RigidbodyType2D.Kinematic;
|
rb.bodyType = RigidbodyType2D.Kinematic;
|
||||||
rb.gravityScale = 0f;
|
rb.gravityScale = 0f;
|
||||||
|
|
||||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.5f, 0.7f));
|
||||||
body.size = new Vector2(0.5f, 0.7f);
|
|
||||||
|
|
||||||
GetOrAddComponent<Animator>(go);
|
// Visual 子节点:挂载精灵 / 动画(EnemyMovement 翻转时操作此节点)
|
||||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
Transform visual = GetOrCreateChild(go.transform, "Visual");
|
||||||
SetupSpriteRenderer(go);
|
visual.localPosition = (Vector3)(Vector2)body.offset;
|
||||||
|
GetOrAddComponent<Animator>(visual.gameObject);
|
||||||
|
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
|
||||||
|
SpriteRenderer sr = SetupSpriteRenderer(visual.gameObject);
|
||||||
|
|
||||||
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
||||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||||
|
GetOrAddComponent<EnemyNavAgent>(go);
|
||||||
|
GetOrAddComponent<NavAgent>(go);
|
||||||
|
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
||||||
|
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||||
|
|
||||||
// HurtBox(初始禁用,附着天花板时不受伤)
|
// HurtBox(初始禁用,悬挂阶段无法被攻击)
|
||||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||||
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||||||
@@ -529,24 +551,35 @@ namespace BaseGames.Editor
|
|||||||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||||
hurtBoxT.gameObject.SetActive(false);
|
hurtBoxT.gameObject.SetActive(false);
|
||||||
|
|
||||||
// AttackHitBox(下坠发动时由能力启用)
|
// LandingHitBox(落地瞬间 AoE,由 CeilingDropAbility 激活)
|
||||||
Transform atkT = GetOrCreateChild(go.transform, "AttackHitBox");
|
Transform landingHitBoxT = GetOrCreateChild(go.transform, "LandingHitBox");
|
||||||
SetLayer(atkT.gameObject, "EnemyHitBox", report);
|
SetLayer(landingHitBoxT.gameObject, "EnemyHitBox", report);
|
||||||
BoxCollider2D atkCol = GetOrAddComponent<BoxCollider2D>(atkT.gameObject);
|
BoxCollider2D landingCol = GetOrAddComponent<BoxCollider2D>(landingHitBoxT.gameObject);
|
||||||
atkCol.isTrigger = true;
|
landingCol.isTrigger = true;
|
||||||
atkCol.size = new Vector2(0.5f, 0.5f);
|
landingCol.size = new Vector2(0.8f, 0.3f);
|
||||||
HitBox atkHitBox = GetOrAddComponent<HitBox>(atkT.gameObject);
|
HitBox landingHitBox = GetOrAddComponent<HitBox>(landingHitBoxT.gameObject);
|
||||||
atkT.gameObject.SetActive(false);
|
landingHitBoxT.gameObject.SetActive(false);
|
||||||
|
|
||||||
|
// ContactDamageZone(地面巡逻时造成接触伤害,落地后由行为树启用)
|
||||||
|
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
|
||||||
|
SetLayer(contactT.gameObject, "EnemyHitBox", report);
|
||||||
|
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
|
||||||
|
contactCol.isTrigger = true;
|
||||||
|
contactCol.radius = 0.35f;
|
||||||
|
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
|
||||||
|
GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
|
||||||
|
contactT.gameObject.SetActive(false);
|
||||||
|
|
||||||
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
|
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
|
||||||
Transform strikeT = GetOrCreateChild(abilitiesT, "CeilingHangStrikeAbility");
|
Transform dropT = GetOrCreateChild(abilitiesT, "CeilingDropAbility");
|
||||||
CeilingHangStrikeAbility strikeAbility = GetOrAddComponent<CeilingHangStrikeAbility>(strikeT.gameObject);
|
CeilingDropAbility dropAbility = GetOrAddComponent<CeilingDropAbility>(dropT.gameObject);
|
||||||
|
|
||||||
// SOs — assign first so OnValidate doesn't warn during wiring
|
// SOs — assign first so OnValidate doesn't warn during wiring
|
||||||
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E002_Stats");
|
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E002_Stats");
|
||||||
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E002_AnimConfig");
|
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E002_AnimConfig");
|
||||||
|
|
||||||
AssignReference(enemyBase, "_stats", enemyStats, report);
|
AssignReference(enemyBase, "_stats", enemyStats, report);
|
||||||
|
AssignReference(enemyBase, "_movement", movement, report);
|
||||||
AssignReference(enemyBase, "_animancer", animancer, report);
|
AssignReference(enemyBase, "_animancer", animancer, report);
|
||||||
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
|
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
|
||||||
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||||||
@@ -555,16 +588,32 @@ namespace BaseGames.Editor
|
|||||||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||||||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||||||
|
|
||||||
AssignAsset(strikeAbility, "_config", report, false, "ABL_E002_Strike");
|
AssignAsset(movement, "_config", report, false, "ENM_E002_Stats");
|
||||||
AssignReference(strikeAbility, "_attackHitBox", atkHitBox, report);
|
AssignAsset(movement, "_animConfig", report, false, "ENM_E002_AnimConfig");
|
||||||
AssignReference(strikeAbility, "_hurtBox", hurtBox, report);
|
AssignReference(movement, "_visualRoot", visual, report);
|
||||||
|
AssignReference(movement, "_animancer", animancer, report);
|
||||||
|
AssignReference(movement, "_spriteRenderer", sr, report);
|
||||||
|
AssignLayerMask(movement, "_groundMask",
|
||||||
|
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
|
||||||
|
report);
|
||||||
|
|
||||||
|
AssignReference(dropAbility, "_landingHitBox", landingHitBox, report);
|
||||||
|
AssignLayerMask(dropAbility, "_groundMask",
|
||||||
|
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
|
||||||
|
report);
|
||||||
|
|
||||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||||
if (dmgSrc != null) AssignReference(atkHitBox, "_defaultSource", dmgSrc, report);
|
if (dmgSrc != null)
|
||||||
|
{
|
||||||
|
AssignReference(landingHitBox, "_defaultSource", dmgSrc, report);
|
||||||
|
AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
|
||||||
|
}
|
||||||
|
|
||||||
SetupSensorHubSlotNames(sensorHub, new[] { "attack_range" }, report);
|
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_range" }, report);
|
||||||
report.Add("★ 将此对象放置于天花板,调整位置使 CapsuleCollider 正好贴合天花板底面。");
|
report.Add("★ 将此对象放置于天花板,调整位置使 CapsuleCollider 正好贴合天花板底面。");
|
||||||
|
report.Add("★ HurtBox / ContactDamageZone 初始禁用;落地后由行为树激活。");
|
||||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E002_HuangZhi.asset。");
|
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E002_HuangZhi.asset。");
|
||||||
|
report.Add("★ BD 树逻辑建议:Idle(悬挂)→ IsSensorDetecting(aggro) → UseAbility(CeilingDrop) → IsGrounded → Patrol(Pace)。");
|
||||||
|
|
||||||
Undo.CollapseUndoOperations(undoGroup);
|
Undo.CollapseUndoOperations(undoGroup);
|
||||||
Selection.activeGameObject = go;
|
Selection.activeGameObject = go;
|
||||||
@@ -573,7 +622,9 @@ namespace BaseGames.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("BaseGames/Scene/Place/Enemy E003 (幼蛭)", priority = 113)]
|
[MenuItem("BaseGames/Scene/Place/Enemy E003 (幼蛭)", priority = 113)]
|
||||||
public static void PlaceE003_YouZhi_Enemy()
|
public static void PlaceE003_YouZhi_Enemy() => PlaceE003_YouZhi_Enemy(EnemyBodyColliderType.Box);
|
||||||
|
|
||||||
|
public static void PlaceE003_YouZhi_Enemy(EnemyBodyColliderType bodyCollider)
|
||||||
{
|
{
|
||||||
var report = new List<string>();
|
var report = new List<string>();
|
||||||
int undoGroup = Undo.GetCurrentGroup();
|
int undoGroup = Undo.GetCurrentGroup();
|
||||||
@@ -588,12 +639,12 @@ namespace BaseGames.Editor
|
|||||||
rb.bodyType = RigidbodyType2D.Kinematic;
|
rb.bodyType = RigidbodyType2D.Kinematic;
|
||||||
rb.gravityScale = 0f;
|
rb.gravityScale = 0f;
|
||||||
|
|
||||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.5f, 0.6f));
|
||||||
body.size = new Vector2(0.5f, 0.6f);
|
Transform visual = GetOrCreateChild(go.transform, "Visual");
|
||||||
|
visual.localPosition = (Vector3)(Vector2)body.offset;
|
||||||
GetOrAddComponent<Animator>(go);
|
GetOrAddComponent<Animator>(visual.gameObject);
|
||||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
|
||||||
SpriteRenderer sr3 = SetupSpriteRenderer(go);
|
SpriteRenderer sr3 = SetupSpriteRenderer(visual.gameObject);
|
||||||
|
|
||||||
E003_YouZhi enemyBase = GetOrAddComponent<E003_YouZhi>(go);
|
E003_YouZhi enemyBase = GetOrAddComponent<E003_YouZhi>(go);
|
||||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||||
@@ -601,7 +652,7 @@ namespace BaseGames.Editor
|
|||||||
GetOrAddComponent<EnemyNavAgent>(go);
|
GetOrAddComponent<EnemyNavAgent>(go);
|
||||||
GetOrAddComponent<NavAgent>(go);
|
GetOrAddComponent<NavAgent>(go);
|
||||||
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
||||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||||
|
|
||||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||||
@@ -638,6 +689,7 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
AssignAsset(movement, "_config", report, false, "ENM_E003_Stats");
|
AssignAsset(movement, "_config", report, false, "ENM_E003_Stats");
|
||||||
AssignAsset(movement, "_animConfig", report, false, "ENM_E003_AnimConfig");
|
AssignAsset(movement, "_animConfig", report, false, "ENM_E003_AnimConfig");
|
||||||
|
AssignReference(movement, "_visualRoot", visual, report);
|
||||||
AssignReference(movement, "_animancer", animancer, report);
|
AssignReference(movement, "_animancer", animancer, report);
|
||||||
AssignReference(movement, "_spriteRenderer", sr3, report);
|
AssignReference(movement, "_spriteRenderer", sr3, report);
|
||||||
AssignLayerMask(movement, "_groundMask",
|
AssignLayerMask(movement, "_groundMask",
|
||||||
@@ -650,7 +702,7 @@ namespace BaseGames.Editor
|
|||||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||||
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
|
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
|
||||||
|
|
||||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro" }, report);
|
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
|
||||||
report.Add("★ 将此对象放置于天花板下方,E003_YouZhi 会在 OnSpawn/ActivateFromCeiling 时执行下坠。");
|
report.Add("★ 将此对象放置于天花板下方,E003_YouZhi 会在 OnSpawn/ActivateFromCeiling 时执行下坠。");
|
||||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E003_YouZhi.asset。");
|
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E003_YouZhi.asset。");
|
||||||
|
|
||||||
@@ -661,7 +713,9 @@ namespace BaseGames.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("BaseGames/Scene/Place/Enemy E004 (蛭母)", priority = 114)]
|
[MenuItem("BaseGames/Scene/Place/Enemy E004 (蛭母)", priority = 114)]
|
||||||
public static void PlaceE004_ZhiMu_Enemy()
|
public static void PlaceE004_ZhiMu_Enemy() => PlaceE004_ZhiMu_Enemy(EnemyBodyColliderType.Box);
|
||||||
|
|
||||||
|
public static void PlaceE004_ZhiMu_Enemy(EnemyBodyColliderType bodyCollider)
|
||||||
{
|
{
|
||||||
var report = new List<string>();
|
var report = new List<string>();
|
||||||
int undoGroup = Undo.GetCurrentGroup();
|
int undoGroup = Undo.GetCurrentGroup();
|
||||||
@@ -678,12 +732,12 @@ namespace BaseGames.Editor
|
|||||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||||
|
|
||||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.8f, 1.2f));
|
||||||
body.size = new Vector2(0.8f, 1.2f);
|
Transform visual = GetOrCreateChild(go.transform, "Visual");
|
||||||
|
visual.localPosition = (Vector3)(Vector2)body.offset;
|
||||||
GetOrAddComponent<Animator>(go);
|
GetOrAddComponent<Animator>(visual.gameObject);
|
||||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
|
||||||
SpriteRenderer sr4 = SetupSpriteRenderer(go);
|
SpriteRenderer sr4 = SetupSpriteRenderer(visual.gameObject);
|
||||||
|
|
||||||
E004_ZhiMu enemyBase = GetOrAddComponent<E004_ZhiMu>(go);
|
E004_ZhiMu enemyBase = GetOrAddComponent<E004_ZhiMu>(go);
|
||||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||||
@@ -692,7 +746,7 @@ namespace BaseGames.Editor
|
|||||||
GetOrAddComponent<EnemyNavAgent>(go);
|
GetOrAddComponent<EnemyNavAgent>(go);
|
||||||
GetOrAddComponent<NavAgent>(go);
|
GetOrAddComponent<NavAgent>(go);
|
||||||
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
||||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||||
|
|
||||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||||
@@ -756,6 +810,7 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
AssignAsset(movement, "_config", report, false, "ENM_E004_Stats");
|
AssignAsset(movement, "_config", report, false, "ENM_E004_Stats");
|
||||||
AssignAsset(movement, "_animConfig", report, false, "ENM_E004_AnimConfig");
|
AssignAsset(movement, "_animConfig", report, false, "ENM_E004_AnimConfig");
|
||||||
|
AssignReference(movement, "_visualRoot", visual, report);
|
||||||
AssignReference(movement, "_animancer", animancer, report);
|
AssignReference(movement, "_animancer", animancer, report);
|
||||||
AssignReference(movement, "_spriteRenderer", sr4, report);
|
AssignReference(movement, "_spriteRenderer", sr4, report);
|
||||||
AssignLayerMask(movement, "_groundMask",
|
AssignLayerMask(movement, "_groundMask",
|
||||||
@@ -772,7 +827,6 @@ namespace BaseGames.Editor
|
|||||||
AssignReference(slamAbl, "_hitBox", slamHitBox, report);
|
AssignReference(slamAbl, "_hitBox", slamHitBox, report);
|
||||||
AssignReference(acidAbl, "_muzzle", acidMuzzleT, report);
|
AssignReference(acidAbl, "_muzzle", acidMuzzleT, report);
|
||||||
AssignReference(chargeAbl, "_chargeHitBox", chargeHitBox, report);
|
AssignReference(chargeAbl, "_chargeHitBox", chargeHitBox, report);
|
||||||
AssignReference(chaseAbl, "_sensorHub", sensorHub, report);
|
|
||||||
|
|
||||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||||
if (dmgSrc != null)
|
if (dmgSrc != null)
|
||||||
@@ -782,7 +836,7 @@ namespace BaseGames.Editor
|
|||||||
AssignReference(chargeHitBox, "_defaultSource", dmgSrc, report);
|
AssignReference(chargeHitBox, "_defaultSource", dmgSrc, report);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "los" }, report);
|
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
|
||||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E004_ZhiMu.asset。");
|
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E004_ZhiMu.asset。");
|
||||||
|
|
||||||
Undo.CollapseUndoOperations(undoGroup);
|
Undo.CollapseUndoOperations(undoGroup);
|
||||||
@@ -792,7 +846,9 @@ namespace BaseGames.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("BaseGames/Scene/Place/Enemy E005 (肥蛭)", priority = 115)]
|
[MenuItem("BaseGames/Scene/Place/Enemy E005 (肥蛭)", priority = 115)]
|
||||||
public static void PlaceE005_FeiZhi_Enemy()
|
public static void PlaceE005_FeiZhi_Enemy() => PlaceE005_FeiZhi_Enemy(EnemyBodyColliderType.Box);
|
||||||
|
|
||||||
|
public static void PlaceE005_FeiZhi_Enemy(EnemyBodyColliderType bodyCollider)
|
||||||
{
|
{
|
||||||
var report = new List<string>();
|
var report = new List<string>();
|
||||||
int undoGroup = Undo.GetCurrentGroup();
|
int undoGroup = Undo.GetCurrentGroup();
|
||||||
@@ -809,12 +865,12 @@ namespace BaseGames.Editor
|
|||||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||||
|
|
||||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.9f, 1.0f));
|
||||||
body.size = new Vector2(0.9f, 1.0f);
|
Transform visual = GetOrCreateChild(go.transform, "Visual");
|
||||||
|
visual.localPosition = (Vector3)(Vector2)body.offset;
|
||||||
GetOrAddComponent<Animator>(go);
|
GetOrAddComponent<Animator>(visual.gameObject);
|
||||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
|
||||||
SpriteRenderer sr5 = SetupSpriteRenderer(go);
|
SpriteRenderer sr5 = SetupSpriteRenderer(visual.gameObject);
|
||||||
|
|
||||||
E005_FeiZhi enemyBase = GetOrAddComponent<E005_FeiZhi>(go);
|
E005_FeiZhi enemyBase = GetOrAddComponent<E005_FeiZhi>(go);
|
||||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||||
@@ -822,7 +878,7 @@ namespace BaseGames.Editor
|
|||||||
GetOrAddComponent<EnemyNavAgent>(go);
|
GetOrAddComponent<EnemyNavAgent>(go);
|
||||||
GetOrAddComponent<NavAgent>(go);
|
GetOrAddComponent<NavAgent>(go);
|
||||||
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
||||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||||
|
|
||||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||||
@@ -863,6 +919,7 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
AssignAsset(movement, "_config", report, false, "ENM_E005_Stats");
|
AssignAsset(movement, "_config", report, false, "ENM_E005_Stats");
|
||||||
AssignAsset(movement, "_animConfig", report, false, "ENM_E005_AnimConfig");
|
AssignAsset(movement, "_animConfig", report, false, "ENM_E005_AnimConfig");
|
||||||
|
AssignReference(movement, "_visualRoot", visual, report);
|
||||||
AssignReference(movement, "_animancer", animancer, report);
|
AssignReference(movement, "_animancer", animancer, report);
|
||||||
AssignReference(movement, "_spriteRenderer", sr5, report);
|
AssignReference(movement, "_spriteRenderer", sr5, report);
|
||||||
AssignLayerMask(movement, "_groundMask",
|
AssignLayerMask(movement, "_groundMask",
|
||||||
@@ -878,7 +935,7 @@ namespace BaseGames.Editor
|
|||||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||||
if (dmgSrc != null) AssignReference(biteHitBox, "_defaultSource", dmgSrc, report);
|
if (dmgSrc != null) AssignReference(biteHitBox, "_defaultSource", dmgSrc, report);
|
||||||
|
|
||||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro" }, report);
|
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
|
||||||
report.Add("★ 在 E005_FeiZhi._deathPreClip 上添加 AnimationEvent 调用 SpawnProjectile(\"spawn_e003\")。");
|
report.Add("★ 在 E005_FeiZhi._deathPreClip 上添加 AnimationEvent 调用 SpawnProjectile(\"spawn_e003\")。");
|
||||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E005_FeiZhi.asset。");
|
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E005_FeiZhi.asset。");
|
||||||
|
|
||||||
@@ -889,7 +946,9 @@ namespace BaseGames.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("BaseGames/Scene/Place/Enemy E006 (讙)", priority = 116)]
|
[MenuItem("BaseGames/Scene/Place/Enemy E006 (讙)", priority = 116)]
|
||||||
public static void PlaceE006_Huan()
|
public static void PlaceE006_Huan() => PlaceE006_Huan(EnemyBodyColliderType.Box);
|
||||||
|
|
||||||
|
public static void PlaceE006_Huan(EnemyBodyColliderType bodyCollider)
|
||||||
{
|
{
|
||||||
var report = new List<string>();
|
var report = new List<string>();
|
||||||
int undoGroup = Undo.GetCurrentGroup();
|
int undoGroup = Undo.GetCurrentGroup();
|
||||||
@@ -906,12 +965,12 @@ namespace BaseGames.Editor
|
|||||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||||
|
|
||||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.7f, 1.0f));
|
||||||
body.size = new Vector2(0.7f, 1.0f);
|
Transform visual = GetOrCreateChild(go.transform, "Visual");
|
||||||
|
visual.localPosition = (Vector3)(Vector2)body.offset;
|
||||||
GetOrAddComponent<Animator>(go);
|
GetOrAddComponent<Animator>(visual.gameObject);
|
||||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
|
||||||
SpriteRenderer sr6 = SetupSpriteRenderer(go);
|
SpriteRenderer sr6 = SetupSpriteRenderer(visual.gameObject);
|
||||||
|
|
||||||
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
||||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||||
@@ -919,7 +978,7 @@ namespace BaseGames.Editor
|
|||||||
GetOrAddComponent<EnemyNavAgent>(go);
|
GetOrAddComponent<EnemyNavAgent>(go);
|
||||||
GetOrAddComponent<NavAgent>(go);
|
GetOrAddComponent<NavAgent>(go);
|
||||||
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
||||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||||
|
|
||||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||||
@@ -966,6 +1025,7 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
AssignAsset(movement, "_config", report, false, "ENM_E006_Stats");
|
AssignAsset(movement, "_config", report, false, "ENM_E006_Stats");
|
||||||
AssignAsset(movement, "_animConfig", report, false, "ENM_E006_AnimConfig");
|
AssignAsset(movement, "_animConfig", report, false, "ENM_E006_AnimConfig");
|
||||||
|
AssignReference(movement, "_visualRoot", visual, report);
|
||||||
AssignReference(movement, "_animancer", animancer, report);
|
AssignReference(movement, "_animancer", animancer, report);
|
||||||
AssignReference(movement, "_spriteRenderer", sr6, report);
|
AssignReference(movement, "_spriteRenderer", sr6, report);
|
||||||
AssignLayerMask(movement, "_groundMask",
|
AssignLayerMask(movement, "_groundMask",
|
||||||
@@ -977,7 +1037,6 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
AssignReference(leapAbl, "_landingHitBox", landHitBox, report);
|
AssignReference(leapAbl, "_landingHitBox", landHitBox, report);
|
||||||
AssignReference(chaseAbl, "_contactDamage", bodyContact, report);
|
AssignReference(chaseAbl, "_contactDamage", bodyContact, report);
|
||||||
AssignReference(chaseAbl, "_sensorHub", sensorHub, report);
|
|
||||||
|
|
||||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||||
if (dmgSrc != null)
|
if (dmgSrc != null)
|
||||||
@@ -986,7 +1045,7 @@ namespace BaseGames.Editor
|
|||||||
AssignReference(landHitBox, "_defaultSource", dmgSrc, report);
|
AssignReference(landHitBox, "_defaultSource", dmgSrc, report);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "wall_ahead", "ledge" }, report);
|
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
|
||||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E006_Huan.asset。");
|
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E006_Huan.asset。");
|
||||||
|
|
||||||
Undo.CollapseUndoOperations(undoGroup);
|
Undo.CollapseUndoOperations(undoGroup);
|
||||||
@@ -996,7 +1055,9 @@ namespace BaseGames.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("BaseGames/Scene/Place/Boss 嘲风 (ChaoFeng)", priority = 117)]
|
[MenuItem("BaseGames/Scene/Place/Boss 嘲风 (ChaoFeng)", priority = 117)]
|
||||||
public static void PlaceChaoFeng()
|
public static void PlaceChaoFeng() => PlaceChaoFeng(EnemyBodyColliderType.Box);
|
||||||
|
|
||||||
|
public static void PlaceChaoFeng(EnemyBodyColliderType bodyCollider)
|
||||||
{
|
{
|
||||||
var report = new List<string>();
|
var report = new List<string>();
|
||||||
int undoGroup = Undo.GetCurrentGroup();
|
int undoGroup = Undo.GetCurrentGroup();
|
||||||
@@ -1014,12 +1075,12 @@ namespace BaseGames.Editor
|
|||||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||||
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||||||
|
|
||||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(1.2f, 2.0f));
|
||||||
body.size = new Vector2(1.2f, 2.0f);
|
Transform visual = GetOrCreateChild(go.transform, "Visual");
|
||||||
|
visual.localPosition = (Vector3)(Vector2)body.offset;
|
||||||
GetOrAddComponent<Animator>(go);
|
GetOrAddComponent<Animator>(visual.gameObject);
|
||||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
|
||||||
SpriteRenderer srBoss = SetupSpriteRenderer(go);
|
SpriteRenderer srBoss = SetupSpriteRenderer(visual.gameObject);
|
||||||
|
|
||||||
ChaoFengBoss bossBase = GetOrAddComponent<ChaoFengBoss>(go);
|
ChaoFengBoss bossBase = GetOrAddComponent<ChaoFengBoss>(go);
|
||||||
EnemyStats bossStats = GetOrAddComponent<EnemyStats>(go);
|
EnemyStats bossStats = GetOrAddComponent<EnemyStats>(go);
|
||||||
@@ -1031,7 +1092,7 @@ namespace BaseGames.Editor
|
|||||||
BossSkillExecutor skillExec = GetOrAddComponent<BossSkillExecutor>(go);
|
BossSkillExecutor skillExec = GetOrAddComponent<BossSkillExecutor>(go);
|
||||||
ChaoFengFloatController floatCtrl = GetOrAddComponent<ChaoFengFloatController>(go);
|
ChaoFengFloatController floatCtrl = GetOrAddComponent<ChaoFengFloatController>(go);
|
||||||
ChaoFengKnockdownCounter knockdown = GetOrAddComponent<ChaoFengKnockdownCounter>(go);
|
ChaoFengKnockdownCounter knockdown = GetOrAddComponent<ChaoFengKnockdownCounter>(go);
|
||||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||||
|
|
||||||
// HurtBox
|
// HurtBox
|
||||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||||
@@ -1078,6 +1139,7 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
AssignAsset(movement, "_config", report, false, "ENM_ChaoFeng_Stats");
|
AssignAsset(movement, "_config", report, false, "ENM_ChaoFeng_Stats");
|
||||||
AssignAsset(movement, "_animConfig", report, false, "ENM_ChaoFeng_AnimConfig");
|
AssignAsset(movement, "_animConfig", report, false, "ENM_ChaoFeng_AnimConfig");
|
||||||
|
AssignReference(movement, "_visualRoot", visual, report);
|
||||||
AssignReference(movement, "_animancer", animancer, report);
|
AssignReference(movement, "_animancer", animancer, report);
|
||||||
AssignReference(movement, "_spriteRenderer", srBoss, report);
|
AssignReference(movement, "_spriteRenderer", srBoss, report);
|
||||||
AssignLayerMask(movement, "_groundMask",
|
AssignLayerMask(movement, "_groundMask",
|
||||||
@@ -1103,7 +1165,7 @@ namespace BaseGames.Editor
|
|||||||
if (hb != null) AssignReference(hb, "_defaultSource", dmgSrc, report);
|
if (hb != null) AssignReference(hb, "_defaultSource", dmgSrc, report);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
|
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
|
||||||
|
|
||||||
report.Add("★ 设置 BossSkillExecutor._bossId = \"ChaoFeng\"。");
|
report.Add("★ 设置 BossSkillExecutor._bossId = \"ChaoFeng\"。");
|
||||||
report.Add("★ 将各 Phase1 HitBox 引用拖入 BossSkillExecutor._hitBoxes 数组。");
|
report.Add("★ 将各 Phase1 HitBox 引用拖入 BossSkillExecutor._hitBoxes 数组。");
|
||||||
@@ -1145,20 +1207,56 @@ namespace BaseGames.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置 EnemySensorHub._slots 的 slotName 字段(Sensor 引用需在 Inspector 中手工绑定)。
|
/// 在 <see cref="PhysicsPerceptionSystem"/> 上预填充 <c>_slots</c> 数组,
|
||||||
|
/// 根据 slotName 自动选择类型、半径、检测层及 GizmoColor。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void SetupSensorHubSlotNames(EnemySensorHub hub, string[] slotNames, List<string> report)
|
private static void SetupPerceptionSystemSlots(PhysicsPerceptionSystem system, string[] slotNames, List<string> report)
|
||||||
{
|
{
|
||||||
var so = new SerializedObject(hub);
|
var so = new SerializedObject(system);
|
||||||
var slots = so.FindProperty("_slots");
|
var slots = so.FindProperty("_slots");
|
||||||
if (slots == null || !slots.isArray)
|
if (slots == null || !slots.isArray)
|
||||||
{
|
{
|
||||||
report?.Add("EnemySensorHub._slots 字段未找到,传感器槽位需手工配置。");
|
report?.Add("PhysicsPerceptionSystem._slots 字段未找到,请检查脚本序列化。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int playerLayer = LayerMask.GetMask("Player");
|
||||||
|
|
||||||
slots.arraySize = slotNames.Length;
|
slots.arraySize = slotNames.Length;
|
||||||
for (int i = 0; i < slotNames.Length; i++)
|
for (int i = 0; i < slotNames.Length; i++)
|
||||||
slots.GetArrayElementAtIndex(i).FindPropertyRelative("slotName").stringValue = slotNames[i];
|
{
|
||||||
|
var elem = slots.GetArrayElementAtIndex(i);
|
||||||
|
string name = slotNames[i];
|
||||||
|
|
||||||
|
elem.FindPropertyRelative("slotName").stringValue = name;
|
||||||
|
|
||||||
|
int enumIdx = 0; // RangeCircle
|
||||||
|
float radius = 3f;
|
||||||
|
int layer = playerLayer;
|
||||||
|
|
||||||
|
switch (name)
|
||||||
|
{
|
||||||
|
case "aggro": enumIdx = 0; radius = 5f; layer = playerLayer; break;
|
||||||
|
case "los": enumIdx = 1; radius = 0f; layer = 0; break;
|
||||||
|
case "attack_melee":enumIdx = 0; radius = 1.5f; layer = playerLayer; break;
|
||||||
|
case "attack_range":enumIdx = 0; radius = 8f; layer = playerLayer; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
elem.FindPropertyRelative("type").enumValueIndex = enumIdx;
|
||||||
|
elem.FindPropertyRelative("radius").floatValue = radius;
|
||||||
|
elem.FindPropertyRelative("detectLayer").intValue = layer;
|
||||||
|
|
||||||
|
// 各 slot 分配语义化默认颜色,可在 Inspector 中按需覆盖
|
||||||
|
Color defaultColor = name switch
|
||||||
|
{
|
||||||
|
"aggro" => new Color(1.00f, 0.60f, 0.10f, 1f), // 橙
|
||||||
|
"los" => new Color(0.00f, 0.80f, 1.00f, 1f), // 青
|
||||||
|
"attack_melee" => new Color(1.00f, 0.20f, 0.20f, 1f), // 红
|
||||||
|
"attack_range" => new Color(1.00f, 0.40f, 0.60f, 1f), // 粉红
|
||||||
|
_ => Color.clear, // 未知 slot 回退为紫色
|
||||||
|
};
|
||||||
|
elem.FindPropertyRelative("gizmoColor").colorValue = defaultColor;
|
||||||
|
}
|
||||||
so.ApplyModifiedPropertiesWithoutUndo();
|
so.ApplyModifiedPropertiesWithoutUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1713,6 +1811,25 @@ namespace BaseGames.Editor
|
|||||||
return Vector3.zero;
|
return Vector3.zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Collider2D CreateBodyCollider(GameObject go, EnemyBodyColliderType type, Vector2 size)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case EnemyBodyColliderType.Capsule:
|
||||||
|
var cap = GetOrAddComponent<CapsuleCollider2D>(go);
|
||||||
|
cap.size = size;
|
||||||
|
return cap;
|
||||||
|
case EnemyBodyColliderType.Circle:
|
||||||
|
var cir = GetOrAddComponent<CircleCollider2D>(go);
|
||||||
|
cir.radius = Mathf.Min(size.x, size.y) * 0.5f;
|
||||||
|
return cir;
|
||||||
|
default: // Box
|
||||||
|
var box = GetOrAddComponent<BoxCollider2D>(go);
|
||||||
|
box.size = size;
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static T GetOrAddComponent<T>(GameObject go) where T : Component
|
private static T GetOrAddComponent<T>(GameObject go) where T : Component
|
||||||
{
|
{
|
||||||
T comp = go.GetComponent<T>();
|
T comp = go.GetComponent<T>();
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ namespace BaseGames.Enemies.AI
|
|||||||
|
|
||||||
Vector2 playerPos = _enemy.PlayerTransform.position;
|
Vector2 playerPos = _enemy.PlayerTransform.position;
|
||||||
|
|
||||||
|
// 若配置了巡逻区域,且玩家超出追击边界 → 放弃追击(优先级高于纯距离限制)
|
||||||
|
var zone = _enemy.PatrolZone;
|
||||||
|
if (zone != null && !zone.ContainsChase(playerPos))
|
||||||
|
return TaskStatus.Failure;
|
||||||
|
|
||||||
if (_enemy.IsPlayerVisible())
|
if (_enemy.IsPlayerVisible())
|
||||||
{
|
{
|
||||||
// 视线恢复:Searching → Tracking,恢复奔跑速度
|
// 视线恢复:Searching → Tracking,恢复奔跑速度
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace BaseGames.Enemies.AI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[TaskName("Is Near Edge?")]
|
[TaskName("Is Near Edge?")]
|
||||||
[TaskCategory("BaseGames/Enemy/State")]
|
[TaskCategory("BaseGames/Enemy/State")]
|
||||||
[TaskDescription("检查前方是否有悬崖边缘(基于 SensorToolkit 或 Raycast)")]
|
[TaskDescription("检查前方是否有悬崖边缘(基于 EnemyNavAgent Raycast 检测)")]
|
||||||
public class BD_IsNearEdge : Conditional
|
public class BD_IsNearEdge : Conditional
|
||||||
{
|
{
|
||||||
private EnemyBase _enemy;
|
private EnemyBase _enemy;
|
||||||
|
|||||||
61
Assets/_Game/Scripts/Enemies/AI/BD_IsOutsideZone.cs
Normal file
61
Assets/_Game/Scripts/Enemies/AI/BD_IsOutsideZone.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#if GRAPH_DESIGNER
|
||||||
|
using UnityEngine;
|
||||||
|
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||||
|
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
|
||||||
|
using BaseGames.Enemies;
|
||||||
|
|
||||||
|
namespace BaseGames.Enemies.AI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// BD Conditional:判断目标坐标是否超出指定区域边界。
|
||||||
|
///
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>未配置 PatrolZone 时返回 Failure(表示"无限制",等同于不超界)。</item>
|
||||||
|
/// <item>超界 → Success;区域内 → Failure。</item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// 典型用法:在 Patrol BT 子树中用 BD_IsOutsideZone 检查敌人坐标,
|
||||||
|
/// 超出巡逻区域时触发归位序列。
|
||||||
|
/// </summary>
|
||||||
|
[TaskName("Is Outside Zone")]
|
||||||
|
[TaskCategory("BaseGames/Enemy/Zone")]
|
||||||
|
[TaskDescription("判断敌人/玩家坐标是否超出巡逻或追击区域;无 Zone 时返回 Failure(不限制)")]
|
||||||
|
public sealed class BD_IsOutsideZone : Conditional
|
||||||
|
{
|
||||||
|
[Tooltip("true = 检查追击区域,false = 检查巡逻区域")]
|
||||||
|
[SerializeField] private bool m_CheckChaseZone = false;
|
||||||
|
|
||||||
|
[Tooltip("true = 检查敌人自身坐标,false = 检查玩家坐标")]
|
||||||
|
[SerializeField] private bool m_CheckEnemy = true;
|
||||||
|
|
||||||
|
private EnemyBase _enemy;
|
||||||
|
|
||||||
|
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
|
||||||
|
|
||||||
|
public override TaskStatus OnUpdate()
|
||||||
|
{
|
||||||
|
if (_enemy == null) return TaskStatus.Failure;
|
||||||
|
|
||||||
|
var zone = _enemy.PatrolZone;
|
||||||
|
if (zone == null) return TaskStatus.Failure; // 无区域 = 不限制
|
||||||
|
|
||||||
|
Vector2 pos;
|
||||||
|
if (m_CheckEnemy)
|
||||||
|
{
|
||||||
|
pos = _enemy.transform.position;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_enemy.PlayerTransform == null) return TaskStatus.Failure;
|
||||||
|
pos = _enemy.PlayerTransform.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inside = m_CheckChaseZone
|
||||||
|
? zone.ContainsChase(pos)
|
||||||
|
: zone.ContainsPatrol(pos);
|
||||||
|
|
||||||
|
return inside ? TaskStatus.Failure : TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
11
Assets/_Game/Scripts/Enemies/AI/BD_IsOutsideZone.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/AI/BD_IsOutsideZone.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bff87912e6defb849bea247df2801172
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -7,23 +7,23 @@ using BaseGames.Enemies.Perception;
|
|||||||
namespace BaseGames.Enemies.AI
|
namespace BaseGames.Enemies.AI
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 条件:EnemySensorHub 中名为 slotName 的 Sensor 是否检测到目标。
|
/// 条件:<see cref="IPerceptionSystem"/> 中名为 slotName 的槽位是否检测到目标。
|
||||||
/// 若 m_Target 为空则使用 EnemyBase.PlayerTransform。
|
/// 若 m_Target 为空则使用 EnemyBase.PlayerTransform。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[TaskName("Is Sensor Detecting?")]
|
[TaskName("Is Sensor Detecting?")]
|
||||||
[TaskCategory("BaseGames/Enemy/Perception")]
|
[TaskCategory("BaseGames/Enemy/Perception")]
|
||||||
[TaskDescription("检查 EnemySensorHub 中指定 Sensor 槽是否检测到目标")]
|
[TaskDescription("检查 PhysicsPerceptionSystem 中指定 Sensor 槽是否检测到目标")]
|
||||||
public sealed class BD_IsSensorDetecting : Conditional
|
public sealed class BD_IsSensorDetecting : Conditional
|
||||||
{
|
{
|
||||||
[SerializeField] private string m_SlotName = "aggro";
|
[SerializeField] private string m_SlotName = "aggro";
|
||||||
[SerializeField] private bool m_AnyTarget = false;
|
[SerializeField] private bool m_AnyTarget = false;
|
||||||
|
|
||||||
private EnemySensorHub _hub;
|
private IPerceptionSystem _hub;
|
||||||
private EnemyBase _enemy;
|
private EnemyBase _enemy;
|
||||||
|
|
||||||
public override void OnAwake()
|
public override void OnAwake()
|
||||||
{
|
{
|
||||||
_hub = gameObject.GetComponent<EnemySensorHub>();
|
_hub = gameObject.GetComponent<IPerceptionSystem>();
|
||||||
_enemy = gameObject.GetComponent<EnemyBase>();
|
_enemy = gameObject.GetComponent<EnemyBase>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,44 +3,38 @@ using UnityEngine;
|
|||||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||||
using BaseGames.Enemies;
|
using BaseGames.Enemies;
|
||||||
using BaseGames.Enemies.Perception;
|
|
||||||
|
|
||||||
namespace BaseGames.Enemies.AI
|
namespace BaseGames.Enemies.AI
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// BD Action:来回踱步巡逻——持续向当前方向移动,遇墙或悬崖时自动翻转方向。
|
/// BD Action:来回踱步巡逻——持续向当前方向移动,遇墙或悬崖时自动翻转方向。
|
||||||
/// 转向检测依赖 EnemySensorHub 的 "wall_ahead" / "ledge" 槽(SensorToolkit)。
|
/// 转向检测通过 <see cref="EnemyMovement.IsWallAhead"/> / <see cref="EnemyMovement.IsLedgeAhead"/>
|
||||||
|
/// 进行;这两项是 EnemyMovement 内置的物理射线检测,不属于感知系统(PhysicsPerceptionSystem)。
|
||||||
///
|
///
|
||||||
/// 若需要按预设路点顺序巡逻,请使用 <see cref="BD_PatrolWaypoints"/>(支持 Transform 引用和内联坐标)。
|
/// 若需要按预设路点顺序巡逻,请使用 <see cref="BD_PatrolWaypoints"/>(支持 Transform 引用和内联坐标)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[TaskName("Patrol (Pace)")]
|
[TaskName("Patrol (Pace)")]
|
||||||
[TaskCategory("BaseGames/Enemy/Movement")]
|
[TaskCategory("BaseGames/Enemy/Movement")]
|
||||||
[TaskDescription("来回踱步巡逻:遇墙或悬崖自动翻转方向(需配置 EnemySensorHub wall_ahead / ledge 槽)")]
|
[TaskDescription("来回踱步巡逻:遇墙或悬崖自动翻转方向(通过 EnemyMovement 物理射线检测,无需感知槽)")]
|
||||||
public class BD_Patrol : Action
|
public class BD_Patrol : Action
|
||||||
{
|
{
|
||||||
private EnemyBase _enemy;
|
private EnemyBase _enemy;
|
||||||
private EnemySensorHub _hub;
|
private EnemyMovement _movement;
|
||||||
private float _dir = 1f;
|
private float _dir = 1f;
|
||||||
private float _flipCooldown; // 翻转后短暂冷却,等待 RaySensor2D 刷新到新朝向
|
private float _flipCooldown; // 翻转后短暂冷却,等待射线刷新到新朝向
|
||||||
|
|
||||||
// 缓存:SensorHub 中对应槽位是否已配置(Awake 时查询一次,避免每帧 Dictionary 查找)
|
|
||||||
private bool _hasWallSensor;
|
|
||||||
private bool _hasEdgeSensor;
|
|
||||||
|
|
||||||
public override void OnAwake()
|
public override void OnAwake()
|
||||||
{
|
{
|
||||||
_enemy = GetComponent<EnemyBase>();
|
_enemy = GetComponent<EnemyBase>();
|
||||||
_hub = GetComponent<EnemySensorHub>();
|
_movement = _enemy?.Movement;
|
||||||
_hasWallSensor = _hub != null && _hub.Get(SensorSlotNames.WallAhead) != null;
|
|
||||||
_hasEdgeSensor = _hub != null && _hub.Get(SensorSlotNames.Ledge) != null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnStart()
|
public override void OnStart()
|
||||||
{
|
{
|
||||||
_enemy?.SetAiPhase(AiPhase.Patrol);
|
_enemy?.SetAiPhase(AiPhase.Patrol);
|
||||||
// 与敌人实际朝向同步,防止任务重入时 _dir 与朝向不符(如战斗后朝向已改变)
|
// 与敌人实际朝向同步,防止任务重入时 _dir 与朝向不符(如战斗后朝向已改变)
|
||||||
if (_enemy?.Movement != null)
|
if (_movement != null)
|
||||||
_dir = _enemy.Movement.FacingDirection;
|
_dir = _movement.FacingDirection;
|
||||||
_flipCooldown = 0f;
|
_flipCooldown = 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +42,13 @@ namespace BaseGames.Enemies.AI
|
|||||||
{
|
{
|
||||||
if (_enemy == null) return TaskStatus.Failure;
|
if (_enemy == null) return TaskStatus.Failure;
|
||||||
|
|
||||||
// 翻转冷却期间跳过传感器检测(等待 RaySensor2D 在新朝向完成刷新)
|
// 翻转冷却期间跳过物理检测(等待射线在新朝向完成刷新)
|
||||||
if (_flipCooldown > 0f)
|
if (_flipCooldown > 0f)
|
||||||
_flipCooldown -= Time.deltaTime;
|
_flipCooldown -= Time.deltaTime;
|
||||||
else if (ShouldFlip())
|
else if (ShouldFlip())
|
||||||
{
|
{
|
||||||
_dir = -_dir;
|
_dir = -_dir;
|
||||||
_flipCooldown = 0.1f; // ~6 帧缓冲(60 fps),防止传感器残留信号导致抖动
|
_flipCooldown = 0.1f; // ~6 帧缓冲(60 fps),防止射线残留信号导致抖动
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemy.MoveInDirection(_dir);
|
_enemy.MoveInDirection(_dir);
|
||||||
@@ -65,11 +59,9 @@ namespace BaseGames.Enemies.AI
|
|||||||
|
|
||||||
private bool ShouldFlip()
|
private bool ShouldFlip()
|
||||||
{
|
{
|
||||||
// 转身进行中时不重复检测,防止 _dir 在转身期间被传感器残留信号反复翻转
|
// 转身进行中时不重复检测,防止 _dir 在转身期间被残留信号反复翻转
|
||||||
if (_enemy.Movement != null && _enemy.Movement.IsTurning) return false;
|
if (_movement == null || _movement.IsTurning) return false;
|
||||||
bool wallHit = _hasWallSensor && _hub.HasAnyDetection(SensorSlotNames.WallAhead);
|
return _movement.IsWallAhead || _movement.IsLedgeAhead;
|
||||||
bool edgeHit = _hasEdgeSensor && _hub.HasAnyDetection(SensorSlotNames.Ledge);
|
|
||||||
return wallHit || edgeHit;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ namespace BaseGames.Enemies.AI
|
|||||||
private bool _reached;
|
private bool _reached;
|
||||||
private bool _pathFailed;
|
private bool _pathFailed;
|
||||||
private bool _subscribed;
|
private bool _subscribed;
|
||||||
|
private Vector2 _returnTarget;
|
||||||
|
|
||||||
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
|
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
|
||||||
|
|
||||||
@@ -54,7 +55,12 @@ namespace BaseGames.Enemies.AI
|
|||||||
// 切换为行走速度
|
// 切换为行走速度
|
||||||
float walkSpeed = _enemy.Stats?.WalkSpeed ?? 2f;
|
float walkSpeed = _enemy.Stats?.WalkSpeed ?? 2f;
|
||||||
_enemy.Nav?.SetSpeed(walkSpeed);
|
_enemy.Nav?.SetSpeed(walkSpeed);
|
||||||
_enemy.MoveTo(_enemy.HomePosition);
|
|
||||||
|
// 优先归位到巡逻区域中心;无区域时退回出生点
|
||||||
|
_returnTarget = _enemy.PatrolZone != null
|
||||||
|
? _enemy.PatrolZone.PatrolCenter
|
||||||
|
: _enemy.HomePosition;
|
||||||
|
_enemy.MoveTo(_returnTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TaskStatus OnUpdate()
|
public override TaskStatus OnUpdate()
|
||||||
@@ -66,7 +72,7 @@ namespace BaseGames.Enemies.AI
|
|||||||
|
|
||||||
// 兜底距离判断(事件可能因帧序问题延迟一帧)
|
// 兜底距离判断(事件可能因帧序问题延迟一帧)
|
||||||
float radius = m_ArriveRadius > 0f ? m_ArriveRadius : (_enemy.Stats?.HomeRadius ?? 0.5f);
|
float radius = m_ArriveRadius > 0f ? m_ArriveRadius : (_enemy.Stats?.HomeRadius ?? 0.5f);
|
||||||
float sqr = ((Vector2)_enemy.transform.position - _enemy.HomePosition).sqrMagnitude;
|
float sqr = ((Vector2)_enemy.transform.position - _returnTarget).sqrMagnitude;
|
||||||
if (sqr <= radius * radius)
|
if (sqr <= radius * radius)
|
||||||
return CompleteReturn();
|
return CompleteReturn();
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
"BaseGames.Enemies",
|
"BaseGames.Enemies",
|
||||||
"BaseGames.Enemies.Boss.Patterns",
|
"BaseGames.Enemies.Boss.Patterns",
|
||||||
"Opsive.BehaviorDesigner.Runtime",
|
"Opsive.BehaviorDesigner.Runtime",
|
||||||
"Kybernetik.Animancer",
|
"Kybernetik.Animancer"
|
||||||
"Micosmo.SensorToolkit"
|
|
||||||
],
|
],
|
||||||
"autoReferenced": true,
|
"autoReferenced": true,
|
||||||
"overrideReferences": false,
|
"overrideReferences": false,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using Animancer;
|
using Animancer;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using BaseGames.Enemies.Perception;
|
|
||||||
|
|
||||||
namespace BaseGames.Enemies.Abilities
|
namespace BaseGames.Enemies.Abilities
|
||||||
{
|
{
|
||||||
@@ -18,7 +17,6 @@ namespace BaseGames.Enemies.Abilities
|
|||||||
|
|
||||||
[Header("感知与接触伤害")]
|
[Header("感知与接触伤害")]
|
||||||
[SerializeField] private BodyContactDamage _contactDamage;
|
[SerializeField] private BodyContactDamage _contactDamage;
|
||||||
[SerializeField] private EnemySensorHub _sensorHub;
|
|
||||||
|
|
||||||
[Tooltip("用于追击感知判断的传感器槽位名,通常为 \"aggro\"")]
|
[Tooltip("用于追击感知判断的传感器槽位名,通常为 \"aggro\"")]
|
||||||
[SerializeField] private string _aggroSlotName = "aggro";
|
[SerializeField] private string _aggroSlotName = "aggro";
|
||||||
@@ -36,7 +34,7 @@ namespace BaseGames.Enemies.Abilities
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_enemy.PlayerTransform == null) break;
|
if (_enemy.PlayerTransform == null) break;
|
||||||
if (_sensorHub != null && !_sensorHub.IsDetecting(_aggroSlotName, _enemy.PlayerTransform.gameObject))
|
if (_enemy.SensorHub != null && !_enemy.SensorHub.IsDetecting(_aggroSlotName, _enemy.PlayerTransform.gameObject))
|
||||||
break;
|
break;
|
||||||
_enemy.MoveTo(_enemy.PlayerTransform.position);
|
_enemy.MoveTo(_enemy.PlayerTransform.position);
|
||||||
yield return null;
|
yield return null;
|
||||||
|
|||||||
@@ -149,11 +149,11 @@ namespace BaseGames.Enemies.Abilities
|
|||||||
|
|
||||||
protected virtual void OnInterrupted(InterruptReason reason) { }
|
protected virtual void OnInterrupted(InterruptReason reason) { }
|
||||||
|
|
||||||
/// <summary>子类辅助:朝向目标。委托给 EnemyMovement.FaceTarget 以保持转身动画系统一致。</summary>
|
/// <summary>子类辅助:朝向目标(写入输入信号,下一 FixedUpdate 由 EnemyMovement 消费)。</summary>
|
||||||
protected void FaceTarget(Transform target)
|
protected void FaceTarget(Transform target)
|
||||||
{
|
{
|
||||||
if (target == null || _enemy?.Movement == null) return;
|
if (target == null || _enemy == null) return;
|
||||||
_enemy.Movement.FaceTarget(target.position);
|
_enemy.FaceTarget(target.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace BaseGames.Enemies.Abilities
|
|||||||
{
|
{
|
||||||
Phase = AbilityRunState.Active;
|
Phase = AbilityRunState.Active;
|
||||||
|
|
||||||
_enemy.Movement?.FaceTarget(_enemy.PlayerTransform.position);
|
_enemy.FacePlayer();
|
||||||
|
|
||||||
if (_faceClip.Clip == null) yield break;
|
if (_faceClip.Clip == null) yield break;
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
"Opsive.BehaviorDesigner.Runtime",
|
"Opsive.BehaviorDesigner.Runtime",
|
||||||
"Unity.Addressables",
|
"Unity.Addressables",
|
||||||
"Unity.ResourceManager",
|
"Unity.ResourceManager",
|
||||||
"Micosmo.SensorToolkit",
|
|
||||||
"BaseGames.Parry"
|
"BaseGames.Parry"
|
||||||
],
|
],
|
||||||
"includePlatforms": [],
|
"includePlatforms": [],
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ namespace BaseGames.Enemies
|
|||||||
[SerializeField] protected EnemyFeedback _feedback;
|
[SerializeField] protected EnemyFeedback _feedback;
|
||||||
[SerializeField] protected HurtBox _hurtBox;
|
[SerializeField] protected HurtBox _hurtBox;
|
||||||
|
|
||||||
|
[Header("区域(可选)")]
|
||||||
|
[Tooltip("地图固定巡逻/追击区域;配置后 BD_ChasePlayer 以区域边界替代 MaxChaseDistance,BD_ReturnToHome 归位至区域中心。留空则沿用出生点 + MaxChaseDistance 旧逻辑。")]
|
||||||
|
[SerializeField] private EnemyPatrolZone _patrolZone;
|
||||||
|
|
||||||
[Header("事件频道")]
|
[Header("事件频道")]
|
||||||
[SerializeField] private BaseGames.Core.Events.StringEventChannelSO _onEnemyDied;
|
[SerializeField] private BaseGames.Core.Events.StringEventChannelSO _onEnemyDied;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -101,6 +105,13 @@ namespace BaseGames.Enemies
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector2 LastKnownPlayerPosition { get; set; }
|
public Vector2 LastKnownPlayerPosition { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 地图固定巡逻/追击区域(可选)。
|
||||||
|
/// 配置后 BD_ChasePlayer 以区域边界为追击上限;BD_ReturnToHome 归位至区域中心。
|
||||||
|
/// 未配置时退回旧逻辑(HomePosition + MaxChaseDistance)。
|
||||||
|
/// </summary>
|
||||||
|
public EnemyPatrolZone PatrolZone => _patrolZone;
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
[Header("── 运行时调试(仅 Editor)──")]
|
[Header("── 运行时调试(仅 Editor)──")]
|
||||||
[SerializeField] private EnemyStateType _dbg_CurrentState;
|
[SerializeField] private EnemyStateType _dbg_CurrentState;
|
||||||
@@ -212,9 +223,9 @@ namespace BaseGames.Enemies
|
|||||||
private readonly EnemyAbilityRegistry _abilities = new EnemyAbilityRegistry();
|
private readonly EnemyAbilityRegistry _abilities = new EnemyAbilityRegistry();
|
||||||
/// <summary>由 _onPlayerSpawned 事件缓存的玩家 Transform,供 BD 任务读取。</summary>
|
/// <summary>由 _onPlayerSpawned 事件缓存的玩家 Transform,供 BD 任务读取。</summary>
|
||||||
public Transform PlayerTransform => _playerTransform;
|
public Transform PlayerTransform => _playerTransform;
|
||||||
/// <summary>感知 Hub(SensorToolkit);供 QuotaManager 暂停/恢复 Sensor 使用。</summary>
|
/// <summary>感知 Hub;供 BD 任务及 QuotaManager 暂停/恢复感知使用。</summary>
|
||||||
public Perception.IPerceptionSystem SensorHub => _sensorHub;
|
public Perception.IPerceptionSystem SensorHub => _sensorHub;
|
||||||
private Perception.EnemySensorHub _sensorHub;
|
private Perception.IPerceptionSystem _sensorHub;
|
||||||
/// <summary>威胁评估器(可选):为原始 LOS 结果叠加反应延迟,使感知更自然。</summary>
|
/// <summary>威胁评估器(可选):为原始 LOS 结果叠加反应延迟,使感知更自然。</summary>
|
||||||
public Perception.EnemyThreatAssessor ThreatAssessor => _threatAssessor;
|
public Perception.EnemyThreatAssessor ThreatAssessor => _threatAssessor;
|
||||||
private Perception.EnemyThreatAssessor _threatAssessor;
|
private Perception.EnemyThreatAssessor _threatAssessor;
|
||||||
@@ -275,26 +286,6 @@ namespace BaseGames.Enemies
|
|||||||
public virtual bool IsPlayerInRange(float range)
|
public virtual bool IsPlayerInRange(float range)
|
||||||
=> _stats != null && _stats.SqrDistanceToPlayer <= range * range;
|
=> _stats != null && _stats.SqrDistanceToPlayer <= range * range;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 检查玩家是否在感知范围内。若 <see cref="EnemyStatsSO.DetectAngleDeg"/> > 0,
|
|
||||||
/// 同时验证玩家是否在自身朝向的扇形角度内。
|
|
||||||
/// </summary>
|
|
||||||
public virtual bool IsPlayerInDetectRange()
|
|
||||||
{
|
|
||||||
if (_stats == null || _playerTransform == null) return false;
|
|
||||||
float detectRange = _statsSO != null ? _statsSO.DetectRange : 6f;
|
|
||||||
if (_stats.SqrDistanceToPlayer > detectRange * detectRange) return false;
|
|
||||||
|
|
||||||
float angleDeg = _statsSO?.DetectAngleDeg ?? 0f;
|
|
||||||
if (angleDeg <= 0f) return true; // 0 = 关闭方向限制
|
|
||||||
|
|
||||||
Vector2 toPlayer = ((Vector2)_playerTransform.position - (Vector2)transform.position).normalized;
|
|
||||||
float facingDir = _movement != null ? _movement.FacingDirection : 1f;
|
|
||||||
var forward = new Vector2(facingDir, 0f);
|
|
||||||
float angle = Vector2.Angle(forward, toPlayer);
|
|
||||||
return angle <= angleDeg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>原始视线检测结果(BatchLOSSystem 写入,无感知延迟修正)。</summary>
|
/// <summary>原始视线检测结果(BatchLOSSystem 写入,无感知延迟修正)。</summary>
|
||||||
public bool HasLineOfSight => _losResult;
|
public bool HasLineOfSight => _losResult;
|
||||||
|
|
||||||
@@ -309,6 +300,23 @@ namespace BaseGames.Enemies
|
|||||||
_movement.PendingInput.FaceDir = 0;
|
_movement.PendingInput.FaceDir = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>朝向世界坐标点(通过输入信号,下一 FixedUpdate 消费)。</summary>
|
||||||
|
public void FaceTarget(Vector2 worldPos)
|
||||||
|
{
|
||||||
|
if (_movement == null) return;
|
||||||
|
_movement.PendingInput.WantFace = true;
|
||||||
|
_movement.PendingInput.FaceTargetPos = worldPos;
|
||||||
|
_movement.PendingInput.FaceDir = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>直接指定朝向方向(+1 右 / -1 左,通过输入信号)。</summary>
|
||||||
|
public void FaceDirection(int dir)
|
||||||
|
{
|
||||||
|
if (_movement == null) return;
|
||||||
|
_movement.PendingInput.WantFace = true;
|
||||||
|
_movement.PendingInput.FaceDir = dir;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 搜查"环顾"子步骤:停止移动,播放原地环顾动画。
|
/// 搜查"环顾"子步骤:停止移动,播放原地环顾动画。
|
||||||
/// 由搜查行为触发;动画细节由角色自己决定,外部无需感知 AnimConfig。
|
/// 由搜查行为触发;动画细节由角色自己决定,外部无需感知 AnimConfig。
|
||||||
@@ -542,7 +550,7 @@ namespace BaseGames.Enemies
|
|||||||
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
|
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
|
||||||
if (_movement == null) _movement = GetComponent<EnemyMovement>();
|
if (_movement == null) _movement = GetComponent<EnemyMovement>();
|
||||||
_poiseSource = GetComponent<IPoiseSource>();
|
_poiseSource = GetComponent<IPoiseSource>();
|
||||||
_sensorHub = GetComponentInChildren<Perception.EnemySensorHub>();
|
_sensorHub = GetComponentInChildren<Perception.IPerceptionSystem>();
|
||||||
_statusEffects = GetComponent<StatusEffects.EnemyStatusEffectManager>();
|
_statusEffects = GetComponent<StatusEffects.EnemyStatusEffectManager>();
|
||||||
_threatAssessor = GetComponent<Perception.EnemyThreatAssessor>();
|
_threatAssessor = GetComponent<Perception.EnemyThreatAssessor>();
|
||||||
_pooledObject = GetComponent<PooledObject>();
|
_pooledObject = GetComponent<PooledObject>();
|
||||||
@@ -782,43 +790,8 @@ namespace BaseGames.Enemies
|
|||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
if (_statsSO == null) return;
|
if (_statsSO == null) return;
|
||||||
|
|
||||||
// ── 侦测范围(淡橙;若配置扇形角则绘制扇形弧)+ 攻击范围(淡红圆)────
|
// 感知范围圆形 Gizmo 由 PhysicsPerceptionSystemEditor [DrawGizmo] 统一绘制,
|
||||||
{
|
// 此处不重复绘制,避免叠加覆盖导致 gizmoColor 设置无效。
|
||||||
var c = new Vector3(transform.position.x, transform.position.y, 0f);
|
|
||||||
var prevM = UnityEditor.Handles.matrix;
|
|
||||||
UnityEditor.Handles.matrix = Matrix4x4.identity;
|
|
||||||
|
|
||||||
// 攻击范围(全圆)
|
|
||||||
UnityEditor.Handles.color = new Color(1f, 0.15f, 0.15f, 0.15f);
|
|
||||||
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.AttackRange);
|
|
||||||
UnityEditor.Handles.color = new Color(1f, 0.15f, 0.15f, 0.55f);
|
|
||||||
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.AttackRange);
|
|
||||||
|
|
||||||
// 侦测范围
|
|
||||||
float angleDeg = _statsSO.DetectAngleDeg;
|
|
||||||
if (angleDeg > 0f)
|
|
||||||
{
|
|
||||||
// 扇形感知:绘制弧形扇区
|
|
||||||
float facing = Application.isPlaying && _movement != null ? _movement.FacingDirection : 1f;
|
|
||||||
Vector3 forward3 = new Vector3(facing, 0f, 0f);
|
|
||||||
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.12f);
|
|
||||||
UnityEditor.Handles.DrawSolidArc(c, Vector3.back, forward3, angleDeg, _statsSO.DetectRange);
|
|
||||||
UnityEditor.Handles.DrawSolidArc(c, Vector3.back, forward3, -angleDeg, _statsSO.DetectRange);
|
|
||||||
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.6f);
|
|
||||||
UnityEditor.Handles.DrawWireArc(c, Vector3.back, forward3, angleDeg, _statsSO.DetectRange);
|
|
||||||
UnityEditor.Handles.DrawWireArc(c, Vector3.back, forward3, -angleDeg, _statsSO.DetectRange);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 全圆感知
|
|
||||||
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.15f);
|
|
||||||
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.DetectRange);
|
|
||||||
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.55f);
|
|
||||||
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.DetectRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
UnityEditor.Handles.matrix = prevM;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 运行时:AI 状态标签(常态可见,无需选中)────────────────
|
// ── 运行时:AI 状态标签(常态可见,无需选中)────────────────
|
||||||
if (Application.isPlaying)
|
if (Application.isPlaying)
|
||||||
@@ -843,10 +816,14 @@ namespace BaseGames.Enemies
|
|||||||
// ── 运行时:LOS 连线 ────────────────────────────────────────
|
// ── 运行时:LOS 连线 ────────────────────────────────────────
|
||||||
if (!Application.isPlaying || _playerTransform == null) return;
|
if (!Application.isPlaying || _playerTransform == null) return;
|
||||||
|
|
||||||
|
float drawDetectRange = _sensorHub != null
|
||||||
|
? _sensorHub.GetSensorRadius(Perception.SensorSlotNames.Aggro)
|
||||||
|
: -1f;
|
||||||
|
|
||||||
Vector3 eyeWorld = transform.position + new Vector3(_statsSO.EyeOffset.x, _statsSO.EyeOffset.y, 0f);
|
Vector3 eyeWorld = transform.position + new Vector3(_statsSO.EyeOffset.x, _statsSO.EyeOffset.y, 0f);
|
||||||
Vector3 playerPos = _playerTransform.position;
|
Vector3 playerPos = _playerTransform.position;
|
||||||
float sqrDist = (playerPos - transform.position).sqrMagnitude;
|
float sqrDist = (playerPos - transform.position).sqrMagnitude;
|
||||||
bool inRange = sqrDist <= _statsSO.DetectRange * _statsSO.DetectRange;
|
bool inRange = drawDetectRange >= 0f && sqrDist <= drawDetectRange * drawDetectRange;
|
||||||
|
|
||||||
// 眼睛位置小圆点(金黄)
|
// 眼睛位置小圆点(金黄)
|
||||||
Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.85f);
|
Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.85f);
|
||||||
@@ -867,24 +844,8 @@ namespace BaseGames.Enemies
|
|||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
if (_statsSO == null) return;
|
if (_statsSO == null) return;
|
||||||
|
|
||||||
// 选中时加亮范围圆
|
// 感知范围圆形 Gizmo 由 PhysicsPerceptionSystemEditor [DrawGizmo] 统一绘制,
|
||||||
{
|
// 此处不重复绘制。
|
||||||
var c = new Vector3(transform.position.x, transform.position.y, 0f);
|
|
||||||
var prevM = UnityEditor.Handles.matrix;
|
|
||||||
UnityEditor.Handles.matrix = Matrix4x4.identity;
|
|
||||||
|
|
||||||
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.25f);
|
|
||||||
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.DetectRange);
|
|
||||||
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.90f);
|
|
||||||
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.DetectRange);
|
|
||||||
|
|
||||||
UnityEditor.Handles.color = new Color(1f, 0.2f, 0.2f, 0.25f);
|
|
||||||
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.AttackRange);
|
|
||||||
UnityEditor.Handles.color = new Color(1f, 0.2f, 0.2f, 0.90f);
|
|
||||||
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.AttackRange);
|
|
||||||
|
|
||||||
UnityEditor.Handles.matrix = prevM;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 运行时:选中时绘制 AiPhase 彩色外圆(突出显示当前状态)
|
// 运行时:选中时绘制 AiPhase 彩色外圆(突出显示当前状态)
|
||||||
if (Application.isPlaying)
|
if (Application.isPlaying)
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ namespace BaseGames.Enemies
|
|||||||
|
|
||||||
private System.Collections.Generic.List<string> BuildLines()
|
private System.Collections.Generic.List<string> BuildLines()
|
||||||
{
|
{
|
||||||
|
|
||||||
var list = new System.Collections.Generic.List<string>(8);
|
var list = new System.Collections.Generic.List<string>(8);
|
||||||
|
|
||||||
// 名称
|
// 名称
|
||||||
|
|||||||
@@ -31,16 +31,36 @@ namespace BaseGames.Enemies
|
|||||||
[Tooltip("动画配置 SO;留空则在 Awake 时自动从 EnemyBase 读取")]
|
[Tooltip("动画配置 SO;留空则在 Awake 时自动从 EnemyBase 读取")]
|
||||||
[SerializeField] private EnemyAnimationConfigSO _animConfig;
|
[SerializeField] private EnemyAnimationConfigSO _animConfig;
|
||||||
|
|
||||||
|
[Header("视觉节点")]
|
||||||
|
[Tooltip("包含 SpriteRenderer / AnimancerComponent 的子节点(Visual);设置后 Awake 自动将其 localPosition 对齐到 Collider2D offset,使视觉中心与碰撞体中心重合。留空则不做偏移处理。")]
|
||||||
|
[SerializeField] private Transform _visualRoot;
|
||||||
|
[Tooltip("精灵资源本身的默认朝向:1 = 右(localScale.x 为正时面朝右),-1 = 左(localScale.x 为正时面朝左)。如果美术资源绘制方向朝左,此值填 -1;朝右填 1。大多数 Unity 项目美术朝右,默认值为 1。")]
|
||||||
|
[SerializeField] private int _spriteDefaultFacingDir = 1;
|
||||||
|
|
||||||
[Header("导航跳跃能力(INavLinkHandler)")]
|
[Header("导航跳跃能力(INavLinkHandler)")]
|
||||||
[Tooltip("可处理的最大跳跃垂直高度(超出则让 TBM 兜底)")]
|
[Tooltip("可处理的最大跳跃垂直高度(超出则让 TBM 兜底)")]
|
||||||
[SerializeField] private float _navJumpMaxHeight = 6f;
|
[SerializeField] private float _navJumpMaxHeight = 6f;
|
||||||
[Tooltip("可处理的最大跳跃水平距离")]
|
[Tooltip("可处理的最大跳跃水平距离")]
|
||||||
[SerializeField] private float _navJumpMaxDist = 10f;
|
[SerializeField] private float _navJumpMaxDist = 10f;
|
||||||
[Tooltip("地面检测射线长度(用于判断跳跃是否落地)")]
|
[Tooltip("用于确定射线起点宽度和底边的 Collider2D;留空则 Awake 时自动查找")]
|
||||||
[SerializeField] private float _groundCheckDist = 0.35f;
|
[SerializeField] private Collider2D _groundCheckCollider;
|
||||||
|
[Tooltip("从碰撞体底边向下的射线检测距离")]
|
||||||
|
[SerializeField] private float _groundCheckDist = 0.15f;
|
||||||
|
[Tooltip("射线数量(1 = 仅中心,>1 时沿碰撞体底边均匀分布)")]
|
||||||
|
[SerializeField] [Min(1)] private int _groundCheckCount = 3;
|
||||||
[Tooltip("地面层 LayerMask")]
|
[Tooltip("地面层 LayerMask")]
|
||||||
[SerializeField] private LayerMask _groundMask;
|
[SerializeField] private LayerMask _groundMask;
|
||||||
|
|
||||||
|
[Header("墙体 / 悬崖检测")]
|
||||||
|
[Tooltip("从碰撞体朝向前边缘水平发射的墙体检测距离(0 = 禁用)")]
|
||||||
|
[SerializeField] private float _wallCheckDist = 0.2f;
|
||||||
|
[Tooltip("悬崖检测:从碰撞体前下角再向前偏移此距离后向下发射射线(用于检测脚边是否有地面)")]
|
||||||
|
[SerializeField] private float _ledgeCheckFwdOffset = 0.1f;
|
||||||
|
[Tooltip("悬崖检测:向下的射线长度;射线未命中地面则 IsLedgeAhead = true(0 = 禁用)")]
|
||||||
|
[SerializeField] private float _ledgeCheckDownDist = 0.4f;
|
||||||
|
[Tooltip("墙体层 LayerMask;留空时复用地面 LayerMask")]
|
||||||
|
[SerializeField] private LayerMask _wallMask;
|
||||||
|
|
||||||
private Rigidbody2D _rb;
|
private Rigidbody2D _rb;
|
||||||
private int _facingDir = 1;
|
private int _facingDir = 1;
|
||||||
private Coroutine _linkCoroutine;
|
private Coroutine _linkCoroutine;
|
||||||
@@ -54,10 +74,20 @@ namespace BaseGames.Enemies
|
|||||||
public EnemyMoveInput PendingInput;
|
public EnemyMoveInput PendingInput;
|
||||||
|
|
||||||
public bool IsGrounded { get; private set; }
|
public bool IsGrounded { get; private set; }
|
||||||
|
/// <summary>前方是否有墙体。在 FixedUpdate 中更新,仅当 _wallCheckDist > 0 时有效。</summary>
|
||||||
|
public bool IsWallAhead { get; private set; }
|
||||||
|
/// <summary>前方是否有悬崖(脚边地面缺失)。在 FixedUpdate 中更新,仅当 _ledgeCheckDownDist > 0 时有效。</summary>
|
||||||
|
public bool IsLedgeAhead { get; private set; }
|
||||||
/// <summary>当前朝向:1 = 右,-1 = 左。</summary>
|
/// <summary>当前朝向:1 = 右,-1 = 左。</summary>
|
||||||
public int FacingDirection => _facingDir;
|
public int FacingDirection => _facingDir;
|
||||||
/// <summary>当前是否正在播放转身动画(移动输入在此期间被屏蔽)。</summary>
|
/// <summary>当前是否正在播放转身动画(移动输入在此期间被屏蔽)。</summary>
|
||||||
public bool IsTurning => _isTurning;
|
public bool IsTurning => _isTurning;
|
||||||
|
/// <summary>
|
||||||
|
/// 当 PathBerserker2d TransformBasedMovement 正在直接驱动 transform.position 时由
|
||||||
|
/// <see cref="Navigation.EnemyNavAgent"/> 设为 true。
|
||||||
|
/// 此时 MoveHorizontal/MoveWithSpeed 仅更新朝向,不写 rb.velocity,防止双重驱动冲突。
|
||||||
|
/// </summary>
|
||||||
|
public bool NavDriving { get; set; }
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
[Header("── 运行时调试(仅 Editor)──")]
|
[Header("── 运行时调试(仅 Editor)──")]
|
||||||
@@ -65,7 +95,10 @@ namespace BaseGames.Enemies
|
|||||||
[SerializeField] private float _dbg_VelocityX;
|
[SerializeField] private float _dbg_VelocityX;
|
||||||
[SerializeField] private float _dbg_VelocityY;
|
[SerializeField] private float _dbg_VelocityY;
|
||||||
[SerializeField] private bool _dbg_IsGrounded;
|
[SerializeField] private bool _dbg_IsGrounded;
|
||||||
|
[SerializeField] private bool _dbg_IsWallAhead;
|
||||||
|
[SerializeField] private bool _dbg_IsLedgeAhead;
|
||||||
[SerializeField] private bool _dbg_IsTurning;
|
[SerializeField] private bool _dbg_IsTurning;
|
||||||
|
[SerializeField] private bool _dbg_NavDriving;
|
||||||
[Header("── 输入信号(仅 Editor)──")]
|
[Header("── 输入信号(仅 Editor)──")]
|
||||||
[SerializeField] private float _dbg_Input_MoveDir;
|
[SerializeField] private float _dbg_Input_MoveDir;
|
||||||
[SerializeField] private float _dbg_Input_MoveSpeed;
|
[SerializeField] private float _dbg_Input_MoveSpeed;
|
||||||
@@ -147,36 +180,111 @@ namespace BaseGames.Enemies
|
|||||||
onComplete?.Invoke();
|
onComplete?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsGroundedCheck() =>
|
private Vector2 GetGroundRayOrigin(int index)
|
||||||
Physics2D.Raycast(_rb.position, Vector2.down, _groundCheckDist, _groundMask);
|
{
|
||||||
|
// 优先用序列化字段,编辑器模式下 Awake 未执行时也能直接 GetComponent
|
||||||
|
var col = _groundCheckCollider != null ? _groundCheckCollider : GetComponent<Collider2D>();
|
||||||
|
if (col == null)
|
||||||
|
return (Vector2)transform.position;
|
||||||
|
|
||||||
|
Bounds b = col.bounds;
|
||||||
|
float x = _groundCheckCount <= 1
|
||||||
|
? b.center.x
|
||||||
|
: Mathf.Lerp(b.min.x, b.max.x, (float)index / (_groundCheckCount - 1));
|
||||||
|
return new Vector2(x, b.min.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsGroundedCheck()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _groundCheckCount; i++)
|
||||||
|
{
|
||||||
|
if (Physics2D.Raycast(GetGroundRayOrigin(i), Vector2.down, _groundCheckDist, _groundMask))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 墙体射线起点:碰撞体朝向侧边缘中心高度
|
||||||
|
private Vector2 GetWallRayOrigin()
|
||||||
|
{
|
||||||
|
var col = _groundCheckCollider != null ? _groundCheckCollider : GetComponent<Collider2D>();
|
||||||
|
if (col == null) return (Vector2)transform.position;
|
||||||
|
Bounds b = col.bounds;
|
||||||
|
float x = _facingDir >= 0 ? b.max.x : b.min.x;
|
||||||
|
return new Vector2(x, b.center.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 悬崖射线起点:碰撞体前下角再向前偏移 _ledgeCheckFwdOffset
|
||||||
|
private Vector2 GetLedgeRayOrigin()
|
||||||
|
{
|
||||||
|
var col = _groundCheckCollider != null ? _groundCheckCollider : GetComponent<Collider2D>();
|
||||||
|
if (col == null) return (Vector2)transform.position;
|
||||||
|
Bounds b = col.bounds;
|
||||||
|
float x = _facingDir >= 0
|
||||||
|
? b.max.x + _ledgeCheckFwdOffset
|
||||||
|
: b.min.x - _ledgeCheckFwdOffset;
|
||||||
|
return new Vector2(x, b.min.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WallAndLedgeCheck()
|
||||||
|
{
|
||||||
|
LayerMask wallLayer = (_wallMask.value != 0) ? _wallMask : _groundMask;
|
||||||
|
|
||||||
|
if (_wallCheckDist > 0f)
|
||||||
|
IsWallAhead = Physics2D.Raycast(
|
||||||
|
GetWallRayOrigin(),
|
||||||
|
new Vector2(_facingDir, 0f),
|
||||||
|
_wallCheckDist,
|
||||||
|
wallLayer);
|
||||||
|
|
||||||
|
if (_ledgeCheckDownDist > 0f)
|
||||||
|
IsLedgeAhead = !Physics2D.Raycast(
|
||||||
|
GetLedgeRayOrigin(),
|
||||||
|
Vector2.down,
|
||||||
|
_ledgeCheckDownDist,
|
||||||
|
_groundMask);
|
||||||
|
}
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
Debug.Assert(_config != null, "[EnemyMovement] _config 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this);
|
Debug.Assert(_config != null, "[EnemyMovement] _config 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this);
|
||||||
_rb = GetComponent<Rigidbody2D>();
|
_rb = GetComponent<Rigidbody2D>();
|
||||||
|
if (_groundCheckCollider == null)
|
||||||
|
_groundCheckCollider = GetComponent<Collider2D>();
|
||||||
|
|
||||||
// 从 Sprite 或 localScale 的初始状态推断朝向,并统一切换为 localScale 翻转。
|
// 从 Sprite 或 localScale 的初始状态推断朝向,并统一切换为 localScale 翻转。
|
||||||
// 这样子对象(含 RaySensor2D)会随 localScale 正确翻转,不再依赖 flipX。
|
// 这样子对象(含 RaySensor2D)会随 localScale 正确翻转,不再依赖 flipX。
|
||||||
if (_spriteRenderer != null)
|
// 三个信号均可能携带初始朝向信息,任意奇数个翻转表示实际方向与默认方向相反:
|
||||||
{
|
// flippedBySprite : SpriteRenderer.flipX
|
||||||
// 两个信号均可能携带初始朝向信息:flipX 或 localScale.x < 0,
|
// flippedByScale : ROOT localScale.x < 0
|
||||||
// XOR 组合:恰好一个翻转 → 面左;两个都翻(互相抵消)→ 面右。
|
// flippedByVisual : _visualRoot.localScale.x < 0(需归一化,否则与 ROOT 产生双重翻转)
|
||||||
bool flippedBySprite = _spriteRenderer.flipX;
|
bool flippedBySprite = _spriteRenderer != null && _spriteRenderer.flipX;
|
||||||
bool flippedByScale = transform.localScale.x < 0f;
|
bool flippedByScale = transform.localScale.x < 0f;
|
||||||
_facingDir = (flippedBySprite ^ flippedByScale) ? -1 : 1;
|
bool flippedByVisual = _visualRoot != null && _visualRoot.localScale.x < 0f;
|
||||||
|
_facingDir = (flippedBySprite ^ flippedByScale ^ flippedByVisual)
|
||||||
|
? -_spriteDefaultFacingDir
|
||||||
|
: _spriteDefaultFacingDir;
|
||||||
|
|
||||||
_spriteRenderer.flipX = false; // 后续由 localScale 驱动,避免双重镜像
|
// 归一化:清除所有翻转来源,仅保留 ROOT localScale.x 作为唯一翻转驱动。
|
||||||
Vector3 s = transform.localScale;
|
if (_spriteRenderer != null)
|
||||||
transform.localScale = new Vector3(Mathf.Abs(s.x) * _facingDir, s.y, s.z);
|
_spriteRenderer.flipX = false;
|
||||||
}
|
if (_visualRoot != null && flippedByVisual)
|
||||||
else
|
|
||||||
{
|
{
|
||||||
_facingDir = transform.localScale.x >= 0f ? 1 : -1;
|
var vs = _visualRoot.localScale;
|
||||||
|
_visualRoot.localScale = new Vector3(Mathf.Abs(vs.x), vs.y, vs.z);
|
||||||
}
|
}
|
||||||
|
Vector3 s = transform.localScale;
|
||||||
|
float signX = (_facingDir == _spriteDefaultFacingDir) ? Mathf.Abs(s.x) : -Mathf.Abs(s.x);
|
||||||
|
transform.localScale = new Vector3(signX, s.y, s.z);
|
||||||
|
|
||||||
|
// 将 Visual 子节点的 localPosition 对齐到 Collider2D offset,使视觉中心与碰撞体中心重合
|
||||||
|
if (_visualRoot != null && _groundCheckCollider != null)
|
||||||
|
_visualRoot.localPosition = _groundCheckCollider.offset;
|
||||||
|
|
||||||
if (_enableTurnAnimation)
|
if (_enableTurnAnimation)
|
||||||
{
|
{
|
||||||
if (_animancer == null) _animancer = GetComponentInParent<AnimancerComponent>(true);
|
// AnimancerComponent 可能在 Visual 子节点上,用 GetComponentInChildren 兼容两种布局
|
||||||
|
if (_animancer == null) _animancer = GetComponentInChildren<AnimancerComponent>(true);
|
||||||
if (_animConfig == null)
|
if (_animConfig == null)
|
||||||
{
|
{
|
||||||
var enemyBase = GetComponentInParent<EnemyBase>(true);
|
var enemyBase = GetComponentInParent<EnemyBase>(true);
|
||||||
@@ -194,7 +302,16 @@ namespace BaseGames.Enemies
|
|||||||
|
|
||||||
private void FixedUpdate()
|
private void FixedUpdate()
|
||||||
{
|
{
|
||||||
|
// localScale.x 为正 → 精灵以 _spriteDefaultFacingDir 方向显示;为负则相反。
|
||||||
|
if (!_isTurning)
|
||||||
|
_facingDir = transform.localScale.x >= 0f ? _spriteDefaultFacingDir : -_spriteDefaultFacingDir;
|
||||||
|
|
||||||
|
// NavDriving: TBM 直接写 transform.position,零速防止物理重力积累和双重驱动冲突。
|
||||||
|
if (NavDriving)
|
||||||
|
_rb.velocity = Vector2.zero;
|
||||||
|
|
||||||
IsGrounded = IsGroundedCheck();
|
IsGrounded = IsGroundedCheck();
|
||||||
|
WallAndLedgeCheck();
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
_dbg_Input_MoveDir = PendingInput.MoveDir;
|
_dbg_Input_MoveDir = PendingInput.MoveDir;
|
||||||
_dbg_Input_MoveSpeed = PendingInput.MoveSpeed;
|
_dbg_Input_MoveSpeed = PendingInput.MoveSpeed;
|
||||||
@@ -209,7 +326,10 @@ namespace BaseGames.Enemies
|
|||||||
_dbg_VelocityX = _rb != null ? _rb.velocity.x : 0f;
|
_dbg_VelocityX = _rb != null ? _rb.velocity.x : 0f;
|
||||||
_dbg_VelocityY = _rb != null ? _rb.velocity.y : 0f;
|
_dbg_VelocityY = _rb != null ? _rb.velocity.y : 0f;
|
||||||
_dbg_IsGrounded = IsGrounded;
|
_dbg_IsGrounded = IsGrounded;
|
||||||
|
_dbg_IsWallAhead = IsWallAhead;
|
||||||
|
_dbg_IsLedgeAhead = IsLedgeAhead;
|
||||||
_dbg_IsTurning = _isTurning;
|
_dbg_IsTurning = _isTurning;
|
||||||
|
_dbg_NavDriving = NavDriving;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +344,8 @@ namespace BaseGames.Enemies
|
|||||||
var facePosSnapshot = PendingInput.FaceTargetPos;
|
var facePosSnapshot = PendingInput.FaceTargetPos;
|
||||||
PendingInput.WantStop = false;
|
PendingInput.WantStop = false;
|
||||||
PendingInput.WantFace = false;
|
PendingInput.WantFace = false;
|
||||||
|
PendingInput.FaceDir = 0; // clear to prevent stale Inspector display
|
||||||
|
PendingInput.FaceTargetPos = default; // clear to prevent stale Inspector display
|
||||||
|
|
||||||
// ── 持久字段:MoveDir / MoveSpeed 不清零 ─────────────────────
|
// ── 持久字段:MoveDir / MoveSpeed 不清零 ─────────────────────
|
||||||
// 解决 FixedUpdate 频率 > Update 频率时的空帧问题:
|
// 解决 FixedUpdate 频率 > Update 频率时的空帧问题:
|
||||||
@@ -256,20 +378,22 @@ namespace BaseGames.Enemies
|
|||||||
public void MoveHorizontal(float dir)
|
public void MoveHorizontal(float dir)
|
||||||
{
|
{
|
||||||
if (_isTurning) return;
|
if (_isTurning) return;
|
||||||
|
UpdateFacing(dir);
|
||||||
|
if (NavDriving) return; // TBM 驱动位置,仅更新朝向
|
||||||
var vel = _rb.velocity;
|
var vel = _rb.velocity;
|
||||||
vel.x = dir * _config.WalkSpeed;
|
vel.x = dir * _config.WalkSpeed;
|
||||||
_rb.velocity = vel;
|
_rb.velocity = vel;
|
||||||
UpdateFacing(dir);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>显式指定速度(BD 追击任务调用)。转身动画期间调用无效。</summary>
|
/// <summary>显式指定速度(BD 追击任务调用)。转身动画期间调用无效。</summary>
|
||||||
public void MoveWithSpeed(float dir, float speed)
|
public void MoveWithSpeed(float dir, float speed)
|
||||||
{
|
{
|
||||||
if (_isTurning) return;
|
if (_isTurning) return;
|
||||||
|
UpdateFacing(dir);
|
||||||
|
if (NavDriving) return; // TBM 驱动位置,仅更新朝向
|
||||||
var vel = _rb.velocity;
|
var vel = _rb.velocity;
|
||||||
vel.x = dir * speed;
|
vel.x = dir * speed;
|
||||||
_rb.velocity = vel;
|
_rb.velocity = vel;
|
||||||
UpdateFacing(dir);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>朝向指定世界坐标(通常传入玩家位置)。</summary>
|
/// <summary>朝向指定世界坐标(通常传入玩家位置)。</summary>
|
||||||
@@ -361,20 +485,54 @@ namespace BaseGames.Enemies
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>转身动画协程:停止水平移动 → 播放 Turn 动画 → 翻转朝向 → 恢复。</summary>
|
/// <summary>转身动画协程:停止水平移动 → 播放 Turn 动画 → 翻转朝向 → 恢复移动动画。</summary>
|
||||||
private IEnumerator TurnCoroutine(int newDir)
|
private IEnumerator TurnCoroutine(int newDir)
|
||||||
{
|
{
|
||||||
_isTurning = true;
|
_isTurning = true;
|
||||||
StopHorizontal();
|
StopHorizontal();
|
||||||
|
|
||||||
// yield return state:Animancer 的 AnimancerState 是 CustomYieldInstruction,
|
// 用 WaitForSeconds 代替 "yield return state":
|
||||||
// 等待动画自然播完,与 Layer/State 速度缩放无关,比手动计时更可靠。
|
// AnimancerState.IsLooping 是只读属性(反映 clip 自身设置),无法强制单次播放;
|
||||||
|
// 若 Turn clip 被误配为 Loop,"yield return state" 的 keepWaiting 永远为 true,
|
||||||
|
// 导致 _isTurning 卡住、走路/攻击动画无法播放。
|
||||||
|
// WaitForSeconds(Length / Speed) 精确等待一个周期,与 clip 的 Loop 设置无关。
|
||||||
var state = _animancer.Play(_animConfig.Turn);
|
var state = _animancer.Play(_animConfig.Turn);
|
||||||
yield return state;
|
float waitSec = state.Length > 0f
|
||||||
|
? state.Length / Mathf.Max(0.001f, Mathf.Abs(state.EffectiveSpeed))
|
||||||
|
: 0.3f;
|
||||||
|
yield return new WaitForSeconds(waitSec);
|
||||||
|
|
||||||
ApplyFacingFlip(newDir);
|
ApplyFacingFlip(newDir);
|
||||||
_isTurning = false;
|
_isTurning = false;
|
||||||
_turnCoroutine = null;
|
_turnCoroutine = null;
|
||||||
|
|
||||||
|
// 转身完成后恢复运动动画:Turn 覆盖了之前的 Walk/Run,
|
||||||
|
// 上层(EnemyBase.SetAiPhase)只在阶段切换时播放一次动画,不会在此处重播。
|
||||||
|
ResumeMovementAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据当前输入状态恢复合适的移动动画(Walk / Run / Idle)。
|
||||||
|
/// 转身协程结束、CancelTurn 时调用,避免动画停留在 Turn 最后一帧。
|
||||||
|
/// </summary>
|
||||||
|
private void ResumeMovementAnimation()
|
||||||
|
{
|
||||||
|
if (_animancer == null || _animConfig == null) return;
|
||||||
|
|
||||||
|
if (PendingInput.WantStop || Mathf.Approximately(PendingInput.MoveDir, 0f))
|
||||||
|
{
|
||||||
|
if (_animConfig.Idle != null) _animancer.Play(_animConfig.Idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有速度且明显超过步行速度 → 跑步动画
|
||||||
|
float spd = PendingInput.MoveSpeed > 0f ? PendingInput.MoveSpeed : 0f;
|
||||||
|
if (_animConfig.Run != null && _config != null && spd > _config.WalkSpeed + 0.05f)
|
||||||
|
_animancer.Play(_animConfig.Run);
|
||||||
|
else if (_animConfig.Walk != null)
|
||||||
|
_animancer.Play(_animConfig.Walk);
|
||||||
|
else if (_animConfig.Idle != null)
|
||||||
|
_animancer.Play(_animConfig.Idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -401,7 +559,9 @@ namespace BaseGames.Enemies
|
|||||||
if (_spriteRenderer != null)
|
if (_spriteRenderer != null)
|
||||||
_spriteRenderer.flipX = false;
|
_spriteRenderer.flipX = false;
|
||||||
Vector3 s = transform.localScale;
|
Vector3 s = transform.localScale;
|
||||||
transform.localScale = new Vector3(Mathf.Abs(s.x) * newDir, s.y, s.z);
|
// newDir 与精灵默认方向一致 → 正比例(不翻转),否则取反(翻转)。
|
||||||
|
float signX = (newDir == _spriteDefaultFacingDir) ? Mathf.Abs(s.x) : -Mathf.Abs(s.x);
|
||||||
|
transform.localScale = new Vector3(signX, s.y, s.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDrawGizmos()
|
private void OnDrawGizmos()
|
||||||
@@ -427,10 +587,39 @@ namespace BaseGames.Enemies
|
|||||||
Gizmos.color = grounded
|
Gizmos.color = grounded
|
||||||
? new Color(0.2f, 1f, 0.35f, 0.90f)
|
? new Color(0.2f, 1f, 0.35f, 0.90f)
|
||||||
: new Color(0.4f, 0.75f, 0.4f, 0.40f);
|
: new Color(0.4f, 0.75f, 0.4f, 0.40f);
|
||||||
Vector3 origin = transform.position;
|
for (int i = 0; i < _groundCheckCount; i++)
|
||||||
|
{
|
||||||
|
Vector3 origin = GetGroundRayOrigin(i);
|
||||||
Gizmos.DrawLine(origin, origin + Vector3.down * _groundCheckDist);
|
Gizmos.DrawLine(origin, origin + Vector3.down * _groundCheckDist);
|
||||||
Gizmos.DrawWireSphere(origin + Vector3.down * _groundCheckDist, 0.04f);
|
Gizmos.DrawWireSphere(origin + Vector3.down * _groundCheckDist, 0.04f);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. 墙体检测射线(命中红色 / 无命中青色)─────────────────
|
||||||
|
if (_wallCheckDist > 0f)
|
||||||
|
{
|
||||||
|
bool hit = Application.isPlaying && IsWallAhead;
|
||||||
|
Gizmos.color = hit
|
||||||
|
? new Color(1f, 0.2f, 0.2f, 0.90f)
|
||||||
|
: new Color(0.2f, 0.9f, 1f, 0.50f);
|
||||||
|
Vector3 wallOrigin = GetWallRayOrigin();
|
||||||
|
Vector3 wallEnd = wallOrigin + new Vector3(_facingDir * _wallCheckDist, 0f, 0f);
|
||||||
|
Gizmos.DrawLine(wallOrigin, wallEnd);
|
||||||
|
Gizmos.DrawWireSphere(wallEnd, 0.04f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 5. 悬崖检测射线(无地面橙色 / 有地面灰色)───────────────
|
||||||
|
if (_ledgeCheckDownDist > 0f)
|
||||||
|
{
|
||||||
|
bool ledge = Application.isPlaying && IsLedgeAhead;
|
||||||
|
Gizmos.color = ledge
|
||||||
|
? new Color(1f, 0.65f, 0.1f, 0.90f)
|
||||||
|
: new Color(0.6f, 0.6f, 0.6f, 0.40f);
|
||||||
|
Vector3 ledgeOrigin = GetLedgeRayOrigin();
|
||||||
|
Vector3 ledgeEnd = ledgeOrigin + Vector3.down * _ledgeCheckDownDist;
|
||||||
|
Gizmos.DrawLine(ledgeOrigin, ledgeEnd);
|
||||||
|
Gizmos.DrawWireSphere(ledgeEnd, 0.04f);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +635,76 @@ namespace BaseGames.Enemies
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
/// <summary>
|
||||||
|
/// 一键在 Enemy Prefab 上创建 Visual 子节点,将 SpriteRenderer / AnimancerComponent
|
||||||
|
/// 迁移到该子节点,并自动将 _visualRoot / _spriteRenderer / EnemyBase._animancer 引用指向新节点。
|
||||||
|
/// 在 Inspector 右键菜单或 Component Header 菜单中调用。
|
||||||
|
/// ⚠️ 请在 Prefab 编辑模式(或 Prefab Stage)中执行,以便变更能正确保存。
|
||||||
|
/// </summary>
|
||||||
|
[ContextMenu("Setup Visual Node")]
|
||||||
|
public void SetupVisualNode()
|
||||||
|
{
|
||||||
|
// 1. 找或创建 Visual 子节点
|
||||||
|
Transform visual = transform.Find("Visual");
|
||||||
|
if (visual == null)
|
||||||
|
{
|
||||||
|
var go = new GameObject("Visual");
|
||||||
|
UnityEditor.Undo.RegisterCreatedObjectUndo(go, "Create Enemy Visual Node");
|
||||||
|
go.transform.SetParent(transform, false);
|
||||||
|
visual = go.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 对齐 localPosition 到 Collider2D offset
|
||||||
|
var col = _groundCheckCollider != null ? _groundCheckCollider : GetComponent<Collider2D>();
|
||||||
|
if (col != null)
|
||||||
|
{
|
||||||
|
UnityEditor.Undo.RecordObject(visual, "Set Visual LocalPosition");
|
||||||
|
visual.localPosition = col.offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 迁移 SpriteRenderer(仅在 Visual 上尚无 SpriteRenderer 时执行)
|
||||||
|
var sr = GetComponent<SpriteRenderer>();
|
||||||
|
if (sr != null && visual.GetComponent<SpriteRenderer>() == null)
|
||||||
|
{
|
||||||
|
UnityEditorInternal.ComponentUtility.CopyComponent(sr);
|
||||||
|
UnityEditorInternal.ComponentUtility.PasteComponentAsNew(visual.gameObject);
|
||||||
|
UnityEditor.Undo.DestroyObjectImmediate(sr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 迁移 AnimancerComponent
|
||||||
|
var anim = GetComponent<AnimancerComponent>();
|
||||||
|
if (anim != null && visual.GetComponent<AnimancerComponent>() == null)
|
||||||
|
{
|
||||||
|
UnityEditorInternal.ComponentUtility.CopyComponent(anim);
|
||||||
|
UnityEditorInternal.ComponentUtility.PasteComponentAsNew(visual.gameObject);
|
||||||
|
UnityEditor.Undo.DestroyObjectImmediate(anim);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 更新 EnemyMovement 字段引用
|
||||||
|
var movSO = new UnityEditor.SerializedObject(this);
|
||||||
|
movSO.FindProperty("_visualRoot").objectReferenceValue = visual;
|
||||||
|
movSO.FindProperty("_spriteRenderer").objectReferenceValue = visual.GetComponent<SpriteRenderer>();
|
||||||
|
movSO.FindProperty("_animancer").objectReferenceValue = visual.GetComponent<AnimancerComponent>();
|
||||||
|
movSO.ApplyModifiedProperties();
|
||||||
|
|
||||||
|
// 6. 更新 EnemyBase._animancer 引用
|
||||||
|
var enemyBase = GetComponent<EnemyBase>();
|
||||||
|
if (enemyBase != null)
|
||||||
|
{
|
||||||
|
var baseSO = new UnityEditor.SerializedObject(enemyBase);
|
||||||
|
baseSO.FindProperty("_animancer").objectReferenceValue = visual.GetComponent<AnimancerComponent>();
|
||||||
|
baseSO.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
UnityEditor.EditorUtility.SetDirty(gameObject);
|
||||||
|
Debug.Log($"[EnemyMovement] Visual node setup complete on '{gameObject.name}'.\n" +
|
||||||
|
$"Visual.localPosition = {visual.localPosition}\n" +
|
||||||
|
$"请在 Prefab 编辑器中手动保存(Ctrl+S)。", this);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// 在 Gizmos 空间绘制带箭头的 2D 有向线段
|
// 在 Gizmos 空间绘制带箭头的 2D 有向线段
|
||||||
private static void DrawArrow2D(Vector3 from, Vector3 to, Color color, float headLen = 0.15f)
|
private static void DrawArrow2D(Vector3 from, Vector3 to, Color color, float headLen = 0.15f)
|
||||||
{
|
{
|
||||||
|
|||||||
128
Assets/_Game/Scripts/Enemies/EnemyPatrolZone.cs
Normal file
128
Assets/_Game/Scripts/Enemies/EnemyPatrolZone.cs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Enemies
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 地图固定巡逻/追击区域(矩形)。
|
||||||
|
///
|
||||||
|
/// 放置于场景中(非敌人子节点),通过 EnemyBase Inspector 的 _patrolZone 字段引用。
|
||||||
|
/// 同一区域可被多个敌人共享(哨兵组、竞技场等)。
|
||||||
|
///
|
||||||
|
/// 区域层级:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><b>巡逻区域(Patrol)</b>:绿色矩形,巡逻点限制范围。</item>
|
||||||
|
/// <item><b>追击区域(Chase)</b>:橙色矩形 = 巡逻区域 + <see cref="ChaseExpandPadding"/>;
|
||||||
|
/// 玩家进入/出此区域触发追击开始/放弃。</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public class EnemyPatrolZone : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("巡逻区域")]
|
||||||
|
[Tooltip("巡逻区域中心相对 transform.position 的偏移(局部偏移,方便在 Scene 中移动节点)")]
|
||||||
|
public Vector2 PatrolOffset = Vector2.zero;
|
||||||
|
|
||||||
|
[Tooltip("巡逻区域尺寸(宽 × 高,单位 m)")]
|
||||||
|
public Vector2 PatrolSize = new Vector2(10f, 4f);
|
||||||
|
|
||||||
|
[Header("追击区域")]
|
||||||
|
[Tooltip("追击区域向四周扩展的边距(m);追击区域 = 巡逻区域各边 + 此值。0 = 不扩展(追击 = 巡逻)")]
|
||||||
|
[Min(0f)]
|
||||||
|
public float ChaseExpandPadding = 8f;
|
||||||
|
|
||||||
|
[Tooltip("自定义追击区域尺寸(宽×高);保持 Vector2.zero 时使用巡逻区域 + ChaseExpandPadding 自动计算")]
|
||||||
|
public Vector2 CustomChaseSize = Vector2.zero;
|
||||||
|
|
||||||
|
// ── 计算属性 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>巡逻区域中心(世界坐标)。</summary>
|
||||||
|
public Vector2 PatrolCenter => (Vector2)transform.position + PatrolOffset;
|
||||||
|
|
||||||
|
/// <summary>追击区域中心与巡逻区域共享。</summary>
|
||||||
|
public Vector2 ChaseCenter => PatrolCenter;
|
||||||
|
|
||||||
|
/// <summary>有效追击区域尺寸:自定义 > 0 时使用自定义,否则巡逻区域 + 边距。</summary>
|
||||||
|
public Vector2 EffectiveChaseSize
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (CustomChaseSize.sqrMagnitude > 0f) return CustomChaseSize;
|
||||||
|
return PatrolSize + Vector2.one * (ChaseExpandPadding * 2f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 空间查询 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>判断世界坐标 <paramref name="worldPos"/> 是否在巡逻区域内。</summary>
|
||||||
|
public bool ContainsPatrol(Vector2 worldPos)
|
||||||
|
{
|
||||||
|
Vector2 delta = worldPos - PatrolCenter;
|
||||||
|
Vector2 half = PatrolSize * 0.5f;
|
||||||
|
return Mathf.Abs(delta.x) <= half.x && Mathf.Abs(delta.y) <= half.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>判断世界坐标 <paramref name="worldPos"/> 是否在追击区域内。</summary>
|
||||||
|
public bool ContainsChase(Vector2 worldPos)
|
||||||
|
{
|
||||||
|
Vector2 delta = worldPos - ChaseCenter;
|
||||||
|
Vector2 half = EffectiveChaseSize * 0.5f;
|
||||||
|
return Mathf.Abs(delta.x) <= half.x && Mathf.Abs(delta.y) <= half.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>将 <paramref name="worldPos"/> 夹紧到巡逻区域内最近点(用于归位目标)。</summary>
|
||||||
|
public Vector2 ClampToPatrol(Vector2 worldPos)
|
||||||
|
{
|
||||||
|
Vector2 center = PatrolCenter;
|
||||||
|
Vector2 half = PatrolSize * 0.5f;
|
||||||
|
return new Vector2(
|
||||||
|
Mathf.Clamp(worldPos.x, center.x - half.x, center.x + half.x),
|
||||||
|
Mathf.Clamp(worldPos.y, center.y - half.y, center.y + half.y)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gizmos ────────────────────────────────────────────────────────
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
private void OnDrawGizmos()
|
||||||
|
{
|
||||||
|
Vector2 patrol = PatrolCenter;
|
||||||
|
Vector2 chase = ChaseCenter;
|
||||||
|
|
||||||
|
// 追击区域(橙色,后绘,垫在巡逻区域下方)
|
||||||
|
Vector2 chaseSize = EffectiveChaseSize;
|
||||||
|
Gizmos.color = new Color(1f, 0.55f, 0.1f, 0.07f);
|
||||||
|
Gizmos.DrawCube(chase, chaseSize);
|
||||||
|
Gizmos.color = new Color(1f, 0.55f, 0.1f, 0.7f);
|
||||||
|
Gizmos.DrawWireCube(chase, chaseSize);
|
||||||
|
|
||||||
|
// 巡逻区域(绿色,前绘,覆盖在追击区域上方)
|
||||||
|
Gizmos.color = new Color(0.2f, 0.9f, 0.2f, 0.12f);
|
||||||
|
Gizmos.DrawCube(patrol, PatrolSize);
|
||||||
|
Gizmos.color = new Color(0.2f, 0.9f, 0.2f, 0.85f);
|
||||||
|
Gizmos.DrawWireCube(patrol, PatrolSize);
|
||||||
|
|
||||||
|
// 中心点
|
||||||
|
Gizmos.color = new Color(0.2f, 0.9f, 0.2f, 1f);
|
||||||
|
Gizmos.DrawSphere(patrol, 0.15f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDrawGizmosSelected()
|
||||||
|
{
|
||||||
|
// 选中时用更鲜艳的线框突出显示
|
||||||
|
Vector2 patrol = PatrolCenter;
|
||||||
|
Gizmos.color = Color.green;
|
||||||
|
Gizmos.DrawWireCube(patrol, PatrolSize);
|
||||||
|
|
||||||
|
Gizmos.color = new Color(1f, 0.7f, 0f);
|
||||||
|
Gizmos.DrawWireCube(ChaseCenter, EffectiveChaseSize);
|
||||||
|
|
||||||
|
// 标注尺寸(仅在 Selected 时绘制)
|
||||||
|
UnityEditor.Handles.color = Color.green;
|
||||||
|
UnityEditor.Handles.Label(patrol + Vector2.right * PatrolSize.x * 0.5f,
|
||||||
|
$"Patrol {PatrolSize.x:F1}×{PatrolSize.y:F1}");
|
||||||
|
UnityEditor.Handles.color = new Color(1f, 0.7f, 0f);
|
||||||
|
Vector2 cs = EffectiveChaseSize;
|
||||||
|
UnityEditor.Handles.Label(ChaseCenter + Vector2.right * cs.x * 0.5f,
|
||||||
|
$"Chase {cs.x:F1}×{cs.y:F1}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Assets/_Game/Scripts/Enemies/EnemyPatrolZone.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/EnemyPatrolZone.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6eaf6ff027820884daab5fa51164c899
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -19,8 +19,8 @@ namespace BaseGames.Enemies
|
|||||||
{
|
{
|
||||||
_currentPoiseLevel = _defaultPoiseLevel;
|
_currentPoiseLevel = _defaultPoiseLevel;
|
||||||
|
|
||||||
// 自动注入到同节点 HurtBox(架构 06 §13)
|
// 自动注入到所有子节点 HurtBox(支持多形状受击区)
|
||||||
if (TryGetComponent<HurtBox>(out var hurtBox))
|
foreach (var hurtBox in GetComponentsInChildren<HurtBox>(true))
|
||||||
hurtBox.SetPoiseSource(this);
|
hurtBox.SetPoiseSource(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ namespace BaseGames.Enemies
|
|||||||
if (bt != null && bt.enabled != active)
|
if (bt != null && bt.enabled != active)
|
||||||
{
|
{
|
||||||
bt.enabled = active;
|
bt.enabled = active;
|
||||||
// 同步暂停/恢复 SensorToolkit Sensor,避免远处敌人无效 tick
|
// 同步暂停/恢复感知系统,避免远处敌人无效 tick
|
||||||
enemy.SensorHub?.SetSuspended(!active);
|
enemy.SensorHub?.SetSuspended(!active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ namespace BaseGames.Enemies.Navigation
|
|||||||
// ── 私有 ────────────────────────────────────────────────────────
|
// ── 私有 ────────────────────────────────────────────────────────
|
||||||
private NavAgent _navAgent;
|
private NavAgent _navAgent;
|
||||||
private TransformBasedMovement _movement;
|
private TransformBasedMovement _movement;
|
||||||
|
private EnemyMovement _enemyMovement;
|
||||||
|
|
||||||
// 能力 handler 注册表(NavLinkType → INavLinkHandler)
|
// 能力 handler 注册表(NavLinkType → INavLinkHandler)
|
||||||
private readonly Dictionary<NavLinkType, INavLinkHandler> _handlers
|
private readonly Dictionary<NavLinkType, INavLinkHandler> _handlers
|
||||||
@@ -53,6 +54,7 @@ namespace BaseGames.Enemies.Navigation
|
|||||||
|
|
||||||
// 连接段状态缓存
|
// 连接段状态缓存
|
||||||
private NavLinkType _currentLinkType = NavLinkType.None;
|
private NavLinkType _currentLinkType = NavLinkType.None;
|
||||||
|
private bool _wasNavOnSegment;
|
||||||
private Vector2 _currentLinkStart;
|
private Vector2 _currentLinkStart;
|
||||||
private Vector2 _currentLinkEnd;
|
private Vector2 _currentLinkEnd;
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ namespace BaseGames.Enemies.Navigation
|
|||||||
{
|
{
|
||||||
_navAgent = GetComponent<NavAgent>();
|
_navAgent = GetComponent<NavAgent>();
|
||||||
_movement = GetComponent<TransformBasedMovement>();
|
_movement = GetComponent<TransformBasedMovement>();
|
||||||
|
_enemyMovement = GetComponent<EnemyMovement>();
|
||||||
|
|
||||||
// 自动发现 INavLinkHandler 组件并注册(包含子对象)
|
// 自动发现 INavLinkHandler 组件并注册(包含子对象)
|
||||||
foreach (var handler in GetComponentsInChildren<INavLinkHandler>(true))
|
foreach (var handler in GetComponentsInChildren<INavLinkHandler>(true))
|
||||||
@@ -196,6 +199,32 @@ namespace BaseGames.Enemies.Navigation
|
|||||||
private void HandlePathFailed(NavAgent _) => OnNavPathFailed?.Invoke();
|
private void HandlePathFailed(NavAgent _) => OnNavPathFailed?.Invoke();
|
||||||
private void HandleGoalReached(NavAgent _) => OnGoalReached?.Invoke();
|
private void HandleGoalReached(NavAgent _) => OnGoalReached?.Invoke();
|
||||||
|
|
||||||
|
// ── NavDriving 信号桥 ───────────────────────────────────────────
|
||||||
|
/// <summary>
|
||||||
|
/// 每物理帧向 <see cref="EnemyMovement"/> 写入导航方向信号,并设置 NavDriving 标志。
|
||||||
|
/// NavDriving=true 时 EnemyMovement 只更新朝向,TBM 保留对 transform.position 的控制权。
|
||||||
|
/// </summary>
|
||||||
|
private void FixedUpdate()
|
||||||
|
{
|
||||||
|
if (_enemyMovement == null || _navAgent == null) return;
|
||||||
|
|
||||||
|
bool onSegment = _navAgent.IsMovingOnSegment;
|
||||||
|
_enemyMovement.NavDriving = onSegment;
|
||||||
|
|
||||||
|
if (onSegment)
|
||||||
|
{
|
||||||
|
float dx = _navAgent.PathSubGoal.x - transform.position.x;
|
||||||
|
_enemyMovement.PendingInput.MoveDir = Mathf.Abs(dx) > 0.01f ? Mathf.Sign(dx) : 0f;
|
||||||
|
}
|
||||||
|
else if (_wasNavOnSegment && !_navAgent.IsFollowingAPath)
|
||||||
|
{
|
||||||
|
// 导航刚结束(到达目标 / 路径失败)→ 清除残留 MoveDir
|
||||||
|
_enemyMovement.PendingInput.WantStop = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_wasNavOnSegment = onSegment;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 工具 ───────────────────────────────────────────────────────
|
// ── 工具 ───────────────────────────────────────────────────────
|
||||||
private static NavLinkType ParseLinkType(string name) => name switch
|
private static NavLinkType ParseLinkType(string name) => name switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using BaseGames.Enemies;
|
||||||
|
|
||||||
namespace BaseGames.Enemies.Navigation
|
namespace BaseGames.Enemies.Navigation
|
||||||
{
|
{
|
||||||
@@ -49,6 +50,7 @@ namespace BaseGames.Enemies.Navigation
|
|||||||
|
|
||||||
// ── 状态 ───────────────────────────────────────────────────────
|
// ── 状态 ───────────────────────────────────────────────────────
|
||||||
private Rigidbody2D _rb;
|
private Rigidbody2D _rb;
|
||||||
|
private EnemyMovement _movement;
|
||||||
private Vector2? _destination;
|
private Vector2? _destination;
|
||||||
private bool _isMoving;
|
private bool _isMoving;
|
||||||
private bool _goalFired;
|
private bool _goalFired;
|
||||||
@@ -68,6 +70,7 @@ namespace BaseGames.Enemies.Navigation
|
|||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
_rb = GetComponent<Rigidbody2D>();
|
_rb = GetComponent<Rigidbody2D>();
|
||||||
|
_movement = GetComponent<EnemyMovement>();
|
||||||
_rb.gravityScale = 0f;
|
_rb.gravityScale = 0f;
|
||||||
_rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
_rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||||
}
|
}
|
||||||
@@ -139,15 +142,25 @@ namespace BaseGames.Enemies.Navigation
|
|||||||
Vector2 newPos = Vector2.MoveTowards(myPos, target, _moveSpeed * Time.fixedDeltaTime);
|
Vector2 newPos = Vector2.MoveTowards(myPos, target, _moveSpeed * Time.fixedDeltaTime);
|
||||||
_rb.MovePosition(newPos);
|
_rb.MovePosition(newPos);
|
||||||
|
|
||||||
// 面向移动方向
|
// 面向移动方向(通过 EnemyMovement 输入信号,保持 _facingDir 与动画系统同步)
|
||||||
float dx = target.x - myPos.x;
|
float dx = target.x - myPos.x;
|
||||||
if (Mathf.Abs(dx) > 0.05f)
|
if (Mathf.Abs(dx) > 0.05f)
|
||||||
{
|
{
|
||||||
|
int dir = dx > 0f ? 1 : -1;
|
||||||
|
if (_movement != null)
|
||||||
|
{
|
||||||
|
_movement.PendingInput.WantFace = true;
|
||||||
|
_movement.PendingInput.FaceDir = dir;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 降级:没有 EnemyMovement 时直接翻转(独立飞行单位)
|
||||||
var s = transform.localScale;
|
var s = transform.localScale;
|
||||||
s.x = Mathf.Abs(s.x) * Mathf.Sign(dx);
|
s.x = Mathf.Abs(s.x) * dir;
|
||||||
transform.localScale = s;
|
transform.localScale = s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateHover()
|
private void UpdateHover()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
using Micosmo.SensorToolkit;
|
|
||||||
|
|
||||||
namespace BaseGames.Enemies.Perception
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 敌人感知 Hub(架构 07_EnemyModule §9)。
|
|
||||||
/// 集中暴露挂载在敌人 Prefab 上的各种 SensorToolkit Sensor,BD 任务通过
|
|
||||||
/// 字符串槽位查询,避免在 BD 任务 Inspector 中拖具体 Sensor 引用。
|
|
||||||
///
|
|
||||||
/// 典型槽位命名约定:
|
|
||||||
/// - "aggro" : RangeSensor2D,玩家入侵警戒圈
|
|
||||||
/// - "attack_melee" : RangeSensor2D,近战触发距离
|
|
||||||
/// - "attack_range" : RangeSensor2D,远程触发距离
|
|
||||||
/// - "los" : LOSSensor2D,视线
|
|
||||||
/// - "wall_ahead" : RaySensor2D,前方墙体检测
|
|
||||||
/// - "ledge" : RaySensor2D,前方悬崖检测
|
|
||||||
/// </summary>
|
|
||||||
[DisallowMultipleComponent]
|
|
||||||
public sealed class EnemySensorHub : MonoBehaviour, IPerceptionSystem
|
|
||||||
{
|
|
||||||
[System.Serializable]
|
|
||||||
public struct SensorSlot
|
|
||||||
{
|
|
||||||
public string slotName;
|
|
||||||
public Sensor sensor;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SerializeField] private SensorSlot[] _slots;
|
|
||||||
|
|
||||||
private Dictionary<string, Sensor> _map;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
_map = new Dictionary<string, Sensor>(_slots?.Length ?? 0);
|
|
||||||
if (_slots == null) return;
|
|
||||||
for (int i = 0; i < _slots.Length; i++)
|
|
||||||
{
|
|
||||||
var s = _slots[i];
|
|
||||||
if (s.sensor != null && !string.IsNullOrEmpty(s.slotName))
|
|
||||||
_map[s.slotName] = s.sensor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Sensor Get(string slotName)
|
|
||||||
{
|
|
||||||
if (_map == null || string.IsNullOrEmpty(slotName)) return null;
|
|
||||||
_map.TryGetValue(slotName, out var s);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsDetecting(string slotName, GameObject target)
|
|
||||||
{
|
|
||||||
var s = Get(slotName);
|
|
||||||
return s != null && target != null && s.IsDetected(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasAnyDetection(string slotName)
|
|
||||||
{
|
|
||||||
var s = Get(slotName);
|
|
||||||
if (s == null) return false;
|
|
||||||
foreach (var _ in s.Detections) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GameObject GetFirstDetection(string slotName)
|
|
||||||
{
|
|
||||||
var s = Get(slotName);
|
|
||||||
if (s == null) return null;
|
|
||||||
foreach (var go in s.Detections) return go;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 暂停或恢复所有插槽的 Sensor。
|
|
||||||
/// 当敌人超出 QuotaManager 活跃范围时调用(关闭),归入活跃范围时恢复(开启)。
|
|
||||||
/// </summary>
|
|
||||||
public void SetSuspended(bool suspended)
|
|
||||||
{
|
|
||||||
if (_slots == null) return;
|
|
||||||
for (int i = 0; i < _slots.Length; i++)
|
|
||||||
{
|
|
||||||
var sensor = _slots[i].sensor;
|
|
||||||
if (sensor != null) sensor.enabled = !suspended;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,15 @@ namespace BaseGames.Enemies.Perception
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 敌人感知系统接口。
|
/// 敌人感知系统接口。
|
||||||
/// EnemyBase 通过此接口与感知实现解耦,支持运行时替换(SensorToolkit / 自定义实现)。
|
/// EnemyBase 通过此接口与感知实现解耦,支持运行时替换。
|
||||||
|
/// 当前实现为 <see cref="PhysicsPerceptionSystem"/>(纯物理射线 / 圆形范围检测)。
|
||||||
|
/// 若未来替换底层传感器实现,只需重新实现此接口,上层代码无需改动。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPerceptionSystem
|
public interface IPerceptionSystem
|
||||||
{
|
{
|
||||||
|
/// <summary>指定槽位是否已配置(用于运行前的能力检测,避免无效查询)。</summary>
|
||||||
|
bool HasSlot(string slotName);
|
||||||
|
|
||||||
/// <summary>指定槽位是否检测到任意目标。</summary>
|
/// <summary>指定槽位是否检测到任意目标。</summary>
|
||||||
bool HasAnyDetection(string slotName);
|
bool HasAnyDetection(string slotName);
|
||||||
|
|
||||||
@@ -17,6 +22,20 @@ namespace BaseGames.Enemies.Perception
|
|||||||
/// <summary>返回指定槽位第一个检测到的对象,无检测则返回 null。</summary>
|
/// <summary>返回指定槽位第一个检测到的对象,无检测则返回 null。</summary>
|
||||||
GameObject GetFirstDetection(string slotName);
|
GameObject GetFirstDetection(string slotName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回指定槽位感知区域的半径(圆形区域)。
|
||||||
|
/// 槽位不存在、非圆形区域或实现不支持时返回 -1。
|
||||||
|
/// 主要供编辑器 Gizmos 绘制使用。
|
||||||
|
/// </summary>
|
||||||
|
float GetSensorRadius(string slotName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回指定槽位检测原点相对于感知组件 transform 的偏移(X 分量已根据朝向翻转)。
|
||||||
|
/// 槽位不存在时返回 <see cref="Vector2.zero"/>。
|
||||||
|
/// 供 EnemyBase.OnDrawGizmos 定位各感知圆心使用,避免所有圆重叠在 transform.position。
|
||||||
|
/// </summary>
|
||||||
|
Vector2 GetSensorOffset(string slotName);
|
||||||
|
|
||||||
/// <summary>暂停或恢复感知系统(LOD / 超出活跃范围时调用)。</summary>
|
/// <summary>暂停或恢复感知系统(LOD / 超出活跃范围时调用)。</summary>
|
||||||
void SetSuspended(bool suspended);
|
void SetSuspended(bool suspended);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Enemies.Perception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 敌人感知系统(自研纯物理实现)。
|
||||||
|
/// 每个 <see cref="PerceptionSlot"/> 独立配置并独立运行,支持四种检测模式:
|
||||||
|
/// • RangeCircle — Physics2D.OverlapCircleNonAlloc(可选 LOS 视线遮挡校验)
|
||||||
|
/// • BatchLOS — 委托 EnemyBase.IsPlayerVisible()(BatchLOSSystem 批量射线)
|
||||||
|
/// • FanCast — 以朝向为轴的扇形多射线视野(支持遮挡层)
|
||||||
|
/// • BoxCast — 矩形区域重叠检测(X 偏移随 localScale.x 自动翻转)
|
||||||
|
///
|
||||||
|
/// EnemyBase.Awake() 通过 GetComponentInChildren<IPerceptionSystem>()
|
||||||
|
/// 自动发现本组件,无需修改 EnemyBase。
|
||||||
|
/// 槽位名称常量统一定义于 <see cref="SensorSlotNames"/>。
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public sealed class PhysicsPerceptionSystem : MonoBehaviour, IPerceptionSystem
|
||||||
|
{
|
||||||
|
// ── 槽位类型 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public enum SlotType
|
||||||
|
{
|
||||||
|
/// <summary>Physics2D 圆形重叠检测</summary>
|
||||||
|
RangeCircle,
|
||||||
|
/// <summary>委托 EnemyBase.IsPlayerVisible()(BatchLOSSystem 批量射线视线检测)</summary>
|
||||||
|
BatchLOS,
|
||||||
|
/// <summary>以朝向为轴的扇形射线视野,遮挡层阻断视线</summary>
|
||||||
|
FanCast,
|
||||||
|
/// <summary>矩形区域重叠检测,X 偏移随 localScale.x 自动翻转</summary>
|
||||||
|
BoxCast
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 槽位定义 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public struct PerceptionSlot
|
||||||
|
{
|
||||||
|
[Tooltip("槽位名称,与 SensorSlotNames 常量保持一致\n(aggro / los / attack_melee / attack_range)")]
|
||||||
|
public string slotName;
|
||||||
|
|
||||||
|
[Tooltip("RangeCircle:Physics2D 圆形范围检测\nBatchLOS:视线射线检测(BatchLOSSystem)\nFanCast:以朝向为轴的扇形射线视野\nBoxCast:矩形区域重叠检测")]
|
||||||
|
public SlotType type;
|
||||||
|
|
||||||
|
[Min(0f)]
|
||||||
|
[Tooltip("RangeCircle / FanCast:检测半径(米)\nBatchLOS:最大视线检测距离(0 = 不限制)\nBoxCast:忽略此值")]
|
||||||
|
public float radius;
|
||||||
|
|
||||||
|
[Tooltip("目标检测层(通常为 Player 层);BatchLOS 忽略此值")]
|
||||||
|
public LayerMask detectLayer;
|
||||||
|
|
||||||
|
[Tooltip("RangeCircle / BoxCast:基础重叠命中后额外校验视线(Physics2D.Raycast)\nFanCast:true = 射线被 losBlockMask 层遮挡;false = 穿透所有障碍物")]
|
||||||
|
public bool requireLOS;
|
||||||
|
|
||||||
|
[Tooltip("requireLOS = true / FanCast(requireLOS = true):视线遮挡检测层(通常为 Platform + Wall)\nFanCast 射线只在 requireLOS = true 时被此层遮挡")]
|
||||||
|
public LayerMask losBlockMask;
|
||||||
|
|
||||||
|
[Header("Origin")]
|
||||||
|
[Tooltip("检测原点相对于 transform.position 的偏移(米)。\nX 分量随 localScale.x 朝向自动翻转;BatchLOS 仅影响 Gizmo,不影响实际射线。")]
|
||||||
|
public Vector2 offset;
|
||||||
|
|
||||||
|
[Header("FanCast")]
|
||||||
|
[Tooltip("FanCast:扇形张角(度),以朝向为中轴左右均匀展开")]
|
||||||
|
public float fanAngle;
|
||||||
|
|
||||||
|
[Tooltip("FanCast:扇形内均匀分布的射线数量(建议 5–11 条)")]
|
||||||
|
[Min(2)]
|
||||||
|
public int fanRayCount;
|
||||||
|
|
||||||
|
[Header("BoxCast")]
|
||||||
|
[Tooltip("BoxCast:检测框尺寸 (宽, 高),单位米")]
|
||||||
|
public Vector2 boxSize;
|
||||||
|
|
||||||
|
[Tooltip("BoxCast:相对于感知中心的偏移,X 分量随 localScale.x 自动翻转")]
|
||||||
|
public Vector2 boxOffset;
|
||||||
|
|
||||||
|
[Header("Gizmos")]
|
||||||
|
[Tooltip("Scene 视图 Gizmo 颜色。留空(全透明黑色)时自动使用紫色回退,以确保可见。")]
|
||||||
|
public Color gizmoColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 字段 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[SerializeField] private PerceptionSlot[] _slots;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, List<GameObject>> _detected =
|
||||||
|
new Dictionary<string, List<GameObject>>();
|
||||||
|
private readonly Collider2D[] _overlapBuffer = new Collider2D[32];
|
||||||
|
private bool _suspended;
|
||||||
|
private EnemyBase _owner;
|
||||||
|
|
||||||
|
// ── Unity 生命周期 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
_owner = GetComponentInParent<EnemyBase>();
|
||||||
|
if (_slots == null) return;
|
||||||
|
foreach (var slot in _slots)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(slot.slotName) && !_detected.ContainsKey(slot.slotName))
|
||||||
|
_detected[slot.slotName] = new List<GameObject>(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FixedUpdate()
|
||||||
|
{
|
||||||
|
if (_suspended || _slots == null) return;
|
||||||
|
foreach (var slot in _slots)
|
||||||
|
RefreshSlot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 内部检测逻辑 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void RefreshSlot(PerceptionSlot slot)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(slot.slotName)) return;
|
||||||
|
if (!_detected.TryGetValue(slot.slotName, out var list)) return;
|
||||||
|
list.Clear();
|
||||||
|
|
||||||
|
switch (slot.type)
|
||||||
|
{
|
||||||
|
case SlotType.BatchLOS:
|
||||||
|
if (_owner != null && _owner.IsPlayerVisible() && _owner.PlayerTransform != null)
|
||||||
|
{
|
||||||
|
if (slot.radius > 0f)
|
||||||
|
{
|
||||||
|
float dist = Vector2.Distance(
|
||||||
|
(Vector2)transform.position, (Vector2)_owner.PlayerTransform.position);
|
||||||
|
if (dist > slot.radius) break;
|
||||||
|
}
|
||||||
|
list.Add(_owner.PlayerTransform.gameObject);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SlotType.RangeCircle:
|
||||||
|
RefreshRangeCircle(slot, list);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SlotType.FanCast:
|
||||||
|
RefreshFanCast(slot, list);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SlotType.BoxCast:
|
||||||
|
RefreshBoxCast(slot, list);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshRangeCircle(in PerceptionSlot slot, System.Collections.Generic.List<GameObject> list)
|
||||||
|
{
|
||||||
|
if (slot.radius <= 0f || slot.detectLayer == 0) return;
|
||||||
|
float facingSign = transform.localScale.x < 0f ? -1f : 1f;
|
||||||
|
Vector2 origin = (Vector2)transform.position + new Vector2(slot.offset.x * facingSign, slot.offset.y);
|
||||||
|
int count = Physics2D.OverlapCircleNonAlloc(origin, slot.radius, _overlapBuffer, slot.detectLayer);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var col = _overlapBuffer[i];
|
||||||
|
if (col == null) continue;
|
||||||
|
if (slot.requireLOS && !HasLineOfSight(origin, col.gameObject, slot.losBlockMask)) continue;
|
||||||
|
list.Add(col.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshFanCast(in PerceptionSlot slot, System.Collections.Generic.List<GameObject> list)
|
||||||
|
{
|
||||||
|
if (slot.radius <= 0f || slot.detectLayer == 0 || slot.fanAngle <= 0f) return;
|
||||||
|
|
||||||
|
float facingSign = transform.localScale.x < 0f ? -1f : 1f;
|
||||||
|
var forward = new Vector2(facingSign, 0f);
|
||||||
|
Vector2 origin = (Vector2)transform.position + new Vector2(slot.offset.x * facingSign, slot.offset.y);
|
||||||
|
float halfAngle = slot.fanAngle * 0.5f;
|
||||||
|
int rays = Mathf.Max(2, slot.fanRayCount);
|
||||||
|
// requireLOS = true:射线被 losBlockMask 遮挡;false:仅检测 detectLayer(穿透障碍物)
|
||||||
|
int castMask = slot.requireLOS
|
||||||
|
? ((int)slot.detectLayer | (int)slot.losBlockMask)
|
||||||
|
: (int)slot.detectLayer;
|
||||||
|
|
||||||
|
for (int r = 0; r < rays; r++)
|
||||||
|
{
|
||||||
|
float t = (float)r / (rays - 1);
|
||||||
|
Vector2 dir = RotateVector(forward, Mathf.Lerp(-halfAngle, halfAngle, t));
|
||||||
|
RaycastHit2D hit = Physics2D.Raycast(origin, dir, slot.radius, castMask);
|
||||||
|
if (hit.collider == null) continue;
|
||||||
|
if (((1 << hit.collider.gameObject.layer) & (int)slot.detectLayer) == 0) continue;
|
||||||
|
|
||||||
|
var go = hit.collider.gameObject;
|
||||||
|
if (!list.Contains(go)) list.Add(go);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshBoxCast(in PerceptionSlot slot, System.Collections.Generic.List<GameObject> list)
|
||||||
|
{
|
||||||
|
if (slot.boxSize.x <= 0f || slot.boxSize.y <= 0f || slot.detectLayer == 0) return;
|
||||||
|
|
||||||
|
float facingSign = transform.localScale.x < 0f ? -1f : 1f;
|
||||||
|
Vector2 origin = (Vector2)transform.position + new Vector2(slot.offset.x * facingSign, slot.offset.y);
|
||||||
|
Vector2 center = origin + new Vector2(slot.boxOffset.x * facingSign, slot.boxOffset.y);
|
||||||
|
|
||||||
|
int count = Physics2D.OverlapBoxNonAlloc(center, slot.boxSize, 0f, _overlapBuffer, slot.detectLayer);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var col = _overlapBuffer[i];
|
||||||
|
if (col == null) continue;
|
||||||
|
if (slot.requireLOS && !HasLineOfSight(origin, col.gameObject, slot.losBlockMask)) continue;
|
||||||
|
list.Add(col.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasLineOfSight(Vector2 origin, GameObject target, LayerMask blockMask)
|
||||||
|
{
|
||||||
|
// 优先使用碰撞体包围盒中心(比 transform.position 更准确,避免偏心轴误判)
|
||||||
|
Vector2 targetPos;
|
||||||
|
var col = target.GetComponent<Collider2D>();
|
||||||
|
targetPos = col != null ? (Vector2)col.bounds.center : (Vector2)target.transform.position;
|
||||||
|
|
||||||
|
var dir = targetPos - origin;
|
||||||
|
float dist = dir.magnitude;
|
||||||
|
if (dist <= 0f) return true;
|
||||||
|
var hit = Physics2D.Raycast(origin, dir / dist, dist, blockMask);
|
||||||
|
// 未命中任何障碍,或者第一个命中的就是目标自身(含子物体)
|
||||||
|
return hit.collider == null || hit.collider.transform.IsChildOf(target.transform) || hit.collider.gameObject == target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2 RotateVector(Vector2 v, float angleDeg)
|
||||||
|
{
|
||||||
|
float rad = angleDeg * Mathf.Deg2Rad;
|
||||||
|
float cos = Mathf.Cos(rad);
|
||||||
|
float sin = Mathf.Sin(rad);
|
||||||
|
return new Vector2(cos * v.x - sin * v.y, sin * v.x + cos * v.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── IPerceptionSystem ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public bool HasSlot(string slotName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(slotName)) return false;
|
||||||
|
// 运行时通过字典; 编辑器模式遍历数组
|
||||||
|
if (_detected.Count > 0) return _detected.ContainsKey(slotName);
|
||||||
|
if (_slots == null) return false;
|
||||||
|
foreach (var s in _slots)
|
||||||
|
if (s.slotName == slotName) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasAnyDetection(string slotName) =>
|
||||||
|
_detected.TryGetValue(slotName, out var list) && list.Count > 0;
|
||||||
|
|
||||||
|
public bool IsDetecting(string slotName, GameObject target)
|
||||||
|
{
|
||||||
|
if (!_detected.TryGetValue(slotName, out var list)) return false;
|
||||||
|
for (int i = 0; i < list.Count; i++)
|
||||||
|
if (list[i] == target) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameObject GetFirstDetection(string slotName)
|
||||||
|
{
|
||||||
|
if (!_detected.TryGetValue(slotName, out var list) || list.Count == 0) return null;
|
||||||
|
return list[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetSensorRadius(string slotName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(slotName) || _slots == null) return -1f;
|
||||||
|
foreach (var s in _slots)
|
||||||
|
if (s.slotName == slotName && s.type == SlotType.RangeCircle)
|
||||||
|
return s.radius;
|
||||||
|
return -1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 GetSensorOffset(string slotName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(slotName) || _slots == null) return Vector2.zero;
|
||||||
|
float facingSign = transform.localScale.x < 0f ? -1f : 1f;
|
||||||
|
foreach (var s in _slots)
|
||||||
|
if (s.slotName == slotName)
|
||||||
|
return new Vector2(s.offset.x * facingSign, s.offset.y);
|
||||||
|
return Vector2.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSuspended(bool suspended)
|
||||||
|
{
|
||||||
|
_suspended = suspended;
|
||||||
|
if (suspended)
|
||||||
|
foreach (var list in _detected.Values)
|
||||||
|
list.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 编辑器 API(仅 UNITY_EDITOR 访问)────────────────────────────────
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
public PerceptionSlot[] EditorSlots => _slots;
|
||||||
|
public IReadOnlyDictionary<string, List<GameObject>> EditorDetected => _detected;
|
||||||
|
public EnemyBase EditorOwner => _owner;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c0026fe36cfaffc4e95698bccd0a8380
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,43 +1,33 @@
|
|||||||
namespace BaseGames.Enemies.Perception
|
namespace BaseGames.Enemies.Perception
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <see cref="EnemySensorHub"/> 槽位名称常量。
|
/// <see cref="PhysicsPerceptionSystem"/> 槽位名称常量。
|
||||||
///
|
///
|
||||||
/// 统一定义字符串键,避免在 BD Task Inspector 和代码中散布魔法字符串。
|
/// 统一定义字符串键,避免在 BD Task Inspector 和代码中散布魔法字符串。
|
||||||
/// Prefab 上 EnemySensorHub 组件的 slotName 字段必须与此处常量保持一致。
|
/// Prefab 上 <see cref="PhysicsPerceptionSystem"/> 组件的 slotName 字段必须与此处常量保持一致。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SensorSlotNames
|
public static class SensorSlotNames
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 警戒范围(RangeSensor2D):玩家进入此圈触发 Alert 阶段。
|
/// 警戒范围(RangeCircle):玩家进入此圈触发 Alert 阶段。
|
||||||
/// 通常半径大于攻击范围,小于视线检测范围。
|
/// 通常半径大于攻击范围,小于视线检测范围。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Aggro = "aggro";
|
public const string Aggro = "aggro";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 视线检测(LOSSensor2D):敌我之间无遮挡时持续为 true。
|
/// 视线检测(BatchLOS):敌我之间无遮挡时持续为 true。
|
||||||
/// 由 BatchLOSSystem 批量计算,BD_IsPlayerVisible 读取结果。
|
/// 由 BatchLOSSystem 批量计算,BD_IsPlayerVisible 读取结果。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LOS = "los";
|
public const string LOS = "los";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 近战攻击范围(RangeSensor2D):玩家进入时触发近战攻击条件。
|
/// 近战攻击范围(RangeCircle):玩家进入时触发近战攻击条件。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string AttackMelee = "attack_melee";
|
public const string AttackMelee = "attack_melee";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 远程攻击范围(RangeSensor2D):玩家进入时触发远程攻击条件。
|
/// 远程攻击范围(RangeCircle):玩家进入时触发远程攻击条件。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string AttackRange = "attack_range";
|
public const string AttackRange = "attack_range";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 前方墙体(RaySensor2D):水平方向检测,用于巡逻转向。
|
|
||||||
/// </summary>
|
|
||||||
public const string WallAhead = "wall_ahead";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 前方悬崖(RaySensor2D):斜向下检测地面是否存在,用于巡逻转向。
|
|
||||||
/// </summary>
|
|
||||||
public const string Ledge = "ledge";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,11 +47,14 @@ namespace BaseGames.Input
|
|||||||
|
|
||||||
// ── Polling ───────────────────────────────────────────────────────────
|
// ── Polling ───────────────────────────────────────────────────────────
|
||||||
public Vector2 MoveInput { get; private set; }
|
public Vector2 MoveInput { get; private set; }
|
||||||
|
/// <summary>跳跃键当前是否处于按下状态。供 JumpState 在进入时检测短按是否已松开。</summary>
|
||||||
|
public bool IsJumpHeld => _jumpAction != null && _jumpAction.IsPressed();
|
||||||
|
|
||||||
// ── Runtime state ─────────────────────────────────────────────────────
|
// ── Runtime state ─────────────────────────────────────────────────────
|
||||||
private InputActionMap _gameplay;
|
private InputActionMap _gameplay;
|
||||||
private InputActionMap _ui;
|
private InputActionMap _ui;
|
||||||
private bool _isBound;
|
private bool _isBound;
|
||||||
|
private InputAction _jumpAction;
|
||||||
|
|
||||||
private void EnsureInitialized()
|
private void EnsureInitialized()
|
||||||
{
|
{
|
||||||
@@ -92,6 +95,7 @@ namespace BaseGames.Input
|
|||||||
_gameplay = null;
|
_gameplay = null;
|
||||||
_ui = null;
|
_ui = null;
|
||||||
_isBound = false;
|
_isBound = false;
|
||||||
|
_jumpAction = null;
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +175,7 @@ namespace BaseGames.Input
|
|||||||
|
|
||||||
BindStarted(_gameplay, "Jump", () => JumpStartedEvent?.Invoke());
|
BindStarted(_gameplay, "Jump", () => JumpStartedEvent?.Invoke());
|
||||||
BindCanceled(_gameplay, "Jump", () => JumpCancelledEvent?.Invoke());
|
BindCanceled(_gameplay, "Jump", () => JumpCancelledEvent?.Invoke());
|
||||||
|
_jumpAction = _gameplay.FindAction("Jump", throwIfNotFound: false);
|
||||||
BindStarted(_gameplay, "Attack", () => AttackEvent?.Invoke());
|
BindStarted(_gameplay, "Attack", () => AttackEvent?.Invoke());
|
||||||
BindStarted(_gameplay, "Parry", () => ParryEvent?.Invoke());
|
BindStarted(_gameplay, "Parry", () => ParryEvent?.Invoke());
|
||||||
BindStarted(_gameplay, "Dash", () => DashEvent?.Invoke());
|
BindStarted(_gameplay, "Dash", () => DashEvent?.Invoke());
|
||||||
|
|||||||
@@ -100,6 +100,20 @@ namespace BaseGames.Player
|
|||||||
// 开启连续碰撞检测(CCD):Kinematic 移动平台通过 MovePosition 将 Dynamic 角色推向墙体时,
|
// 开启连续碰撞检测(CCD):Kinematic 移动平台通过 MovePosition 将 Dynamic 角色推向墙体时,
|
||||||
// CCD 会沿移动路径追踪碰撞,确保角色在物理层被墙体表面拦截,而不是在离散步骤中穿透墙体。
|
// CCD 会沿移动路径追踪碰撞,确保角色在物理层被墙体表面拦截,而不是在离散步骤中穿透墙体。
|
||||||
_rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
_rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||||
|
if (_config != null)
|
||||||
|
_config.OnConfigChanged += OnMovementConfigChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (_config != null)
|
||||||
|
_config.OnConfigChanged -= OnMovementConfigChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMovementConfigChanged()
|
||||||
|
{
|
||||||
|
if (_rb != null)
|
||||||
|
_rb.gravityScale = _config.DefaultGravityScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FixedUpdate()
|
private void FixedUpdate()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace BaseGames.Player
|
namespace BaseGames.Player
|
||||||
@@ -26,6 +27,11 @@ namespace BaseGames.Player
|
|||||||
[Tooltip("松开跳跃键时速度保留比例(变高跳)。推荐 0.35,越小跳跃越低。")]
|
[Tooltip("松开跳跃键时速度保留比例(变高跳)。推荐 0.35,越小跳跃越低。")]
|
||||||
[Range(0f, 1f)]
|
[Range(0f, 1f)]
|
||||||
public float JumpCutMultiplier = 0.35f;
|
public float JumpCutMultiplier = 0.35f;
|
||||||
|
[Tooltip("短按最小跳跃保护窗口(秒)。\n" +
|
||||||
|
"窗口内松开跳跃键不会立即截断速度;窗口结束时统一判断是否截断,\n" +
|
||||||
|
"保证无论短按多快/多慢,最小跳跃高度始终一致(消除帧级手感抖动)。\n" +
|
||||||
|
"推荐 0.08(≈5帧@60fps)。调高 → 最小跳更高;调低 → 最小跳更低。")]
|
||||||
|
public float MinJumpTime = 0.08f;
|
||||||
|
|
||||||
[Header("跳跃 — 顶点悬停")]
|
[Header("跳跃 — 顶点悬停")]
|
||||||
[Tooltip("顶点悬停触发阈值(单位/秒)。当 |垂直速度| 低于此值时,重力缩减为 ApexGravityMultiplier 倍,\n产生\"滞空感\"。推荐 3。调高 → 悬停段更长;调低 → 悬停段更短乃至消失。")]
|
[Tooltip("顶点悬停触发阈值(单位/秒)。当 |垂直速度| 低于此值时,重力缩减为 ApexGravityMultiplier 倍,\n产生\"滞空感\"。推荐 3。调高 → 悬停段更长;调低 → 悬停段更短乃至消失。")]
|
||||||
@@ -92,5 +98,16 @@ namespace BaseGames.Player
|
|||||||
|
|
||||||
[Header("重力")]
|
[Header("重力")]
|
||||||
public float DefaultGravityScale = 3f;
|
public float DefaultGravityScale = 3f;
|
||||||
|
|
||||||
|
// ── 运行时热更新通知 ──────────────────────────────────────────────────
|
||||||
|
/// <summary>
|
||||||
|
/// Inspector 中修改配置时触发(仅编辑器),订阅方可立即应用新值,无需重启 PlayMode。
|
||||||
|
/// </summary>
|
||||||
|
public event Action OnConfigChanged;
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
OnConfigChanged?.Invoke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ namespace BaseGames.Player.States
|
|||||||
{
|
{
|
||||||
private bool _isDoubleJump;
|
private bool _isDoubleJump;
|
||||||
|
|
||||||
|
// 最小跳跃窗口:窗口内松键记录 _cutPending,窗口结束时统一执行 CutJump,
|
||||||
|
// 保证短按始终在相同 vy 时截断 → 最小跳跃高度完全一致,消除帧级手感抖动。
|
||||||
|
private float _minJumpTimer;
|
||||||
|
private bool _cutPending;
|
||||||
|
|
||||||
public JumpState(PlayerController owner) : base(owner) { }
|
public JumpState(PlayerController owner) : base(owner) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -40,6 +45,7 @@ namespace BaseGames.Player.States
|
|||||||
Move.Jump();
|
Move.Jump();
|
||||||
|
|
||||||
_isDoubleJump = false; // 消耗标记
|
_isDoubleJump = false; // 消耗标记
|
||||||
|
ResetMinJumpWindow();
|
||||||
Input.JumpCancelledEvent += OnJumpCancelled;
|
Input.JumpCancelledEvent += OnJumpCancelled;
|
||||||
// 开启上升阶段贴墙 vy 保护:防止物理摩擦降低跳跃最高点
|
// 开启上升阶段贴墙 vy 保护:防止物理摩擦降低跳跃最高点
|
||||||
Move.SetPreserveVyOnWallContact(true);
|
Move.SetPreserveVyOnWallContact(true);
|
||||||
@@ -96,6 +102,7 @@ namespace BaseGames.Player.States
|
|||||||
{
|
{
|
||||||
Owner.UseAirJump();
|
Owner.UseAirJump();
|
||||||
Move.DoubleJump();
|
Move.DoubleJump();
|
||||||
|
ResetMinJumpWindow(); // 二段跳重置最小跳跃窗口
|
||||||
// 播放空中跳跃动画,未配置时回退到 Jump
|
// 播放空中跳跃动画,未配置时回退到 Jump
|
||||||
var airJumpClip = AnimCfg?.AirJump ?? AnimCfg?.Jump;
|
var airJumpClip = AnimCfg?.AirJump ?? AnimCfg?.Jump;
|
||||||
if (airJumpClip != null) Anim?.Play(airJumpClip);
|
if (airJumpClip != null) Anim?.Play(airJumpClip);
|
||||||
@@ -134,6 +141,20 @@ namespace BaseGames.Player.States
|
|||||||
|
|
||||||
public override void OnStateFixedUpdate()
|
public override void OnStateFixedUpdate()
|
||||||
{
|
{
|
||||||
|
// ── 最小跳跃窗口:窗口内松键不立即截断,窗口结束时统一处理 ─────────────
|
||||||
|
// 保证短按(无论在窗口内哪帧松键)都在相同 vy 时执行 CutJump → 一致的最小跳跃高度。
|
||||||
|
if (_minJumpTimer > 0f)
|
||||||
|
{
|
||||||
|
_minJumpTimer -= Time.fixedDeltaTime;
|
||||||
|
if (_minJumpTimer <= 0f)
|
||||||
|
{
|
||||||
|
// 窗口到期:已收到松键事件,或当前键未按住(InputBuffer 延迟导致进入时键已释放)
|
||||||
|
if (_cutPending || !Input.IsJumpHeld)
|
||||||
|
Move.CutJump();
|
||||||
|
_cutPending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 顶点悬停:第一判断 |vy|,动态切换重力缩放系数 ────────────────
|
// ── 顶点悬停:第一判断 |vy|,动态切换重力缩放系数 ────────────────
|
||||||
// |垂直速度| 低于顶点阈值时,重力缩减至 ApexGravityMultiplier 倍,
|
// |垂直速度| 低于顶点阈值时,重力缩减至 ApexGravityMultiplier 倍,
|
||||||
// 产生角色在跳跃顶点起起“滒空”的手感,属于高重力平台游戏的标志性特征。
|
// 产生角色在跳跃顶点起起“滒空”的手感,属于高重力平台游戏的标志性特征。
|
||||||
@@ -191,6 +212,18 @@ namespace BaseGames.Player.States
|
|||||||
Input.JumpCancelledEvent -= OnJumpCancelled;
|
Input.JumpCancelledEvent -= OnJumpCancelled;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnJumpCancelled() => Move.CutJump();
|
private void OnJumpCancelled()
|
||||||
|
{
|
||||||
|
if (_minJumpTimer > 0f)
|
||||||
|
_cutPending = true; // 窗口内:推迟到窗口结束时执行,保证一致的截断时机
|
||||||
|
else
|
||||||
|
Move.CutJump(); // 窗口已过(长按):立即截断 → 变高跳
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetMinJumpWindow()
|
||||||
|
{
|
||||||
|
_minJumpTimer = Cfg.MinJumpTime;
|
||||||
|
_cutPending = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,12 +226,12 @@ namespace BaseGames.Player.States
|
|||||||
_overlayLayer = _animancer.Layers[1];
|
_overlayLayer = _animancer.Layers[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注入 HurtBox 依赖
|
// 注入 HurtBox 依赖(覆盖所有子节点 HurtBox,支持多形状受击区)
|
||||||
if (_hurtBox != null)
|
foreach (var hb in GetComponentsInChildren<HurtBox>(true))
|
||||||
{
|
{
|
||||||
if (_shield != null) _hurtBox.SetShieldable(_shield);
|
if (_shield != null) hb.SetShieldable(_shield);
|
||||||
if (_parrySystem != null) _hurtBox.SetParrySystem(_parrySystem);
|
if (_parrySystem != null) hb.SetParrySystem(_parrySystem);
|
||||||
_hurtBox.SetPoiseSource(this);
|
hb.SetPoiseSource(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将唯一配置点(_inputReader)注入到 ParrySystem。
|
// 将唯一配置点(_inputReader)注入到 ParrySystem。
|
||||||
|
|||||||
Reference in New Issue
Block a user