Unity 미니 던전 게임 #003

주환서·2026년 2월 22일

개요

  1. 3개의 방이 있습니다.
  2. 각 방의 몬스터들을 해치우면 다음 방으로 갈 수 있는 문이 열립니다.
  3. 각 방의 몬스터들은 갈수록 강해집니다.(크기 Up, Hp Up, 공격력 Up)

구현 과정

기본적인 기믹들

  1. 우선 Player는 RigidBody를 가지고 있고 맵의 바닥과 벽들에는 Collider 컴포넌트들을 가지고 있습니다. 장애물을 설치하고 NavMesh를 이용해서 물리적인 충돌을 만듭니다.
  2. Player의 이동과 공격을 구현하고, Monster들의 현재 상태, 능력치를 설정합니다.
  3. Player의 공격을 구현할 때 인위적으로 총이 나가는 포지션 오브젝트를 만들고 Combat 스트립트에서 직렬화 해서 공격을 구현했습니다.
  4. 총알 관리를 큐(Queue)를 이용해서 메모리 사용을 줄이고, 총알은 오브젝트와 닿으면 삭제처리를 했습니다.
  5. 총알과 몬스터는 Prefabs로 관리합니다.

Player 이동, 공격 구현

// CharacterMovement.cs
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.SceneManagement;

public enum GameMode
{
    LOBBY, SPACE_INVADER
}

public class CharacterMovement : MonoBehaviour
{
    Transform _transform;
    Animator _anim;
    float _h, _v, _r;

    [SerializeField] private float _moveSpeed = 5f;  // 초당 이동 거리
    [SerializeField] private float _turnSpeed = 60f; // 초당 회전 속도

    [SerializeField] private GameMode _mode = GameMode.LOBBY;

    void Start()
    {
        _transform = gameObject.GetComponent<Transform>();
        _anim = gameObject.GetComponent<Animator>();

        Cursor.lockState = CursorLockMode.Locked;   // 커서를 화면 중앙에 고정, 안 보임
        if (SceneManager.GetActiveScene().name == "Lobby")
        {
            _mode = GameMode.LOBBY;
        }
        else if(SceneManager.GetActiveScene().name == "SpaceInvader" ||
            SceneManager.GetActiveScene().name == "Practice")
        {
            _mode = GameMode.SPACE_INVADER;
        }
    }

    void Update()
    {
        switch(_mode)
        {
            case GameMode.LOBBY:
                UpdateForLobby();
                break;
            case GameMode.SPACE_INVADER:
                UpdateForSpaceInvader();
                break;
        }
    }

    void UpdateForLobby()
    {
        _r = Input.GetAxis("Mouse X");
        _transform.Rotate(Vector3.up * _r * _turnSpeed * Time.deltaTime);

        _h = Input.GetAxis("Horizontal");
        _v = Input.GetAxis("Vertical");

        if (_v != 0)
        {
            _transform.Translate(Vector3.forward * _v * _moveSpeed * Time.deltaTime);
        }
        if (_h != 0)
        {
            _transform.Translate(Vector3.right * _h * _moveSpeed * Time.deltaTime);
        }

        _anim.SetFloat("Forward", _v);
        _anim.SetFloat("Side", _h);

    }

    void UpdateForSpaceInvader()
    {
        _h = Input.GetAxis("Horizontal");
        if (_h != 0)
        {
            _transform.Translate(Vector3.right * _h * _moveSpeed * Time.deltaTime);
        }

        _anim.SetFloat("Side", _h);
    }
}
// Combat.cs
/*
 * 1. Vector의 개념과 연산 (합, 차, 스칼라 곱) 정리하기
 * 2. 특정 위치에 가면 동작하는 기믹으로 발전
 * 3. 자유 기믹 1종 추가
 */

using Unity.Cinemachine;
using UnityEngine;

