using UnityEngine; // Simple 2D enemy brain: // detects the player, chases on the X axis, and deals damage after an attack windup. [RequireComponent(typeof(Enemy))] [RequireComponent(typeof(Rigidbody2D))] public class EnemyAI : MonoBehaviour { private enum AIState { Idle, Chase, Attack } [Header("Target")] [SerializeField] private Transform _target; [SerializeField] private bool _autoFindPlayer = true; [SerializeField] private LayerMask _targetLayer = 1 << 7; // Player layer [SerializeField] private float _detectRange = 6f; [SerializeField] private float _detectVerticalRange = 2.5f; [SerializeField] private float _loseRange = 8f; [SerializeField] private float _loseVerticalRange = 4f; [Header("Movement")] [SerializeField] private float _moveSpeed = 2f; [SerializeField] private float _stopDistance = 0.75f; [Header("Attack")] [SerializeField] private float _attackRange = 1f; [SerializeField] private float _attackVerticalTolerance = 1f; [SerializeField] private int _attackDamage = 10; [SerializeField] private float _attackWindup = 0.25f; [SerializeField] private float _attackLockDuration = 0.45f; [SerializeField] private float _attackCooldown = 1f; [SerializeField] private Vector2 _attackHitVelocity = new Vector2(3f, 0f); [SerializeField] private string _targetHitReactionAnimationState; [Header("Animation")] [SerializeField] private string _idleAnimationState = ""; [SerializeField] private string _runAnimationState = ""; [SerializeField] private string _attackAnimationState = ""; private Enemy _enemy; private Health _health; private Rigidbody2D _rb; private Animator _anim; private SpriteRenderer _spriteRenderer; private Health _targetHealth; private IDamageable _targetDamageable; private AIState _state = AIState.Idle; private bool _hasAggro; private bool _isAttacking; private bool _hasAppliedAttackDamage; private float _attackTimer; private float _attackCooldownTimer; private float _facingDirection = 1f; private string _activeAnimationState; private void Awake() { _enemy = GetComponent(); _health = GetComponent(); _rb = GetComponent(); _anim = GetComponentInChildren(); _spriteRenderer = GetComponentInChildren(); } private void OnEnable() { _state = AIState.Idle; _hasAggro = false; _isAttacking = false; _hasAppliedAttackDamage = false; _attackTimer = 0f; _attackCooldownTimer = 0f; ResolveTarget(); } private void Update() { TickCooldown(); if (_health != null && _health.IsDead) { StopHorizontalMovement(); enabled = false; return; } ResolveTarget(); if (_target == null) { if (_isAttacking) CancelAttack(); _state = AIState.Idle; PlayStateAnimation(); return; } if (!CanControl()) { if (_isAttacking) CancelAttack(); if (_enemy != null && !_enemy.CanUseAI) return; _state = AIState.Idle; PlayStateAnimation(); return; } RefreshAggro(); UpdateFacing(); if (_isAttacking) { TickAttack(); PlayStateAnimation(); return; } if (!_hasAggro) { _state = AIState.Idle; PlayStateAnimation(); return; } if (IsTargetInAttackRange() && _attackCooldownTimer <= 0f) BeginAttack(); else _state = IsTargetInAttackRange() ? AIState.Idle : AIState.Chase; PlayStateAnimation(); } private void FixedUpdate() { if (_rb == null) return; if (_health != null && _health.IsDead) { StopHorizontalMovement(); return; } if (_enemy != null && !_enemy.CanUseAI) return; if (!CanMove()) { StopHorizontalMovement(); return; } Vector2 velocity = _rb.linearVelocity; float dx = _target.position.x - transform.position.x; if (Mathf.Abs(dx) <= _stopDistance) velocity.x = 0f; else velocity.x = Mathf.Sign(dx) * _moveSpeed; _rb.linearVelocity = velocity; } private void ResolveTarget() { if (_target != null) { CacheTargetComponents(); return; } if (!_autoFindPlayer) return; PlayerController player = FindFirstObjectByType(); if (player == null) return; _target = player.transform; CacheTargetComponents(); } private void CacheTargetComponents() { if (_target == null) { _targetHealth = null; _targetDamageable = null; return; } _targetHealth = _target.GetComponent(); _targetDamageable = _target.GetComponent(); } private bool CanControl() { if (_enemy != null && !_enemy.CanUseAI) return false; if (_targetHealth != null && _targetHealth.IsDead) return false; return true; } private bool CanMove() { return _rb != null && _target != null && CanControl() && _hasAggro && !_isAttacking && _state == AIState.Chase && !IsTargetInAttackRange(); } private void RefreshAggro() { if (_target == null) { _hasAggro = false; return; } Vector2 delta = _target.position - transform.position; float absX = Mathf.Abs(delta.x); float absY = Mathf.Abs(delta.y); if (_hasAggro) { _hasAggro = absX <= _loseRange && absY <= _loseVerticalRange; return; } _hasAggro = absX <= _detectRange && absY <= _detectVerticalRange; } private bool IsTargetInAttackRange() { if (_target == null) return false; Vector2 delta = _target.position - transform.position; return Mathf.Abs(delta.x) <= _attackRange && Mathf.Abs(delta.y) <= _attackVerticalTolerance; } private void BeginAttack() { _state = AIState.Attack; _isAttacking = true; _hasAppliedAttackDamage = false; _attackTimer = 0f; _attackCooldownTimer = Mathf.Max(_attackCooldown, _attackLockDuration); StopHorizontalMovement(); UpdateFacing(); } private void TickAttack() { _attackTimer += Time.deltaTime; StopHorizontalMovement(); if (!_hasAppliedAttackDamage && _attackTimer >= _attackWindup) { _hasAppliedAttackDamage = true; TryApplyAttackDamage(); } if (_attackTimer >= _attackLockDuration) { _isAttacking = false; _state = _hasAggro && !IsTargetInAttackRange() ? AIState.Chase : AIState.Idle; } } private void CancelAttack() { _isAttacking = false; _hasAppliedAttackDamage = false; _attackTimer = 0f; } private void TryApplyAttackDamage() { if (!IsTargetInAttackRange()) return; if (_targetDamageable == null) CacheTargetComponents(); if (_targetDamageable == null) return; if (!IsTargetLayerValid()) return; Vector2 hitVelocity = new Vector2(_attackHitVelocity.x * _facingDirection, _attackHitVelocity.y); _targetDamageable.TakeDamage(_attackDamage, hitVelocity, _targetHitReactionAnimationState); } private bool IsTargetLayerValid() { if (_target == null) return false; if (_targetLayer.value == 0) return true; return (_targetLayer.value & (1 << _target.gameObject.layer)) != 0; } private void TickCooldown() { if (_attackCooldownTimer > 0f) _attackCooldownTimer -= Time.deltaTime; } private void StopHorizontalMovement() { if (_rb == null) return; Vector2 velocity = _rb.linearVelocity; velocity.x = 0f; _rb.linearVelocity = velocity; } private void UpdateFacing() { if (_target == null) return; float dx = _target.position.x - transform.position.x; if (Mathf.Abs(dx) <= 0.01f) return; _facingDirection = Mathf.Sign(dx); if (_spriteRenderer != null) _spriteRenderer.flipX = _facingDirection < 0f; } private void PlayStateAnimation() { string nextState = _state switch { AIState.Chase => _runAnimationState, AIState.Attack => _attackAnimationState, _ => _idleAnimationState }; if (_anim == null || string.IsNullOrEmpty(nextState)) return; if (_activeAnimationState == nextState) return; _anim.Play(nextState); _activeAnimationState = nextState; } private void OnDrawGizmosSelected() { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, _detectRange); Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, _attackRange); } }