refactor: rework environmental spawner, remove pooling, add trigger-based particle switching
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled

- Убран пулинг из `EnvironmentalCollectibleBase` и `EnvironmentalCollectibleSpawner`:
  при подборе и деспауне частицы теперь уничтожаются через `Destroy()`,
  что устраняет проблему переиспользования объектов старого типа при смене частиц.
- Удалены поля `usePooling`, `_ownerPool`, `_originPrefab` из `EnvironmentalCollectibleBase`;
  упрощены методы `Collect()` и `Despawn()`.
- Добавлено время жизни частиц: поле `defaultLifetime` в `EnvironmentalCollectibleBase`,
  таймер `_age` с автоматическим деспауном по истечении; виртуальный `OnDespawned()` для наследников.
- В `SpawnEntry` добавлены `overrideLifetime` и `lifetimeOverride` для переопределения времени жизни.
- Добавлена структура `NamedSpawnEntry` — именованные записи с ключом (`string key`),
  полным `SpawnEntry` и флагом `hardReplace`, настраиваемые в инспекторе.
- Спаунер получает игрока из `GameManager.Instance.PlayerObj` вместо `FindGameObjectWithTag`;
  подписка на `GameManager.PlayerSpawned` для автообновления ссылки.
- Добавлен постепенный спаун: `MaintainFixedCount()` запускает корутину `SpawnRoutine()`
  с рандомной задержкой `spawnDelay` между объектами.
- Добавлены диапазоны случайного масштаба (`scaleRange`) и угла поворота (`rotationRange`) при спауне.
- Добавлены публичные методы переключения типа частиц для `UnityEvent`:
  `SetForcedPrefab`, `SetForcedPrefabHard`, `SetForcedPrefabByObject`, `SetForcedPrefabByObjectHard`,
  `SetForcedPrefabByIndex`, `SetForcedPrefabHardByIndex`, `SetForcedEntry`, `SetForcedEntryByName`,
  `SetEntryByName` (мягкая постепенная замена через `GradualReplaceRoutine`),
  `ClearForcedPrefab`, `SetOverrideArea`.
- Исправлено сравнение prefab по ссылкам на сравнение по `name`, устраняя проблему
  когда один и тот же ассет из разных массивов (`spawnEntries` / `namedEntries`) не совпадал.
- Удалён `EnvironmentalZoneTrigger.cs` (триггер-зона не нужна как отдельный скрипт).
- Удалены `EnvironmentalPool` из использования в спаунере и базовом классе.
- Префабы `Coin.prefab` и `Coin_Robotail.prefab` перемещены в `Prefabs/Props/Collectibles/`;
  добавлены `LeafCollectible.prefab` и `BatCollectible.prefab`.