public class Combat : MonoBehaviour
{
    [SerializeField] private Transform _firePosition;   // 생성 위치, 회전
    [SerializeField] private float _fireInterval = 0.2f;
    [SerializeField] private float _hp = 50f;

    private bool _isAlive = true;
    private float _fireCooldown;

    private CinemachineImpulseSource _impulseSource;
    public bool IsAlive { get => _isAlive; }

    void Start()
    {
        _fireCooldown = _fireInterval;

        _impulseSource = GetComponent<CinemachineImpulseSource>();
    }

    void Update()
    {
        if (_fireCooldown > 0)
        {
            _fireCooldown -= Time.deltaTime;
        }

        if (_fireCooldown > 0) return;

        if (Input.GetAxis("Fire1") > 0f)
        {
            // 총알 생성
            GameObject bullet = PoolManager.Instance.Get();
            bullet.transform.position = _firePosition.position;
            bullet.transform.rotation = _firePosition.rotation;
            bullet.GetComponent<BulletController>().Fire();

            // 플레이어 공격력으로 보정
            //bullet.GetComponent<BulletController>().Init(30f);
            _fireCooldown = _fireInterval;

            if (_impulseSource != null)
            {
                Debug.Log("반동");
                _impulseSource.GenerateImpulse();
            }
        }
    }

    public void TakeDamage(float damage)
    {
        if (_isAlive == false) return;

        _hp -= damage;
        if (_hp <= 0f)
        {
            _isAlive = false;
        }
    }
}
using UnityEditor.EditorTools;
using UnityEngine;

public class BulletController : MonoBehaviour
{
    [SerializeField] private float _fireForce = 1000f;

    Rigidbody _rb;

    private float _damage = 10f;
    public float Damage { get { return _damage; } private set { _damage = value; } }

    private void Awake()
    {
        _rb = GetComponent<Rigidbody>();
    }

    private void OnCollisionEnter(Collision collision)
    {
        PoolManager.Instance.Return(gameObject);
    }

    public void Init(float damage = 10f)
    {
        _damage = damage;
    }

    public void Fire()
    {
        _rb.linearVelocity = Vector3.zero;
        _rb.angularVelocity = Vector3.zero;

        //_rb.AddRelativeForce(Vector3.forward * _fireForce, ForceMode.Acceleration);

        _rb.AddForce(
            transform.forward * _fireForce, ForceMode.Acceleration);
    }
}

Monster 상태

using System.Collections;
using UnityEngine;
using UnityEngine.AI;

// FSM : Finite State Machine 유한 상태 기계
public enum MonsterState
{
    IDLE,
    PATROL,
    CHASE,
    COMBAT,
    DAMAGED,
    DEAD
}

public class MonsterAI : MonoBehaviour
{
    [SerializeField] private MonsterState _state = MonsterState.IDLE;

    private Animator _anim;
    private Collider _collider;
    private NavMeshAgent _agent;

    // 현재 실행 중인 코루틴
    private Coroutine _currentCoroutine;

    [Header("[Idle State]")]
    [SerializeField] private float _idleTime = 0f;

    [Header("[Patrol State]")]
    [SerializeField] private GameObject _destObj; // 목적지
    [SerializeField] private Vector3 _patrolDest; // 목적지 좌표
    [SerializeField] private float _rotateSpeed = 120f; // 회전 속도
    [SerializeField] private float _patrolAccel = 8f;
    [SerializeField] private float _patrolRange = 5f; // 걷는 범위
    [SerializeField] private float _patrolMoveSpeed = 3f; // 이동 속도
    [SerializeField] private float _patrolAngularSpeed = 350f; // 회전 속도

    [Header("[Chase State]")]
    [SerializeField] private float _chaseSpeed = 8f; // 추격 속도
    [SerializeField] private float _chaseAccel = 8f; // 추격 속도
    [SerializeField] private float _chaseStoppingDistance = 2f; // 추격 속도
    [SerializeField] private float _chaseRange = 8f; // 추격 범위
    [SerializeField] private float _chaseAngularSpeed = 3f;
    private Vector3 _prevPosition; // 추격 시작 전 원래 위치
    private GameObject _target; // 플레이어 타겟

