Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Добавлен `ScoreSystem` — singleton-компонент, отслеживающий монеты, убитых врагов и экологические очки; финальный счёт вычисляется по формуле `Score = E × (P + 45C + 90K + 10K²) − 0.5T` с раздельными множителями для победы и проигрыша. - Добавлен `EnvironmentalCollectibleBase` — абстрактный базовый класс для подбираемых экологических объектов: срабатывает по триггеру, поддерживает пулинг, при подборе регистрирует очки в `ScoreSystem`. - Добавлен `LeafCollectible` — листик с синусоидальным горизонтальным дрейфом, вращением и детекцией земли. - Добавлен `BatCollectible` — летучая мышь с синусоидальным вертикальным движением, ограниченным горизонтальным блужданием и случайными сменами направления; переворачивает спрайт по X при смене направления. - Добавлен `EnvironmentalPool` — простой объектный пул по prefab-ключу с прогревом (`Warmup`) и возвратом объектов. - Добавлен `EnvironmentalCollectibleSpawner` — взвешенный случайный спаунер, поддерживающий фиксированное количество активных объектов; поддерживает лимиты по типу, принудительный выбор одного prefab и область спауна через `BoxCollider2D` или ручной `Bounds`. - В `CoinCollectible.TryPickup` добавлен вызов `ScoreSystem.Instance?.RegisterCoinCollected()`. - В `EnemyController.Die` добавлен guard через флаг `_isDead` для предотвращения повторного срабатывания; добавлен вызов `ScoreSystem.Instance?.RegisterEnemyKilled()`. - В `GameManager` добавлен вызов `ScoreSystem.Instance?.ResetStats()` при старте новой игры и при потере жизни.
330 lines
11 KiB
C#
330 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using RR.Common.Entities.Props;
|
|
using UnityEngine;
|
|
|
|
namespace RR.Game2D.Behaviour
|
|
{
|
|
public sealed class EnvironmentalCollectibleSpawner : MonoBehaviour
|
|
{
|
|
[Serializable]
|
|
public struct SpawnEntry
|
|
{
|
|
public EnvironmentalCollectibleBase prefab;
|
|
public int weight;
|
|
public bool overridePoints;
|
|
public int pointsOverride;
|
|
public int limit;
|
|
}
|
|
|
|
[Header("Spawn Count")]
|
|
[SerializeField] private int fixedCount = 8;
|
|
[SerializeField] private int maxSpawnAttempts = 12;
|
|
|
|
[Header("Spawn Area")]
|
|
[SerializeField] private BoxCollider2D spawnArea;
|
|
[SerializeField] private Vector2 areaCenter;
|
|
[SerializeField] private Vector2 areaSize = new(8f, 4f);
|
|
|
|
[Header("Player")]
|
|
[SerializeField] private Transform player;
|
|
[SerializeField] private string playerTag = "Player";
|
|
[SerializeField] private float minDistanceToPlayer = 1.5f;
|
|
[SerializeField] private float maxDistanceToPlayer = 25f;
|
|
|
|
[Header("Prefabs")]
|
|
[SerializeField] private bool usePooling = true;
|
|
[SerializeField] private SpawnEntry[] spawnEntries;
|
|
|
|
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 void Start()
|
|
{
|
|
ResolvePlayer();
|
|
WarmupPool();
|
|
MaintainFixedCount();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
foreach (EnvironmentalCollectibleBase collectible in _activeCollectibles)
|
|
{
|
|
if (collectible != null)
|
|
collectible.Collected -= HandleCollected;
|
|
}
|
|
}
|
|
|
|
public void SetSingleSpawnPrefab(EnvironmentalCollectibleBase prefab)
|
|
{
|
|
_forcedPrefab = prefab != null ? prefab.gameObject : null;
|
|
RefreshActiveCollectibles();
|
|
MaintainFixedCount();
|
|
}
|
|
|
|
public void SetSingleSpawnPrefabByIndex(int index)
|
|
{
|
|
if (spawnEntries == null || index < 0 || index >= spawnEntries.Length)
|
|
return;
|
|
|
|
SpawnEntry entry = spawnEntries[index];
|
|
SetSingleSpawnPrefab(entry.prefab);
|
|
}
|
|
|
|
public void ClearSingleSpawnPrefab()
|
|
{
|
|
_forcedPrefab = null;
|
|
MaintainFixedCount();
|
|
}
|
|
|
|
private void MaintainFixedCount()
|
|
{
|
|
int safety = fixedCount * Mathf.Max(1, maxSpawnAttempts);
|
|
while (_activeCollectibles.Count < fixedCount && safety > 0)
|
|
{
|
|
if (!TrySpawnOne())
|
|
break;
|
|
|
|
safety--;
|
|
}
|
|
}
|
|
|
|
private bool TrySpawnOne()
|
|
{
|
|
ResolvePlayer();
|
|
|
|
if (!TryPickSpawnEntry(out SpawnEntry entry))
|
|
return false;
|
|
|
|
if (!TryFindSpawnPosition(out Vector3 spawnPosition))
|
|
return false;
|
|
|
|
GameObject prefab = entry.prefab.gameObject;
|
|
GameObject instance = usePooling && _pool != null
|
|
? _pool.Get(prefab, spawnPosition, Quaternion.identity)
|
|
: Instantiate(prefab, spawnPosition, Quaternion.identity, transform);
|
|
|
|
if (instance == null) return false;
|
|
|
|
EnvironmentalCollectibleBase collectible = instance.GetComponent<EnvironmentalCollectibleBase>();
|
|
if (collectible == null)
|
|
{
|
|
Debug.LogError($"[{nameof(EnvironmentalCollectibleSpawner)}] Prefab {prefab.name} is missing EnvironmentalCollectibleBase.", prefab);
|
|
if (!usePooling)
|
|
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);
|
|
|
|
_activeCollectibles.Add(collectible);
|
|
_prefabByCollectible[collectible] = prefab;
|
|
|
|
if (_activeCountByPrefab.TryGetValue(prefab, out int currentCount))
|
|
_activeCountByPrefab[prefab] = currentCount + 1;
|
|
else
|
|
_activeCountByPrefab[prefab] = 1;
|
|
|
|
return true;
|
|
}
|
|
|
|
private void HandleCollected(EnvironmentalCollectibleBase collectible)
|
|
{
|
|
if (collectible == null) return;
|
|
|
|
collectible.Collected -= HandleCollected;
|
|
|
|
if (_prefabByCollectible.TryGetValue(collectible, out GameObject prefab) &&
|
|
_activeCountByPrefab.TryGetValue(prefab, out int currentCount))
|
|
{
|
|
_activeCountByPrefab[prefab] = Mathf.Max(0, currentCount - 1);
|
|
}
|
|
|
|
_activeCollectibles.Remove(collectible);
|
|
_prefabByCollectible.Remove(collectible);
|
|
|
|
MaintainFixedCount();
|
|
}
|
|
|
|
private bool TryPickSpawnEntry(out SpawnEntry entry)
|
|
{
|
|
entry = default;
|
|
if (spawnEntries == null || spawnEntries.Length == 0)
|
|
return false;
|
|
|
|
int totalWeight = 0;
|
|
for (int i = 0; i < spawnEntries.Length; i++)
|
|
{
|
|
SpawnEntry current = spawnEntries[i];
|
|
if (current.prefab == null || current.weight <= 0) continue;
|
|
|
|
GameObject prefab = current.prefab.gameObject;
|
|
if (_forcedPrefab != null && prefab != _forcedPrefab) continue;
|
|
|
|
if (current.limit > 0 &&
|
|
_activeCountByPrefab.TryGetValue(prefab, out int activeCount) &&
|
|
activeCount >= current.limit)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
totalWeight += current.weight;
|
|
}
|
|
|
|
if (totalWeight <= 0)
|
|
return false;
|
|
|
|
int pick = UnityEngine.Random.Range(0, totalWeight);
|
|
for (int i = 0; i < spawnEntries.Length; i++)
|
|
{
|
|
SpawnEntry current = spawnEntries[i];
|
|
if (current.prefab == null || current.weight <= 0) continue;
|
|
|
|
GameObject prefab = current.prefab.gameObject;
|
|
if (_forcedPrefab != null && prefab != _forcedPrefab) continue;
|
|
|
|
if (current.limit > 0 &&
|
|
_activeCountByPrefab.TryGetValue(prefab, out int activeCount) &&
|
|
activeCount >= current.limit)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
pick -= current.weight;
|
|
if (pick < 0)
|
|
{
|
|
entry = current;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TryFindSpawnPosition(out Vector3 spawnPosition)
|
|
{
|
|
Bounds bounds = GetSpawnBounds();
|
|
|
|
for (int i = 0; i < maxSpawnAttempts; i++)
|
|
{
|
|
float x = UnityEngine.Random.Range(bounds.min.x, bounds.max.x);
|
|
float y = UnityEngine.Random.Range(bounds.min.y, bounds.max.y);
|
|
spawnPosition = new Vector3(x, y, 0f);
|
|
|
|
if (player == null)
|
|
return true;
|
|
|
|
float distance = Vector2.Distance(player.position, spawnPosition);
|
|
if (distance < minDistanceToPlayer) continue;
|
|
if (maxDistanceToPlayer > 0f && distance > maxDistanceToPlayer) continue;
|
|
|
|
return true;
|
|
}
|
|
|
|
spawnPosition = transform.position;
|
|
return false;
|
|
}
|
|
|
|
private Bounds GetSpawnBounds()
|
|
{
|
|
if (spawnArea != null)
|
|
return spawnArea.bounds;
|
|
|
|
Vector3 center = transform.TransformPoint(areaCenter);
|
|
Vector3 size = new(areaSize.x, areaSize.y, 0f);
|
|
return new Bounds(center, size);
|
|
}
|
|
|
|
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++)
|
|
{
|
|
SpawnEntry entry = spawnEntries[i];
|
|
if (entry.prefab == null) continue;
|
|
_pool.Warmup(entry.prefab.gameObject, Mathf.Max(1, fixedCount));
|
|
}
|
|
}
|
|
|
|
private void RefreshActiveCollectibles()
|
|
{
|
|
if (_forcedPrefab == null)
|
|
return;
|
|
|
|
_refreshBuffer.Clear();
|
|
foreach (EnvironmentalCollectibleBase collectible in _activeCollectibles)
|
|
{
|
|
if (collectible == null) continue;
|
|
|
|
if (_prefabByCollectible.TryGetValue(collectible, out GameObject prefab) && prefab != _forcedPrefab)
|
|
_refreshBuffer.Add(collectible);
|
|
}
|
|
|
|
for (int i = 0; i < _refreshBuffer.Count; i++)
|
|
{
|
|
DespawnCollectible(_refreshBuffer[i]);
|
|
}
|
|
|
|
_refreshBuffer.Clear();
|
|
}
|
|
|
|
private void DespawnCollectible(EnvironmentalCollectibleBase collectible)
|
|
{
|
|
if (collectible == null) return;
|
|
|
|
collectible.Collected -= HandleCollected;
|
|
|
|
if (_prefabByCollectible.TryGetValue(collectible, out GameObject prefab))
|
|
{
|
|
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);
|
|
}
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
Bounds bounds = GetSpawnBounds();
|
|
Gizmos.color = new Color(0.15f, 0.8f, 0.25f, 0.35f);
|
|
Gizmos.DrawCube(bounds.center, bounds.size);
|
|
Gizmos.color = new Color(0.15f, 0.8f, 0.25f, 1f);
|
|
Gizmos.DrawWireCube(bounds.center, bounds.size);
|
|
}
|
|
}
|
|
}
|