Files
WhiteMan_Unity2D/Assets/02_Scripts/Enemy/EnemyAI.cs

376 lines
9.7 KiB
C#

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);
[Header("Animation")]
[SerializeField] private string _idleAnimationState = "Idle";
[SerializeField] private string _runAnimationState = "Run";
[SerializeField] private string _attackAnimationState = "PunchA";
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<Enemy>();
_health = GetComponent<Health>();
_rb = GetComponent<Rigidbody2D>();
_anim = GetComponentInChildren<Animator>();
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
}
private void OnEnable()
{
_state = AIState.Idle;
_hasAggro = false;
_isAttacking = false;
_hasAppliedAttackDamage = false;
_attackTimer = 0f;
_attackCooldownTimer = 0f;
if (_enemy != null)
_enemy.OnDamaged += HandleDamaged;
ResolveTarget();
}
private void OnDisable()
{
if (_enemy != null)
_enemy.OnDamaged -= HandleDamaged;
}
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<PlayerController>();
if (player == null) return;
_target = player.transform;
CacheTargetComponents();
}
private void CacheTargetComponents()
{
if (_target == null)
{
_targetHealth = null;
_targetDamageable = null;
return;
}
_targetHealth = _target.GetComponent<Health>();
_targetDamageable = _target.GetComponent<IDamageable>();
}
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;
_state = AIState.Idle;
_activeAnimationState = null;
}
private void HandleDamaged()
{
CancelAttack();
_attackCooldownTimer = Mathf.Max(_attackCooldownTimer, _attackCooldown);
}
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);
}
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);
}
}