    [Header("[Combat State]")]
    [SerializeField] private float _attackRange = 3.5f; // 공격 사거리
    [SerializeField] private float _damage = 5f; // 공격력
    [SerializeField] private float _attackInterval = 2f; // 공격 쿨타임
    [SerializeField] private float _hp = 20f; // 체력

    // 데미지 처리
    private float _pendingDamage = 0f;
    private bool _isAlive = true; // 생존 여부
    public bool IsAlive { get { return _isAlive; } }

    [Header("Damaged State")]
    [SerializeField] private float _damageStunTime = 0.5f; // 불렛 피격 시 경직 시간

    [Header("Dead State")]
    [SerializeField] private float _deadTime = 2f; // 몬스터 사망 후 없어지는 시간

    // 애니메이터 컨트롤러 파라미터 들고오기
    private int _hashPatrol = Animator.StringToHash("Patrol");
    private int _hashChase = Animator.StringToHash("Chase");
    private int _hashCombat = Animator.StringToHash("Combat");
    private int _hashDamaged = Animator.StringToHash("Damaged");
    private int _hashDead = Animator.StringToHash("Dead");

    private void Start()
    {
        _anim = GetComponent<Animator>();
        _collider = GetComponent<Collider>();
        _agent = GetComponent<NavMeshAgent>();

        // 게임 시작 시 초기 상태
        if (_agent != null)
        {
            _agent.updateRotation = true;
            _agent.angularSpeed = _rotateSpeed;
        }
        ChangeState(_state);
    }

    //private void Update()
    //{
    //    if (Input.GetKeyDown(KeyCode.Space))
    //    {
    //        if (_agent)
    //        {
    //            _agent.SetDestination(_target.transform.position);
    //        }
    //    }
    //}

