Carrot
본문 바로가기
Unity/멋쟁이사자처럼 부트캠프

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(48일차) - Nav 실습 및 FPS 게임 (5)

by 독기품은토끼 2025. 7. 24.
✅ 오늘의 학습 목표
1. Navigation 실습
2. FPS 게임 (5)

1. 에이전트 동적이동

NavMesh의 영역이 크면 클수록 비효율적인 측면이 있다.

그래서 에이전트 주변만 NavMesh가 만들어지도록 코드를 구현해 보겠다.

using Unity.AI.Navigation;
using UnityEngine;
using UnityEngine.AI;

public class AgentController : MonoBehaviour
{
    public Camera camera;
    private NavMeshAgent agent;
    public NavMeshSurface surface;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        surface.transform.position = agent.transform.position;
        surface.BuildNavMesh();
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Ray ray = camera.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            
            if (Physics.Raycast(ray, out hit, Mathf.Infinity))
            {
                agent.SetDestination(hit.point);
            }
        }

        if (Vector3.Distance(transform.position, surface.transform.position) > 20f)
        {
            surface.transform.position = agent.transform.position;
            surface.BuildNavMesh();
        }
    }
}

 

  • 마우스 왼쪽 버튼을 누르면 카메라에서 마우스 위치로 Ray를 쏨
  • 에이전트가 현재 NavMeshSurface 중심에서 20m 이상 멀어지면 NavMeshSurface를 에이전트 위치로 이동
  • 그 위치 기준으로 NavMesh를 다시 생성(BuildNavMesh)

 

2. FPS 게임에 Navigation 적용

Enemy의 이동은 원래 CharacterController 써서 이동 방향 직접 계산했었는데
이번엔 Unity의 NavMeshAgent를 써서 조금 더 자연스럽게 길 찾기 + 이동 처리까지 구현해 보도록 하겠다.

using UnityEngine.AI;

public class EnemyFSM : MonoBehaviour
{
    private NavMeshAgent smith;

    void Start()
    {
        smith = GetComponent<NavMeshAgent>();
    }

    private void Move()
    {
        else if (Vector3.Distance(transform.position, player.position) > attackDistance)
        {
            //Vector3 dir = (player.position - transform.position).normalized; // 플레이어 쪽으로 가는 방향 구하기
            //cc.Move(dir * moveSpeed * Time.deltaTime);
            //transform.forward = dir; // 이동 방향 정면으로 적용

            smith.isStopped = true;
            smith.ResetPath(); // 경로 재설정

            smith.stoppingDistance = attackDistance;
            smith.SetDestination(player.position);
        }
    }

    private void Return()
    {
        if (Vector3.Distance(transform.position, originPos) > 0.1f)
        {
            //Vector3 dir = (originPos - transform.position).normalized;
            //cc.Move(dir * moveSpeed * Time.deltaTime);
            //transform.forward = dir;

            smith.SetDestination(originPos);
            smith.stoppingDistance = 0f;
        }
    }

    public void HitEnemy(int hitPower)
    {
        smith.isStopped = true;
        smith.ResetPath();
    }
}

 

  • stoppingDistance : 목표지점에 도착하면 그 거리에서 딱 멈추게 하는 용도
  • ResetPath() : 상태가 바뀌었을 때 예전 경로 따라가지 않도록 경로 리셋

 

1. Nav Link

 

내비게이션 메시를 베이크 했을 때

이동 가능한 영역이 높이 등으로 인해 끊어져있는 경우 내비 메시 링크 기능을 이용해서 임의로 연결할 수 있다.

 

 

이렇게 적 오브젝트가 Link를 타고 장애물을 넘어서 플레이어에게 다가오는 것을 확인할 수 있다.

지금은 Enemy가 엄청 부자연스러운 움직임으로 장애물을 넘어서 왔는데

이 부분은 아래 다른 실습을 하면서 고치는 방법을 다룰 것이다.

 

3. Nav 실습 - 3D

🥕 예행 작업
1. Scene 생성 (NavMesh2)

 

우선 새로운 씬을 생성한 후에 프로빌더로 위와 같이 오브젝트들을 배치해 준 후 Bake 작업을 해주었다.

 

using UnityEngine;
using UnityEngine.AI;

