Danro/Assets/Scripts/Behaviour/EntitySpawner.cs
Danil Iarosh cb2e83a596
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(ui, prizes, spawners, editor): expand prize flow and add spawning/editor tooling
- доработан flow призовой витрины и связанного UI:
  добавлены `PrizeStandButtonView` и `RulesPrizeListPage`, обновлены
  `PrizeStandController`, `PrizeStandPrize`, `PrizeStandModal`,
  `PrizeStandSettingsModal`, `LevelPrizeSettingsModal`, `QRCodeSettingsModal`,
  `BalanceDisplay`, `ButtonTextFocusColor` и связанные prefab'ы,
  чтобы улучшить настройку, навигацию, отображение кнопок и prize flow
- расширена система настроек и сохранения значений:
  `OptionPicker` получил отдельный формат `ValueSelector`,
  обновлены `SettingsDatabase`, `SettingEntryDrawer`, `SettingUIItem`,
  `SettingsUIGenerator`, `ValueSelectorRow` и `MainSettingsDatabase.asset`,
  а сохранение option picker-значений, включая `gameplay.gameDifficulty`,
  исправлено и синхронизировано с остальными настройками
- добавлены универсальные спавнеры сущностей:
  реализованы `EntitySpawner` и `EntitySpawnerManager` с поддержкой
  весов префабов, точек спавна, заполнения по проценту, ограничений
  на количество и автоспавна при инициализации
- доработана логика игрового спавна и сложности:
  процент инициализационного спавна может зависеть от настройки
  `gameplay.gameDifficulty` с градациями 25/50/75/100,
  в одном цикле массового спавна повторно не используется та же точка,
  а gizmo спавнера теперь показывает только сами точки без линий
- упрощена работа в редакторе Unity и добавлены editor-инструменты:
  в `GameManager` лицензия пропускается при запуске в `UNITY_EDITOR`,
  а `HierarchySelectionCounter`, `HierarchySelectionCountOverlay`
  и `HierarchyBulkRenamerWindow` добавляют подсчет выделения,
  realtime overlay в `SceneView` и массовое переименование объектов
  по правилам нумерации из Unity Project Settings
- синхронизированы сцены, ресурсы и UI-ассеты под новый flow:
  обновлены `MainMenu`, `Level UI`, уровни `1-1` и `1-2`,
  строки настроек, кнопки, TMP/font assets, изображения UI,
  `prizes.json` и recovery-данные после правок интерфейса и логики
- очищены и переразложены часть внешних и служебных ресурсов:
  удалён пакет `Lava_Flowing_Shader`, переименованы/заменены
  отдельные UI-prefab'ы и добавлены сопутствующие проектные артефакты,
  которые сейчас используются обновлённой конфигурацией проекта
2026-04-28 14:05:05 +03:00

364 lines
11 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);
}
public void SpawnCount(int count)
{
if (count <= 0)
return;
HashSet<int> usedSpawnPointIndices = new();
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();
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;
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<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);
}
}
}
}