361 lines
9.3 KiB
C#
361 lines
9.3 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);
|
|
[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<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;
|
|
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<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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|