feat: add ScoreSystem, environmental collectibles, object pool and spawner
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
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()` при старте новой игры и при потере жизни.
This commit is contained in:
parent
1cecbf2c11
commit
de87b318b7
@ -208388,7 +208388,7 @@ VisualEffect:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1465160039}
|
||||
m_Enabled: 1
|
||||
m_Asset: {fileID: 8926484042661614526, guid: f81f06c7755accd42bf8e15cf6687eee, type: 3}
|
||||
m_Asset: {fileID: 8926484042661614526, guid: 484c0e7574383dd4b9e863848199e369, type: 3}
|
||||
m_InitialEventName: OnPlay
|
||||
m_InitialEventNameOverriden: 0
|
||||
m_StartSeed: 0
|
||||
|
||||
@ -23,6 +23,7 @@ namespace RR.Game2D.Behaviour
|
||||
private Animator _animator;
|
||||
private Rigidbody2D _rb;
|
||||
private Collider2D _col;
|
||||
private bool _isDead;
|
||||
|
||||
private GameObject _playerObj;
|
||||
|
||||
@ -147,6 +148,10 @@ namespace RR.Game2D.Behaviour
|
||||
}
|
||||
public void Die()
|
||||
{
|
||||
if (_isDead) return;
|
||||
_isDead = true;
|
||||
|
||||
ScoreSystem.Instance?.RegisterEnemyKilled();
|
||||
_animator.Play("Death");
|
||||
// Destroy(gameObject);
|
||||
}
|
||||
@ -177,4 +182,4 @@ namespace RR.Game2D.Behaviour
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
329
Assets/Scripts/Behaviour/EnvironmentalCollectibleSpawner.cs
Normal file
329
Assets/Scripts/Behaviour/EnvironmentalCollectibleSpawner.cs
Normal file
@ -0,0 +1,329 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3bdb5d5e24e42b43aece26257a11ede
|
||||
60
Assets/Scripts/Behaviour/EnvironmentalPool.cs
Normal file
60
Assets/Scripts/Behaviour/EnvironmentalPool.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System.Collections.Generic;
|
||||
using RR.Common.Entities.Props;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RR.Game2D.Behaviour
|
||||
{
|
||||
public sealed class EnvironmentalPool : MonoBehaviour
|
||||
{
|
||||
private readonly Dictionary<GameObject, Queue<GameObject>> _poolByPrefab = new();
|
||||
|
||||
public void Warmup(GameObject prefab, int count)
|
||||
{
|
||||
if (prefab == null || count <= 0) return;
|
||||
|
||||
Queue<GameObject> queue = GetQueue(prefab);
|
||||
for (int i = queue.Count; i < count; i++)
|
||||
{
|
||||
GameObject instance = Instantiate(prefab, transform);
|
||||
instance.SetActive(false);
|
||||
queue.Enqueue(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation)
|
||||
{
|
||||
if (prefab == null) return null;
|
||||
|
||||
Queue<GameObject> queue = GetQueue(prefab);
|
||||
GameObject instance = queue.Count > 0 ? queue.Dequeue() : Instantiate(prefab, transform);
|
||||
|
||||
Transform cachedTransform = instance.transform;
|
||||
cachedTransform.SetPositionAndRotation(position, rotation);
|
||||
instance.SetActive(true);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void Return(GameObject prefab, GameObject instance)
|
||||
{
|
||||
if (prefab == null || instance == null) return;
|
||||
|
||||
EnvironmentalCollectibleBase collectible = instance.GetComponent<EnvironmentalCollectibleBase>();
|
||||
if (collectible != null)
|
||||
collectible.transform.SetParent(transform, false);
|
||||
|
||||
instance.SetActive(false);
|
||||
GetQueue(prefab).Enqueue(instance);
|
||||
}
|
||||
|
||||
private Queue<GameObject> GetQueue(GameObject prefab)
|
||||
{
|
||||
if (_poolByPrefab.TryGetValue(prefab, out Queue<GameObject> queue))
|
||||
return queue;
|
||||
|
||||
queue = new Queue<GameObject>();
|
||||
_poolByPrefab.Add(prefab, queue);
|
||||
return queue;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Behaviour/EnvironmentalPool.cs.meta
Normal file
2
Assets/Scripts/Behaviour/EnvironmentalPool.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6b98ac2c91fab545952a401bce4e156
|
||||
151
Assets/Scripts/Behaviour/ScoreSystem.cs
Normal file
151
Assets/Scripts/Behaviour/ScoreSystem.cs
Normal file
@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using RR.Game2D.Core.Managers;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RR.Game2D.Behaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Система очков:
|
||||
/// хранит количество собранных монет и убитых врагов,
|
||||
/// считает итоговый счет при завершении игры.
|
||||
/// Формула: Score = E * (P + 45C + 90K + 10K^2) - 0.5T
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-900)]
|
||||
public class ScoreSystem : MonoBehaviour
|
||||
{
|
||||
public static ScoreSystem Instance { get; private set; }
|
||||
|
||||
[Header("Коэффициенты исхода")]
|
||||
[SerializeField] private float winMultiplier = 1f;
|
||||
[SerializeField] private float loseMultiplier = 0.6f;
|
||||
|
||||
public int CollectedCoins { get; private set; }
|
||||
public int TotalCollectedCoins => CollectedCoins;
|
||||
public int KilledEnemies { get; private set; }
|
||||
public int EnvironmentalPoints { get; private set; }
|
||||
public int FinalScore { get; private set; }
|
||||
public bool IsFinalized { get; private set; }
|
||||
|
||||
public event Action<int, int> StatsChanged;
|
||||
public event Action<int> EnvironmentalPointsChanged;
|
||||
public event Action<int, float, bool> ScoreCalculated;
|
||||
|
||||
private bool _subscribed;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
{
|
||||
if (Instance != null) return;
|
||||
|
||||
GameObject go = new(nameof(ScoreSystem));
|
||||
go.AddComponent<ScoreSystem>();
|
||||
DontDestroyOnLoad(go);
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
ResetStats();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
TrySubscribe();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
TrySubscribe();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Unsubscribe();
|
||||
}
|
||||
|
||||
public void RegisterCoinCollected(int amount = 1)
|
||||
{
|
||||
if (amount <= 0 || IsFinalized) return;
|
||||
|
||||
// This is the cumulative coin count for score calculation.
|
||||
// It is intentionally separate from GameManager.Coins, which can wrap at 100 to award an extra life.
|
||||
CollectedCoins += amount;
|
||||
StatsChanged?.Invoke(CollectedCoins, KilledEnemies);
|
||||
}
|
||||
|
||||
public void RegisterEnemyKilled(int amount = 1)
|
||||
{
|
||||
if (amount <= 0 || IsFinalized) return;
|
||||
KilledEnemies += amount;
|
||||
StatsChanged?.Invoke(CollectedCoins, KilledEnemies);
|
||||
}
|
||||
|
||||
public void RegisterEnvironmentalPoints(int amount = 1)
|
||||
{
|
||||
if (amount <= 0 || IsFinalized) return;
|
||||
|
||||
EnvironmentalPoints += amount;
|
||||
EnvironmentalPointsChanged?.Invoke(EnvironmentalPoints);
|
||||
}
|
||||
|
||||
public void ResetStats()
|
||||
{
|
||||
CollectedCoins = 0;
|
||||
KilledEnemies = 0;
|
||||
EnvironmentalPoints = 0;
|
||||
FinalScore = 0;
|
||||
IsFinalized = false;
|
||||
StatsChanged?.Invoke(CollectedCoins, KilledEnemies);
|
||||
EnvironmentalPointsChanged?.Invoke(EnvironmentalPoints);
|
||||
}
|
||||
|
||||
private void TrySubscribe()
|
||||
{
|
||||
if (_subscribed || GameManager.Instance == null) return;
|
||||
|
||||
GameManager.Instance.GameWonEvent += OnGameWon;
|
||||
GameManager.Instance.GameOverEvent += OnGameOver;
|
||||
_subscribed = true;
|
||||
}
|
||||
|
||||
private void Unsubscribe()
|
||||
{
|
||||
if (!_subscribed || GameManager.Instance == null) return;
|
||||
|
||||
GameManager.Instance.GameWonEvent -= OnGameWon;
|
||||
GameManager.Instance.GameOverEvent -= OnGameOver;
|
||||
_subscribed = false;
|
||||
}
|
||||
|
||||
private void OnGameWon(float gameTime)
|
||||
{
|
||||
Calculate(gameTime, true);
|
||||
}
|
||||
|
||||
private void OnGameOver(float gameTime)
|
||||
{
|
||||
Calculate(gameTime, false);
|
||||
}
|
||||
|
||||
private void Calculate(float gameTime, bool isWin)
|
||||
{
|
||||
if (IsFinalized) return;
|
||||
|
||||
float e = isWin ? winMultiplier : loseMultiplier;
|
||||
float baseScore = EnvironmentalPoints + 45f * CollectedCoins + 90f * KilledEnemies + 10f * KilledEnemies * KilledEnemies;
|
||||
float rawScore = e * baseScore - 0.5f * gameTime;
|
||||
|
||||
FinalScore = Mathf.Max(0, Mathf.RoundToInt(rawScore));
|
||||
IsFinalized = true;
|
||||
|
||||
ScoreCalculated?.Invoke(FinalScore, gameTime, isWin);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Behaviour/ScoreSystem.cs.meta
Normal file
2
Assets/Scripts/Behaviour/ScoreSystem.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ccb3fbb53a48fd845b8b3e131f31c449
|
||||
77
Assets/Scripts/Entities/Props/BatCollectible.cs
Normal file
77
Assets/Scripts/Entities/Props/BatCollectible.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace RR.Common.Entities.Props
|
||||
{
|
||||
public sealed class BatCollectible : EnvironmentalCollectibleBase
|
||||
{
|
||||
[SerializeField] private float moveSpeed = 1.75f;
|
||||
[SerializeField] private float verticalAmplitude = 0.35f;
|
||||
[SerializeField] private float verticalFrequency = 2.4f;
|
||||
[SerializeField] private float horizontalWaveAmplitude = 0.2f;
|
||||
[SerializeField] private float horizontalWaveFrequency = 1.35f;
|
||||
[SerializeField] private float maxRange = 3.5f;
|
||||
[SerializeField] private float directionChangeIntervalMin = 1.5f;
|
||||
[SerializeField] private float directionChangeIntervalMax = 3.5f;
|
||||
|
||||
private Vector3 _anchorPosition;
|
||||
private Vector3 _currentPosition;
|
||||
private float _time;
|
||||
private float _directionTimer;
|
||||
private float _nextDirectionChangeTime;
|
||||
private int _moveDirection;
|
||||
|
||||
protected override void OnSpawned()
|
||||
{
|
||||
_anchorPosition = SpawnPosition;
|
||||
_currentPosition = transform.position;
|
||||
_time = 0f;
|
||||
_directionTimer = 0f;
|
||||
_moveDirection = 1;
|
||||
_nextDirectionChangeTime = Random.Range(directionChangeIntervalMin, directionChangeIntervalMax);
|
||||
}
|
||||
|
||||
protected override void TickBehaviour(float dt)
|
||||
{
|
||||
_time += dt;
|
||||
_directionTimer += dt;
|
||||
|
||||
if (_directionTimer >= _nextDirectionChangeTime)
|
||||
{
|
||||
FlipDirection();
|
||||
_directionTimer = 0f;
|
||||
_nextDirectionChangeTime = Random.Range(directionChangeIntervalMin, directionChangeIntervalMax);
|
||||
}
|
||||
|
||||
Vector3 position = _currentPosition;
|
||||
position.x += _moveDirection * moveSpeed * dt;
|
||||
|
||||
float xOffsetFromAnchor = position.x - _anchorPosition.x;
|
||||
if (Mathf.Abs(xOffsetFromAnchor) > maxRange)
|
||||
{
|
||||
position.x = _anchorPosition.x + Mathf.Sign(xOffsetFromAnchor) * maxRange;
|
||||
FlipDirection();
|
||||
_directionTimer = 0f;
|
||||
_nextDirectionChangeTime = Random.Range(directionChangeIntervalMin, directionChangeIntervalMax);
|
||||
}
|
||||
|
||||
float verticalOffset = Mathf.Sin(_time * verticalFrequency) * verticalAmplitude;
|
||||
float horizontalOffset = Mathf.Sin(_time * horizontalWaveFrequency) * horizontalWaveAmplitude;
|
||||
|
||||
position.y = _anchorPosition.y + verticalOffset;
|
||||
position.x += horizontalOffset * dt;
|
||||
|
||||
_currentPosition = position;
|
||||
transform.position = position;
|
||||
}
|
||||
|
||||
private void FlipDirection()
|
||||
{
|
||||
_moveDirection = -_moveDirection;
|
||||
|
||||
Vector3 scale = transform.localScale;
|
||||
float xScale = Mathf.Abs(scale.x);
|
||||
scale.x = _moveDirection > 0 ? xScale : -xScale;
|
||||
transform.localScale = scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Entities/Props/BatCollectible.cs.meta
Normal file
2
Assets/Scripts/Entities/Props/BatCollectible.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a07d694adfb80d4594401abc8aa8509
|
||||
@ -1,5 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
using RR.Game2D.Behaviour;
|
||||
using RR.Game2D.Core.Managers;
|
||||
|
||||
namespace RR.Common.Entities.Props
|
||||
@ -10,10 +11,11 @@ namespace RR.Common.Entities.Props
|
||||
{
|
||||
base.TryPickup(newOwner, optionalAttachPoint);
|
||||
|
||||
ScoreSystem.Instance?.RegisterCoinCollected();
|
||||
GameManager.Instance.AddCoin();
|
||||
Destroy(gameObject);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
122
Assets/Scripts/Entities/Props/EnvironmentalCollectibleBase.cs
Normal file
122
Assets/Scripts/Entities/Props/EnvironmentalCollectibleBase.cs
Normal file
@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using RR.Game2D.Behaviour;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RR.Common.Entities.Props
|
||||
{
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public abstract class EnvironmentalCollectibleBase : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private int points = 1;
|
||||
[SerializeField] private string playerTag = "Player";
|
||||
[SerializeField] private bool usePooling = true;
|
||||
|
||||
private bool _collected;
|
||||
private int _defaultPoints;
|
||||
private Collider2D _trigger;
|
||||
private EnvironmentalPool _ownerPool;
|
||||
private GameObject _originPrefab;
|
||||
private Vector3 _spawnPosition;
|
||||
|
||||
public event Action<EnvironmentalCollectibleBase> Collected;
|
||||
|
||||
public int Points => points;
|
||||
public Vector3 SpawnPosition => _spawnPosition;
|
||||
public bool UsesPooling => usePooling;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
_defaultPoints = Mathf.Max(0, points);
|
||||
_trigger = GetComponent<Collider2D>();
|
||||
ValidateTrigger();
|
||||
}
|
||||
|
||||
protected virtual void Update()
|
||||
{
|
||||
TickBehaviour(Time.deltaTime);
|
||||
}
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
if (points < 0) points = 0;
|
||||
_defaultPoints = points;
|
||||
ValidateTrigger();
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
// Unity 2D trigger callbacks usually require at least one of the colliding objects to have a Rigidbody2D.
|
||||
if (_collected || other == null) return;
|
||||
if (!other.CompareTag(playerTag)) return;
|
||||
|
||||
Collect(other.transform);
|
||||
}
|
||||
|
||||
public void InitializeForSpawn(
|
||||
EnvironmentalPool ownerPool,
|
||||
GameObject originPrefab,
|
||||
Vector3 spawnPosition,
|
||||
int? pointsOverride)
|
||||
{
|
||||
_ownerPool = ownerPool;
|
||||
_originPrefab = originPrefab;
|
||||
_spawnPosition = spawnPosition;
|
||||
points = pointsOverride.HasValue ? Mathf.Max(0, pointsOverride.Value) : _defaultPoints;
|
||||
|
||||
ResetStateForReuse();
|
||||
}
|
||||
|
||||
public void ResetStateForReuse()
|
||||
{
|
||||
_collected = false;
|
||||
_spawnPosition = transform.position;
|
||||
OnSpawned();
|
||||
}
|
||||
|
||||
protected void Collect(Transform collector)
|
||||
{
|
||||
if (_collected) return;
|
||||
|
||||
_collected = true;
|
||||
OnCollected();
|
||||
|
||||
if (points > 0)
|
||||
ScoreSystem.Instance?.RegisterEnvironmentalPoints(points);
|
||||
|
||||
Collected?.Invoke(this);
|
||||
|
||||
if (usePooling && _ownerPool != null && _originPrefab != null)
|
||||
{
|
||||
_ownerPool.Return(_originPrefab, gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
protected virtual void OnSpawned()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void OnCollected()
|
||||
{
|
||||
}
|
||||
|
||||
protected abstract void TickBehaviour(float dt);
|
||||
|
||||
private void ValidateTrigger()
|
||||
{
|
||||
if (_trigger == null)
|
||||
_trigger = GetComponent<Collider2D>();
|
||||
|
||||
if (_trigger == null)
|
||||
{
|
||||
Debug.LogError($"[{nameof(EnvironmentalCollectibleBase)}] Missing Collider2D on {name}.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_trigger.isTrigger)
|
||||
Debug.LogError($"[{nameof(EnvironmentalCollectibleBase)}] Collider2D on {name} must have isTrigger enabled.", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a5ce1a7cb145b445a31372bc2fa1af6
|
||||
72
Assets/Scripts/Entities/Props/LeafCollectible.cs
Normal file
72
Assets/Scripts/Entities/Props/LeafCollectible.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace RR.Common.Entities.Props
|
||||
{
|
||||
public sealed class LeafCollectible : EnvironmentalCollectibleBase
|
||||
{
|
||||
[Header("Fall")]
|
||||
[SerializeField] private float fallSpeed = 1.25f;
|
||||
[SerializeField] private float horizontalAmplitude = 0.4f;
|
||||
[SerializeField] private float horizontalFrequency = 1.1f;
|
||||
[SerializeField] private float rotationAmplitude = 22f;
|
||||
[SerializeField] private float rotationSpeed = 2.2f;
|
||||
|
||||
[Header("Grounding")]
|
||||
[SerializeField] private bool stopAtGround = true;
|
||||
[SerializeField] private LayerMask groundMask = ~0;
|
||||
[SerializeField] private float groundCheckDistance = 0.2f;
|
||||
[SerializeField] private float minY = -10f;
|
||||
[SerializeField] private float groundedSwayAmplitude = 0.08f;
|
||||
[SerializeField] private float groundedSwaySpeed = 1.5f;
|
||||
|
||||
private Vector3 _basePosition;
|
||||
private float _time;
|
||||
private bool _isGrounded;
|
||||
|
||||
protected override void OnSpawned()
|
||||
{
|
||||
_basePosition = transform.position;
|
||||
_time = 0f;
|
||||
_isGrounded = false;
|
||||
}
|
||||
|
||||
protected override void TickBehaviour(float dt)
|
||||
{
|
||||
_time += dt;
|
||||
|
||||
Vector3 position = _basePosition;
|
||||
float horizontalOffset = Mathf.Sin(_time * horizontalFrequency) * horizontalAmplitude;
|
||||
|
||||
if (_isGrounded)
|
||||
{
|
||||
position.x += Mathf.Sin(_time * groundedSwaySpeed) * groundedSwayAmplitude;
|
||||
transform.position = position;
|
||||
transform.rotation = Quaternion.Euler(0f, 0f, Mathf.Sin(_time * rotationSpeed) * (rotationAmplitude * 0.25f));
|
||||
return;
|
||||
}
|
||||
|
||||
position.y -= fallSpeed * dt;
|
||||
position.x += horizontalOffset;
|
||||
|
||||
bool reachedGround = position.y <= minY;
|
||||
if (!reachedGround && stopAtGround)
|
||||
{
|
||||
RaycastHit2D hit = Physics2D.Raycast(position, Vector2.down, groundCheckDistance, groundMask);
|
||||
reachedGround = hit.collider != null;
|
||||
}
|
||||
|
||||
if (reachedGround)
|
||||
{
|
||||
_isGrounded = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_basePosition.y = position.y;
|
||||
_basePosition.x = SpawnPosition.x;
|
||||
}
|
||||
|
||||
transform.position = position;
|
||||
transform.rotation = Quaternion.Euler(0f, 0f, Mathf.Sin(_time * rotationSpeed) * rotationAmplitude);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Entities/Props/LeafCollectible.cs.meta
Normal file
2
Assets/Scripts/Entities/Props/LeafCollectible.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5b7e46ee9c8087468860b8f11049bdc
|
||||
@ -341,6 +341,7 @@ public void TogglePause() => SetPause(!IsPaused);
|
||||
LivesChanged?.Invoke(Lives);
|
||||
Coins = 0;
|
||||
CoinsChanged?.Invoke(Coins);
|
||||
ScoreSystem.Instance?.ResetStats();
|
||||
|
||||
_gameStartTime = Time.time;
|
||||
Logger.Log(gameObject.name,$"Game start time = {_gameStartTime}");
|
||||
@ -379,6 +380,7 @@ public void TogglePause() => SetPause(!IsPaused);
|
||||
LivesChanged?.Invoke(Lives);
|
||||
Coins = 0;
|
||||
CoinsChanged?.Invoke(Coins);
|
||||
ScoreSystem.Instance?.ResetStats();
|
||||
|
||||
// Сбрасываем таймер бездействия
|
||||
if (Inactivity != null) Inactivity.ResetInactivityTimer();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c1afb5231ad35646a78a949f3a90ca7
|
||||
guid: f534e2680fea30549b21bbae25117e86
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71c4377b7b16cdc469bf877b9a66695d
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
487574
Assets/_Recovery/0.unity
487574
Assets/_Recovery/0.unity
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92bdad2e73fe34a4eab7de385fd6c412
|
||||
guid: 0ad40d78cfd21f6409db0bd7afe62c44
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
|
||||
6
Mario-like platformer.slnx
Normal file
6
Mario-like platformer.slnx
Normal file
@ -0,0 +1,6 @@
|
||||
<Solution>
|
||||
<Project Path="VFXGraph.OutputEventHandlers.csproj" />
|
||||
<Project Path="Assembly-CSharp.csproj" />
|
||||
<Project Path="Assembly-CSharp-Editor.csproj" />
|
||||
<Project Path="VFXGraph.OutputEventHandlers.Editor.csproj" />
|
||||
</Solution>
|
||||
Loading…
x
Reference in New Issue
Block a user