This commit is contained in:
Данил Ярош 2026-03-03 14:55:58 +03:00
parent de87b318b7
commit 4374c6f63f
14 changed files with 1007 additions and 156 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2a197c719b8349a4ab403591cf98c834
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,197 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &3316243588096306883
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3722284284489069898}
- component: {fileID: -349059794222274119}
- component: {fileID: 7133210491211008283}
m_Layer: 0
m_Name: BatCollectible
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3722284284489069898
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3316243588096306883}
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: 1
m_Children:
- {fileID: 932494216999854972}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!61 &-349059794222274119
BoxCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3316243588096306883}
m_Enabled: 1
serializedVersion: 3
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_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: -0.002134174}
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
m_Size: {x: 1, y: 0.7225633}
m_EdgeRadius: 0
--- !u!114 &7133210491211008283
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3316243588096306883}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0a07d694adfb80d4594401abc8aa8509, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.Entities.Props.BatCollectible
points: 1
playerTag: Player
usePooling: 1
defaultLifetime: 10
moveSpeed: 1.75
verticalAmplitude: 0.35
verticalFrequency: 2.4
horizontalWaveAmplitude: 0.2
horizontalWaveFrequency: 1.35
maxRange: 3.5
directionChangeIntervalMin: 1.5
directionChangeIntervalMax: 3.5
--- !u!1 &3870501198317419823
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 932494216999854972}
- component: {fileID: 4329754353811481997}
m_Layer: 0
m_Name: bat_0
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &932494216999854972
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3870501198317419823}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.05, y: 0.05, z: 0.05}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 3722284284489069898}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &4329754353811481997
SpriteRenderer:
serializedVersion: 2
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3870501198317419823}
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_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, 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_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_Sprite: {fileID: 21300000, guid: 580cb7a0781814a49a32652f9a97e49d, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
m_FlipY: 0
m_DrawMode: 0
m_Size: {x: 19.73, y: 14.64}
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_SpriteSortPoint: 0

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 76ff5cdfacce8e04ca29a050fd1b41db
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,202 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &7680590638374317877
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4550815583012328490}
- component: {fileID: 759722639321813396}
m_Layer: 0
m_Name: Square
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &4550815583012328490
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7680590638374317877}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.4, y: 0.4, z: 0.4}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 7358477386062440731}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &759722639321813396
SpriteRenderer:
serializedVersion: 2
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7680590638374317877}
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_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, 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_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_Sprite: {fileID: 5781445747943558089, guid: a191b676b9627ca44aceb3a028607100, type: 3}
m_Color: {r: 0.2140805, g: 0.5754717, b: 0.13843894, 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_SpriteSortPoint: 0
--- !u!1 &8928884138224752087
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7358477386062440731}
- component: {fileID: -205316951964799471}
- component: {fileID: -6130303908877127613}
m_Layer: 0
m_Name: LeafCollectible
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &7358477386062440731
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8928884138224752087}
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: 1
m_Children:
- {fileID: 4550815583012328490}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!61 &-205316951964799471
BoxCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8928884138224752087}
m_Enabled: 1
serializedVersion: 3
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_CompositeOperation: 0
m_CompositeOrder: 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
m_Size: {x: 1, y: 1}
m_EdgeRadius: 0
--- !u!114 &-6130303908877127613
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8928884138224752087}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d5b7e46ee9c8087468860b8f11049bdc, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.Entities.Props.LeafCollectible
points: 1
playerTag: Player
usePooling: 1
defaultLifetime: 10
fallSpeed: 1.25
horizontalAmplitude: 0.4
horizontalFrequency: 1.1
rotationAmplitude: 22
rotationSpeed: 2.2
stopAtGround: 0
groundMask:
serializedVersion: 2
m_Bits: 64
groundCheckDistance: 0.2
minY: -10
groundedSwayAmplitude: 0.08
groundedSwaySpeed: 1.5

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b1002877f2c88eb47911b3d3b30db95c
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -46,7 +46,7 @@ TextureImporter:
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@ -151,7 +151,7 @@ TextureImporter:
customData:
physicsShape: []
bones: []
spriteID:
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:

View File