    // 총알 충동 함수
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Bullet"))
        {
            BulletController bullet = collision.gameObject.GetComponent<BulletController>();
            TakeDamage(bullet.Damage);
        }
    }

    // 상태를 변경하고, 상태에 맞는 코루틴을 시작하는 함수
    private void ChangeState(MonsterState monState)
    {
        // 상태 전환 하고 이전에 실행 중이던 코루틴이 있으면 멈춤(다른 상태랑 꼬이게 하지 않기 위해)
        if (_currentCoroutine != null)
        {
            StopCoroutine(_currentCoroutine);   // 다른 상태랑 중복 되지 않게 일단 한번 멈춤
        }

        // 기본 몬스터 상태 = monState;
        _state = monState;
        ApplyAgentSetting(_state);

        // 새로운 상태에 맞는 코루틴을 시작, _ingCoroutine에 저장
        switch (_state)
        {
            case MonsterState.IDLE: _currentCoroutine = StartCoroutine(Co_Idle()); break;
            case MonsterState.PATROL: _currentCoroutine = StartCoroutine(Co_Patrol()); break;
            case MonsterState.CHASE: _currentCoroutine = StartCoroutine(Co_Chase()); break;
            case MonsterState.COMBAT: _currentCoroutine = StartCoroutine(Co_Combat()); break;
            case MonsterState.DAMAGED: _currentCoroutine = StartCoroutine(Co_Damaged()); break;
            case MonsterState.DEAD: _currentCoroutine = StartCoroutine(Co_Dead()); break;
        }
    }

    private IEnumerator Co_Idle()
    {
        SetAnimation(); // 모든 애니메이션 초기화

        // 대기 상태에 이동 멈춤
        if (_agent) _agent.isStopped = true;

        // 대기할 시간
        float waitTime = Random.Range(2f, 4f);
        _target = GameObject.FindGameObjectWithTag("Player");
        float elapsed = 0f; // 경과 시간

        //if (_agent)
        //{
        //    _agent.isStopped = true;
        //}

        // Update 역할 while로 매 프레임 체크
        while (true)
        {
            // 플레이어가 chase 범위 안이면
            if (IsFindTarget(_chaseRange))
            {
                // chase 상태로 전환
                ChangeState(MonsterState.CHASE);
                yield break; // 코루틴 즉시 종료 다음 내용은 진행 하지 않음
            }

            // 대기 시간이 지났으면
            elapsed += Time.deltaTime;
            if (elapsed >= waitTime)
            {
                // patrol 상태로 전환
                ChangeState(MonsterState.PATROL);
                yield break; // 코루틴 즉시 종료
            }
            // 다음 프레임까지 대기. 대기 끝난 후 뭘할지 정함
            yield return null;
        }
    }

    private IEnumerator Co_Patrol()
    {
        SetAnimation(hashPatrol: true); // 걷기 파라미터 트루

        // 현재 위치 기준으로 랜덤한 목적지 설정
        _patrolDest = transform.position + new Vector3(
            Random.Range(-_patrolRange, _patrolRange),
            0f,
            Random.Range(-_patrolRange, _patrolRange)
        );

        //if (_agent)
        //{
        //    _agent.isStopped = false;
        //}

        // 따라갈 큐브 위치 이동
        if (_destObj) _destObj.transform.position = _patrolDest;
        _target = GameObject.FindGameObjectWithTag("Player");

        // 목적지 설정, 이동
        if (_agent)
        {
            _agent.isStopped = false;
            _agent.speed = _patrolMoveSpeed;
            _agent.SetDestination(_patrolDest);
        }

        // update역할 while로 매 프레임 확인
        while (true)
        {
            // chase 범위안에서 타겟을 찾았으면 상태 변경
            if (IsFindTarget(_chaseRange))
            {
                ChangeState(MonsterState.CHASE);
                yield break;
            }

            // 목적지 도착 체크
            if (_agent && !_agent.pathPending && _agent.remainingDistance <= 0.2f)
            {
                ChangeState(MonsterState.IDLE);
                yield break;
            }

            // 이동 중인 방향을 바라보게 회전
            //if (_agent.velocity.sqrMagnitude > 0.1f)
            //{
            //    RotatetoDirection(_agent.velocity.normalized);
            //}

            //float dist = Vector3.Distance(transform.position, _patrolDest);
            //if (dist <= 0.1f) // 목적지에 거의 도달했으면
            //{
            //    ChangeState(MonsterState.IDLE); // 다시 idle 상태
            //    yield break;
            //}

            // 목적지 방향으로 이동하고 회전함
            //Vector3 dir = MovetoDirection(_patrolDest, _moveSpeed);
            //RotatetoDirection(dir);

            yield return null; // 대기 후 선택
        }
    }

    private IEnumerator Co_Chase()
    {
        SetAnimation(hashChase: true); // chase 파라미터 트루

        _prevPosition = transform.position; // 추격 시작전 위치 
        _target = GameObject.FindGameObjectWithTag("Player");

        // 추격 시작
        if (_agent)
        {
            _agent.isStopped = false;
            _agent.speed = _chaseSpeed;
        }

        // while 매프레임확인
        while (true)
        {
            // 타겟이 없으면(범위안에 없거나 죽으면) IDLE
            if (_target == null)
            {
                ChangeState(MonsterState.IDLE);
                yield break;
            }

            float dist = Vector3.Distance(transform.position, _target.transform.position);

            // 목적지 갱신
            if (_agent) _agent.SetDestination(_target.transform.position);

            // 공격 범위 췤
            if (dist <= _attackRange)
            {
                ChangeState(MonsterState.COMBAT); // 컴뱃 전환
                yield break;
            }

            // 범위 밖이면 
            if (dist > _chaseRange)
            {
                ChangeState(MonsterState.IDLE); // 대기 상태로
                yield break;
            }

            // 회전
            //if (_agent.velocity.sqrMagnitude > 0.1f)
            //{
            //    RotatetoDirection(_agent.velocity.normalized);
            //}

            // 플레이어 방향으로 이동 회전
            //if (_agent)
            //{
            //    _agent.SetDestination(_target.transform.position);
            //}

            //yield return new WaitForSeconds(0.1f);
            //Vector3 dir = MovetoDirection(_target.transform.position, _chaseSpeed);
            //RotatetoDirection(dir);

            yield return null; // 대기 후 상태 전환
        }
    }

    private IEnumerator Co_Combat()
    {
        SetAnimation(hashCombat: true); // combat 파라미터 트루면
        _target = GameObject.FindGameObjectWithTag("Player");

        // 공격 중에는 이동을 하지 않음
        if (_agent)
        {
            _agent.isStopped = true;
            _agent.velocity = Vector3.zero;
        }

        // 공격 쿨타임 초기화
        float currentCooldown = _attackInterval;

        // update
        while (true)
        {
            // 플레이어가 없거나 죽었으면 IDLE
            if (_target == null || _target.GetComponent<Combat>().IsAlive == false)
            {
                ChangeState(MonsterState.IDLE);
                yield break;
            }

            // 공격 중 플레이어 바라보게 하려면 수동 회전 해야함
            Vector3 dirToTarget = (_target.transform.position - transform.position).normalized;
            RotatetoDirection(dirToTarget);

            // 쿨타임 감소
            currentCooldown -= Time.deltaTime;

            // 공격 가능 시간이 되었을 때
            if (currentCooldown <= 0)
            {
                float distTarget = Vector3.Distance(transform.position, _target.transform.position);

                // 공격 범위 밖이면
                if (distTarget > _attackRange)
                {
                    ChangeState(MonsterState.CHASE); // 다시 쫓아감
                    yield break;
                }

                // else
                // 실제 공격 수행
                _target.GetComponent<Combat>().TakeDamage(_damage);
                currentCooldown = _attackInterval; // 쿨타임 리셋
            }

            yield return null; // 대기 후 상태 설정
        }
    }

    private IEnumerator Co_Damaged()
    {
        _anim.SetTrigger(_hashDamaged); // 데미지 파라미터 트리거 발동
        _hp -= _pendingDamage; // 데미지 적용

        // 피격 시 정지
        if (_agent)
        {
            _agent.isStopped = true;
            _agent.velocity = Vector3.zero;
        }

        // 체력이 0 이하라면 사망 후 상태 전환
        if (_hp <= 0)
        {
            _isAlive = false;
            ChangeState(MonsterState.DEAD);
            yield break;
        }

        // 스턴 시간동안 대기 - 0.5초
        yield return new WaitForSeconds(_damageStunTime);

        // 조건에 따라 다음 행동 결정
        if (IsFindTarget(_chaseRange)) // 체이스 범위
        {
            if (IsFindTarget(_attackRange)) // 공격 범위
            {
                ChangeState(MonsterState.COMBAT); // 바로 컴뱃 상태
            }
            else
            {
                ChangeState(MonsterState.CHASE); // 아니면 추격
            }
        }
        else
        {
            ChangeState(MonsterState.IDLE); // 주변에 없으면 idle
        }
    }

    private IEnumerator Co_Dead()
    {
        _anim.SetTrigger(_hashDead); // dead 트리거 발동
        _collider.enabled = false; // 시체랑 충돌하지 않도록 콜라이더 끔

        // 죽으면 navmeshagent 끄기
        if (_agent) _agent.enabled = false;

        // 사망 모션이 끝날 때까지 대기 - 2초
        yield return new WaitForSeconds(_deadTime);

        // 오브젝트 없어짐
        Destroy(gameObject);
    }

    // 이동 함수
    //private Vector3 MovetoDirection(Vector3 dest, float speed)
    //{
    //    Vector3 dir = dest - transform.position;
    //    dir.y = 0f; // 높이 무시
    //    dir.Normalize(); // 방향 벡터 정규화(크기 1로)
    //    transform.Translate(dir * speed * Time.deltaTime, Space.World); // 실제 이동

    //    return dir;
    //}

    // 방향 바라보게하는 함수
    private void RotatetoDirection(Vector3 dir)
    {
        if (dir == Vector3.zero) return; // 방향이 없으면 회전 안 함
        Quaternion targetRot = Quaternion.LookRotation(dir);
        // 회전
        transform.rotation = Quaternion.Slerp(
            transform.rotation, targetRot, _rotateSpeed * Time.deltaTime);
    }
    

    // 범위 안에 타겟 있나 없나
    private bool IsFindTarget(float range)
    {
        if (_target)
        {
            float distTarget = Vector3.Distance(transform.position, _target.transform.position);
            if (distTarget < range)
            {
                return true;
            }
        }
        return false;
    }

    // 데미지 처리 함수
    public void TakeDamage(float damage)
    {
        _pendingDamage = damage;
        // 데미지를 입으면 현재 어떤 상태이든 DAMAGED 상태로 전환(에니메이터 컨트롤러에 ANY STATE)
        ChangeState(MonsterState.DAMAGED);
    }

    // 파라미터 한번에 세팅
    public void SetAnimation(bool hashPatrol = false, bool hashChase = false, bool hashCombat = false)
    {
        _anim.SetBool(_hashPatrol, hashPatrol);
        _anim.SetBool(_hashChase, hashChase);
        _anim.SetBool(_hashCombat, hashCombat);
    }

    private void ApplyAgentSetting(MonsterState state)
    {
        if (_agent == null) return;

        switch (state)
        {
            case MonsterState.IDLE:
                _agent.isStopped = true;        // 멈춤
                _agent.velocity = Vector3.zero; // 미끄러짐 방지
                _agent.ResetPath();             // 경로 초기화
                break;

            case MonsterState.PATROL:
                _agent.isStopped = false;
                _agent.speed = _patrolMoveSpeed;
                _agent.acceleration = _patrolAccel; // 가속도 
                _agent.angularSpeed = _patrolAngularSpeed; // 회전 속도
                break;

            case MonsterState.CHASE:
                _agent.isStopped = false;
                _agent.speed = _chaseSpeed;
                _agent.acceleration = _chaseAccel;
                _agent.angularSpeed = _chaseAngularSpeed;
                _agent.stoppingDistance = _chaseStoppingDistance;
                break;

            case MonsterState.COMBAT:
                _agent.isStopped = true;        // 이동 멈춤
                _agent.velocity = Vector3.zero; // 관성 제거
                _agent.ResetPath();             // 경로 삭제
                break;

            case MonsterState.DAMAGED:
                _agent.isStopped = true;
                _agent.velocity = Vector3.zero;
                _agent.ResetPath();
                break;

            case MonsterState.DEAD:
                _agent.isStopped = true;
                _agent.velocity = Vector3.zero;
                _agent.ResetPath();
                _agent.enabled = false; // 사망
                break;
        }
    }

    public void Initialize(float speed, float damage, float hp, Vector3 scale)
    {
        _chaseSpeed = speed;
        _damage = damage;
        _hp = hp;
        transform.localScale = scale;   // 몬스터 크기 변경

        // 변경된 이동속도를 네비게이션에 즉시 적용해줘야함
        if (_agent != null) _agent.speed = _chaseSpeed;
    }
}

