using System; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace RR.Game2D.Behaviour { [DisallowMultipleComponent] public sealed class EntitySpawner : MonoBehaviour { [Serializable] public struct SpawnPrefabEntry { public GameObject prefab; [Min(1)] public int weight; } [Header("Prefabs")] [SerializeField] private SpawnPrefabEntry[] prefabs; [Header("Spawn Timing")] [SerializeField] private bool spawnOnEnable = true; [SerializeField] private bool loop = true; [Min(0f)] [SerializeField] private float initialDelay; [Tooltip("Минимальный и максимальный интервал между спавнами.")] [SerializeField] private Vector2 spawnInterval = new(1f, 2f); [Header("Spawn Limits")] [Tooltip("Максимум одновременно существующих объектов. 0 или меньше — без лимита.")] [SerializeField] private int maxAlive = 5; [Tooltip("Максимум объектов за всю сессию работы спавнера. 0 или меньше — без лимита.")] [SerializeField] private int maxTotalSpawned; [Header("Spawn Points")] [Tooltip("Если массив пуст, используется позиция самого спавнера.")] [SerializeField] private Transform[] spawnPoints; [SerializeField] private bool randomizeSpawnPoint = true; [Header("Transform")] [SerializeField] private Transform spawnedParent; [Tooltip("Случайный угол по оси Z для 2D-объектов.")] [SerializeField] private Vector2 randomZRotation; private readonly List _spawnedObjects = new(); private Coroutine _spawnCoroutine; private int _totalSpawned; public int AliveCount { get { CleanupSpawnedObjects(); return _spawnedObjects.Count; } } public int TotalSpawned => _totalSpawned; public int MaxAliveLimit => maxAlive; private void OnEnable() { if (spawnOnEnable) StartSpawning(); } private void OnDisable() { StopSpawning(); } public void StartSpawning() { if (!isActiveAndEnabled) return; if (_spawnCoroutine != null) StopCoroutine(_spawnCoroutine); _spawnCoroutine = StartCoroutine(SpawnRoutine()); } public void StopSpawning() { if (_spawnCoroutine == null) return; StopCoroutine(_spawnCoroutine); _spawnCoroutine = null; } public GameObject SpawnNow() { return SpawnNowInternal(null); } private void PopulateUsedSpawnPoints(HashSet usedIndices) { if (spawnPoints == null || spawnPoints.Length == 0 || usedIndices == null) return; CleanupSpawnedObjects(); for (int i = 0; i < _spawnedObjects.Count; i++) { GameObject obj = _spawnedObjects[i]; if (obj == null) continue; for (int j = 0; j < spawnPoints.Length; j++) { if (spawnPoints[j] == null) continue; if (Vector3.Distance(obj.transform.position, spawnPoints[j].position) < 0.1f) { usedIndices.Add(j); break; } } } } public void SpawnCount(int count) { if (count <= 0) return; HashSet usedSpawnPointIndices = new(); PopulateUsedSpawnPoints(usedSpawnPointIndices); for (int i = 0; i < count; i++) { if (SpawnNowInternal(usedSpawnPointIndices) == null) break; } } public int SpawnToAliveCount(int targetAliveCount) { CleanupSpawnedObjects(); int target = Mathf.Max(0, targetAliveCount); int currentAlive = _spawnedObjects.Count; int toSpawn = target - currentAlive; int spawnedCount = 0; HashSet usedSpawnPointIndices = new(); PopulateUsedSpawnPoints(usedSpawnPointIndices); if (toSpawn <= 0) return 0; for (int i = 0; i < toSpawn; i++) { if (SpawnNowInternal(usedSpawnPointIndices) == null) break; spawnedCount++; } return spawnedCount; } public int SpawnPercent(float percent) { if (maxAlive <= 0) { Debug.LogWarning( $"[{nameof(EntitySpawner)}] Cannot spawn by percent on '{name}' because Max Alive is not configured.", this); return 0; } float normalizedPercent = NormalizePercent(percent); int targetAliveCount = Mathf.CeilToInt(maxAlive * normalizedPercent); return SpawnToAliveCount(targetAliveCount); } public void ClearSpawned() { StopSpawning(); for (int i = 0; i < _spawnedObjects.Count; i++) { GameObject spawnedObject = _spawnedObjects[i]; if (spawnedObject != null) Destroy(spawnedObject); } _spawnedObjects.Clear(); } public void ResetSpawner() { _totalSpawned = 0; CleanupSpawnedObjects(); } private IEnumerator SpawnRoutine() { if (initialDelay > 0f) yield return new WaitForSeconds(initialDelay); do { SpawnNow(); if (!loop) break; float delay = UnityEngine.Random.Range( Mathf.Min(spawnInterval.x, spawnInterval.y), Mathf.Max(spawnInterval.x, spawnInterval.y)); if (delay > 0f) yield return new WaitForSeconds(delay); else yield return null; } while (enabled && gameObject.activeInHierarchy); _spawnCoroutine = null; } private GameObject SpawnNowInternal(HashSet usedSpawnPointIndices) { CleanupSpawnedObjects(); if (!CanSpawn()) return null; GameObject prefab = PickPrefab(); if (prefab == null) return null; if (!TryPickSpawnPoint(usedSpawnPointIndices, out Transform spawnPoint)) return null; Vector3 spawnPosition = spawnPoint != null ? spawnPoint.position : transform.position; Quaternion spawnRotation = spawnPoint != null ? spawnPoint.rotation : transform.rotation; float zAngle = UnityEngine.Random.Range(randomZRotation.x, randomZRotation.y); spawnRotation *= Quaternion.Euler(0f, 0f, zAngle); GameObject instance = Instantiate(prefab, spawnPosition, spawnRotation, spawnedParent); _spawnedObjects.Add(instance); _totalSpawned++; return instance; } private bool CanSpawn() { if (prefabs == null || prefabs.Length == 0) return false; if (maxAlive > 0 && _spawnedObjects.Count >= maxAlive) return false; if (maxTotalSpawned > 0 && _totalSpawned >= maxTotalSpawned) return false; return true; } private GameObject PickPrefab() { int totalWeight = 0; for (int i = 0; i < prefabs.Length; i++) { if (prefabs[i].prefab == null) continue; totalWeight += Mathf.Max(1, prefabs[i].weight); } if (totalWeight <= 0) return null; int randomValue = UnityEngine.Random.Range(0, totalWeight); int currentWeight = 0; for (int i = 0; i < prefabs.Length; i++) { if (prefabs[i].prefab == null) continue; currentWeight += Mathf.Max(1, prefabs[i].weight); if (randomValue < currentWeight) return prefabs[i].prefab; } return prefabs[prefabs.Length - 1].prefab; } private bool TryPickSpawnPoint(HashSet usedSpawnPointIndices, out Transform spawnPoint) { if (spawnPoints == null || spawnPoints.Length == 0) { if (usedSpawnPointIndices != null && !usedSpawnPointIndices.Add(-1)) { spawnPoint = null; return false; } spawnPoint = transform; return true; } if (randomizeSpawnPoint) { List availableIndices = new(); for (int i = 0; i < spawnPoints.Length; i++) { if (spawnPoints[i] == null) continue; if (usedSpawnPointIndices != null && usedSpawnPointIndices.Contains(i)) continue; availableIndices.Add(i); } if (availableIndices.Count == 0) { spawnPoint = null; return false; } int randomListIndex = UnityEngine.Random.Range(0, availableIndices.Count); int chosenIndex = availableIndices[randomListIndex]; usedSpawnPointIndices?.Add(chosenIndex); spawnPoint = spawnPoints[chosenIndex]; return true; } for (int i = 0; i < spawnPoints.Length; i++) { if (spawnPoints[i] == null) continue; if (usedSpawnPointIndices != null && usedSpawnPointIndices.Contains(i)) continue; usedSpawnPointIndices?.Add(i); spawnPoint = spawnPoints[i]; return true; } spawnPoint = null; return false; } private void CleanupSpawnedObjects() { for (int i = _spawnedObjects.Count - 1; i >= 0; i--) { if (_spawnedObjects[i] == null) _spawnedObjects.RemoveAt(i); } } private static float NormalizePercent(float percent) { if (percent > 1f) percent *= 0.01f; return Mathf.Clamp01(percent); } private void OnDrawGizmosSelected() { Gizmos.color = new Color(0.25f, 0.9f, 0.4f, 0.9f); if (spawnPoints == null || spawnPoints.Length == 0) { Gizmos.DrawWireSphere(transform.position, 0.2f); return; } for (int i = 0; i < spawnPoints.Length; i++) { Transform spawnPoint = spawnPoints[i]; if (spawnPoint == null) continue; Gizmos.DrawWireSphere(spawnPoint.position, 0.2f); } } } }