This commit is contained in:
qhdyd1122
2026-01-12 15:14:08 +09:00
25 changed files with 759 additions and 25 deletions

View File

@@ -0,0 +1,26 @@
using UnityEngine;
public class EnemyAttack : MonoBehaviour
{
[Header("Attack Settings")]
[SerializeField] private float damage = 10f;
[SerializeField] private float attackCooldown = 1.0f; // 공격 간격 (1초)
private float _nextAttackTime = 0f;
// EnemyAttack.cs
private void OnCollisionStay(Collision collision)
{
if (Time.time >= _nextAttackTime)
{
// 상대방에게서 IDamageable 인터페이스가 있는지 확인
IDamageable target = collision.gameObject.GetComponent<IDamageable>();
if (target != null)
{
target.TakeDamage(damage);
_nextAttackTime = Time.time + attackCooldown;
}
}
}
}

View File

@@ -0,0 +1,45 @@
using UnityEngine;
using UnityEngine.AI; // NavMesh 기능을 위해 필수
public class EnemyAI : MonoBehaviour
{
private NavMeshAgent _agent;
private Transform _target;
void Awake()
{
_agent = GetComponent<NavMeshAgent>();
}
void Start()
{
// --- 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번 방법 끝 ---
// 기존 타겟(Core) 설정 로직
GameObject coreObj = GameObject.FindWithTag("Core");
if (coreObj != null)
{
_target = coreObj.transform;
}
}
void FixedUpdate()
{
if (_target != null && _agent.isOnNavMesh)
{
_agent.SetDestination(_target.position);
}
}
}

View File

@@ -0,0 +1,23 @@
using UnityEngine;
using System;
public class Core : MonoBehaviour, IDamageable
{
[SerializeField] private float maxHealth = 100f;
private float _currentHealth;
// 체력이 변경될 때 UI 등에 알리기 위한 이벤트 (Observer 패턴)
public static event Action<float> OnHealthChanged;
public static event Action OnCoreDestroyed;
void Awake() => _currentHealth = maxHealth;
public void TakeDamage(float amount)
{
_currentHealth -= amount;
OnHealthChanged?.Invoke(_currentHealth / maxHealth);
if (_currentHealth <= 0)
OnCoreDestroyed?.Invoke();
}
}

View File

@@ -0,0 +1,36 @@
using UnityEngine;
using UnityEngine.SceneManagement; // 씬 재시작용
public class GameManager : MonoBehaviour
{
private bool _isGameOver = false;
private void OnEnable()
{
// Core의 파괴 이벤트를 구독
Core.OnCoreDestroyed += GameOver;
}
private void OnDisable()
{
Core.OnCoreDestroyed -= GameOver;
}
private void GameOver()
{
if (_isGameOver) return;
_isGameOver = true;
Debug.Log("Game Over! Core has been destroyed.");
// 여기에 패배 UI 표시 로직 등을 넣습니다.
// 예: 3초 후 게임 재시작
Invoke(nameof(RestartGame), 3f);
}
private void RestartGame()
{
// 현재 활성화된 씬을 다시 로드
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
}

View File

@@ -0,0 +1,18 @@
using UnityEngine;
using System;
// Gate.cs
public class Gate : MonoBehaviour, IDamageable
{
[SerializeField] private float maxHealth = 50f;
private float _currentHealth;
void Awake() => _currentHealth = maxHealth;
public void TakeDamage(float amount)
{
_currentHealth -= amount;
if (_currentHealth <= 0)
gameObject.SetActive(false);
}
}

View File

@@ -0,0 +1,5 @@
// IDamageable.cs
public interface IDamageable
{
void TakeDamage(float amount);
}

View File

@@ -0,0 +1,27 @@
using UnityEngine;
public class Portal : MonoBehaviour
{
[SerializeField] private Transform destination; // 순간이동할 목적지 (반대편 포탈의 위치)
[SerializeField] private float cooldown = 1f; // 연속 이동 방지 쿨타임
private float _lastTeleportTime;
private void OnTriggerEnter(Collider other)
{
// 플레이어 태그 확인 및 쿨타임 체크
if (other.CompareTag("Player") && Time.time > _lastTeleportTime + cooldown)
{
// 상대방 포탈의 쿨타임도 같이 설정해야 무한 루프를 방지함
Portal destPortal = destination.GetComponent<Portal>();
if (destPortal != null) destPortal._lastTeleportTime = Time.time;
_lastTeleportTime = Time.time;
// 플레이어 위치 이동
// CharacterController나 Rigidbody를 사용 중이라면 이동 방식에 주의
other.transform.position = destination.position;
Debug.Log("Teleported to " + destination.name);
}
}
}

View File

@@ -0,0 +1,76 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
[System.Serializable] // 이 속성이 있어야 인스펙터에 노출됩니다.
public class Wave
{
public string waveName; // 웨이브 식별용 이름
public GameObject enemyPrefab; // 소환할 적 프리팹
public int count; // 소환할 마릿수
public float spawnRate; // 적 한 마리당 소환 간격 (초)
}
public class WaveManager : MonoBehaviour
{
[Header("Wave Settings")]
[SerializeField] private List<Wave> waves; // 웨이브 데이터 리스트
[SerializeField] private float timeBetweenWaves = 5f; // 웨이브 간 대기 시간
[SerializeField] private Transform[] spawnPoints; // 적이 나타날 위치들
private Transform _target;
private int _currentWaveIndex = 0;
void Start()
{
_target = GameObject.FindWithTag("Core").transform;
// 게임 시작 시 웨이브 루틴 시작
StartCoroutine(StartWaveRoutine());
}
IEnumerator StartWaveRoutine()
{
// 모든 웨이브를 순회
while (_currentWaveIndex < waves.Count)
{
Wave currentWave = waves[_currentWaveIndex];
Debug.Log($"Wave {_currentWaveIndex + 1}: {currentWave.waveName} 시작!");
// 1. 적 소환 로직
for (int i = 0; i < currentWave.count; i++)
{
SpawnEnemy(currentWave.enemyPrefab);
// 지정된 간격만큼 대기 (이게 코루틴의 핵심입니다)
yield return new WaitForSeconds(currentWave.spawnRate);
}
// 2. 다음 웨이브 전까지 대기
Debug.Log("웨이브 종료. 다음 웨이브 대기 중...");
yield return new WaitForSeconds(timeBetweenWaves);
_currentWaveIndex++;
}
Debug.Log("모든 웨이브가 종료되었습니다!");
}
void SpawnEnemy(GameObject enemyPrefab)
{
// 1. 스폰 위치 선택
Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
// 2. 방향 계산 (Core - SpawnPoint)
Vector3 directionToCore = (_target.position - spawnPoint.position).normalized;
// 3. 방향을 Quaternion 회전값으로 변환 (Y축 기준으로만 회전하도록 설정)
// 윗방향(Vector3.up)을 축으로 하여 해당 방향을 바라보게 함
Quaternion lookRotation = Quaternion.LookRotation(new Vector3(directionToCore.x, 0, directionToCore.z));
// 4. 계산된 위치와 회전값으로 생성
Instantiate(enemyPrefab, spawnPoint.position, lookRotation);
}
}