Danro/Assets/Scripts/Behaviour/EntitySpawner.cs
Danil Iarosh ad744301dd Fix spawner zone override targeting and BoxCollider2D bounds
- Добавлено свойство spawnerNameFilter (по умолчанию 'Enemy') в EntitySpawnerZone, чтобы зоны переопределения применялись только к спавнерам врагов и не заменяли префабы в CoinSpawner (спавнерах кристаллов) или других спавнерах предметов.
- Реализована точная математическая проверка IsPointInside для BoxCollider2D, что делает ее независимой от Z-координаты и тиков физического движка Unity.
- Исправлено поведение в EntitySpawner: теперь при поиске зоны проверяется zone.MatchesSpawner.
2026-08-13 14:12:19 +03:00

406 lines
12 KiB
C#

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<GameObject> _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<int> 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<int> 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<int> 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<int> usedSpawnPointIndices)
{
CleanupSpawnedObjects();
if (!CanSpawn())
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;
GameObject prefab = PickPrefab(spawnPosition);
if (prefab == null)
return null;
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(Vector3 spawnPosition)
{
// Проверка, находится ли эта точка спавна внутри какой-либо активной зоны переопределения
SpawnPrefabEntry[] activePrefabs = prefabs;
for (int i = 0; i < EntitySpawnerZone.ActiveZones.Count; i++)
{
EntitySpawnerZone zone = EntitySpawnerZone.ActiveZones[i];
if (zone != null && zone.MatchesSpawner(this) && zone.IsPointInside(spawnPosition))
{
activePrefabs = zone.Prefabs;
break;
}
}
if (activePrefabs == null || activePrefabs.Length == 0)
return null;
int totalWeight = 0;
for (int i = 0; i < activePrefabs.Length; i++)
{
if (activePrefabs[i].prefab == null)
continue;
totalWeight += Mathf.Max(1, activePrefabs[i].weight);
}
if (totalWeight <= 0)
return null;
int randomValue = UnityEngine.Random.Range(0, totalWeight);
int currentWeight = 0;
for (int i = 0; i < activePrefabs.Length; i++)
{
if (activePrefabs[i].prefab == null)
continue;
currentWeight += Mathf.Max(1, activePrefabs[i].weight);
if (randomValue < currentWeight)
return activePrefabs[i].prefab;
}
return activePrefabs[activePrefabs.Length - 1].prefab;
}
private bool TryPickSpawnPoint(HashSet<int> 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<int> 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);
}
}
}
}