public class AgentController : MonoBehaviour
{
    private NavMeshAgent agent;
    public Transform[] points;
    private int index;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.SetDestination(points[index].position);
    }

    void Update()
    {
        if (Vector3.Distance(transform.position, points[index].position) < 3.5f)
        {
            index++;
            if (index >= points.Length)
                index = 0;

            agent.SetDestination(points[index].position);
        }
    }
}

 

플레이어 오브젝트가 points 배열에 있는 위치들을 순서대로 따라가면서 반복 이동하게 구현한 코드이다.

RPG 게임에 NPC들이 아마 이런 원리로 이동하는 거라 생각된다.

 

 

이제 플레이어가 제대로 이동하도록

플레이어 오브젝트에 Agent 컴포넌트를 추가해 주고

좌표 포인트들을 모두 넣어주었다.

 

 

 

그리고 NavMesh Modifier 컴포넌트를 통해서 원하는 지형의 비용값을 조절할 수 있다.

 

 

분홍색으로 Nav가 표현된 부분의 비용은 3으로 옆에 있는 다리의 값보다 높기 때문에 해당 다리를 건너지 않는다.

 

 

 

Nav Link를 사용해 보면 알겠지만

Nav Link의 저 노란선.. (명칭을 모르겠음) 을 기준으로 오브젝트가 이동하기 때문에

오브젝트가 부자연스럽게 이동하는 것을 볼 수 있을 것이다.

 

 

 

이 부분을 해결해 주기 위해서 AI Navgation 패키지의 Sample을 다운받아주자

그러면 Agent Link Mover라는 스크립트가 생성되는데 이 스크립트를 오브젝트에 추가해 주면 된다.

 

이동 방식 설명 특징/활용 예시
Parabola 포물선 궤적을 따라 이동 점프 연출, 장애물 넘기, 자연스러운 이동
Teleport 순간적으로 도착 지점으로 이동 포탈, 엘리베이터, 순간이동 연출
Normal Speed 링크 구간을 일반 이동 속도로 통과 걷거나 뛰는 느낌 그대로 연결됨
Curve 커브 애니메이션을 따라 이동 맞춤형 연출 가능 (예: 점프, 슬라이드 등)

 

 

 

마지막으로 Nav Mesh Obstacle을 활용해 보겠다.

Nav Mesh 위에 배치된 장애물 오브젝트를 정의하는 컴포넌트로 Agent가 해당 영역을 우회하거나 멈추도록 만드는 역할을 한다.

Carve를 체크해 주어야 실시간으로 영역을 자를 수 있다

 

 

이렇게 경로를 막아버리면 다른 경로를 빠르게 찾아가고 다시 경로를 생기게 해 주면 돌아오는 것을 확인할 수 있다.

 

🚨 NavMesh 주요 컴포넌트

컴포넌트 주요 역할 적용 대상 작용 방식
NavMeshAgent 이동 주체 플레이어, NPC 등 NavMesh 위 경로 따라 자동 이동
NavMeshLink 연결 지점 NavMesh 영역 사이 NavMesh 사이 이동 가능하게 연결
NavMeshSurface NavMesh 생성 지형, 구조물, 바닥 등 Bake 기능 제공
NavMeshModifier NavMesh 특성 변경 특정 오브젝트 포함/제외 및 지역 속성 변경
NavMeshObstacle 장애물 지정 상자, 벽, 트랩 등 Agent가 피하거나 멈춤
 
 

4. Nav 실습 - 2D

🥕 예행 작업
1. Scene 생성 (NavMesh Plus 2D)
2. Script 생성 (MovePlayer, FollowPlayer)
3. Package 다운로드 (Nav 2D용)
 

GitHub - h8man/NavMeshPlus: Unity NavMesh 2D Pathfinding

Unity NavMesh 2D Pathfinding. Contribute to h8man/NavMeshPlus development by creating an account on GitHub.

github.com

 

타일맵으로 배경(흰색)과 장애물(검은색)을 배치해 주고 플레이어 오브젝트(캡슐)와 적(삼각형)을 배치해 주었다.

 

using UnityEngine;

public class MovePlayer : MonoBehaviour
{
    public float moveSpeed = 10f;

    void Update()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        var dir = new Vector3(h, v, 0).normalized;

        transform.position += dir * moveSpeed * Time.deltaTime;
    }
}

 

플레이어 이동을 위해 플레이어 오브젝트에 해당 스크립트를 추가해 주었고

 

using UnityEngine;
using UnityEngine.AI;