GameManager

using System.Collections;
using System.Collections.Generic;
using Unity.Cinemachine;
using UnityEngine;

public class RoomManager : MonoBehaviour
{
    [Header("오브젝트 직렬화")]
    [SerializeField] private GameObject monsterPrefab;
    [SerializeField] private Transform[] spawnPoints;
    [SerializeField] private GameObject door;

    [Header("몬스터 스탯 설정")]
    [SerializeField] private int spawnCount = 1;
    [SerializeField] private float moveSpeed = 8f;
    [SerializeField] private float damage = 5f;
    [SerializeField] private float hp = 20f;
    [SerializeField] private Vector3 monsterScale = Vector3.one;

    [Header("시네머신 카메라 연결")]
    [SerializeField] private CinemachineCamera spawnCam; // 몬스터 쪽 카메라
    [SerializeField] private CinemachineCamera doorCam;  // 문 열리는거 찍는 카메라

    private List<GameObject> _activeMonsters = new List<GameObject>();
    private bool _isCleared = false;
    private bool _isSpawned = false;
    
    public void StartRoomEvent()
    {
        StartCoroutine(Co_RoomSequence());
    }

    // 연출
    private IEnumerator Co_RoomSequence()
    {
        // 몬스터 스폰 위치 카메라
        if (spawnCam != null) spawnCam.Priority = 20; // CM_Player보다 높게 설정
        yield return new WaitForSeconds(1.5f); // 카메라가 날아가는 시간 대기

        // 몬스터 스폰
        SpawnMonsters();
        yield return new WaitForSeconds(1.0f); // 스폰된 모습 1초간 보기

        // 카메라 다시 플레이어 쪽
        if (spawnCam != null) spawnCam.Priority = 0;
        yield return new WaitForSeconds(1.5f); // 돌아오는 시간 대기

        // 몬스터 다 죽일 때 까지 게임 플레이
        while (true)
        {
            if (CheckAllDead()) break;
            yield return new WaitForSeconds(0.5f);
        }

        // 문 열리는거 찍음
        if (doorCam != null) doorCam.Priority = 20;
        yield return new WaitForSeconds(1.5f); // 날아가는 시간 대기

        // 문 열림
        OpenDoor();
        yield return new WaitForSeconds(1.0f); // 열리는 시간 대기

        // 다시 플레이어 카메라
        if (doorCam != null) doorCam.Priority = 0;
    }