@ -1581,6 +1581,19 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_Target: {fileID: 1476321865}
m_TargetAssemblyTypeName: RR.Game2D.Behaviour.EnvironmentalCollectibleSpawner,
Assembly-CSharp
m_MethodName: SetEntryByName
m_Mode: 5
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument: bat
m_BoolArgument: 0
m_CallState: 2
OnEventTriggerExit:
m_PersistentCalls:
m_Calls: []
@ -2839,7 +2852,7 @@ Transform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1329653979}
- {fileID: 1476321862}
m_Father: {fileID: 1053302775}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &95746896
@ -96243,71 +96256,6 @@ SpriteRenderer:
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_SpriteSortPoint: 0
--- !u!1001 &568219572
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 94085880}
m_Modifications:
- target: {fileID: 1754008902089564202, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_Name
value: Checkpoint_Particles
objectReference: {fileID: 0}
- target: {fileID: 1754008902089564202, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6931528425163354462, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_Enabled
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8213225719790051476, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8213225719790051476, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8213225719790051476, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8213225719790051476, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 8213225719790051476, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_LocalRotation.x
value: -0
objectReference: {fileID: 0}
- target: {fileID: 8213225719790051476, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_LocalRotation.y
value: -0
objectReference: {fileID: 0}
- target: {fileID: 8213225719790051476, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_LocalRotation.z
value: -0
objectReference: {fileID: 0}
- target: {fileID: 8213225719790051476, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8213225719790051476, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8213225719790051476, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
--- !u!1 &575351087
GameObject:
m_ObjectHideFlags: 0
@ -129463,6 +129411,19 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_Target: {fileID: 1476321865}
m_TargetAssemblyTypeName: RR.Game2D.Behaviour.EnvironmentalCollectibleSpawner,
Assembly-CSharp
m_MethodName: SetEntryByName
m_Mode: 5
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument: leaf
m_BoolArgument: 0
m_CallState: 2
OnEventTriggerExit:
m_PersistentCalls:
m_Calls: []
@ -198897,6 +198858,19 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_Target: {fileID: 1476321865}
m_TargetAssemblyTypeName: RR.Game2D.Behaviour.EnvironmentalCollectibleSpawner,
Assembly-CSharp
m_MethodName: SetEntryByName
m_Mode: 5
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument: leaf
m_BoolArgument: 0
m_CallState: 2
OnEventTriggerExit:
m_PersistentCalls:
m_Calls: []
@ -203128,11 +203102,6 @@ BoxCollider2D:
m_CorrespondingSourceObject: {fileID: -9173128868689176959, guid: e5783e81b8b0fd648974f0ec7ebde930, type: 3}
m_PrefabInstance: {fileID: 1731914843}
m_PrefabAsset: {fileID: 0}
--- !u!4 &1329653979 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 8213225719790051476, guid: c95e72e011a10ca43a3cb1bd524cdc19, type: 3}
m_PrefabInstance: {fileID: 568219572}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &1331373545
PrefabInstance:
m_ObjectHideFlags: 0
@ -208304,7 +208273,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!4 &1465160040
Transform:
m_ObjectHideFlags: 0
@ -208341,7 +208310,7 @@ VFXRenderer:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1465160039}
m_Enabled: 1
m_Enabled: 0
m_CastShadows: 0
m_ReceiveShadows: 0
m_DynamicOccludee: 1
@ -208388,7 +208357,7 @@ VisualEffect:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1465160039}
m_Enabled: 1
m_Asset: {fileID: 8926484042661614526, guid: 484c0e7574383dd4b9e863848199e369, type: 3}
m_Asset: {fileID: 8926484042661614526, guid: f81f06c7755accd42bf8e15cf6687eee, type: 3}
m_InitialEventName: OnPlay
m_InitialEventNameOverriden: 0
m_StartSeed: 0
@ -208618,6 +208587,165 @@ BoxCollider2D:
m_AutoTiling: 0
m_Size: {x: 3, y: 7}
m_EdgeRadius: 0
--- !u!1 &1476321861
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1476321862}
- component: {fileID: 1476321863}
- component: {fileID: 1476321866}
- component: {fileID: 1476321865}
- component: {fileID: 1476321864}
m_Layer: 0
m_Name: CollectibleSpawner
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1476321862
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1476321861}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 13, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 94085880}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!61 &1476321863
BoxCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1476321861}
m_Enabled: 1
serializedVersion: 3
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_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: -0.5, y: 0.5}
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
m_Size: {x: 32, y: 4}
m_EdgeRadius: 0
--- !u!114 &1476321864
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1476321861}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a6b98ac2c91fab545952a401bce4e156, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Game2D.Behaviour.EnvironmentalPool
--- !u!114 &1476321865
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1476321861}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c3bdb5d5e24e42b43aece26257a11ede, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Game2D.Behaviour.EnvironmentalCollectibleSpawner
fixedCount: 8
maxSpawnAttempts: 12
spawnDelay: {x: 0.15, y: 0.4}
spawnArea: {fileID: 1476321863}
areaCenter: {x: 0, y: 0}
areaSize: {x: 8, y: 4}
scaleRange: {x: 0.6, y: 1}
rotationRange: {x: -15, y: 15}
minDistanceToPlayer: 1.5
maxDistanceToPlayer: 25
spawnEntries:
- prefab: {fileID: -6130303908877127613, guid: b1002877f2c88eb47911b3d3b30db95c, type: 3}
weight: 100
overridePoints: 0
pointsOverride: 0
overrideLifetime: 0
lifetimeOverride: 0
limit: 0
namedEntries:
- key: leaf
entry:
prefab: {fileID: -6130303908877127613, guid: b1002877f2c88eb47911b3d3b30db95c, type: 3}
weight: 100
overridePoints: 0
pointsOverride: 0
overrideLifetime: 0
lifetimeOverride: 0
limit: 0
hardReplace: 0
- key: bat
entry:
prefab: {fileID: 7133210491211008283, guid: 76ff5cdfacce8e04ca29a050fd1b41db, type: 3}
weight: 100
overridePoints: 0
pointsOverride: 0
overrideLifetime: 0
lifetimeOverride: 0
limit: 0
hardReplace: 0
--- !u!114 &1476321866
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1476321861}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a0e43ee58f3b28e459ebefd25720dcfe, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Game2D.Core.Managers.Parallax
parallaxFactorX: 1
parallaxFactorY: 1
lockYAxis: 0
--- !u!1 &1476793369
GameObject:
m_ObjectHideFlags: 0
@ -210554,7 +210682,7 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1528420253}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
@ -212141,6 +212269,19 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_Target: {fileID: 1476321865}
m_TargetAssemblyTypeName: RR.Game2D.Behaviour.EnvironmentalCollectibleSpawner,
Assembly-CSharp
m_MethodName: SetEntryByName
m_Mode: 5
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument: leaf
m_BoolArgument: 0
m_CallState: 2
OnEventTriggerExit:
m_PersistentCalls:
m_Calls: []