public class FollowPlayer : MonoBehaviour
{
    private Transform player;
    private NavMeshAgent agent;

    void Start()
    {
        player = GameObject.Find("Player").transform;
        agent = GetComponent<NavMeshAgent>();
    }

    void Update()
    {
        agent.SetDestination(player.position);
    }
}

 

적은 플레이어를 따라가도록 Agent를 구현해 주었다.

 

 

오브젝트 컴포넌트는 위와 같이 추가해 주었다.

이렇게 하면 2D에서도 Nav 기능을 사용할 수 있다!

 

 

 

 

5. FPS 게임 - 플레이 모드 추가

지금 프로젝트는 마우스 오른쪽 버튼을 클릭하면 수류탄이 던져졌다.

보통 FPS 게임은 마우스 오른쪽 버튼을 클릭하면 스나이퍼의 경우 줌 인/아웃이 적용되었다.

이 부분을 구현해 주기 위해 if문과 switch문을 활용해 플레이 모드를 나누는 로직을 구현해 주겠다.

 

public class FPSPlayerFire : MonoBehaviour
{
    private enum WeaponMode { Normal, Sniper };
    private WeaponMode wMode;

    public TextMeshProUGUI wModeText;

    private bool ZoomMode = false;

    void Start()
    {
        wMode = WeaponMode.Normal;
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(1))
        {
            switch (wMode)
            {
                // 노말모드 - 수류탄 투척
                case WeaponMode.Normal:
                    GameObject bomb = Instantiate(bombFactory);
                    bomb.transform.position = firePosition.transform.position;

                    Rigidbody rb = bomb.GetComponent<Rigidbody>();
                    rb.AddForce(Camera.main.transform.forward * throwPower, ForceMode.Impulse);

                    break;
                
                // 스나이퍼 모드 - 스나이퍼 줌 In/Out
                case WeaponMode.Sniper:
                    //if (!ZoomMode)
                    //{
                    //    Camera.main.fieldOfView = 15f;
                    //    ZoomMode = true;
                    //}
                    //else
                    //{
                    //    Camera.main.fieldOfView = 60f;
                    //    ZoomMode = false;
                    //}
                    float fov = ZoomMode ? 60f : 15f;
                    Camera.main.fieldOfView = fov;
                    ZoomMode = !ZoomMode;
                    
                    break;
            }
        }

        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            wMode = WeaponMode.Normal;
            Camera.main.fieldOfView = 60f;
            wModeText.text = "Normal Mode";
        }
        else if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            wMode = WeaponMode.Sniper;
            wModeText.text = "Sniper Mode";
        }
    }
}

 

키보드 상단에 있는 1과 2를 눌렀을 때 노말 모드와 스나이퍼 모드로 적용되도록 구현해 주었고

어느 모드가 적용되었는지 시각적으로 표현하기 위해 Text UI도 추가해 주었다.

 

 

 

6. FPS 게임 - 무기 효과 추가

게임의 몰입도를 위하여 총알을 발사했을 때 불꽃이 튀는 이펙트를 넣어주려고 한다.

 

🥕 예행 작업
1. Asset 다운로드
 

Easy FPS | 캐릭터 | Unity Asset Store

Get the Easy FPS package from Mario Haberle and speed up your game development process. Find this & other 캐릭터 options on the Unity Asset Store.

assetstore.unity.com

 

M67 Grenade | 3D 무기 | Unity Asset Store

Elevate your workflow with the M67 Grenade asset from Bude Eldalah. Find this & other 무기 options on the Unity Asset Store.

assetstore.unity.com

public class FPSPlayerFire : MonoBehaviour
{
    public GameObject[] eff_Flash;

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            StartCoroutine(ShootEffectOn(0.05f));
        }
    }

    IEnumerator ShootEffectOn(float duration)
    {
        int num = Random.Range(0, eff_Flash.Length - 1);
        eff_Flash[num].SetActive(true);

        yield return new WaitForSeconds(duration);
        eff_Flash[num].SetActive(false);
    }
}

 

 

총의 총구가 있는 부분에서 이펙트가 발생할 수 있도록 빈 오브젝트(Muzzle Position)를 만들어주고

하위에 이펙트 프리팹을 넣어주었다.

 

0.05초 동안 이펙트가 발생되도록 구현해 주었고

이펙트는 랜덤으로 발생되도록 해주었다.