Enemy의 이동 및 공격 로직 개선

포탈 추가
This commit is contained in:
2026-01-13 11:24:32 +09:00
parent 022bc48bc5
commit f54c4b35b9
8 changed files with 837 additions and 292 deletions

View File

@@ -1,26 +1,69 @@
using System.Collections;
using UnityEngine;
public class EnemyAttack : MonoBehaviour
{
[Header("Attack Settings")]
[SerializeField] private float damage = 10f;
[SerializeField] private float attackCooldown = 1.0f; // 공격 간격 (1초)
public float damage = 10f;
public float attackRange = 2.0f;
public float attackCooldown = 1.0f;
private float _nextAttackTime = 0f;
private Material _enemyMaterial;
private float _nextAttackTime;
private Transform _target;
private IDamageable _targetDamageable;
// EnemyAttack.cs
private void OnCollisionStay(Collision collision)
// 쉐이더 프로퍼티 ID (문자열보다 성능이 좋음)
private static readonly int EmissionColorProp = Shader.PropertyToID("_EmissionColor");
void Update()
{
if (Time.time >= _nextAttackTime)
// 이동 스크립트로부터 현재 목표를 실시간으로 가져옵니다.
if (_target == null)
{
// 상대방에게서 IDamageable 인터페이스가 있는지 확인
IDamageable target = collision.gameObject.GetComponent<IDamageable>();
// EnemyMoveDefault에서 _target을 public이나 프로퍼티로 만들어 가져오세요
_target = GetComponent<EnemyMoveDefault>().Target;
if (_target != null) _targetDamageable = _target.GetComponent<IDamageable>();
return;
}
if (target != null)
float distance = Vector3.Distance(transform.position, _target.position);
// 사거리 안에 있고 타겟이 살아있을 때만 공격
if (distance <= attackRange)
{
if (Time.time >= _nextAttackTime)
{
target.TakeDamage(damage);
_nextAttackTime = Time.time + attackCooldown;
Attack();
}
}
}
private void Attack()
{
if (_target == null) return;
if (_targetDamageable == null)
{
_targetDamageable = _target.GetComponentInParent<IDamageable>();
}
if (_targetDamageable != null)
{
_targetDamageable.TakeDamage(damage);
_nextAttackTime = Time.time + attackCooldown;
}
else
{
// 대상이 IDamageable이 아니거나 이미 파괴됨
_target = null;
}
}
public void SetAttackTarget(Transform newTarget)
{
_target = newTarget;
if (_target != null)
_targetDamageable = _target.GetComponent<IDamageable>();
}
}

View File

