feat: add ScoreSystem, environmental collectibles, object pool and spawner
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:
Данил Ярош 2026-03-03 09:14:49 +03:00
parent 1cecbf2c11
commit de87b318b7
22 changed files with 444732 additions and 86463 deletions

View File

@ -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

View File

@ -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
}
}
}

View 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);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c3bdb5d5e24e42b43aece26257a11ede

View 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;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a6b98ac2c91fab545952a401bce4e156

View 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);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ccb3fbb53a48fd845b8b3e131f31c449

View 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;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0a07d694adfb80d4594401abc8aa8509

View File

@ -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;
}
}
}
}

View 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);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1a5ce1a7cb145b445a31372bc2fa1af6

View 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);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d5b7e46ee9c8087468860b8f11049bdc

View File

@ -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();

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 3c1afb5231ad35646a78a949f3a90ca7
guid: f534e2680fea30549b21bbae25117e86
folderAsset: yes
DefaultImporter:
externalObjects: {}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 71c4377b7b16cdc469bf877b9a66695d
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 92bdad2e73fe34a4eab7de385fd6c412
guid: 0ad40d78cfd21f6409db0bd7afe62c44
DefaultImporter:
externalObjects: {}
userData:

View 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>