View File

@ -1,6 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using RR.Common.Entities.Props;
using RR.Game2D.Core.Managers;
using UnityEngine;
namespace RR.Game2D.Behaviour
@ -14,50 +16,68 @@ namespace RR.Game2D.Behaviour
public int weight;
public bool overridePoints;
public int pointsOverride;
public bool overrideLifetime;
[Tooltip("Время жизни в секундах. 0 или меньше — бесконечно.")]
public float lifetimeOverride;
public int limit;
}
[Serializable]
public struct NamedSpawnEntry
{
[Tooltip("Имя, по которому триггер выбирает этот entry (например \"leaves\", \"bats\").")]
public string key;
public SpawnEntry entry;
[Tooltip("Мгновенно убрать все чужие объекты при активации.")]
public bool hardReplace;
}
[Header("Spawn Count")]
[SerializeField] private int fixedCount = 8;
[SerializeField] private int maxSpawnAttempts = 12;
[Tooltip("Задержка между спауном каждого объекта (секунды). 0 — все сразу.")]
[SerializeField] private Vector2 spawnDelay = new(0.15f, 0.4f);
[Header("Spawn Area")]
[SerializeField] private BoxCollider2D spawnArea;
[SerializeField] private Vector2 areaCenter;
[SerializeField] private Vector2 areaSize = new(8f, 4f);
[Header("Random Scale & Rotation")]
[Tooltip("Минимальный и максимальный uniform-масштаб при спауне.")]
[SerializeField] private Vector2 scaleRange = new(0.8f, 1.2f);
[Tooltip("Минимальный и максимальный угол поворота по Z (градусы) при спауне.")]
[SerializeField] private Vector2 rotationRange = new(0f, 0f);
[Header("Player")]
[SerializeField] private Transform player;
[SerializeField] private string playerTag = "Player";
[SerializeField] private float minDistanceToPlayer = 1.5f;
[SerializeField] private float maxDistanceToPlayer = 25f;
private Transform _player;
[Header("Prefabs")]
[SerializeField] private bool usePooling = true;
[SerializeField] private SpawnEntry[] spawnEntries;
[Header("Trigger Entries")]
[Tooltip("Именованные SpawnEntry для вызова из триггеров. Используйте SetForcedEntryByName(string) в UnityEvent.")]
[SerializeField] private NamedSpawnEntry[] namedEntries;
private readonly HashSet<EnvironmentalCollectibleBase> _activeCollectibles = new();
private readonly Dictionary<EnvironmentalCollectibleBase, GameObject> _prefabByCollectible = new();
private readonly Dictionary<GameObject, int> _activeCountByPrefab = new();
private readonly List<EnvironmentalCollectibleBase> _refreshBuffer = new();
private EnvironmentalPool _pool;
private GameObject _forcedPrefab;
private void Awake()
{
if (usePooling)
{
_pool = GetComponent<EnvironmentalPool>();
if (_pool == null)
_pool = gameObject.AddComponent<EnvironmentalPool>();
}
}
private SpawnEntry? _forcedEntry;
private BoxCollider2D _overrideArea;
private Coroutine _spawnCoroutine;
private Coroutine _gradualReplaceCoroutine;
private void Start()
{
ResolvePlayer();
WarmupPool();
if (GameManager.Instance != null)
GameManager.Instance.PlayerSpawned += OnPlayerSpawned;
MaintainFixedCount();
}
@ -68,40 +88,239 @@ namespace RR.Game2D.Behaviour
if (collectible != null)
collectible.Collected -= HandleCollected;
}
if (GameManager.Instance != null)
GameManager.Instance.PlayerSpawned -= OnPlayerSpawned;
}
public void SetSingleSpawnPrefab(EnvironmentalCollectibleBase prefab)
#region Public API — смена типа спауна
/// <summary>
/// Мягко переключает спаунер на prefab (компонент). Старые объекты доживают.
/// </summary>
public void SetForcedPrefab(EnvironmentalCollectibleBase prefab)
{
_forcedPrefab = prefab != null ? prefab.gameObject : null;
ApplyForced(prefab != null ? prefab.gameObject : null, null, false);
}
/// <summary>
/// Жёстко переключает на prefab (компонент). Все «чужие» объекты убираются сразу.
/// </summary>
public void SetForcedPrefabHard(EnvironmentalCollectibleBase prefab)
{
ApplyForced(prefab != null ? prefab.gameObject : null, null, true);
}
/// <summary>
/// Мягко переключает спаунер на prefab (GameObject).
/// Удобно вызывать из UnityEvent — просто перетащить prefab в инспекторе.
/// </summary>
public void SetForcedPrefabByObject(GameObject prefabObject)
{
ApplyForced(prefabObject, null, false);
}
/// <summary>
/// Жёстко переключает на prefab (GameObject). Все «чужие» объекты убираются сразу.
/// </summary>
public void SetForcedPrefabByObjectHard(GameObject prefabObject)
{
ApplyForced(prefabObject, null, true);
}
/// <summary>
/// Мягко переключает на prefab по индексу в массиве spawnEntries.
/// </summary>
public void SetForcedPrefabByIndex(int index)
{
if (!TryGetEntry(index, out SpawnEntry entry)) return;
ApplyForced(entry.prefab.gameObject, entry, false);
}
/// <summary>
/// Жёстко переключает на prefab по индексу — все «чужие» убираются сразу.
/// </summary>
public void SetForcedPrefabHardByIndex(int index)
{
if (!TryGetEntry(index, out SpawnEntry entry)) return;
ApplyForced(entry.prefab.gameObject, entry, true);
}
/// <summary>
/// Переключает спаунер на полностью кастомный SpawnEntry (из кода).
/// </summary>
public void SetForcedEntry(SpawnEntry entry, bool hardReplace = false)
{
if (entry.prefab == null) return;
ApplyForced(entry.prefab.gameObject, entry, hardReplace);
}
/// <summary>
/// Переключает спаунер по строковому ключу из массива namedEntries.
/// Вызывайте из UnityEvent (EventTrigger) — Unity покажет поле ввода строки.
/// </summary>
public void SetForcedEntryByName(string key)
{
if (namedEntries == null || string.IsNullOrEmpty(key)) return;
for (int i = 0; i < namedEntries.Length; i++)
{
if (string.Equals(namedEntries[i].key, key, StringComparison.OrdinalIgnoreCase))
{
NamedSpawnEntry named = namedEntries[i];
if (named.entry.prefab == null) return;
ApplyForced(named.entry.prefab.gameObject, named.entry, named.hardReplace);
return;
}
}
Debug.LogWarning($"[{nameof(EnvironmentalCollectibleSpawner)}] Named entry '{key}' not found.", this);
}
/// <summary>
/// Мягко переключает тип спауна по ключу из namedEntries.
/// Существующие объекты другого типа убираются постепенно (по одному с задержкой)
/// и заменяются новыми.
/// </summary>
public void SetEntryByName(string key)
{
if (namedEntries == null || string.IsNullOrEmpty(key)) return;
for (int i = 0; i < namedEntries.Length; i++)
{
if (string.Equals(namedEntries[i].key, key, StringComparison.OrdinalIgnoreCase))
{
NamedSpawnEntry named = namedEntries[i];
if (named.entry.prefab == null) return;
_forcedPrefab = named.entry.prefab.gameObject;
_forcedEntry = named.entry;
StartGradualReplace();
return;
}
}
Debug.LogWarning($"[{nameof(EnvironmentalCollectibleSpawner)}] Named entry '{key}' not found.", this);
}
/// <summary>
/// Возвращает спаунер в смешанный режим (все записи из spawnEntries).
/// </summary>
public void ClearForcedPrefab()
{
_forcedPrefab = null;
_forcedEntry = null;
MaintainFixedCount();
}
/// <summary>
/// Временно подменяет область спауна (например, из триггер-зоны).
/// Передайте null чтобы вернуть оригинальную область.
/// </summary>
public void SetOverrideArea(BoxCollider2D area)
{
_overrideArea = area;
}
#endregion
private void ApplyForced(GameObject prefabObj, SpawnEntry? entry, bool hardReplace)
{
_forcedPrefab = prefabObj;
_forcedEntry = entry;
RefreshActiveCollectibles();
MaintainFixedCount();
}
public void SetSingleSpawnPrefabByIndex(int index)
private bool TryGetEntry(int index, out SpawnEntry entry)
{
entry = default;
if (spawnEntries == null || index < 0 || index >= spawnEntries.Length)
return;
return false;
SpawnEntry entry = spawnEntries[index];
SetSingleSpawnPrefab(entry.prefab);
}
public void ClearSingleSpawnPrefab()
{
_forcedPrefab = null;
MaintainFixedCount();
entry = spawnEntries[index];
return entry.prefab != null;
}
private void MaintainFixedCount()
{
if (_spawnCoroutine != null)
StopCoroutine(_spawnCoroutine);
if (!gameObject.activeInHierarchy) return;
_spawnCoroutine = StartCoroutine(SpawnRoutine());
}
private IEnumerator SpawnRoutine()
{
int safety = fixedCount * Mathf.Max(1, maxSpawnAttempts);
bool isFirst = true;
while (_activeCollectibles.Count < fixedCount && safety > 0)
{
// Задержка перед каждым спауном (кроме первого — чтобы был хоть один сразу)
if (!isFirst && spawnDelay.y > 0f)
yield return new WaitForSeconds(UnityEngine.Random.Range(spawnDelay.x, spawnDelay.y));
isFirst = false;
if (!TrySpawnOne())
break;
safety--;
}
_spawnCoroutine = null;
}
private void StartGradualReplace()
{
if (_gradualReplaceCoroutine != null)
StopCoroutine(_gradualReplaceCoroutine);
if (!gameObject.activeInHierarchy) return;
_gradualReplaceCoroutine = StartCoroutine(GradualReplaceRoutine());
}
private IEnumerator GradualReplaceRoutine()
{
if (_forcedPrefab == null)
{
_gradualReplaceCoroutine = null;
yield break;
}
string forcedName = _forcedPrefab.name;
while (true)
{
EnvironmentalCollectibleBase toRemove = null;
foreach (EnvironmentalCollectibleBase c in _activeCollectibles)
{
if (c == null) continue;
if (_prefabByCollectible.TryGetValue(c, out GameObject p) && p.name != forcedName)
{
toRemove = c;
break;
}
}
if (toRemove == null) break;
DespawnCollectible(toRemove);
MaintainFixedCount();
if (spawnDelay.y > 0f)
yield return new WaitForSeconds(UnityEngine.Random.Range(spawnDelay.x, spawnDelay.y));
else
yield return null;
}
_gradualReplaceCoroutine = null;
}
private bool TrySpawnOne()
@ -115,25 +334,31 @@ namespace RR.Game2D.Behaviour
return false;
GameObject prefab = entry.prefab.gameObject;
GameObject instance = usePooling && _pool != null
? _pool.Get(prefab, spawnPosition, Quaternion.identity)
: Instantiate(prefab, spawnPosition, Quaternion.identity, transform);
GameObject instance = Instantiate(prefab, spawnPosition, Quaternion.identity, transform);
if (instance == null) return false;
// Случайный масштаб и поворот
float scale = UnityEngine.Random.Range(scaleRange.x, scaleRange.y);
instance.transform.localScale = Vector3.one * scale;
float angle = UnityEngine.Random.Range(rotationRange.x, rotationRange.y);
instance.transform.rotation = Quaternion.Euler(0f, 0f, angle);
EnvironmentalCollectibleBase collectible = instance.GetComponent<EnvironmentalCollectibleBase>();
if (collectible == null)
{
Debug.LogError($"[{nameof(EnvironmentalCollectibleSpawner)}] Prefab {prefab.name} is missing EnvironmentalCollectibleBase.", prefab);
if (!usePooling)
Destroy(instance);
Destroy(instance);
return false;
}
collectible.transform.SetParent(transform, true);
collectible.Collected -= HandleCollected;
collectible.Collected += HandleCollected;
collectible.InitializeForSpawn(_pool, prefab, spawnPosition, entry.overridePoints ? entry.pointsOverride : null);
collectible.InitializeForSpawn(
spawnPosition,
entry.overridePoints ? entry.pointsOverride : null,
entry.overrideLifetime ? entry.lifetimeOverride : null);
_activeCollectibles.Add(collectible);
_prefabByCollectible[collectible] = prefab;
@ -166,10 +391,35 @@ namespace RR.Game2D.Behaviour
private bool TryPickSpawnEntry(out SpawnEntry entry)
{
// Если задан полный SpawnEntry — используем его напрямую
if (_forcedEntry.HasValue)
{
SpawnEntry forced = _forcedEntry.Value;
if (forced.prefab == null)
{
entry = default;
return false;
}
// Проверяем лимит
if (forced.limit > 0 &&
_activeCountByPrefab.TryGetValue(forced.prefab.gameObject, out int fc) &&
fc >= forced.limit)
{
entry = default;
return false;
}
entry = forced;
return true;
}
entry = default;
if (spawnEntries == null || spawnEntries.Length == 0)
return false;
string forcedName = _forcedPrefab != null ? _forcedPrefab.name : null;
int totalWeight = 0;
for (int i = 0; i < spawnEntries.Length; i++)
{
@ -177,7 +427,7 @@ namespace RR.Game2D.Behaviour
if (current.prefab == null || current.weight <= 0) continue;
GameObject prefab = current.prefab.gameObject;
if (_forcedPrefab != null && prefab != _forcedPrefab) continue;
if (forcedName != null && prefab.name != forcedName) continue;
if (current.limit > 0 &&
_activeCountByPrefab.TryGetValue(prefab, out int activeCount) &&
@ -189,6 +439,17 @@ namespace RR.Game2D.Behaviour
totalWeight += current.weight;
}
// Фильтрация по _forcedPrefab — если prefab не найден в spawnEntries, создаём временный entry
if (totalWeight <= 0 && _forcedPrefab != null)
{
EnvironmentalCollectibleBase comp = _forcedPrefab.GetComponent<EnvironmentalCollectibleBase>();
if (comp != null)
{
entry = new SpawnEntry { prefab = comp, weight = 1 };
return true;
}
}
if (totalWeight <= 0)
return false;
@ -199,7 +460,7 @@ namespace RR.Game2D.Behaviour
if (current.prefab == null || current.weight <= 0) continue;
GameObject prefab = current.prefab.gameObject;
if (_forcedPrefab != null && prefab != _forcedPrefab) continue;
if (forcedName != null && prefab.name != forcedName) continue;
if (current.limit > 0 &&
_activeCountByPrefab.TryGetValue(prefab, out int activeCount) &&
@ -229,10 +490,10 @@ namespace RR.Game2D.Behaviour
float y = UnityEngine.Random.Range(bounds.min.y, bounds.max.y);
spawnPosition = new Vector3(x, y, 0f);
if (player == null)
if (_player == null)
return true;
float distance = Vector2.Distance(player.position, spawnPosition);
float distance = Vector2.Distance(_player.position, spawnPosition);
if (distance < minDistanceToPlayer) continue;
if (maxDistanceToPlayer > 0f && distance > maxDistanceToPlayer) continue;
@ -245,6 +506,9 @@ namespace RR.Game2D.Behaviour
private Bounds GetSpawnBounds()
{
if (_overrideArea != null)
return _overrideArea.bounds;
if (spawnArea != null)
return spawnArea.bounds;
@ -255,36 +519,37 @@ namespace RR.Game2D.Behaviour
private void ResolvePlayer()
{
if (player != null) return;
GameObject playerObject = GameObject.FindGameObjectWithTag(playerTag);
if (playerObject != null)
player = playerObject.transform;
}
private void WarmupPool()
{
if (!usePooling || _pool == null || spawnEntries == null) return;
for (int i = 0; i < spawnEntries.Length; i++)
if (GameManager.Instance?.PlayerObj != null)
{
SpawnEntry entry = spawnEntries[i];
if (entry.prefab == null) continue;
_pool.Warmup(entry.prefab.gameObject, Mathf.Max(1, fixedCount));
_player = GameManager.Instance.PlayerObj.transform;
return;
}
// Fallback на случай, если GameManager ещё не проинициализирован
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
if (playerObject != null)
_player = playerObject.transform;
}
private void OnPlayerSpawned(GameObject playerObj)
{
_player = playerObj != null ? playerObj.transform : null;
}
private void RefreshActiveCollectibles()
{
if (_forcedPrefab == null)
return;
string forcedName = _forcedPrefab.name;
_refreshBuffer.Clear();
foreach (EnvironmentalCollectibleBase collectible in _activeCollectibles)
{
if (collectible == null) continue;
if (_prefabByCollectible.TryGetValue(collectible, out GameObject prefab) && prefab != _forcedPrefab)
if (_prefabByCollectible.TryGetValue(collectible, out GameObject prefab) && prefab.name != forcedName)
_refreshBuffer.Add(collectible);
}
@ -306,15 +571,12 @@ namespace RR.Game2D.Behaviour
{
if (_activeCountByPrefab.TryGetValue(prefab, out int currentCount))
_activeCountByPrefab[prefab] = Mathf.Max(0, currentCount - 1);
if (usePooling && _pool != null)
_pool.Return(prefab, collectible.gameObject);
else
Destroy(collectible.gameObject);
}
_activeCollectibles.Remove(collectible);
_prefabByCollectible.Remove(collectible);
Destroy(collectible.gameObject);
}
private void OnDrawGizmosSelected()

View File

@ -9,37 +9,52 @@ namespace RR.Common.Entities.Props
{
[SerializeField] private int points = 1;
[SerializeField] private string playerTag = "Player";
[SerializeField] private bool usePooling = true;
[Tooltip("Время жизни в секундах. 0 или меньше — бесконечно.")]
[SerializeField] private float defaultLifetime;
private bool _collected;
private int _defaultPoints;
private float _defaultLifetimeBackup;
private float _lifetime;
private float _age;
private Collider2D _trigger;
private EnvironmentalPool _ownerPool;
private GameObject _originPrefab;
private Vector3 _spawnPosition;
public event Action<EnvironmentalCollectibleBase> Collected;
public int Points => points;
public Vector3 SpawnPosition => _spawnPosition;
public bool UsesPooling => usePooling;
protected virtual void Awake()
{
_defaultPoints = Mathf.Max(0, points);
_defaultLifetimeBackup = defaultLifetime;
_trigger = GetComponent<Collider2D>();
ValidateTrigger();
}
protected virtual void Update()
{
TickBehaviour(Time.deltaTime);
float dt = Time.deltaTime;
if (_lifetime > 0f && !_collected)
{
_age += dt;
if (_age >= _lifetime)
{
Despawn();
return;
}
}
TickBehaviour(dt);
}
protected virtual void OnValidate()
{
if (points < 0) points = 0;
_defaultPoints = points;
_defaultLifetimeBackup = defaultLifetime;
ValidateTrigger();
}
@ -53,15 +68,14 @@ namespace RR.Common.Entities.Props
}
public void InitializeForSpawn(
EnvironmentalPool ownerPool,
GameObject originPrefab,
Vector3 spawnPosition,
int? pointsOverride)
int? pointsOverride,
float? lifetimeOverride = null)
{
_ownerPool = ownerPool;
_originPrefab = originPrefab;
_spawnPosition = spawnPosition;
points = pointsOverride.HasValue ? Mathf.Max(0, pointsOverride.Value) : _defaultPoints;
_lifetime = lifetimeOverride.HasValue ? lifetimeOverride.Value : _defaultLifetimeBackup;
_age = 0f;
ResetStateForReuse();
}
@ -69,6 +83,7 @@ namespace RR.Common.Entities.Props
public void ResetStateForReuse()
{
_collected = false;
_age = 0f;
_spawnPosition = transform.position;
OnSpawned();
}
@ -84,13 +99,21 @@ namespace RR.Common.Entities.Props
ScoreSystem.Instance?.RegisterEnvironmentalPoints(points);
Collected?.Invoke(this);
Destroy(gameObject);
}
if (usePooling && _ownerPool != null && _originPrefab != null)
{
_ownerPool.Return(_originPrefab, gameObject);
return;
}
/// <summary>
/// Деспаун по истечении времени жизни — без начисления очков.
/// </summary>
private void Despawn()
{
if (_collected) return;
_collected = true;
OnDespawned();
Collected?.Invoke(this);
Destroy(gameObject);
}
@ -102,6 +125,10 @@ namespace RR.Common.Entities.Props
{
}
protected virtual void OnDespawned()
{
}
protected abstract void TickBehaviour(float dt);
private void ValidateTrigger()