2026-05-20 몬스터AI추가
This commit is contained in:
360
Assets/02_Scripts/Enemy/EnemyAI.cs
Normal file
360
Assets/02_Scripts/Enemy/EnemyAI.cs
Normal file
@@ -0,0 +1,360 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user