feat: Enhance Physics Perception System with new detection modes and performance optimizations
- Updated PhysicsPerceptionSystem to support seven detection modes: RangeCircle, BatchLOS, FanCast, BoxCast, Sight, RayCast, and TriggerZone. - Improved documentation for each detection mode, including performance optimization strategies. - Introduced PerceptionTriggerProxy for event-driven detection in TriggerZone slots. - Added SightBatchSystem to manage Sight slots efficiently, reducing CPU spikes during high enemy counts. - Updated SensorSlotNames to reflect new detection modes and their purposes. - Enhanced internal logic for detecting targets and managing detection events.
This commit is contained in:
@@ -12,13 +12,14 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 81da55e0fcf99d34693cbc5a348225c3, type: 3}
|
||||
m_Name: PLY_PlayerMovementConfig
|
||||
m_EditorClassIdentifier:
|
||||
RunSpeed: 6
|
||||
RunSpeed: 7
|
||||
AirDragFactor: 1
|
||||
JumpForce: 17.5
|
||||
JumpForce: 18
|
||||
CoyoteTime: 0.12
|
||||
FallGravityMult: 2
|
||||
MaxFallSpeed: 15
|
||||
JumpCutMultiplier: 0.321
|
||||
FallGravityMult: 1
|
||||
MaxFallSpeed: 20
|
||||
JumpCutMultiplier: 0.054
|
||||
MinJumpTime: 0.08
|
||||
ApexThreshold: 3
|
||||
ApexGravityMultiplier: 0.3
|
||||
MaxAirJumps: 5
|
||||
|
||||
@@ -1244,16 +1244,17 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_hudRoot: {fileID: 1496719665}
|
||||
_pauseMenuRoot: {fileID: 414932415}
|
||||
_deathScreenRoot: {fileID: 1071624567}
|
||||
_settingsRoot: {fileID: 83002174}
|
||||
_mapRoot: {fileID: 1189402268}
|
||||
_shopRoot: {fileID: 1859511082}
|
||||
_panels: []
|
||||
_addressablePanels: []
|
||||
_addressablePanelParent: {fileID: 0}
|
||||
_onGameStateChanged: {fileID: 11400000, guid: aa9c327d03e82c84e87d054545412578, type: 2}
|
||||
_onPauseRequested: {fileID: 11400000, guid: a02c7f0e5fa99054bac624adc82c4a53, type: 2}
|
||||
_onFastTravelOpen: {fileID: 11400000, guid: 9f308b954701a484083fb120aa6c7ee3, type: 2}
|
||||
_onShopOpen: {fileID: 11400000, guid: 804a6cfdb23f0554195cebcf8270756f, type: 2}
|
||||
_onMapOpen: {fileID: 11400000, guid: b972e8c7aec9da34d80381e643d49cb2, type: 2}
|
||||
_onCharmPanelOpen: {fileID: 0}
|
||||
_onSpellSelectOpen: {fileID: 0}
|
||||
--- !u!1 &737017260
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1676,6 +1677,8 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
_deathMessage: {fileID: 0}
|
||||
_btnRespawn: {fileID: 307714527}
|
||||
_showDelay: 1.5
|
||||
_defaultDeathText: "\u6C7A\u6B7B"
|
||||
_onDeathScreenConfirmed: {fileID: 11400000, guid: c5237081444b4b54682df1087095fc89, type: 2}
|
||||
--- !u!1 &1100545355
|
||||
GameObject:
|
||||
@@ -1977,6 +1980,7 @@ MonoBehaviour:
|
||||
_onSceneLoadRequest: {fileID: 11400000, guid: 7a4675ba5f3b784448ce2d1e0048f119, type: 2}
|
||||
_onFadeInRequest: {fileID: 11400000, guid: f8d520fe699782b4184ff72ce5200c25, type: 2}
|
||||
_onFadeOutRequest: {fileID: 11400000, guid: a17901d6793dcf2409e2672ffb383208, type: 2}
|
||||
_onSceneWorldStateRestored: {fileID: 0}
|
||||
_sceneLoader: {fileID: 1100545357}
|
||||
_roomFadeDuration: 0.05
|
||||
_sceneFadeDuration: 0.4
|
||||
@@ -2488,8 +2492,7 @@ MonoBehaviour:
|
||||
_springContainer: {fileID: 0}
|
||||
_springIconPrefab: {fileID: 0}
|
||||
_formIcons: []
|
||||
_interactText: {fileID: 0}
|
||||
_interactPromptRoot: {fileID: 0}
|
||||
_interactPromptWidget: {fileID: 0}
|
||||
_onHPChanged: {fileID: 0}
|
||||
_onMaxHPChanged: {fileID: 0}
|
||||
_onSoulPowerChanged: {fileID: 0}
|
||||
@@ -2497,8 +2500,6 @@ MonoBehaviour:
|
||||
_onLingZhuChanged: {fileID: 0}
|
||||
_onSpringChargesChanged: {fileID: 0}
|
||||
_onFormChanged: {fileID: 0}
|
||||
_onShowInteractPrompt: {fileID: 0}
|
||||
_onHideInteractPrompt: {fileID: 0}
|
||||
--- !u!1 &1657595859
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -12884,6 +12884,128 @@ MonoBehaviour:
|
||||
BarrelClipping: 0.25
|
||||
Anamorphism: 0
|
||||
BlendHint: 0
|
||||
--- !u!1 &785719612
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 785719613}
|
||||
- component: {fileID: 785719614}
|
||||
- component: {fileID: 785719615}
|
||||
- component: {fileID: 785719616}
|
||||
m_Layer: 0
|
||||
m_Name: Visual
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &785719613
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 785719612}
|
||||
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: 1864792379}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!95 &785719614
|
||||
Animator:
|
||||
serializedVersion: 5
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 785719612}
|
||||
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!114 &785719615
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 785719612}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 0ad50f81b1d25c441943c37a89ba23f6, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_Animator: {fileID: 785719614}
|
||||
_Transitions: {fileID: 0}
|
||||
_ActionOnDisable: 0
|
||||
--- !u!212 &785719616
|
||||
SpriteRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 785719612}
|
||||
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!1 &787854959
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -13667,6 +13789,106 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2015427670}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &831117703
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 831117707}
|
||||
- component: {fileID: 831117706}
|
||||
- component: {fileID: 831117705}
|
||||
- component: {fileID: 831117704}
|
||||
m_Layer: 25
|
||||
m_Name: ContactDamageZone
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &831117704
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 831117703}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 6def12af0589a9545b80eb5accf61bb6, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_repeatInterval: 0.5
|
||||
--- !u!114 &831117705
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 831117703}
|
||||
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 &831117706
|
||||
CircleCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 831117703}
|
||||
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 &831117707
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 831117703}
|
||||
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: 1864792379}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &837279661
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -15429,6 +15651,63 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1319376623}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &911393355
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 911393356}
|
||||
- component: {fileID: 911393357}
|
||||
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 &911393356
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 911393355}
|
||||
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: 1650269713}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &911393357
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 911393355}
|
||||
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 &926028880
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -24217,7 +24496,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 0
|
||||
m_IsActive: 1
|
||||
--- !u!61 &1354690326
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -208573,6 +208852,39 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1521123847}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1650269712
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1650269713}
|
||||
m_Layer: 0
|
||||
m_Name: Abilities
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1650269713
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1650269712}
|
||||
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: 911393356}
|
||||
- {fileID: 1832569569}
|
||||
m_Father: {fileID: 1864792379}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1651018827
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -210305,6 +210617,88 @@ PolygonCollider2D:
|
||||
- {x: -13.5, y: 2.5}
|
||||
- {x: -13.5, y: -9.5}
|
||||
m_UseDelaunayMesh: 0
|
||||
--- !u!1 &1758953872
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1758953875}
|
||||
- component: {fileID: 1758953874}
|
||||
- component: {fileID: 1758953873}
|
||||
m_Layer: 27
|
||||
m_Name: HurtBox
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1758953873
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1758953872}
|
||||
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 &1758953874
|
||||
CapsuleCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1758953872}
|
||||
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 &1758953875
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1758953872}
|
||||
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: 1864792379}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1766894770
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -211346,6 +211740,74 @@ MonoBehaviour:
|
||||
_noiseFrequency: 1
|
||||
_dedicatedCamera: {fileID: 873533458}
|
||||
_dedicatedPriority: 20
|
||||
--- !u!1 &1832569568
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1832569569}
|
||||
- component: {fileID: 1832569570}
|
||||
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 &1832569569
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1832569568}
|
||||
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: 1650269713}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1832569570
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1832569568}
|
||||
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: 831117704}
|
||||
_aggroSlotName: aggro
|
||||
references:
|
||||
version: 2
|
||||
RefIds: []
|
||||
--- !u!1 &1836019285
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -211554,6 +212016,331 @@ MonoBehaviour:
|
||||
_noiseFrequency: 1
|
||||
_dedicatedCamera: {fileID: 2038960454}
|
||||
_dedicatedPriority: 20
|
||||
--- !u!1 &1864792369
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1864792379}
|
||||
- component: {fileID: 1864792378}
|
||||
- component: {fileID: 1864792377}
|
||||
- component: {fileID: 1864792376}
|
||||
- component: {fileID: 1864792375}
|
||||
- component: {fileID: 1864792374}
|
||||
- component: {fileID: 1864792373}
|
||||
- component: {fileID: 1864792372}
|
||||
- component: {fileID: 1864792371}
|
||||
- component: {fileID: 1864792370}
|
||||
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 &1864792370
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1864792369}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c0026fe36cfaffc4e95698bccd0a8380, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_slots:
|
||||
- slotName: aggro
|
||||
type: 3
|
||||
offset: {x: 0, y: 0}
|
||||
gizmoColor: {r: 0, g: 0, b: 0, a: 0}
|
||||
isDisabled: 0
|
||||
tickInterval: 0
|
||||
radius: 4.62
|
||||
detectLayer:
|
||||
serializedVersion: 2
|
||||
m_Bits: 512
|
||||
fanAngle: 50
|
||||
fanRayCount: 5
|
||||
boxSize: {x: 4.38, y: 1.86}
|
||||
boxOffset: {x: 2.14, y: 0}
|
||||
losBlockMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 8388608
|
||||
losRayCount: 5
|
||||
losMinVisibility: 0
|
||||
rayDirection: {x: 0, y: 0}
|
||||
rayLength: 3.9
|
||||
raySpread: 78.2
|
||||
rayCount: 9
|
||||
obstructLayer:
|
||||
serializedVersion: 2
|
||||
m_Bits: 8388608
|
||||
--- !u!114 &1864792371
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1864792369}
|
||||
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: 4294967295
|
||||
--- !u!114 &1864792372
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1864792369}
|
||||
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 &1864792373
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1864792369}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3864fd1487d130847b11b82f276d11b6, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
height: 1
|
||||
maxSlopeAngle: 180
|
||||
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 &1864792374
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1864792369}
|
||||
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: 785719616}
|
||||
_enableTurnAnimation: 0
|
||||
_animancer: {fileID: 785719615}
|
||||
_animConfig: {fileID: 11400000, guid: 06936c5bc3358904cb269abdfa60ed14, type: 2}
|
||||
_visualRoot: {fileID: 785719613}
|
||||
_spriteDefaultFacingDir: 1
|
||||
_navJumpMaxHeight: 6
|
||||
_navJumpMaxDist: 10
|
||||
_groundCheckCollider: {fileID: 0}
|
||||
_groundCheckDist: 0.15
|
||||
_groundCheckCount: 3
|
||||
_groundMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 68159744
|
||||
_wallCheckDist: 0.2
|
||||
_ledgeCheckFwdOffset: 0.1
|
||||
_ledgeCheckDownDist: 0.4
|
||||
_wallMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
_dbg_FacingDirection: 0
|
||||
_dbg_VelocityX: 0
|
||||
_dbg_VelocityY: 0
|
||||
_dbg_IsGrounded: 0
|
||||
_dbg_IsWallAhead: 0
|
||||
_dbg_IsLedgeAhead: 0
|
||||
_dbg_IsTurning: 0
|
||||
_dbg_NavDriving: 0
|
||||
_dbg_Input_MoveDir: 0
|
||||
_dbg_Input_MoveSpeed: 0
|
||||
_dbg_Input_WantStop: 0
|
||||
_dbg_Input_WantFace: 0
|
||||
_dbg_Input_FaceTargetPos: {x: 0, y: 0}
|
||||
_dbg_Input_FaceDir: 0
|
||||
--- !u!114 &1864792375
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1864792369}
|
||||
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 &1864792376
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1864792369}
|
||||
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: 1864792375}
|
||||
_movement: {fileID: 1864792374}
|
||||
_combat: {fileID: 0}
|
||||
_animancer: {fileID: 785719615}
|
||||
_feedback: {fileID: 0}
|
||||
_hurtBox: {fileID: 1758953873}
|
||||
_patrolZone: {fileID: 0}
|
||||
_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!61 &1864792377
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1864792369}
|
||||
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}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0, y: 0}
|
||||
oldSize: {x: 0, y: 0}
|
||||
newSize: {x: 0, y: 0}
|
||||
adaptiveTilingThreshold: 0
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
serializedVersion: 2
|
||||
m_Size: {x: 0.6, y: 0.8}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!50 &1864792378
|
||||
Rigidbody2D:
|
||||
serializedVersion: 4
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1864792369}
|
||||
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 &1864792379
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1864792369}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: -3.98, y: 8.743559, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 785719613}
|
||||
- {fileID: 1758953875}
|
||||
- {fileID: 831117707}
|
||||
- {fileID: 1650269713}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1865796628
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -216366,3 +217153,4 @@ SceneRoots:
|
||||
- {fileID: 1865796631}
|
||||
- {fileID: 1354690328}
|
||||
- {fileID: 783576435}
|
||||
- {fileID: 1864792379}
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Enemies.Perception;
|
||||
using SlotType = BaseGames.Enemies.Perception.PhysicsPerceptionSystem.SlotType;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
[CustomEditor(typeof(PhysicsPerceptionSystem))]
|
||||
public sealed class PhysicsPerceptionSystemEditor : UnityEditor.Editor
|
||||
{
|
||||
// ── Inspector state ───────────────────────────────────────────────────
|
||||
|
||||
private SerializedProperty _slotsProp;
|
||||
private readonly List<bool> _foldouts = new List<bool>();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_slotsProp = serializedObject.FindProperty("_slots");
|
||||
SyncFoldouts();
|
||||
}
|
||||
|
||||
private void SyncFoldouts()
|
||||
{
|
||||
if (_slotsProp == null) return;
|
||||
while (_foldouts.Count < _slotsProp.arraySize) _foldouts.Add(true);
|
||||
while (_foldouts.Count > _slotsProp.arraySize) _foldouts.RemoveAt(_foldouts.Count - 1);
|
||||
}
|
||||
|
||||
// ── Scene 视图 Gizmo ──────────────────────────────────────────────────
|
||||
|
||||
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected | GizmoType.InSelectionHierarchy)]
|
||||
@@ -25,15 +45,14 @@ namespace BaseGames.Editor
|
||||
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;
|
||||
Color fill = new Color(baseColor.r, baseColor.g, baseColor.b, isSelected ? 0.12f : 0.04f);
|
||||
Color outline = new Color(baseColor.r, baseColor.g, baseColor.b, 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:
|
||||
case SlotType.RangeCircle:
|
||||
if (slot.radius <= 0f) break;
|
||||
Handles.color = fill;
|
||||
Handles.DrawSolidDisc(slotCenter, Vector3.forward, slot.radius);
|
||||
@@ -41,7 +60,7 @@ namespace BaseGames.Editor
|
||||
Handles.DrawWireDisc(slotCenter, Vector3.forward, slot.radius);
|
||||
break;
|
||||
|
||||
case PhysicsPerceptionSystem.SlotType.FanCast:
|
||||
case SlotType.FanCast:
|
||||
if (slot.radius > 0f && slot.fanAngle > 0f)
|
||||
{
|
||||
DrawFanGizmo(slotCenter, facingSign, slot, fill, outline);
|
||||
@@ -49,7 +68,7 @@ namespace BaseGames.Editor
|
||||
}
|
||||
break;
|
||||
|
||||
case PhysicsPerceptionSystem.SlotType.BoxCast:
|
||||
case SlotType.BoxCast:
|
||||
if (slot.boxSize.x > 0f && slot.boxSize.y > 0f)
|
||||
{
|
||||
DrawBoxGizmo(slotCenter, facingSign, slot, fill, outline);
|
||||
@@ -57,17 +76,27 @@ namespace BaseGames.Editor
|
||||
}
|
||||
break;
|
||||
|
||||
// BatchLOS: eye-position marker + optional range disc + runtime ray
|
||||
case PhysicsPerceptionSystem.SlotType.BatchLOS:
|
||||
case SlotType.BatchLOS:
|
||||
DrawBatchLOSGizmo(system, slotCenter, slot, outline, isSelected);
|
||||
break;
|
||||
|
||||
case SlotType.Sight:
|
||||
DrawSightGizmo(system, slotCenter, facingSign, slot, fill, outline, isSelected);
|
||||
break;
|
||||
|
||||
case SlotType.RayCast:
|
||||
DrawRayCastGizmo(slotCenter, facingSign, slot, outline, isSelected);
|
||||
break;
|
||||
|
||||
case SlotType.TriggerZone:
|
||||
DrawTriggerZoneGizmo(slotCenter, slot, outline, isSelected);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Handles.color = Color.white;
|
||||
}
|
||||
|
||||
/// <summary>在 Slot 原点处画一个小十字点,帮助可视化 offset 配置。</summary>
|
||||
static void DrawOriginDot(Vector3 pos, Color color)
|
||||
{
|
||||
Handles.color = color;
|
||||
@@ -75,80 +104,217 @@ namespace BaseGames.Editor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅当颜色字段为 Unity 默认值 Color(0,0,0,0)(全零 = 未配置)时,
|
||||
/// 返回易辨识的紫色回退(alpha=1);用户明确设置的任何颜色(包括近黑色)均原样保留。
|
||||
/// Color(0,0,0,0) 是未配置的默认值,返回紫色回退;用户设置的颜色原样保留。
|
||||
/// </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 Color ResolveGizmoColor(Color c) =>
|
||||
c.r + c.g + c.b + c.a < 0.01f ? 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;
|
||||
Color fill = new Color(slotColor.r, slotColor.g, slotColor.b, 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;
|
||||
Color rim = new Color(slotColor.r, slotColor.g, slotColor.b, 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;
|
||||
Vector3 tip = 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));
|
||||
Handles.DrawLine(tip, tip + new Vector3(-facingSign * headLen, headLen, 0f));
|
||||
Handles.DrawLine(tip, tip + 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);
|
||||
float r = i * 45f * Mathf.Deg2Rad;
|
||||
var d = new Vector3(Mathf.Cos(r), Mathf.Sin(r), 0f);
|
||||
Handles.DrawLine(slotCenter + d * innerR, slotCenter + d * 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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sight 槽位 Gizmo:视野锥形(或全向圆形)+ 眼点 + LOS 能力提示射线 + 运行时检测连线。
|
||||
/// </summary>
|
||||
static void DrawSightGizmo(PhysicsPerceptionSystem system, Vector3 slotCenter, float facingSign,
|
||||
PhysicsPerceptionSystem.PerceptionSlot slot, Color fill, Color outline, bool isSelected)
|
||||
{
|
||||
if (slot.radius <= 0f) return;
|
||||
|
||||
bool hasCone = slot.fanAngle > 0f && slot.fanAngle < 360f;
|
||||
|
||||
if (hasCone)
|
||||
{
|
||||
DrawFanGizmo(slotCenter, facingSign, slot, fill, outline);
|
||||
}
|
||||
else
|
||||
{
|
||||
Handles.color = fill;
|
||||
Handles.DrawSolidDisc(slotCenter, Vector3.forward, slot.radius);
|
||||
Handles.color = outline;
|
||||
Handles.DrawWireDisc(slotCenter, Vector3.forward, slot.radius);
|
||||
}
|
||||
|
||||
// 眼点(视线传感器标志)
|
||||
Handles.color = outline;
|
||||
Handles.DrawSolidDisc(slotCenter, Vector3.forward, 0.07f);
|
||||
|
||||
// 选中时:视线射线提示(表明有遮挡检测能力)
|
||||
if (isSelected)
|
||||
{
|
||||
float innerR = 0.10f;
|
||||
float outerR = Mathf.Min(slot.radius * 0.35f, 0.40f);
|
||||
if (hasCone)
|
||||
{
|
||||
float halfAngle = slot.fanAngle * 0.5f;
|
||||
int lines = Mathf.Max(3, slot.fanRayCount > 0 ? slot.fanRayCount : 5);
|
||||
for (int i = 0; i <= lines; i++)
|
||||
{
|
||||
float t = (float)i / lines;
|
||||
float ang = Mathf.Lerp(-halfAngle, halfAngle, t);
|
||||
var dir = RotateVec3(new Vector3(facingSign, 0f, 0f), ang).normalized;
|
||||
Handles.DrawDottedLine(slotCenter + dir * innerR, slotCenter + dir * (outerR * 2.5f), 3f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
float r = i * 45f * Mathf.Deg2Rad;
|
||||
var d = new Vector3(Mathf.Cos(r), Mathf.Sin(r), 0f);
|
||||
Handles.DrawDottedLine(slotCenter + d * innerR, slotCenter + d * (outerR * 2.5f), 3f);
|
||||
}
|
||||
}
|
||||
|
||||
// LOS 图标文本(Scene 视图)
|
||||
GUIStyle style = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
fontSize = 9,
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
normal = { textColor = new Color(outline.r, outline.g, outline.b, 0.85f) }
|
||||
};
|
||||
Handles.Label(slotCenter + new Vector3(0f, slot.radius + 0.2f, 0f), "LOS", style);
|
||||
}
|
||||
|
||||
// 运行时:绿线连到检测到的目标
|
||||
if (!Application.isPlaying) return;
|
||||
var detected = system.EditorDetected;
|
||||
if (detected == null || !detected.TryGetValue(slot.slotName, out var hits) || hits.Count == 0) return;
|
||||
|
||||
Color green = new Color(0.2f, 1.0f, 0.3f, 0.9f);
|
||||
Handles.color = green;
|
||||
foreach (var go in hits)
|
||||
{
|
||||
if (go == null) continue;
|
||||
Handles.DrawLine(slotCenter, go.transform.position);
|
||||
Handles.DrawSolidDisc(go.transform.position, Vector3.forward, 0.09f);
|
||||
}
|
||||
}
|
||||
|
||||
static void DrawRayCastGizmo(Vector3 slotCenter, float facingSign,
|
||||
PhysicsPerceptionSystem.PerceptionSlot slot, Color outline, bool isSelected)
|
||||
{
|
||||
if (slot.rayLength <= 0f) return;
|
||||
|
||||
Color lineColor = new Color(outline.r, outline.g, outline.b, isSelected ? 0.90f : 0.50f);
|
||||
Handles.color = lineColor;
|
||||
|
||||
// Base direction in local space with X flipped for facing
|
||||
Vector2 baseDir2D = slot.rayDirection.sqrMagnitude < 0.001f
|
||||
? Vector2.right
|
||||
: slot.rayDirection.normalized;
|
||||
var baseDir = new Vector3(baseDir2D.x * facingSign, baseDir2D.y, 0f).normalized;
|
||||
|
||||
float spread = Mathf.Clamp(slot.raySpread, 0f, 180f);
|
||||
int count = (spread > 0f && slot.rayCount > 1) ? Mathf.Clamp(slot.rayCount, 2, 9) : 1;
|
||||
|
||||
if (count == 1 || spread <= 0f)
|
||||
{
|
||||
Vector3 end = slotCenter + baseDir * slot.rayLength;
|
||||
Handles.DrawLine(slotCenter, end);
|
||||
DrawArrowHead(slotCenter, end, lineColor, 0.08f);
|
||||
}
|
||||
else
|
||||
{
|
||||
float halfSpread = spread * 0.5f;
|
||||
for (int r = 0; r < count; r++)
|
||||
{
|
||||
float t = (float)r / (count - 1);
|
||||
float ang = Mathf.Lerp(-halfSpread, halfSpread, t);
|
||||
var dir = RotateVec3(baseDir, ang).normalized;
|
||||
Vector3 end = slotCenter + dir * slot.rayLength;
|
||||
Handles.DrawLine(slotCenter, end);
|
||||
}
|
||||
// Highlight center ray
|
||||
Vector3 centerEnd = slotCenter + baseDir * slot.rayLength;
|
||||
Color bright = new Color(lineColor.r, lineColor.g, lineColor.b, 1f);
|
||||
DrawArrowHead(slotCenter, centerEnd, bright, 0.08f);
|
||||
}
|
||||
|
||||
// Origin dot
|
||||
Handles.color = lineColor;
|
||||
Handles.DrawSolidDisc(slotCenter, Vector3.forward, 0.05f);
|
||||
}
|
||||
|
||||
static void DrawArrowHead(Vector3 from, Vector3 tip, Color color, float size)
|
||||
{
|
||||
Handles.color = color;
|
||||
Vector3 dir = (tip - from).normalized;
|
||||
Handles.DrawLine(tip, tip - dir * size + new Vector3(-dir.y, dir.x, 0f) * size * 0.5f);
|
||||
Handles.DrawLine(tip, tip - dir * size + new Vector3( dir.y, -dir.x, 0f) * size * 0.5f);
|
||||
}
|
||||
|
||||
static void DrawTriggerZoneGizmo(Vector3 slotCenter,
|
||||
PhysicsPerceptionSystem.PerceptionSlot slot, Color outline, bool isSelected)
|
||||
{
|
||||
Color dotColor = new Color(outline.r, outline.g, outline.b, isSelected ? 0.90f : 0.55f);
|
||||
Handles.color = dotColor;
|
||||
Handles.DrawSolidDisc(slotCenter, Vector3.forward, 0.10f);
|
||||
|
||||
float ring = 0.22f;
|
||||
Handles.color = new Color(outline.r, outline.g, outline.b, isSelected ? 0.60f : 0.25f);
|
||||
Handles.DrawWireDisc(slotCenter, Vector3.forward, ring);
|
||||
Handles.DrawWireDisc(slotCenter, Vector3.forward, ring * 1.55f);
|
||||
|
||||
if (isSelected)
|
||||
{
|
||||
GUIStyle style = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
fontSize = 9,
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
normal = { textColor = new Color(outline.r, outline.g, outline.b, 0.85f) }
|
||||
};
|
||||
Handles.Label(slotCenter + new Vector3(0f, ring * 1.55f + 0.15f, 0f),
|
||||
string.IsNullOrEmpty(slot.slotName) ? "TZ" : $"TZ:{slot.slotName}", style);
|
||||
}
|
||||
}
|
||||
|
||||
static void DrawFanGizmo(Vector3 center, float facingSign,
|
||||
PhysicsPerceptionSystem.PerceptionSlot slot, Color fill, Color outline)
|
||||
{
|
||||
@@ -160,7 +326,6 @@ namespace BaseGames.Editor
|
||||
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);
|
||||
@@ -173,7 +338,6 @@ namespace BaseGames.Editor
|
||||
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),
|
||||
@@ -181,33 +345,126 @@ namespace BaseGames.Editor
|
||||
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);
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
// ── Custom Inspector ──────────────────────────────────────────────────
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
SyncFoldouts();
|
||||
|
||||
if (_slotsProp == null)
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField("感知槽位", EditorStyles.boldLabel);
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
SyncFoldouts();
|
||||
|
||||
int pendingMoveUp = -1;
|
||||
int pendingMoveDown = -1;
|
||||
int pendingDuplicate = -1;
|
||||
int pendingDelete = -1;
|
||||
|
||||
for (int i = 0; i < _slotsProp.arraySize; i++)
|
||||
{
|
||||
var elem = _slotsProp.GetArrayElementAtIndex(i);
|
||||
var nameProp = elem.FindPropertyRelative("slotName");
|
||||
var typeProp = elem.FindPropertyRelative("type");
|
||||
|
||||
var isDisabledProp = elem.FindPropertyRelative("isDisabled");
|
||||
var tickIntervalProp = elem.FindPropertyRelative("tickInterval");
|
||||
bool isDisabled = isDisabledProp != null && isDisabledProp.boolValue
|
||||
&& tickIntervalProp != null && tickIntervalProp.intValue > 0;
|
||||
string disabledTag = isDisabled ? " ⊘" : "";
|
||||
string label = string.IsNullOrEmpty(nameProp.stringValue)
|
||||
? $"Slot {i}{disabledTag}"
|
||||
: $"[{i}] {nameProp.stringValue} ({(SlotType)typeProp.enumValueIndex}){disabledTag}";
|
||||
|
||||
// ── 槽位标题行(折叠 + 操作按钮)──────────────────────────
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
_foldouts[i] = EditorGUILayout.Foldout(_foldouts[i], label, true);
|
||||
GUILayout.FlexibleSpace();
|
||||
using (new EditorGUI.DisabledScope(i == 0))
|
||||
if (GUILayout.Button("▲", EditorStyles.miniButton, GUILayout.Width(22))) pendingMoveUp = i;
|
||||
using (new EditorGUI.DisabledScope(i == _slotsProp.arraySize - 1))
|
||||
if (GUILayout.Button("▼", EditorStyles.miniButton, GUILayout.Width(22))) pendingMoveDown = i;
|
||||
if (GUILayout.Button("⊕", EditorStyles.miniButton, GUILayout.Width(22))) pendingDuplicate = i;
|
||||
Color _prevSlotBtnColor = GUI.color;
|
||||
GUI.color = new Color(1f, 0.5f, 0.5f);
|
||||
if (GUILayout.Button("✕", EditorStyles.miniButton, GUILayout.Width(22))) pendingDelete = i;
|
||||
GUI.color = _prevSlotBtnColor;
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (_foldouts[i])
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
DrawSlotFields(elem, (SlotType)typeProp.enumValueIndex, nameProp.stringValue);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
EditorGUILayout.Space(1);
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
// ── 底部工具栏:添加新槽位 ──────────────────────────────────────
|
||||
EditorGUILayout.Space(2);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("⊕ 添加槽位", GUILayout.Width(120)))
|
||||
{
|
||||
_slotsProp.InsertArrayElementAtIndex(_slotsProp.arraySize);
|
||||
_foldouts.Add(true);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// ── 延迟执行槽位操作(避免在 GUI 循环中修改数组)──────────────
|
||||
if (pendingDelete >= 0)
|
||||
{
|
||||
_slotsProp.DeleteArrayElementAtIndex(pendingDelete);
|
||||
SyncFoldouts();
|
||||
}
|
||||
else if (pendingDuplicate >= 0)
|
||||
{
|
||||
_slotsProp.InsertArrayElementAtIndex(pendingDuplicate);
|
||||
_foldouts.Insert(pendingDuplicate + 1, true);
|
||||
}
|
||||
else if (pendingMoveUp >= 0)
|
||||
{
|
||||
_slotsProp.MoveArrayElement(pendingMoveUp, pendingMoveUp - 1);
|
||||
(_foldouts[pendingMoveUp], _foldouts[pendingMoveUp - 1]) =
|
||||
(_foldouts[pendingMoveUp - 1], _foldouts[pendingMoveUp]);
|
||||
}
|
||||
else if (pendingMoveDown >= 0)
|
||||
{
|
||||
_slotsProp.MoveArrayElement(pendingMoveDown, pendingMoveDown + 1);
|
||||
(_foldouts[pendingMoveDown], _foldouts[pendingMoveDown + 1]) =
|
||||
(_foldouts[pendingMoveDown + 1], _foldouts[pendingMoveDown]);
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
// ── 运行时检测结果 ──
|
||||
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)
|
||||
{
|
||||
@@ -220,8 +477,195 @@ namespace BaseGames.Editor
|
||||
}
|
||||
EditorGUILayout.LabelField(kvp.Key, kvp.Value.Count > 0 ? sb.ToString() : "—");
|
||||
}
|
||||
|
||||
Repaint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按槽位类型条件渲染字段——每个 SlotType 只显示与其相关的参数。
|
||||
/// </summary>
|
||||
private void DrawSlotFields(SerializedProperty elem, SlotType slotType, string slotName)
|
||||
{
|
||||
// ── 通用字段(所有类型)────────────────────────────────────────
|
||||
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("slotName"),
|
||||
new GUIContent("槽位名称", "与 SensorSlotNames 常量保持一致"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("type"),
|
||||
new GUIContent("检测类型"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("offset"),
|
||||
new GUIContent("原点偏移", "相对于 transform.position,X 随朝向自动翻转"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("gizmoColor"),
|
||||
new GUIContent("Gizmo 颜色", "全透明黑色 = 自动使用紫色回退"));
|
||||
|
||||
EditorGUILayout.Space(2);
|
||||
EditorGUILayout.LabelField("─ 运行时控制 ─", EditorStyles.miniLabel);
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("isDisabled"),
|
||||
new GUIContent("禁用", "勾选后停用本槽位(tickInterval > 0 时生效)"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("tickInterval"),
|
||||
new GUIContent("刷新间隔 (帧)", "0 = 历史兼容(每帧,isDisabled 无效)\n1 = 每帧\n3 = 每 3 帧(Sight 推荐)\n同间隔槽位自动错帧执行"));
|
||||
|
||||
EditorGUILayout.Space(2);
|
||||
|
||||
// ── 按类型显示相关字段 ─────────────────────────────────────────
|
||||
|
||||
switch (slotType)
|
||||
{
|
||||
case SlotType.RangeCircle:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("radius"),
|
||||
new GUIContent("半径 (m)", "OverlapCircle 检测半径"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层"));
|
||||
break;
|
||||
|
||||
case SlotType.BatchLOS:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("radius"),
|
||||
new GUIContent("检测半径 (m)", "OverlapCircle 半径,必须 > 0"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层", "目标所在层(通常为 Player 层)"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("losBlockMask"),
|
||||
new GUIContent("视线遮挡层", "遮挡射线的层(Platform / Wall);留 0 则不做遮挡检测"));
|
||||
break;
|
||||
|
||||
case SlotType.FanCast:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("radius"),
|
||||
new GUIContent("半径 (m)", "扇形检测半径"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("fanAngle"),
|
||||
new GUIContent("扇形角度 (°)", "以朝向为中轴,左右对称展开"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("fanRayCount"),
|
||||
new GUIContent("Gizmo 分隔线数", "仅影响 Scene 视图,不影响检测精度(建议 3–9)"));
|
||||
break;
|
||||
|
||||
case SlotType.BoxCast:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("boxSize"),
|
||||
new GUIContent("矩形尺寸 (m)", "宽 × 高"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("boxOffset"),
|
||||
new GUIContent("矩形偏移", "相对于原点偏移,X 随朝向翻转"));
|
||||
break;
|
||||
|
||||
case SlotType.Sight:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("radius"),
|
||||
new GUIContent("视野半径 (m)", "Sight 传感器检测半径"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("fanAngle"),
|
||||
new GUIContent("视锥角度 (°)", "0 或 ≥ 360 = 全向 360°;否则以朝向为中轴左右展开"));
|
||||
EditorGUILayout.Space(2);
|
||||
EditorGUILayout.LabelField("─ LOS 遮挡设置(Sight 专用)─", EditorStyles.miniLabel);
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("losBlockMask"),
|
||||
new GUIContent("遮挡层", "遮挡物所在层(Platform / Wall / Ground);设为 0 则视线始终通过"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("losRayCount"),
|
||||
new GUIContent("LOS 采样点数", "1=中心 3=中心+上+下(推荐) 5=中心+上下左右"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("losMinVisibility"),
|
||||
new GUIContent("最低可见度 (0–1)", "0=任一采样点通过即可 1=全部通过(严格)"));
|
||||
break;
|
||||
|
||||
case SlotType.RayCast:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("rayDirection"),
|
||||
new GUIContent("射线方向 (本地空间)", "X 分量随朝向自动翻转;(1,0)=正前方,(0,-1)=正下方"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("rayLength"),
|
||||
new GUIContent("射线长度 (m)"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("obstructLayer"),
|
||||
new GUIContent("遮挡层", "射线碰到此层后立即阻断(设为 0 = 射线穿透所有物体)"));
|
||||
EditorGUILayout.Space(2);
|
||||
EditorGUILayout.LabelField("─ 扩散(多射线)─", EditorStyles.miniLabel);
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("raySpread"),
|
||||
new GUIContent("扩散角 (°)", "0 = 单根射线;>0 时在此角度范围内均匀分布 N 根射线"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("rayCount"),
|
||||
new GUIContent("射线根数", "raySpread > 0 时生效,建议 3–9"));
|
||||
break;
|
||||
|
||||
case SlotType.TriggerZone:
|
||||
EditorGUILayout.HelpBox(
|
||||
"TriggerZone 槽位为事件驱动,零轮询开销。\n" +
|
||||
"需在此 GameObject 的子节点上挂载 PerceptionTriggerProxy,\n" +
|
||||
"设置相同 slotName,且 Collider2D.isTrigger = true。",
|
||||
MessageType.Info);
|
||||
EditorGUILayout.Space(4);
|
||||
DrawTriggerZoneManagement(slotName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTriggerZoneManagement(string slotName)
|
||||
{
|
||||
var system = (PhysicsPerceptionSystem)target;
|
||||
|
||||
EditorGUILayout.LabelField("── TriggerZone 子节点 ──", EditorStyles.miniLabel);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(slotName))
|
||||
{
|
||||
EditorGUILayout.HelpBox("请先填写槽位名称,再管理代理节点。", MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找所有匹配的代理节点
|
||||
var allProxies = system.GetComponentsInChildren<PerceptionTriggerProxy>(true);
|
||||
var matchList = new List<PerceptionTriggerProxy>();
|
||||
foreach (var p in allProxies)
|
||||
if (p.slotName == slotName) matchList.Add(p);
|
||||
|
||||
if (matchList.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox($"未找到 slotName = \"{slotName}\" 的代理节点。", MessageType.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var proxy in matchList)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
EditorGUILayout.ObjectField(proxy, typeof(PerceptionTriggerProxy), true);
|
||||
|
||||
if (GUILayout.Button("选中", EditorStyles.miniButton, GUILayout.Width(44)))
|
||||
{
|
||||
Selection.activeGameObject = proxy.gameObject;
|
||||
EditorGUIUtility.PingObject(proxy.gameObject);
|
||||
}
|
||||
|
||||
var col = proxy.GetComponent<Collider2D>();
|
||||
bool badTrigger = col == null || !col.isTrigger;
|
||||
Color prevC = GUI.color;
|
||||
GUI.color = badTrigger ? Color.yellow : new Color(0.5f, 1f, 0.5f);
|
||||
EditorGUILayout.LabelField(
|
||||
badTrigger ? "⚠ isTrigger" : "✓ Trigger",
|
||||
GUILayout.Width(76));
|
||||
GUI.color = prevC;
|
||||
|
||||
GUI.color = new Color(1f, 0.5f, 0.5f);
|
||||
if (GUILayout.Button("删除", EditorStyles.miniButton, GUILayout.Width(44)))
|
||||
Undo.DestroyObjectImmediate(proxy.gameObject);
|
||||
GUI.color = prevC;
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(3);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button($"⊕ 创建代理节点 ({slotName})", GUILayout.Width(220)))
|
||||
{
|
||||
var go = new GameObject($"TriggerZone_{slotName}");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Create TriggerZone Proxy");
|
||||
go.transform.SetParent(system.transform, false);
|
||||
go.transform.localPosition = Vector3.zero;
|
||||
|
||||
// CircleCollider2D must be added BEFORE PerceptionTriggerProxy ([RequireComponent])
|
||||
var circle = go.AddComponent<CircleCollider2D>();
|
||||
circle.isTrigger = true;
|
||||
circle.radius = 1.5f;
|
||||
|
||||
var proxy = go.AddComponent<PerceptionTriggerProxy>();
|
||||
proxy.slotName = slotName;
|
||||
|
||||
Selection.activeGameObject = go;
|
||||
EditorGUIUtility.PingObject(go);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1240,12 +1240,22 @@ namespace BaseGames.Editor
|
||||
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;
|
||||
case "patrol": enumIdx = 0; radius = 5f; layer = 0; break;
|
||||
case "alert": enumIdx = 0; radius = 3f; layer = playerLayer; break;
|
||||
case "sight": enumIdx = 4; radius = 6f; layer = playerLayer; break;
|
||||
}
|
||||
|
||||
elem.FindPropertyRelative("type").enumValueIndex = enumIdx;
|
||||
elem.FindPropertyRelative("radius").floatValue = radius;
|
||||
elem.FindPropertyRelative("detectLayer").intValue = layer;
|
||||
|
||||
// sight 槽位默认设置推荐的 LOS 采样点数(3:中心+上+下)
|
||||
if (name == "sight")
|
||||
{
|
||||
var losRayCountProp = elem.FindPropertyRelative("losRayCount");
|
||||
if (losRayCountProp != null) losRayCountProp.intValue = 3;
|
||||
}
|
||||
|
||||
// 各 slot 分配语义化默认颜色,可在 Inspector 中按需覆盖
|
||||
Color defaultColor = name switch
|
||||
{
|
||||
@@ -1253,6 +1263,9 @@ namespace BaseGames.Editor
|
||||
"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), // 粉红
|
||||
"patrol" => new Color(0.20f, 0.90f, 0.20f, 1f), // 绿
|
||||
"alert" => new Color(1.00f, 0.90f, 0.10f, 1f), // 黄
|
||||
"sight" => new Color(0.30f, 0.85f, 1.00f, 1f), // 浅蓝(LOS 传感器)
|
||||
_ => Color.clear, // 未知 slot 回退为紫色
|
||||
};
|
||||
elem.FindPropertyRelative("gizmoColor").colorValue = defaultColor;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Conditional:玩家是否可见(LOS 检测)。
|
||||
/// 读取 EnemyBase._losResult 字段(由 BatchLOSSystem 每帧写入,或降级 3 帧节流 Raycast)。
|
||||
/// 读取 EnemyBase.IsPlayerVisible(),结果来自 PhysicsPerceptionSystem(LOS / Sight 槽位)。
|
||||
/// </summary>
|
||||
[TaskName("Is Player Visible?")]
|
||||
[TaskCategory("BaseGames/Enemy/Perception")]
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Jobs;
|
||||
using BaseGames.Core;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// 批量视线检测系统(架构 07_EnemyModule §12)。
|
||||
/// 每 FixedUpdate:
|
||||
/// 1. 读取上一帧 RaycastHit2D 结果,回调各注册者。
|
||||
/// 2. 重新构建本帧 RaycastCommand 批次,使用 Physics2D.RaycastAll 同步模式执行。
|
||||
///
|
||||
/// ⚠️ Unity 2022.3 中 Physics2D 批量命令(RaycastCommand2D)尚未稳定,
|
||||
/// 此实现使用 FixedUpdate 内顺序 Raycast2D(节流),确保零 GC 分配。
|
||||
/// 当敌人数量 > 20 时建议切换到 Job System RaycastCommand。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-200)]
|
||||
public class BatchLOSSystem : MonoBehaviour
|
||||
{
|
||||
[SerializeField, Min(1)] private int _maxRequestersPerFrame = 8;
|
||||
|
||||
private void Awake() => ServiceLocator.Register<BatchLOSSystem>(this);
|
||||
private void OnDestroy() => ServiceLocator.Unregister<BatchLOSSystem>(this);
|
||||
|
||||
private readonly List<ILOSRequester> _requesters = new();
|
||||
private readonly HashSet<ILOSRequester> _requesterSet = new(); // O(1) 包含查询
|
||||
// _indexMap 记录每个 requester 在 _requesters 中的下标,供 Unregister 实现 O(1) 删除
|
||||
private readonly Dictionary<ILOSRequester, int> _indexMap = new();
|
||||
private int _currentOffset = 0;
|
||||
|
||||
// ── 注册 ──────────────────────────────────────────────────────────
|
||||
public void Register(ILOSRequester requester)
|
||||
{
|
||||
if (_requesterSet.Add(requester))
|
||||
{
|
||||
_indexMap[requester] = _requesters.Count;
|
||||
_requesters.Add(requester);
|
||||
}
|
||||
}
|
||||
|
||||
public void Unregister(ILOSRequester requester)
|
||||
{
|
||||
if (!_requesterSet.Remove(requester)) return;
|
||||
|
||||
int idx = _indexMap[requester];
|
||||
int last = _requesters.Count - 1;
|
||||
|
||||
// Swap-and-pop:将末尾元素移动到被删除的位置,避免 O(n) 搬移
|
||||
if (idx != last)
|
||||
{
|
||||
var moved = _requesters[last];
|
||||
_requesters[idx] = moved;
|
||||
_indexMap[moved] = idx;
|
||||
}
|
||||
|
||||
_requesters.RemoveAt(last);
|
||||
_indexMap.Remove(requester);
|
||||
|
||||
// 修正偏移量,防止越界
|
||||
if (_currentOffset >= _requesters.Count) _currentOffset = 0;
|
||||
}
|
||||
|
||||
// ── FixedUpdate ───────────────────────────────────────────────────
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (_requesters.Count == 0) return;
|
||||
|
||||
// 每帧轮询部分请求者(均匀分配,避免单帧全量射线)
|
||||
int count = Mathf.Min(_maxRequestersPerFrame, _requesters.Count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int idx = (_currentOffset + i) % _requesters.Count;
|
||||
var requester = _requesters[idx];
|
||||
|
||||
bool hasLOS = false;
|
||||
if (requester != null)
|
||||
{
|
||||
Vector2 origin = requester.LOSOrigin;
|
||||
Vector2 target = requester.LOSTarget;
|
||||
Vector2 direction = target - origin;
|
||||
float distance = direction.magnitude;
|
||||
|
||||
if (distance > 0.01f)
|
||||
{
|
||||
// direction / distance == direction.normalized,避免重复开方
|
||||
var hit = Physics2D.Raycast(origin, direction / distance, distance, requester.LOSBlockingMask);
|
||||
// 若无遮挡物(hit.collider == null),则视线畅通
|
||||
hasLOS = hit.collider == null;
|
||||
}
|
||||
|
||||
|
||||
requester.ReceiveLOSResult(hasLOS);
|
||||
}
|
||||
}
|
||||
|
||||
_currentOffset = (_currentOffset + count) % Mathf.Max(1, _requesters.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ namespace BaseGames.Enemies
|
||||
/// ⚠️ _nav 字段类型为 IPathAgent(在 BaseGames.Enemies.Navigation 中实现具体类)。
|
||||
/// 实现 IPoolable:配合 PooledObject 支持对象池复用,避免频繁 Destroy/Instantiate。
|
||||
/// </summary>
|
||||
public class EnemyBase : MonoBehaviour, IDamageable, ILOSRequester, IPoolable
|
||||
public class EnemyBase : MonoBehaviour, IDamageable, IPoolable
|
||||
{
|
||||
[Header("标识")]
|
||||
[SerializeField] private string _enemyId; // 任务系统 / Boss 进程追踪用,如 "Enemy_SpiderGuard"
|
||||
@@ -286,11 +286,14 @@ namespace BaseGames.Enemies
|
||||
public virtual bool IsPlayerInRange(float range)
|
||||
=> _stats != null && _stats.SqrDistanceToPlayer <= range * range;
|
||||
|
||||
/// <summary>原始视线检测结果(BatchLOSSystem 写入,无感知延迟修正)。</summary>
|
||||
public bool HasLineOfSight => _losResult;
|
||||
/// <summary>视线检测结果(由 PhysicsPerceptionSystem 的 LOS / Sight slot 提供)。</summary>
|
||||
public bool HasLineOfSight =>
|
||||
_sensorHub != null &&
|
||||
(_sensorHub.HasAnyDetection(Perception.SensorSlotNames.LOS) ||
|
||||
_sensorHub.HasAnyDetection(Perception.SensorSlotNames.Sight));
|
||||
|
||||
public virtual bool IsPlayerVisible()
|
||||
=> _threatAssessor != null ? _threatAssessor.IsThreatDetected : _losResult;
|
||||
=> _threatAssessor != null ? _threatAssessor.IsThreatDetected : HasLineOfSight;
|
||||
|
||||
public virtual void FacePlayer()
|
||||
{
|
||||
@@ -644,31 +647,16 @@ namespace BaseGames.Enemies
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
_onPlayerSpawned?.Subscribe(SetPlayerTransform).AddTo(_subs);
|
||||
Core.ServiceLocator.GetOrDefault<AI.BatchLOSSystem>()?.Register(this);
|
||||
}
|
||||
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
Core.ServiceLocator.GetOrDefault<AI.BatchLOSSystem>()?.Unregister(this);
|
||||
_subs.Clear();
|
||||
}
|
||||
|
||||
protected virtual void OnDestroy() { }
|
||||
|
||||
// LOS 缓存(BatchLOSSystem 写入;降级时由 3 帧节流 Raycast 写入)
|
||||
private bool _losResult;
|
||||
|
||||
// ── ILOSRequester ──────────────────────────────────────────────────
|
||||
public Vector2 LOSOrigin => (Vector2)transform.position + _statsSO.EyeOffset;
|
||||
public Vector2 LOSTarget => _playerTransform != null
|
||||
? (Vector2)_playerTransform.position
|
||||
: (Vector2)transform.position;
|
||||
public LayerMask LOSBlockingMask => _statsSO.LOSBlockingMask;
|
||||
|
||||
public void ReceiveLOSResult(bool hasLineOfSight)
|
||||
{
|
||||
_losResult = hasLineOfSight;
|
||||
}
|
||||
|
||||
// BehaviorTree 引用(#if GRAPH_DESIGNER 保护,避免空引用)
|
||||
#if GRAPH_DESIGNER
|
||||
@@ -831,9 +819,9 @@ namespace BaseGames.Enemies
|
||||
Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.85f);
|
||||
Gizmos.DrawWireSphere(eyeWorld, 0.07f);
|
||||
|
||||
if (inRange || _losResult)
|
||||
if (inRange || HasLineOfSight)
|
||||
{
|
||||
Gizmos.color = _losResult
|
||||
Gizmos.color = HasLineOfSight
|
||||
? new Color(1f, 0.5f, 0f, 0.85f)
|
||||
: new Color(0.6f, 0.6f, 0.6f, 0.25f);
|
||||
Gizmos.DrawLine(eyeWorld, playerPos);
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace BaseGames.Enemies
|
||||
[Tooltip("Stagger / KnockUp 伤害阈值及击飞参数")]
|
||||
public HitTierConfig HitTiers;
|
||||
|
||||
[Header("视线检测(BatchLOSSystem)")]
|
||||
[Header("视线检测(遗留配置,已迁移到 PhysicsPerceptionSystem 各 Slot 的 losBlockMask)")]
|
||||
[Tooltip("相对 transform.position 的眼睛偏移量")]
|
||||
public Vector2 EyeOffset = new Vector2(0f, 0.8f);
|
||||
[Tooltip("遮挡 LOS 的物理图层")]
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// BatchLOSSystem 的注册接口(架构 07_EnemyModule §12)。
|
||||
/// 实现此接口的 EnemyBase 子类可以注册到 BatchLOSSystem,
|
||||
/// 以批处理方式接收 LOS(Line of Sight)检测结果。
|
||||
/// </summary>
|
||||
public interface ILOSRequester
|
||||
{
|
||||
/// <summary>射线起点(通常是眼部位置)。</summary>
|
||||
Vector2 LOSOrigin { get; }
|
||||
|
||||
/// <summary>射线终点(通常是玩家位置)。</summary>
|
||||
Vector2 LOSTarget { get; }
|
||||
|
||||
/// <summary>遮挡 LOS 的物理图层。</summary>
|
||||
LayerMask LOSBlockingMask { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 接收 LOS 检测结果。
|
||||
/// <paramref name="hasLineOfSight"/>: true = 有视线,false = 被遮挡。
|
||||
/// </summary>
|
||||
void ReceiveLOSResult(bool hasLineOfSight);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace BaseGames.Enemies.Perception
|
||||
///
|
||||
/// 工作原理:
|
||||
/// <list type="bullet">
|
||||
/// <item>读取 <see cref="EnemyBase.HasLineOfSight"/>(原始 LOS,BatchLOSSystem 写入)。</item>
|
||||
/// <item>读取 <see cref="EnemyBase.HasLineOfSight"/>(由 PhysicsPerceptionSystem 的 LOS/Sight 槽位驱动)。</item>
|
||||
/// <item>连续感知到玩家超过 <see cref="reactionDelay"/> 秒后,<see cref="IsThreatDetected"/> 才变为 true。</item>
|
||||
/// <item>一旦丢失 LOS,<see cref="IsThreatDetected"/> 立即重置为 false(保持对"躲起来"的快速响应)。</item>
|
||||
/// </list>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Perception
|
||||
{
|
||||
/// <summary>
|
||||
/// 挂载在 PhysicsPerceptionSystem 子节点上的触发器代理组件。
|
||||
/// 与 <see cref="PhysicsPerceptionSystem"/> 的 <c>TriggerZone</c> 槽位配合使用:
|
||||
/// 当物理触发器产生 Enter / Exit 事件时,通知父系统的感知字典,
|
||||
/// 同时触发 <see cref="PhysicsPerceptionSystem.OnEnterDetection"/> /
|
||||
/// <see cref="PhysicsPerceptionSystem.OnExitDetection"/> 委托。
|
||||
///
|
||||
/// <b>使用步骤:</b>
|
||||
/// 1. 在 PhysicsPerceptionSystem 的子 GameObject 上添加本组件。
|
||||
/// 2. 确保同一 GameObject 或子节点上有 Collider2D,且 <c>isTrigger = true</c>。
|
||||
/// 3. 填写 <see cref="slotName"/>(与父系统中同名 TriggerZone 槽位对应)。
|
||||
/// 4. 设置 <see cref="detectLayer"/>(只有命中该层的碰撞体才会触发通知)。
|
||||
/// 5. <see cref="ParentSystem"/> 由父系统 Awake() 自动赋值,无需手动操作。
|
||||
/// </summary>
|
||||
[AddComponentMenu("BaseGames/Enemies/Perception Trigger Proxy")]
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public sealed class PerceptionTriggerProxy : MonoBehaviour
|
||||
{
|
||||
[Tooltip("对应 PhysicsPerceptionSystem 中 TriggerZone 槽位的 slotName")]
|
||||
public string slotName;
|
||||
|
||||
[Tooltip("目标检测层,只有命中此层的碰撞体才通知父系统")]
|
||||
public LayerMask detectLayer;
|
||||
|
||||
/// <summary>由父 PhysicsPerceptionSystem 在 Awake() 中自动注入,无需手动赋值。</summary>
|
||||
internal PhysicsPerceptionSystem ParentSystem { get; set; }
|
||||
|
||||
// ── Unity 生命周期 ────────────────────────────────────────────────────
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
var col = GetComponent<Collider2D>();
|
||||
if (col != null && !col.isTrigger)
|
||||
Debug.LogWarning(
|
||||
$"[PerceptionTriggerProxy] '{name}' 上的 Collider2D.isTrigger 未勾选," +
|
||||
"TriggerZone 槽位将无法工作。", this);
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (ParentSystem == null || string.IsNullOrEmpty(slotName)) return;
|
||||
if (detectLayer != 0 && ((1 << other.gameObject.layer) & (int)detectLayer) == 0) return;
|
||||
ParentSystem.OnTriggerZoneEnter(slotName, other.gameObject);
|
||||
}
|
||||
|
||||
private void OnTriggerExit2D(Collider2D other)
|
||||
{
|
||||
if (ParentSystem == null || string.IsNullOrEmpty(slotName)) return;
|
||||
if (detectLayer != 0 && ((1 << other.gameObject.layer) & (int)detectLayer) == 0) return;
|
||||
ParentSystem.OnTriggerZoneExit(slotName, other.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a482b11f99a870f4ea28cd36b716a69b
|
||||
guid: e8052de08fa173e479e190f652a1c04d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -5,15 +5,26 @@ using UnityEngine;
|
||||
namespace BaseGames.Enemies.Perception
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人感知系统(自研纯物理实现)。
|
||||
/// 每个 <see cref="PerceptionSlot"/> 独立配置并独立运行,支持四种检测模式:
|
||||
/// • RangeCircle — Physics2D.OverlapCircleNonAlloc(可选 LOS 视线遮挡校验)
|
||||
/// • BatchLOS — 委托 EnemyBase.IsPlayerVisible()(BatchLOSSystem 批量射线)
|
||||
/// • FanCast — 以朝向为轴的扇形多射线视野(支持遮挡层)
|
||||
/// • BoxCast — 矩形区域重叠检测(X 偏移随 localScale.x 自动翻转)
|
||||
/// 敌人感知系统(自研纯物理实现,商业级设计)。
|
||||
/// 每个 <see cref="PerceptionSlot"/> 独立配置并独立运行,支持七种检测模式:
|
||||
/// • <b>RangeCircle</b> — OverlapCircle 纯几何范围检测(无遮挡)
|
||||
/// • <b>BatchLOS</b> — OverlapCircle + 单次 Raycast 自研批量视线检测(低开销,有帧延迟)
|
||||
/// • <b>FanCast</b> — OverlapCircle + 角度过滤扇形视野(纯几何,无遮挡)
|
||||
/// • <b>BoxCast</b> — OverlapBox 矩形区域检测(纯几何,无遮挡)
|
||||
/// • <b>Sight</b> — OverlapCircle + 可选扇形角度 + 强制多点 LOS 遮挡(专用视线传感器)
|
||||
/// • <b>RayCast</b> — 单/多根方向射线传感器,支持扩散角和遮挡层
|
||||
/// • <b>TriggerZone</b> — 物理触发器事件驱动(子节点 PerceptionTriggerProxy,零轮询)
|
||||
///
|
||||
/// <b>遮挡检测设计原则:</b>
|
||||
/// RangeCircle / FanCast / BoxCast 是纯几何传感器,不做遮挡校验。
|
||||
/// 需要视线遮挡判断时,使用 <b>Sight</b> 槽位(内置多点 LOS 采样,始终执行遮挡检测)。
|
||||
/// 单向射线遮挡使用 <b>RayCast</b> 槽位(obstructLayer 控制阻断层)。
|
||||
///
|
||||
/// <b>性能优化:</b>
|
||||
/// • <see cref="PerceptionSlot.tickInterval"/> — 每 N 帧刷新一次(错帧执行,分散开销)
|
||||
/// • <see cref="PerceptionSlot.isDisabled"/> — 运行时动态禁用单个槽位
|
||||
/// • <see cref="SightBatchSystem"/> — 场景单例,全局控制 Sight 射线预算
|
||||
///
|
||||
/// EnemyBase.Awake() 通过 GetComponentInChildren<IPerceptionSystem>()
|
||||
/// 自动发现本组件,无需修改 EnemyBase。
|
||||
/// 槽位名称常量统一定义于 <see cref="SensorSlotNames"/>。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
@@ -23,14 +34,20 @@ namespace BaseGames.Enemies.Perception
|
||||
|
||||
public enum SlotType
|
||||
{
|
||||
/// <summary>Physics2D 圆形重叠检测</summary>
|
||||
/// <summary>Physics2D.OverlapCircle 纯几何范围检测(无遮挡)</summary>
|
||||
RangeCircle,
|
||||
/// <summary>委托 EnemyBase.IsPlayerVisible()(BatchLOSSystem 批量射线视线检测)</summary>
|
||||
/// <summary>OverlapCircle + 单条遮挡射线(自包含 LOS 检测,低开销)</summary>
|
||||
BatchLOS,
|
||||
/// <summary>以朝向为轴的扇形射线视野,遮挡层阻断视线</summary>
|
||||
/// <summary>OverlapCircle + 角度过滤扇形视野(纯几何,无遮挡)</summary>
|
||||
FanCast,
|
||||
/// <summary>矩形区域重叠检测,X 偏移随 localScale.x 自动翻转</summary>
|
||||
BoxCast
|
||||
/// <summary>OverlapBox 矩形区域检测,X 偏移随 localScale.x 自动翻转(纯几何,无遮挡)</summary>
|
||||
BoxCast,
|
||||
/// <summary>OverlapCircle + 可选扇形角度过滤 + 强制多点 LOS 遮挡校验(专用视线传感器)</summary>
|
||||
Sight,
|
||||
/// <summary>单/多根方向射线检测,支持扩散角和遮挡层(对标 RaySensor2D)</summary>
|
||||
RayCast,
|
||||
/// <summary>物理触发器事件驱动,依赖子节点 PerceptionTriggerProxy(零轮询开销)</summary>
|
||||
TriggerZone,
|
||||
}
|
||||
|
||||
// ── 槽位定义 ──────────────────────────────────────────────────────────
|
||||
@@ -38,56 +55,119 @@ namespace BaseGames.Enemies.Perception
|
||||
[Serializable]
|
||||
public struct PerceptionSlot
|
||||
{
|
||||
[Tooltip("槽位名称,与 SensorSlotNames 常量保持一致\n(aggro / los / attack_melee / attack_range)")]
|
||||
// ── 通用 ───────────────────────────────────────────────────────
|
||||
|
||||
[Tooltip("槽位名称,与 SensorSlotNames 常量保持一致\n(aggro / patrol / alert / los / attack_melee / attack_range / sight)")]
|
||||
public string slotName;
|
||||
|
||||
[Tooltip("RangeCircle:Physics2D 圆形范围检测\nBatchLOS:视线射线检测(BatchLOSSystem)\nFanCast:以朝向为轴的扇形射线视野\nBoxCast:矩形区域重叠检测")]
|
||||
[Tooltip("RangeCircle:纯几何圆形范围\nBatchLOS:OverlapCircle + 单射线遮挡(自包含 LOS)\nFanCast:扇形视野(纯几何)\nBoxCast:矩形区域(纯几何)\nSight:视线传感器(多射线遮挡)\nRayCast:方向射线传感器\nTriggerZone:触发器事件驱动(需子节点 PerceptionTriggerProxy)")]
|
||||
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,不影响实际射线。")]
|
||||
[Tooltip("检测原点相对于 transform.position 的偏移(米)。\nX 分量随 localScale.x 朝向自动翻转。")]
|
||||
public Vector2 offset;
|
||||
|
||||
[Header("FanCast")]
|
||||
[Tooltip("FanCast:扇形张角(度),以朝向为中轴左右均匀展开")]
|
||||
[Tooltip("Scene 视图 Gizmo 颜色。留空(全透明黑色)时自动使用紫色回退。")]
|
||||
public Color gizmoColor;
|
||||
|
||||
[Tooltip("勾选后禁用本槽位检测(可运行时动态切换)。\n注:tickInterval = 0 时本字段无效(历史兼容模式:始终启用)。")]
|
||||
public bool isDisabled;
|
||||
|
||||
[Min(0)]
|
||||
[Tooltip("每 N 个 FixedUpdate 帧刷新一次。\n0 = 历史兼容(每帧刷新,isDisabled 无效)\n1 = 每帧刷新\n3 = 每 3 帧刷新(Sight 推荐)\n多个相同间隔的槽位自动错帧执行。")]
|
||||
public int tickInterval;
|
||||
|
||||
// ── 范围(RangeCircle / FanCast / BatchLOS / Sight)───────────
|
||||
|
||||
[Min(0f)]
|
||||
[Tooltip("RangeCircle / FanCast / Sight:检测半径(米)\nBatchLOS:最大视线距离(0 = 不限)\nBoxCast / RayCast / TriggerZone:忽略")]
|
||||
public float radius;
|
||||
|
||||
// ── 检测层(RangeCircle / FanCast / BoxCast / Sight / RayCast)
|
||||
|
||||
[Tooltip("目标检测层(通常为 Player 层);BatchLOS / TriggerZone 忽略此值")]
|
||||
public LayerMask detectLayer;
|
||||
|
||||
// ── FanCast & Sight ────────────────────────────────────────────
|
||||
|
||||
[Tooltip("FanCast:扇形张角(度),以朝向为中轴左右均匀展开\nSight:视野锥角(度),设为 0 或 ≥ 360 表示全向 360°")]
|
||||
public float fanAngle;
|
||||
|
||||
[Tooltip("FanCast:扇形内均匀分布的射线数量(建议 5–11 条)")]
|
||||
[Min(2)]
|
||||
[Tooltip("FanCast 专用:Scene 视图 Gizmo 扇形分隔线数量(不影响检测,建议 3–9)")]
|
||||
public int fanRayCount;
|
||||
|
||||
[Header("BoxCast")]
|
||||
// ── BoxCast ────────────────────────────────────────────────────
|
||||
|
||||
[Tooltip("BoxCast:检测框尺寸 (宽, 高),单位米")]
|
||||
public Vector2 boxSize;
|
||||
|
||||
[Tooltip("BoxCast:相对于感知中心的偏移,X 分量随 localScale.x 自动翻转")]
|
||||
public Vector2 boxOffset;
|
||||
|
||||
[Header("Gizmos")]
|
||||
[Tooltip("Scene 视图 Gizmo 颜色。留空(全透明黑色)时自动使用紫色回退,以确保可见。")]
|
||||
public Color gizmoColor;
|
||||
// ── Sight LOS(仅 Sight 类型使用)────────────────────────────
|
||||
|
||||
[Tooltip("[Sight 专用] 视线遮挡检测层(通常为 Platform + Wall + Ground)。\n⚠ 留 0 时视线始终通过(losBlockMask = 0 = 无遮挡层)。")]
|
||||
public LayerMask losBlockMask;
|
||||
|
||||
[Range(1, 5)]
|
||||
[Tooltip("[Sight 专用] LOS 多点采样数量:\n1 = 包围盒中心(最快)\n3 = 中心 + 上 + 下(推荐)\n5 = 中心 + 上下左右(最稳健)")]
|
||||
public int losRayCount;
|
||||
|
||||
[Range(0f, 1f)]
|
||||
[Tooltip("[Sight 专用] 可见度阈值(0–1):\n0 = 任意 1 条通过即可(宽松)\n0.5 = 50% 通过\n1 = 全部通过(严格)")]
|
||||
public float losMinVisibility;
|
||||
|
||||
// ── RayCast ───────────────────────────────────────────────────
|
||||
|
||||
[Tooltip("[RayCast 专用] 射线方向(本地空间),X 分量随朝向自动翻转。\n向量长度不需要为 1,运行时自动归一化。\n示例:(1,0)=正前方 (0,-1)=正下方 (1,-1)=右前下45°")]
|
||||
public Vector2 rayDirection;
|
||||
|
||||
[Min(0f)]
|
||||
[Tooltip("[RayCast 专用] 射线长度(米)")]
|
||||
public float rayLength;
|
||||
|
||||
[Range(0f, 180f)]
|
||||
[Tooltip("[RayCast 专用] 多射线扩散角(度):\n0 = 单根射线\n> 0 时在此角度范围内均匀分布 rayCount 根射线")]
|
||||
public float raySpread;
|
||||
|
||||
[Range(1, 9)]
|
||||
[Tooltip("[RayCast 专用] 射线根数(raySpread > 0 时生效;= 1 时始终单根)")]
|
||||
public int rayCount;
|
||||
|
||||
[Tooltip("[RayCast 专用] 遮挡层(射线碰到此层后停止,不再检测后续目标);0 = 不检测遮挡,射线穿透所有物体")]
|
||||
public LayerMask obstructLayer;
|
||||
}
|
||||
|
||||
// ── 事件 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>某槽位首次检测到目标时触发。参数:(slotName, target)。</summary>
|
||||
public event Action<string, GameObject> OnEnterDetection;
|
||||
|
||||
/// <summary>某槽位失去对目标的检测时触发。参数:(slotName, target)。</summary>
|
||||
public event Action<string, GameObject> OnExitDetection;
|
||||
|
||||
// ── 字段 ──────────────────────────────────────────────────────────────
|
||||
|
||||
[SerializeField] private PerceptionSlot[] _slots;
|
||||
|
||||
// 当前帧检测结果
|
||||
private readonly Dictionary<string, List<GameObject>> _detected =
|
||||
new Dictionary<string, List<GameObject>>();
|
||||
|
||||
// 上帧检测结果(Enter/Exit diff 用,HashSet 提供 O(1) Contains)
|
||||
private readonly Dictionary<string, HashSet<GameObject>> _prevDetected =
|
||||
new Dictionary<string, HashSet<GameObject>>();
|
||||
|
||||
// 实例缓冲区(单线程 FixedUpdate 安全)
|
||||
private readonly Collider2D[] _overlapBuffer = new Collider2D[32];
|
||||
|
||||
// 静态共享缓冲区(零 GC,全局单线程安全)
|
||||
private static readonly Vector2[] _losPointsBuf = new Vector2[5];
|
||||
private static readonly RaycastHit2D[] _rayHitBuf = new RaycastHit2D[16];
|
||||
private static readonly List<GameObject> _enterBuf = new List<GameObject>(8);
|
||||
private static readonly List<GameObject> _exitBuf = new List<GameObject>(8);
|
||||
|
||||
private int _fixedTick;
|
||||
private bool _suspended;
|
||||
private EnemyBase _owner;
|
||||
|
||||
@@ -96,59 +176,163 @@ namespace BaseGames.Enemies.Perception
|
||||
private void Awake()
|
||||
{
|
||||
_owner = GetComponentInParent<EnemyBase>();
|
||||
if (_slots == null) return;
|
||||
foreach (var slot in _slots)
|
||||
|
||||
if (_slots != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(slot.slotName) && !_detected.ContainsKey(slot.slotName))
|
||||
_detected[slot.slotName] = new List<GameObject>(4);
|
||||
foreach (var slot in _slots)
|
||||
if (!string.IsNullOrEmpty(slot.slotName))
|
||||
EnsureSlotDict(slot.slotName);
|
||||
}
|
||||
|
||||
// TriggerZone:发现所有子节点代理并绑定
|
||||
var proxies = GetComponentsInChildren<PerceptionTriggerProxy>(true);
|
||||
foreach (var proxy in proxies)
|
||||
{
|
||||
if (string.IsNullOrEmpty(proxy.slotName)) continue;
|
||||
EnsureSlotDict(proxy.slotName);
|
||||
proxy.ParentSystem = this;
|
||||
}
|
||||
|
||||
// 注册到 SightBatchSystem(如存在)
|
||||
if (SightBatchSystem.Instance != null)
|
||||
SightBatchSystem.Instance.Register(this);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (SightBatchSystem.Instance != null)
|
||||
SightBatchSystem.Instance.Unregister(this);
|
||||
|
||||
var proxies = GetComponentsInChildren<PerceptionTriggerProxy>(true);
|
||||
foreach (var proxy in proxies)
|
||||
if (proxy.ParentSystem == this) proxy.ParentSystem = null;
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (_suspended || _slots == null) return;
|
||||
foreach (var slot in _slots)
|
||||
RefreshSlot(slot);
|
||||
_fixedTick++;
|
||||
|
||||
bool hasSightBatch = SightBatchSystem.Instance != null;
|
||||
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
{
|
||||
ref var slot = ref _slots[i];
|
||||
if (string.IsNullOrEmpty(slot.slotName)) continue;
|
||||
|
||||
// TriggerZone 完全事件驱动,不轮询
|
||||
if (slot.type == SlotType.TriggerZone) continue;
|
||||
|
||||
// Sight 槽由 SightBatchSystem 统一调度,此处跳过
|
||||
if (slot.type == SlotType.Sight && hasSightBatch) continue;
|
||||
|
||||
// tickInterval = 0:历史兼容模式(每帧刷新,isDisabled 无效)
|
||||
if (slot.tickInterval == 0)
|
||||
{
|
||||
RefreshSlot(ref slot, i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 新行为:isDisabled + 错帧节流
|
||||
if (slot.isDisabled) continue;
|
||||
if ((_fixedTick + i) % slot.tickInterval != 0) continue;
|
||||
|
||||
RefreshSlot(ref slot, i);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureSlotDict(string name)
|
||||
{
|
||||
if (!_detected.ContainsKey(name))
|
||||
{
|
||||
_detected[name] = new List<GameObject>(4);
|
||||
_prevDetected[name] = new HashSet<GameObject>();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 内部检测逻辑 ──────────────────────────────────────────────────────
|
||||
|
||||
private void RefreshSlot(PerceptionSlot slot)
|
||||
private void RefreshSlot(ref PerceptionSlot slot, int slotIndex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(slot.slotName)) return;
|
||||
if (!_detected.TryGetValue(slot.slotName, out var list)) return;
|
||||
|
||||
// Enter/Exit diff:保存上帧结果
|
||||
if ((OnEnterDetection != null || OnExitDetection != null)
|
||||
&& _prevDetected.TryGetValue(slot.slotName, out var prev))
|
||||
{
|
||||
prev.Clear();
|
||||
foreach (var go in list) prev.Add(go);
|
||||
}
|
||||
|
||||
list.Clear();
|
||||
|
||||
switch (slot.type)
|
||||
{
|
||||
case SlotType.BatchLOS:
|
||||
if (_owner != null && _owner.IsPlayerVisible() && _owner.PlayerTransform != null)
|
||||
case SlotType.BatchLOS: RefreshBatchLOS(ref slot, list); break;
|
||||
case SlotType.RangeCircle: RefreshRangeCircle(ref slot, list); break;
|
||||
case SlotType.FanCast: RefreshFanCast(ref slot, list); break;
|
||||
case SlotType.BoxCast: RefreshBoxCast(ref slot, list); break;
|
||||
case SlotType.Sight: RefreshSight(ref slot, list); break;
|
||||
case SlotType.RayCast: RefreshRayCast(ref slot, list); break;
|
||||
}
|
||||
|
||||
FireDiffEvents(slot.slotName);
|
||||
}
|
||||
|
||||
private void FireDiffEvents(string slotName)
|
||||
{
|
||||
if (slot.radius > 0f)
|
||||
if (OnEnterDetection == null && OnExitDetection == null) return;
|
||||
if (!_prevDetected.TryGetValue(slotName, out var prev)) return;
|
||||
if (!_detected.TryGetValue(slotName, out var curr)) return;
|
||||
|
||||
_enterBuf.Clear();
|
||||
_exitBuf.Clear();
|
||||
|
||||
foreach (var go in curr)
|
||||
if (!prev.Contains(go)) _enterBuf.Add(go);
|
||||
|
||||
foreach (var go in prev)
|
||||
{
|
||||
float dist = Vector2.Distance(
|
||||
(Vector2)transform.position, (Vector2)_owner.PlayerTransform.position);
|
||||
if (dist > slot.radius) break;
|
||||
bool stillDetected = false;
|
||||
for (int i = 0; i < curr.Count; i++)
|
||||
if (curr[i] == go) { stillDetected = true; break; }
|
||||
if (!stillDetected) _exitBuf.Add(go);
|
||||
}
|
||||
list.Add(_owner.PlayerTransform.gameObject);
|
||||
|
||||
foreach (var go in _enterBuf) OnEnterDetection?.Invoke(slotName, go);
|
||||
foreach (var go in _exitBuf) OnExitDetection?.Invoke(slotName, go);
|
||||
}
|
||||
break;
|
||||
|
||||
case SlotType.RangeCircle:
|
||||
RefreshRangeCircle(slot, list);
|
||||
break;
|
||||
private void RefreshBatchLOS(ref PerceptionSlot slot, List<GameObject> list)
|
||||
{
|
||||
if (slot.radius <= 0f || slot.detectLayer == 0) return;
|
||||
|
||||
case SlotType.FanCast:
|
||||
RefreshFanCast(slot, list);
|
||||
break;
|
||||
float facingSign = transform.localScale.x < 0f ? -1f : 1f;
|
||||
Vector2 origin = (Vector2)transform.position + new Vector2(slot.offset.x * facingSign, slot.offset.y);
|
||||
|
||||
case SlotType.BoxCast:
|
||||
RefreshBoxCast(slot, list);
|
||||
break;
|
||||
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.losBlockMask != 0)
|
||||
{
|
||||
Vector2 targetCenter = col.bounds.center;
|
||||
Vector2 dir = targetCenter - origin;
|
||||
float dist = dir.magnitude;
|
||||
if (dist > 0.001f)
|
||||
{
|
||||
var hit = Physics2D.Raycast(origin, dir / dist, dist, slot.losBlockMask);
|
||||
if (hit.collider != null) continue; // 被遮挡
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshRangeCircle(in PerceptionSlot slot, System.Collections.Generic.List<GameObject> list)
|
||||
list.Add(col.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshRangeCircle(ref PerceptionSlot slot, List<GameObject> list)
|
||||
{
|
||||
if (slot.radius <= 0f || slot.detectLayer == 0) return;
|
||||
float facingSign = transform.localScale.x < 0f ? -1f : 1f;
|
||||
@@ -157,40 +341,30 @@ namespace BaseGames.Enemies.Perception
|
||||
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);
|
||||
if (col != null) list.Add(col.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshFanCast(in PerceptionSlot slot, System.Collections.Generic.List<GameObject> list)
|
||||
private void RefreshFanCast(ref PerceptionSlot slot, 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);
|
||||
var forward = new Vector2(facingSign, 0f);
|
||||
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++)
|
||||
int count = Physics2D.OverlapCircleNonAlloc(origin, slot.radius, _overlapBuffer, slot.detectLayer);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
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);
|
||||
var col = _overlapBuffer[i];
|
||||
if (col == null) continue;
|
||||
if (Vector2.Angle(forward, (Vector2)col.bounds.center - origin) > halfAngle) continue;
|
||||
if (!list.Contains(col.gameObject)) list.Add(col.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshBoxCast(in PerceptionSlot slot, System.Collections.Generic.List<GameObject> list)
|
||||
private void RefreshBoxCast(ref PerceptionSlot slot, List<GameObject> list)
|
||||
{
|
||||
if (slot.boxSize.x <= 0f || slot.boxSize.y <= 0f || slot.detectLayer == 0) return;
|
||||
|
||||
@@ -200,35 +374,185 @@ namespace BaseGames.Enemies.Perception
|
||||
|
||||
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) list.Add(col.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 视线传感器:OverlapCircle + 可选扇形角度过滤 + 强制多点 LOS 遮挡校验。
|
||||
/// 唯一内置遮挡检测的轮询槽位类型。
|
||||
/// </summary>
|
||||
private void RefreshSight(ref PerceptionSlot slot, 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);
|
||||
Vector2 forward = new Vector2(facingSign, 0f);
|
||||
float halfAngle = slot.fanAngle * 0.5f;
|
||||
bool limitAngle = slot.fanAngle > 0f && slot.fanAngle < 360f;
|
||||
|
||||
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);
|
||||
if (limitAngle && Vector2.Angle(forward, (Vector2)col.bounds.center - origin) > halfAngle) continue;
|
||||
if (!HasLineOfSight(origin, col.gameObject,
|
||||
slot.losBlockMask, Mathf.Max(1, slot.losRayCount), slot.losMinVisibility)) continue;
|
||||
if (!list.Contains(col.gameObject)) list.Add(col.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasLineOfSight(Vector2 origin, GameObject target, LayerMask blockMask)
|
||||
/// <summary>
|
||||
/// 方向射线传感器:单/多根射线,支持扩散角和遮挡阻断。
|
||||
/// 射线方向在本地空间定义,X 分量随朝向自动翻转。
|
||||
/// </summary>
|
||||
private void RefreshRayCast(ref PerceptionSlot slot, List<GameObject> list)
|
||||
{
|
||||
// 优先使用碰撞体包围盒中心(比 transform.position 更准确,避免偏心轴误判)
|
||||
Vector2 targetPos;
|
||||
var col = target.GetComponent<Collider2D>();
|
||||
targetPos = col != null ? (Vector2)col.bounds.center : (Vector2)target.transform.position;
|
||||
if (slot.rayLength <= 0f || slot.detectLayer == 0) return;
|
||||
|
||||
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;
|
||||
}
|
||||
float facingSign = transform.localScale.x < 0f ? -1f : 1f;
|
||||
Vector2 origin = (Vector2)transform.position + new Vector2(slot.offset.x * facingSign, slot.offset.y);
|
||||
|
||||
private static Vector2 RotateVector(Vector2 v, float angleDeg)
|
||||
// 射线基准方向(本地空间,X 随朝向翻转,fallback = 正前方)
|
||||
Vector2 baseDir = new Vector2(slot.rayDirection.x * facingSign, slot.rayDirection.y);
|
||||
if (baseDir.sqrMagnitude < 0.0001f) baseDir = new Vector2(facingSign, 0f);
|
||||
baseDir.Normalize();
|
||||
|
||||
int rayCount = Mathf.Max(1, slot.rayCount);
|
||||
float spread = slot.raySpread;
|
||||
LayerMask combinedMask = slot.detectLayer | slot.obstructLayer;
|
||||
|
||||
for (int r = 0; r < rayCount; r++)
|
||||
{
|
||||
float rad = angleDeg * Mathf.Deg2Rad;
|
||||
Vector2 dir;
|
||||
if (rayCount == 1 || spread <= 0f)
|
||||
{
|
||||
dir = baseDir;
|
||||
}
|
||||
else
|
||||
{
|
||||
float t = (float)r / (rayCount - 1);
|
||||
float angDeg = Mathf.Lerp(-spread * 0.5f, spread * 0.5f, t);
|
||||
float rad = angDeg * 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);
|
||||
dir = new Vector2(cos * baseDir.x - sin * baseDir.y,
|
||||
sin * baseDir.x + cos * baseDir.y);
|
||||
}
|
||||
|
||||
int hits = Physics2D.RaycastNonAlloc(origin, dir, _rayHitBuf, slot.rayLength, combinedMask);
|
||||
for (int j = 0; j < hits; j++)
|
||||
{
|
||||
var hit = _rayHitBuf[j];
|
||||
if (hit.collider == null) continue;
|
||||
int layer = hit.collider.gameObject.layer;
|
||||
|
||||
// 遮挡层:阻断当前射线(不检测后续目标)
|
||||
if (slot.obstructLayer != 0 && ((1 << layer) & (int)slot.obstructLayer) != 0) break;
|
||||
|
||||
// 检测层:加入结果
|
||||
if (((1 << layer) & (int)slot.detectLayer) != 0)
|
||||
{
|
||||
var go = hit.collider.gameObject;
|
||||
if (!list.Contains(go)) list.Add(go);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── TriggerZone 回调(由 PerceptionTriggerProxy 调用)────────────────
|
||||
|
||||
internal void OnTriggerZoneEnter(string slotName, GameObject go)
|
||||
{
|
||||
if (!_detected.TryGetValue(slotName, out var list)) return;
|
||||
if (list.Contains(go)) return;
|
||||
list.Add(go);
|
||||
OnEnterDetection?.Invoke(slotName, go);
|
||||
}
|
||||
|
||||
internal void OnTriggerZoneExit(string slotName, GameObject go)
|
||||
{
|
||||
if (!_detected.TryGetValue(slotName, out var list)) return;
|
||||
if (!list.Remove(go)) return;
|
||||
OnExitDetection?.Invoke(slotName, go);
|
||||
}
|
||||
|
||||
// ── SightBatchSystem 调用入口(内部 API)─────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 由 <see cref="SightBatchSystem"/> 调用,执行本系统所有 Sight 槽位的刷新。
|
||||
/// 批量系统负责频率控制,不参与 tickInterval 逻辑。
|
||||
/// </summary>
|
||||
internal void ExecuteSightSlots()
|
||||
{
|
||||
if (_suspended || _slots == null) return;
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
{
|
||||
ref var slot = ref _slots[i];
|
||||
if (slot.type != SlotType.Sight) continue;
|
||||
if (slot.isDisabled && slot.tickInterval > 0) continue;
|
||||
if (string.IsNullOrEmpty(slot.slotName)) continue;
|
||||
RefreshSlot(ref slot, i);
|
||||
}
|
||||
}
|
||||
|
||||
// ── LOS 工具 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 多点采样 LOS 遮挡检测。
|
||||
/// <para><paramref name="blockMask"/> = 0 时始终返回 true(无遮挡层配置 = 视线始终通过)。</para>
|
||||
/// </summary>
|
||||
private static bool HasLineOfSight(Vector2 origin, GameObject target,
|
||||
LayerMask blockMask, int rayCount = 1, float minVisibility = 0f)
|
||||
{
|
||||
if (blockMask == 0) return true;
|
||||
|
||||
var col = target.GetComponent<Collider2D>();
|
||||
int n = FillLOSPoints(col, target.transform, Mathf.Clamp(rayCount, 1, 5));
|
||||
if (n == 0) return true;
|
||||
|
||||
int passed = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var dir = _losPointsBuf[i] - origin;
|
||||
float dist = dir.magnitude;
|
||||
if (dist <= 0f) { passed++; continue; }
|
||||
var hit = Physics2D.Raycast(origin, dir / dist, dist, blockMask);
|
||||
bool clear = hit.collider == null
|
||||
|| hit.collider.transform.IsChildOf(target.transform)
|
||||
|| hit.collider.gameObject == target;
|
||||
if (clear) passed++;
|
||||
}
|
||||
|
||||
float visibility = (float)passed / n;
|
||||
return minVisibility <= 0f ? passed > 0 : visibility >= minVisibility;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将目标包围盒的 LOS 采样点写入 <see cref="_losPointsBuf"/>(80% 缩进避免边缘误判)。
|
||||
/// </summary>
|
||||
private static int FillLOSPoints(Collider2D col, Transform fallback, int count)
|
||||
{
|
||||
if (col != null)
|
||||
{
|
||||
var b = col.bounds;
|
||||
var center = (Vector2)b.center;
|
||||
var ext = (Vector2)b.extents * 0.8f;
|
||||
|
||||
_losPointsBuf[0] = center;
|
||||
if (count >= 2) _losPointsBuf[1] = center + new Vector2(0f, ext.y);
|
||||
if (count >= 3) _losPointsBuf[2] = center + new Vector2(0f, -ext.y);
|
||||
if (count >= 4) _losPointsBuf[3] = center + new Vector2( ext.x, 0f);
|
||||
if (count >= 5) _losPointsBuf[4] = center + new Vector2(-ext.x, 0f);
|
||||
return count;
|
||||
}
|
||||
|
||||
_losPointsBuf[0] = (Vector2)fallback.position;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ── IPerceptionSystem ─────────────────────────────────────────────────
|
||||
@@ -236,7 +560,6 @@ namespace BaseGames.Enemies.Perception
|
||||
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)
|
||||
@@ -265,7 +588,7 @@ namespace BaseGames.Enemies.Perception
|
||||
{
|
||||
if (string.IsNullOrEmpty(slotName) || _slots == null) return -1f;
|
||||
foreach (var s in _slots)
|
||||
if (s.slotName == slotName && s.type == SlotType.RangeCircle)
|
||||
if (s.slotName == slotName && (s.type == SlotType.RangeCircle || s.type == SlotType.Sight))
|
||||
return s.radius;
|
||||
return -1f;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace BaseGames.Enemies.Perception
|
||||
|
||||
/// <summary>
|
||||
/// 视线检测(BatchLOS):敌我之间无遮挡时持续为 true。
|
||||
/// 由 BatchLOSSystem 批量计算,BD_IsPlayerVisible 读取结果。
|
||||
/// 由 PhysicsPerceptionSystem 自研批量计算(OverlapCircle + 单次 Raycast),BD_IsPlayerVisible 读取结果。
|
||||
/// </summary>
|
||||
public const string LOS = "los";
|
||||
|
||||
@@ -29,5 +29,23 @@ namespace BaseGames.Enemies.Perception
|
||||
/// 远程攻击范围(RangeCircle):玩家进入时触发远程攻击条件。
|
||||
/// </summary>
|
||||
public const string AttackRange = "attack_range";
|
||||
|
||||
/// <summary>
|
||||
/// 巡逻范围(RangeCircle):定义敌人允许巡逻的地图固定区域半径。
|
||||
/// 超出此范围时触发"返回巡逻点"逻辑。
|
||||
/// </summary>
|
||||
public const string Patrol = "patrol";
|
||||
|
||||
/// <summary>
|
||||
/// 警觉半径(RangeCircle):进入此圈时敌人从待机/巡逻切换到 Alert 状态,
|
||||
/// 通常比 Aggro 小,用于区分"察觉"和"追击"两个阶段。
|
||||
/// </summary>
|
||||
public const string Alert = "alert";
|
||||
|
||||
/// <summary>
|
||||
/// 视线感知(Sight):带强制 LOS 遮挡检测的视锥传感器。
|
||||
/// 仅当目标在视野锥内且无障碍物遮挡时才触发,是"看见玩家"的核心传感器。
|
||||
/// </summary>
|
||||
public const string Sight = "sight";
|
||||
}
|
||||
}
|
||||
|
||||
107
Assets/_Game/Scripts/Enemies/Perception/SightBatchSystem.cs
Normal file
107
Assets/_Game/Scripts/Enemies/Perception/SightBatchSystem.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Perception
|
||||
{
|
||||
/// <summary>
|
||||
/// 场景级 Sight 槽位批量调度器(Phase 3 性能优化)。
|
||||
///
|
||||
/// <b>设计意图:</b>
|
||||
/// Sight 槽位内置多点 LOS 射线检测,当场景中存在大量敌人时(10+ 个),
|
||||
/// 每帧同步刷新所有 Sight 槽位会产生明显的 CPU 峰值。
|
||||
/// 本组件将全局 Sight 更新量限制在 <see cref="maxSystemsPerFrame"/> 个/帧,
|
||||
/// 通过轮询方式公平分配每帧的 Sight 预算,把瞬时峰值摊平到多帧。
|
||||
///
|
||||
/// <b>使用方式:</b>
|
||||
/// 1. 在场景中添加一个空 GameObject,挂载本组件(建议放在 Managers 节点下)。
|
||||
/// 2. 本组件自动在 <c>DefaultExecutionOrder(-50)</c> 执行,比默认 PhysicsPerceptionSystem 早。
|
||||
/// 3. 当本组件存在时,PhysicsPerceptionSystem.FixedUpdate() 会跳过所有 Sight 槽位的更新,
|
||||
/// 由本组件统一调度(优雅降级:本组件不存在时 Sight 每帧正常更新)。
|
||||
///
|
||||
/// <b>性能参考(测试用):</b>
|
||||
/// • maxSystemsPerFrame = 4,60fps,20 个敌人 → 平均每帧 4 × losRayCount 次射线,
|
||||
/// 每个敌人 Sight 刷新周期约 5 帧(~83ms),适合慢速视线感应。
|
||||
/// • 增大此值可减少延迟,减小此值可进一步降低每帧开销。
|
||||
/// </summary>
|
||||
[AddComponentMenu("BaseGames/Enemies/Sight Batch System")]
|
||||
[DefaultExecutionOrder(-50)]
|
||||
public sealed class SightBatchSystem : MonoBehaviour
|
||||
{
|
||||
// ── 单例 ──────────────────────────────────────────────────────────────
|
||||
|
||||
public static SightBatchSystem Instance { get; private set; }
|
||||
|
||||
// ── 配置 ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Min(1)]
|
||||
[Tooltip("每帧最多更新多少个 PhysicsPerceptionSystem 的 Sight 槽位。\n" +
|
||||
"建议值:场景敌人数 / 5(使平均刷新延迟 ≈ 5 帧)。\n" +
|
||||
"值越大 = 延迟越低但 CPU 峰值越高;值越小反之。")]
|
||||
[SerializeField] private int maxSystemsPerFrame = 4;
|
||||
|
||||
// ── 内部状态 ──────────────────────────────────────────────────────────
|
||||
|
||||
private readonly List<PhysicsPerceptionSystem> _registrants =
|
||||
new List<PhysicsPerceptionSystem>(32);
|
||||
|
||||
private int _offset;
|
||||
|
||||
// ── 单例生命周期 ──────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogWarning("[SightBatchSystem] 场景中存在多个 SightBatchSystem,将销毁多余实例。", this);
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
// ── 注册 / 注销 ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>PhysicsPerceptionSystem.Awake() 自动调用。</summary>
|
||||
public void Register(PhysicsPerceptionSystem system)
|
||||
{
|
||||
if (system == null || _registrants.Contains(system)) return;
|
||||
_registrants.Add(system);
|
||||
}
|
||||
|
||||
/// <summary>PhysicsPerceptionSystem.OnDestroy() 自动调用。O(1) 交换删除。</summary>
|
||||
public void Unregister(PhysicsPerceptionSystem system)
|
||||
{
|
||||
int idx = _registrants.IndexOf(system);
|
||||
if (idx < 0) return;
|
||||
int last = _registrants.Count - 1;
|
||||
_registrants[idx] = _registrants[last];
|
||||
_registrants.RemoveAt(last);
|
||||
|
||||
// 防止 _offset 越界
|
||||
if (_offset >= _registrants.Count)
|
||||
_offset = 0;
|
||||
}
|
||||
|
||||
// ── 调度 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
int total = _registrants.Count;
|
||||
if (total == 0) return;
|
||||
|
||||
int budget = Mathf.Min(maxSystemsPerFrame, total);
|
||||
for (int i = 0; i < budget; i++)
|
||||
{
|
||||
int idx = (_offset + i) % total;
|
||||
_registrants[idx].ExecuteSightSlots();
|
||||
}
|
||||
|
||||
_offset = (_offset + budget) % total;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89145f6fbc97f53419fa3ce81fcb6342
|
||||
guid: c4da817926ac7c741a2c33ec552b249e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
Reference in New Issue
Block a user