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()` при старте новой игры и при потере жизни.
186 lines
6.2 KiB
C#
186 lines
6.2 KiB
C#
using UnityEngine;
|
|
|
|
using RR.Common.Interfaces;
|
|
using RR.Common.Utility;
|
|
using RR.Game2D.Core.Managers;
|
|
using RR.Game2D.Behaviour.ScriptableObjects;
|
|
|
|
namespace RR.Game2D.Behaviour
|
|
{
|
|
[RequireComponent(typeof(Rigidbody2D),
|
|
typeof(Collider2D),
|
|
typeof(Animator))]
|
|
public class EnemyController : MonoBehaviour, IDamageable
|
|
{
|
|
[SerializeField] public EnemySFXAsset sfxAsset;
|
|
[SerializeField] private EnemyControllerConfiguration config;
|
|
|
|
private Health health;
|
|
[ReadOnlyInInspector] public float CurrentHealth => health?.CurrentHealth ?? 0;
|
|
|
|
[HideInInspector] public float MaxHealth => health?.MaxHealth ?? 0;
|
|
|
|
private Animator _animator;
|
|
private Rigidbody2D _rb;
|
|
private Collider2D _col;
|
|
private bool _isDead;
|
|
|
|
private GameObject _playerObj;
|
|
|
|
// Аниматор
|
|
private static readonly int Speed = Animator.StringToHash("Speed");
|
|
|
|
private void Start()
|
|
{
|
|
_rb = GetComponent<Rigidbody2D>();
|
|
_col = GetComponent<Collider2D>();
|
|
_animator = GetComponent<Animator>();
|
|
|
|
_playerObj = GameManager.Instance.PlayerObj;
|
|
if (_playerObj == null)
|
|
{
|
|
_playerObj = GameObject.FindGameObjectWithTag("Player");
|
|
}
|
|
|
|
health = GetComponent<Health>();
|
|
if (health == null)
|
|
{
|
|
health = gameObject.AddComponent<Health>();
|
|
}
|
|
health.ResetHealth();
|
|
health.SetOwner(this);
|
|
health.SetMaxHealth(config.maxHealth);
|
|
}
|
|
|
|
private void FixedUpdate()
|
|
{
|
|
if (!_playerObj) return;
|
|
|
|
_playerObj.TryGetComponent(out PlayerController playerController);
|
|
|
|
Vector3 playerCenter = _playerObj.transform.position;
|
|
|
|
if (PlayerInSight() && !playerController.isDeadAtMoment)
|
|
{
|
|
|
|
Vector3 toPlayer = playerCenter - transform.position;
|
|
float dist = toPlayer.magnitude + 0.1f;
|
|
|
|
if (dist > 0)
|
|
{
|
|
_rb.linearVelocity = new Vector2(config.moveSpeed * toPlayer.normalized.x, 0);
|
|
}
|
|
// if (dist > 0)
|
|
// rb.linearVelocity = toPlayer / dist * moveSpeed;
|
|
// else
|
|
// rb.linearVelocity = Vector2.zero;
|
|
}
|
|
else
|
|
{
|
|
_rb.linearVelocity = Vector2.MoveTowards(
|
|
_rb.linearVelocity, Vector2.zero, config.acceleration * Time.fixedDeltaTime);
|
|
}
|
|
|
|
_animator.SetFloat(Speed, Mathf.Abs(_rb.linearVelocity.magnitude));
|
|
}
|
|
|
|
private void OnCollisionEnter2D(Collision2D collision)
|
|
{
|
|
if (!collision.collider.CompareTag("Player")) return;
|
|
|
|
Rigidbody2D playerRb = collision.collider.GetComponent<Rigidbody2D>();
|
|
_playerObj.TryGetComponent(out PlayerController playerController);
|
|
if (playerRb == null) return;
|
|
Vector2 contactNormal = collision.GetContact(0).normal;
|
|
|
|
if (playerController.isDeadAtMoment) return;
|
|
|
|
if (contactNormal.y < -0.5f)
|
|
{
|
|
playerRb.linearVelocity = new Vector2(playerRb.linearVelocity.x, 35f);
|
|
TakeDamage(MaxHealth);
|
|
// TakeDamage(1);
|
|
}
|
|
else
|
|
{
|
|
playerController.TakeDamage(1f);
|
|
}
|
|
}
|
|
|
|
private bool PlayerInSight()
|
|
{
|
|
if (!_playerObj) return false;
|
|
|
|
Vector3 playerPos = _playerObj.transform.position;
|
|
Collider2D playerCol = _playerObj.GetComponent<Collider2D>();
|
|
playerPos.y = playerCol.bounds.center.y;
|
|
|
|
Vector2 eyePos = new(_rb.position.x, _col.bounds.center.y);
|
|
|
|
Vector2 toPlayer = (Vector2)playerPos - eyePos;
|
|
float dist = toPlayer.magnitude;
|
|
if (dist > config.viewDistance) return false;
|
|
|
|
Vector2 forward = transform.localEulerAngles.y >= 180f ? Vector2.right : Vector2.left;
|
|
|
|
float cosHalf = Mathf.Cos(0.5f * config.viewAngle * Mathf.Deg2Rad);
|
|
float dot = Vector2.Dot(forward.normalized, toPlayer / Mathf.Max(dist, 1e-6f));
|
|
if (dot < cosHalf) return false;
|
|
|
|
int mask = config.obstacleMask;
|
|
mask &= ~(1 << gameObject.layer);
|
|
mask &= ~(1 << _playerObj.layer);
|
|
|
|
Vector2 origin = eyePos + forward.normalized * 0.05f;
|
|
|
|
RaycastHit2D hit = Physics2D.Raycast(origin, toPlayer.normalized, dist, mask);
|
|
|
|
return !hit.collider;
|
|
}
|
|
|
|
public void TakeDamage(float amount)
|
|
{
|
|
health.TakeDamage(amount);
|
|
}
|
|
public void Heal(float amount)
|
|
{
|
|
health.Heal(amount);
|
|
}
|
|
public void Die()
|
|
{
|
|
if (_isDead) return;
|
|
_isDead = true;
|
|
|
|
ScoreSystem.Instance?.RegisterEnemyKilled();
|
|
_animator.Play("Death");
|
|
// Destroy(gameObject);
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void OnDrawGizmos()
|
|
{
|
|
if (!_playerObj) return;
|
|
|
|
Vector2 origin = new(_rb.position.x, _col.bounds.center.y);
|
|
Vector2 forward = transform.localEulerAngles.y >= 180f ? Vector2.right : Vector2.left;
|
|
float half = config.viewAngle * 0.5f;
|
|
|
|
Vector2 edgeA = (Quaternion.Euler(0, 0, +half) * forward);
|
|
Vector2 edgeB = (Quaternion.Euler(0, 0, -half) * forward);
|
|
|
|
Gizmos.color = Color.yellow;
|
|
Gizmos.DrawLine(origin, origin + edgeA * config.viewDistance);
|
|
Gizmos.DrawLine(origin, origin + edgeB * config.viewDistance);
|
|
Gizmos.DrawWireSphere(origin, config.viewDistance);
|
|
|
|
Vector2 toPlayer = (Vector2)_playerObj.transform.position - origin;
|
|
|
|
if (!(toPlayer.magnitude <= config.viewDistance)) return;
|
|
|
|
Gizmos.color = Color.red;
|
|
Gizmos.DrawLine(transform.position, _playerObj.transform.position);
|
|
}
|
|
#endif
|
|
}
|
|
}
|