    private void SpawnMonsters()
    {
        for (int i = 0; i < spawnCount; i++)
        {
            Transform spawnPos = spawnPoints[i % spawnPoints.Length];
            GameObject newMonster = Instantiate(monsterPrefab, spawnPos.position, spawnPos.rotation);

            MonsterAI ai = newMonster.GetComponent<MonsterAI>();
            if (ai != null)
            {
                ai.Initialize(moveSpeed, damage, hp, monsterScale);
            }

            _activeMonsters.Add(newMonster);
        }
        _isSpawned = true;
    }

    private bool CheckAllDead()
    {
        if (!_isSpawned) return false;

        foreach (GameObject monster in _activeMonsters)
        {
            if (monster != null) return false; // 살아있는 놈 있다면 다시
        }
        return true; // 다 죽었음
    }

    private void OpenDoor()
    {
        if (door != null) door.SetActive(false);
        _isCleared = true;
    }
}

네비게이션

  • 각 방의 바닥에 NavMesh Surface 컴포넌트를 추가해서 장애물들을 자동으로 인식하고 오브젝트들의 충돌을 구현합니다.
  • NavMesh: 캐릭터가 걸어 다닐 바닥과 갈 수 없는 장애물을 구분해 놓은 그물망.
  • NavMesh 구현: 지형 설정을 하고 Bake를 합니다. 움직일 캐릭터에 Agent를 설정합니다.(NavMesh Agent)

문제 해결 방법

  1. 너무 많은 총알을 생성하고 파괴하다보니 메모리 할당과 해제과 많이 반복 되고 가비지 컬랙터가 작동해서 프레임 드랍이 발생할 수 밖에 없었습니다. PoolManager 스크립트를 만들어서 총알을 큐로 관리해서 해결했습니다. 이거는 다음 포스팅에서 설명

0개의 댓글