212 lines
7.9 KiB
C#
212 lines
7.9 KiB
C#
using UnityEngine;
|
||
|
||
namespace _Project.Scripts.PlayerUtils
|
||
{
|
||
[RequireComponent(typeof(Rigidbody), typeof(CapsuleCollider))]
|
||
public class EnemyAI : MonoBehaviour, IDamageable
|
||
{
|
||
[Header("Класс и Профиль Врага")]
|
||
public BodyClass enemyClass = BodyClass.Trooper;
|
||
[Tooltip("Если не назначен, сгенерируется автоматически под выбранный класс")]
|
||
public BodyProfile profile;
|
||
|
||
[Header("Настройки ИИ")]
|
||
[SerializeField] private float detectionRange = 30f;
|
||
[SerializeField] private float eyeHeight = 1.5f;
|
||
[SerializeField] private LayerMask obstacleMask = ~0;
|
||
|
||
public float CurrentHealth { get; private set; }
|
||
public bool IsDead { get; private set; }
|
||
|
||
// Можно вселяться в любого живого врага (или если у него осталось мало ХП — на твой выбор)
|
||
public bool CanBePossessed => !IsDead;
|
||
|
||
private Rigidbody _rb;
|
||
private Transform _playerTransform;
|
||
private float _fireTimer;
|
||
private float _strafeTimer;
|
||
private int _strafeDir = 1;
|
||
private LineRenderer _sniperLaser;
|
||
|
||
private void Awake()
|
||
{
|
||
_rb = GetComponent<Rigidbody>();
|
||
_rb.freezeRotation = true;
|
||
}
|
||
|
||
private void Start()
|
||
{
|
||
if (Player.Instance != null)
|
||
_playerTransform = Player.Instance.transform;
|
||
|
||
if (profile == null || profile.bodyClass != enemyClass)
|
||
{
|
||
profile = BodyProfile.CreateDefault(enemyClass);
|
||
}
|
||
|
||
CurrentHealth = profile.maxHealth;
|
||
|
||
if (enemyClass == BodyClass.Sniper && _sniperLaser == null)
|
||
{
|
||
SetupSniperLaser();
|
||
}
|
||
}
|
||
|
||
private void SetupSniperLaser()
|
||
{
|
||
_sniperLaser = gameObject.AddComponent<LineRenderer>();
|
||
_sniperLaser.startWidth = 0.02f;
|
||
_sniperLaser.endWidth = 0.02f;
|
||
_sniperLaser.material = new Material(Shader.Find("Sprites/Default"));
|
||
_sniperLaser.startColor = new Color(1f, 0f, 0f, 0.7f);
|
||
_sniperLaser.endColor = new Color(1f, 0f, 0f, 0.1f);
|
||
_sniperLaser.enabled = false;
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
if (IsDead || _playerTransform == null) return;
|
||
|
||
_fireTimer -= Time.deltaTime;
|
||
_strafeTimer -= Time.deltaTime;
|
||
if (_strafeTimer <= 0f)
|
||
{
|
||
_strafeDir = Random.value > 0.5f ? 1 : -1;
|
||
_strafeTimer = Random.Range(0.6f, 1.5f);
|
||
}
|
||
|
||
Vector3 eyePos = transform.position + Vector3.up * eyeHeight;
|
||
Vector3 targetPos = _playerTransform.position + Vector3.up * 1.0f;
|
||
Vector3 toPlayer = targetPos - eyePos;
|
||
float dist = toPlayer.magnitude;
|
||
|
||
if (dist > detectionRange || !HasLineOfSight(eyePos, targetPos, dist))
|
||
{
|
||
if (_sniperLaser != null) _sniperLaser.enabled = false;
|
||
return;
|
||
}
|
||
|
||
// Поворот лицом к игроку по горизонтали
|
||
Vector3 flatDir = new Vector3(toPlayer.x, 0f, toPlayer.z).normalized;
|
||
if (flatDir.sqrMagnitude > 0.001f)
|
||
{
|
||
transform.rotation = Quaternion.Slerp(
|
||
transform.rotation, Quaternion.LookRotation(flatDir), Time.deltaTime * 10f);
|
||
}
|
||
|
||
// Лазер снайпера виден за 0.5 сек до выстрела
|
||
if (_sniperLaser != null)
|
||
{
|
||
_sniperLaser.enabled = _fireTimer < 0.5f;
|
||
if (_sniperLaser.enabled)
|
||
{
|
||
_sniperLaser.SetPosition(0, eyePos);
|
||
_sniperLaser.SetPosition(1, targetPos);
|
||
}
|
||
}
|
||
|
||
// Стрельба по игроку
|
||
if (dist <= profile.weaponRange && _fireTimer <= 0f)
|
||
{
|
||
ShootAtPlayer(eyePos, toPlayer.normalized);
|
||
}
|
||
}
|
||
|
||
private void FixedUpdate()
|
||
{
|
||
if (IsDead || _playerTransform == null) return;
|
||
|
||
Vector3 eyePos = transform.position + Vector3.up * eyeHeight;
|
||
Vector3 targetPos = _playerTransform.position + Vector3.up * 1.0f;
|
||
Vector3 toPlayer = targetPos - eyePos;
|
||
float dist = toPlayer.magnitude;
|
||
|
||
if (dist > detectionRange || !HasLineOfSight(eyePos, targetPos, dist))
|
||
{
|
||
_rb.linearVelocity = new Vector3(0f, _rb.linearVelocity.y, 0f);
|
||
return;
|
||
}
|
||
|
||
Vector3 forward = new Vector3(toPlayer.x, 0f, toPlayer.z).normalized;
|
||
Vector3 right = Vector3.Cross(Vector3.up, forward);
|
||
Vector3 moveDir = Vector3.zero;
|
||
|
||
// Тактика передвижения зависит от класса!
|
||
switch (enemyClass)
|
||
{
|
||
case BodyClass.Scout:
|
||
// Агрессивно сближается зигзагами
|
||
if (dist > 3.5f) moveDir = (forward + right * (_strafeDir * 0.65f)).normalized;
|
||
else moveDir = right * _strafeDir;
|
||
break;
|
||
|
||
case BodyClass.Trooper:
|
||
// Держит среднюю дистанцию (8-14м) и стрейфит
|
||
if (dist > 14f) moveDir = forward + right * (_strafeDir * 0.3f);
|
||
else if (dist < 7f) moveDir = -forward + right * (_strafeDir * 0.5f);
|
||
else moveDir = right * _strafeDir;
|
||
moveDir.Normalize();
|
||
break;
|
||
|
||
case BodyClass.Juggernaut:
|
||
// Медленно и неотвратимо идёт напролом
|
||
if (dist > 4f) moveDir = forward;
|
||
break;
|
||
|
||
case BodyClass.Sniper:
|
||
// Держится далеко (отступает, если игрок ближе 15м)
|
||
if (dist < 15f) moveDir = -forward + right * (_strafeDir * 0.25f);
|
||
else if (dist > 28f) moveDir = forward;
|
||
moveDir.Normalize();
|
||
break;
|
||
}
|
||
|
||
Vector3 vel = moveDir * (profile.moveSpeed * 0.8f);
|
||
_rb.linearVelocity = new Vector3(vel.x, _rb.linearVelocity.y, vel.z);
|
||
}
|
||
|
||
private bool HasLineOfSight(Vector3 from, Vector3 to, float dist)
|
||
{
|
||
Vector3 dir = (to - from).normalized;
|
||
if (Physics.Raycast(from, dir, out RaycastHit hit, dist, obstacleMask, QueryTriggerInteraction.Ignore))
|
||
{
|
||
if (hit.transform == _playerTransform || hit.transform.root == _playerTransform)
|
||
return true;
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
private void ShootAtPlayer(Vector3 origin, Vector3 baseDir)
|
||
{
|
||
_fireTimer = profile.fireRate * 1.6f;
|
||
|
||
int pellets = Mathf.Max(1, profile.pelletsPerShot);
|
||
float spread = Mathf.Max(1.5f, profile.spreadAngle + 1.5f);
|
||
|
||
// Сдвигаем точку вылета чуть вперёд от глаз врага
|
||
Vector3 muzzlePos = origin + baseDir * 0.6f;
|
||
|
||
for (int i = 0; i < pellets; i++)
|
||
{
|
||
Vector3 dir = Quaternion.Euler(
|
||
Random.Range(-spread, spread),
|
||
Random.Range(-spread, spread), 0f) * baseDir;
|
||
|
||
Projectile.Spawn(muzzlePos, dir, profile, false, gameObject, obstacleMask);
|
||
}
|
||
}
|
||
|
||
public void TakeDamage(float amount)
|
||
{
|
||
if (IsDead) return;
|
||
CurrentHealth -= amount;
|
||
|
||
if (CurrentHealth <= 0f)
|
||
{
|
||
IsDead = true;
|
||
Destroy(gameObject);
|
||
}
|
||
}
|
||
}
|
||
} |