Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- В метод PickPrefab класса EntitySpawner добавлено логирование Debug.Log при успешном переопределении префаба зоной. - Это позволяет легко проверять, какие спавнеры и на каких точках подверглись влиянию зоны переопределения.
421 lines
13 KiB
C#
421 lines
13 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;
|
|
EntitySpawnerZone matchedZone = null;
|
|
|
|
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;
|
|
matchedZone = zone;
|
|
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;
|
|
GameObject selectedPrefab = null;
|
|
|
|
for (int i = 0; i < activePrefabs.Length; i++)
|
|
{
|
|
if (activePrefabs[i].prefab == null)
|
|
continue;
|
|
|
|
currentWeight += Mathf.Max(1, activePrefabs[i].weight);
|
|
if (randomValue < currentWeight)
|
|
{
|
|
selectedPrefab = activePrefabs[i].prefab;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (selectedPrefab == null)
|
|
selectedPrefab = activePrefabs[activePrefabs.Length - 1].prefab;
|
|
|
|
if (selectedPrefab != null && matchedZone != null)
|
|
{
|
|
Debug.Log($"[EntitySpawner] Спавнер '{gameObject.name}' переопределил префаб для точки {spawnPosition} на '{selectedPrefab.name}' по зоне '{matchedZone.gameObject.name}'.");
|
|
}
|
|
|
|
return selectedPrefab;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|