@@ -1,45 +1,129 @@
using System.Collections;
using UnityEngine;
using UnityEngine.AI; // NavMesh 기능을 위해 필수
using UnityEngine.AI;
public class EnemyAI : MonoBehaviour
[RequireComponent(typeof(NavMeshAgent))]
public class EnemyMoveDefault : MonoBehaviour
{
[Header("Movement Settings")]
[SerializeField] private Transform _finalTarget; // 최종 목적지 (Core)
[SerializeField] private float _updateInterval = 0.2f; // 경로 갱신 간격
[SerializeField] private float _detectionRange = 1.5f; // 전방 장애물 감지 거리
private Transform _currentTarget; // 현재 추적 중인 대상
private NavMeshAgent _agent;
private Transform _target;
private EnemyAttack _attackScript;
private Coroutine _pathUpdateCoroutine;
// EnemyAttack 스크립트에서 현재 타겟을 참조하기 위한 프로퍼티
public Transform Target => _currentTarget;
void Awake()
{
_agent = GetComponent<NavMeshAgent>();
_attackScript = GetComponent<EnemyAttack>();
// 인스펙터에서 비어있다면 태그로 자동으로 찾음
if (_finalTarget == null)
{
GameObject core = GameObject.FindWithTag("Core"); // Core 오브젝트에 "Core" 태그를 달아주세요.
if (core != null) _finalTarget = core.transform;
}
_currentTarget = _finalTarget;
}
void Start()
void OnEnable()
{
// --- 2번 방법: 위치 보정 로직 시작 ---
// 현재 위치에서 반경 2.0f 이내에 가장 가까운 NavMesh가 있는지 검사합니다.
if (NavMesh.SamplePosition(transform.position, out NavMeshHit hit, 2.0f, NavMesh.AllAreas))
{
// 에이전트를 해당 위치로 강제 순간이동(Warp) 시킵니다.
// 이 작업은 에러를 방지하고 에이전트를 활성화합니다.
_agent.Warp(hit.position);
}
else
{
Debug.LogError($"{gameObject.name} 근처에 NavMesh를 찾을 수 없습니다! 스폰 위치를 확인하세요.");
}
// --- 2번 방법 끝 ---
if (_pathUpdateCoroutine != null) StopCoroutine(_pathUpdateCoroutine);
_pathUpdateCoroutine = StartCoroutine(UpdatePathRoutine());
}
// 기존 타겟(Core) 설정 로직
GameObject coreObj = GameObject.FindWithTag("Core");
if (coreObj != null)
void OnDisable()
{
if (_pathUpdateCoroutine != null) StopCoroutine(_pathUpdateCoroutine);
}
IEnumerator UpdatePathRoutine()
{
// Agent가 NavMesh에 배치될 시간을 줌
yield return null;
WaitForSeconds delay = new WaitForSeconds(_updateInterval);
while (true)
{
_target = coreObj.transform;
// 1. 타겟 유효성 체크: 타겟이 파괴되었으면 다시 코어로 설정
if (_currentTarget == null || !_currentTarget.gameObject.activeInHierarchy)
{
_currentTarget = _finalTarget;
_agent.isStopped = false;
}
if (_currentTarget != null && _agent.isOnNavMesh)
{
float distance = Vector3.Distance(transform.position, _currentTarget.position);
// 2. 공격 사거리 체크 (EnemyAttack의 사거리 참조)
if (distance <= _attackScript.attackRange)
{
// 사거리 안이면 멈춤
_agent.isStopped = true;
_agent.velocity = Vector3.zero;
}
else
{
// 3. 사거리 밖이면 전방 장애물(성벽 등) 감지
DetectObstacle();
// 4. 장애물 때문에 멈춘 게 아니라면 목적지로 전진
if (!_agent.isStopped)
{
_agent.SetDestination(_currentTarget.position);
}
}
}
yield return delay;
}
}
void FixedUpdate()
/// <summary>
/// 레이캐스트를 쏴서 전방에 길을 막는 IDamageable(성벽)이 있는지 확인
/// </summary>
private void DetectObstacle()
{
if (_target != null && _agent.isOnNavMesh)
RaycastHit hit;
// 적의 위치(발밑)보다 약간 위(가슴 높이)에서 전방으로 레이 발사
Vector3 rayOrigin = transform.position + Vector3.up * 1.0f;
// Scene 뷰에서 레이를 시각적으로 확인 (디버깅용)
Debug.DrawRay(rayOrigin, transform.forward * _detectionRange, Color.yellow);
if (Physics.Raycast(rayOrigin, transform.forward, out hit, _detectionRange))
{
_agent.SetDestination(_target.position);
// 부딪힌 대상에게서 IDamageable 인터페이스 확인
IDamageable damageable = hit.collider.GetComponentInParent<IDamageable>();
// 대상이 존재하고, 그 대상이 현재 코어가 아닐 때 (즉, 가로막는 성벽일 때)
if (damageable != null && hit.collider.transform != _finalTarget)
{
_currentTarget = hit.collider.transform;
// 공격 스크립트에도 즉시 새로운 타겟을 알려줌
_attackScript.SetAttackTarget(_currentTarget);
// 일단 멈추고 다음 루프에서 사거리 체크를 받도록 함
_agent.isStopped = true;
_agent.velocity = Vector3.zero;
}
}
}
/// <summary>
/// 외부(스패너 등)에서 최종 목적지를 설정해줄 때 사용
/// </summary>
public void SetFinalTarget(Transform target)
{
_finalTarget = target;
_currentTarget = target;
}
}

View File

@@ -1,19 +1,34 @@
using UnityEngine;
using System;
// Gate.cs
public class Gate : MonoBehaviour, IDamageable
{
[SerializeField] private float maxHealth = 50f;
[SerializeField] private float currentHealth = 50f;
private float CurrentHealth;
// 체력이 변경될 때 UI 등에 알리기 위한 이벤트 (Observer 패턴)
public static event Action<float> OnHealthChanged;
public static event Action OnGateDestroyed;
void Awake() => currentHealth = maxHealth;
public void TakeDamage(float amount)
{
currentHealth -= amount;
if (currentHealth <= 0)
OnHealthChanged?.Invoke(currentHealth / maxHealth);
if (currentHealth <= 0)
{
gameObject.SetActive(false);
var obstacle = GetComponent<UnityEngine.AI.NavMeshObstacle>();
if(obstacle != null)
{
obstacle.carving = false;
obstacle.enabled = false;
}
OnGateDestroyed?.Invoke();
Destroy(gameObject, 0.1f);
}
}
}

View File

@@ -11,17 +11,22 @@ public class Portal : MonoBehaviour
// 플레이어 태그 확인 및 쿨타임 체크
if (other.CompareTag("Player") && Time.time > _lastTeleportTime + cooldown)
{
// 상대방 포탈의 쿨타임도 같이 설정해야 무한 루프를 방지함
CharacterController cc = other.GetComponent<CharacterController>();
// 1. 상대방 포탈 쿨타임 설정
Portal destPortal = destination.GetComponent<Portal>();
if (destPortal != null) destPortal._lastTeleportTime = Time.time;
_lastTeleportTime = Time.time;
// 플레이어 위치 이동
// CharacterController나 Rigidbody를 사용 중이라면 이동 방식에 주의
other.transform.position = destination.position;
// 2. CharacterController 잠시 끄기 (중요!)
if (cc != null) cc.enabled = false;
// 3. 위치 이동
other.transform.position = destination.position;
Debug.Log("Teleported to " + destination.name);
// 4. 다시 켜기
if (cc != null) cc.enabled = true;
}
}
}