[유니티]탑다운 슈팅 따라하기 #11 네비게이션

안녕하세요 유랑입니다.

 

 

실력향상을 위해서 오늘은 유튜브를 따라하면서 공부하겠습니다.

궁금하신점 있으시다면 댓글로 남겨주세요^^

 

 

 

1. 탑다운 슈팅 따라하기

 

 

이번 강의는 Sebastian Lague님께서 만든 예제이며,

유튜브를 보시면 자세한 내용을 배우실 수 있습니다.

 

유튜브 사이트 => 유튜브 

 

 

 

유니티 슈팅

 

 

 

1-1) 네비게이션

 

 

이번 시간에는 네비게이션을 적용해 보겠습니다.

네비게이션 전용 바닥을 만들고,

그걸 이용해 맵을 구성해 줄게요.

 

 

 

유니티 슈팅

 

 

 

1-2) 네비게이션용 바닥 만들기

 

 

Quad를 생성한 후 다음과 같이 값을 변경해 주세요.

이름은 Navmesh floor라고 정의하였습니다.

 

 

 

유니티 슈팅

 

 

메뉴창에서 Window => AI => Navigation을 선택해 주세요.

그리고 Navmesh floor를 선택 후 네비설정을 해두겠습니다.

네비게이션 오브젝트로 설정하려면 Navigation Static을 꼭 체크해 주세요!!

 

 

유니티 슈팅

 

 

설정하였으면 Bake를 이용해 맵을 구워줍니다.

이 때 구워준다는 것은 맵 데이터를 토대로 네비게이션 구역을 만들어 주는 것입니다ㅎㅎ

 

 

유니티 슈팅

 

 

짜잔 이렇게 적용되었습니다.

 

 

유니티 슈팅

 

 

 

1-3) 스크립트 작성 - MapGenerator

 

 

MapGenerator 스크립트에서 네비게이션 맵 정보와

그리고 이 정보를 이용해 navmeshFloor 크기도 변경해 주겠습니다.

앞으로 맵 크기는 최대 사이즈를 기준으로 랜덤하게 바뀔 것이므로 

최대맵 사이즈와 타일 사이즈 정보도 추가해 주었습니다.

 

 

 

유니티 슈팅

 

 

지난 시간에 깜빡한게 tilePosition = CoordToPosition(x, y)를 빼먹었군요.

tileSize 정보도 추가해 줍니다.

 

 

유니티 슈팅

 

 

여기서 위의 값을 토대로 navmeshFloor 크기도 변경해 주겠습니다.

 

 

유니티 슈팅

 

 

 

1-4) 장애물 설정

 

 

 

navmeshFloor에서 CastShadows 부분을 off로 변경해 줍니다.(Obstacle이 아님)

그래야 게임뷰에서 보이지 않습니다.

그리고 Obstacle 프리팹에 NavMeshObstacle을 추가해 주세요.

그래야 네비게이션 정보에서 해당 부분이 장애물로 인식됩니다.

 

 

 

유니티 슈팅

 

 

다시 Bake 해주시면 장애물을 제외한 네비게이션 정보가 나타나게 됩니다.

 

 

유니티 슈팅

 

 

 

1-5) 네비게이션 마스크

 

 

이번에는 맵 밖에 있는 네비게이션 정보들을 없애줄게요.

그래야 맵 밖으로 나가지 않겠죠.

네비게이션 마스크를 적용해 주겠습니다.

 

 

 

유니티 슈팅

 

 

맵을 중심으로 상하좌우에 맵 마스크를 생성해 줍니다.

 

 

유니티 슈팅

 

전체 코드는 다음과 같습니다.

 

 

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class MapGenerator : MonoBehaviour
{
    public Transform tilePrefab; // 타일 프리팹
    public Transform obstaclePrefab; // 장애물 프리팹
    public Transform navmeshFloor; // 네비게이션 맵
    public Transform navmeshMaskPrefab; // 네비게이션 마스크용 프리팹
    public Vector2 mapSize; // 맵 사이즈
    public Vector2 maxMapSize; // 최대맵 사이즈

    [Range(0, 1)]
    public float outlinePercent; // 테두리 영역
    [Range(0, 1)]
    public float obstaclePercent; // 장애물 영역

    public float tileSize; // 타일 사이즈

    List<Coord> allTileCoords; // 모든 좌표 리스트
    Queue<Coord> shuffledTileCoords; // 셔플된 좌표 리스트

    public int seed = 10;
    Coord mapCentre; // 맵 정중앙

    void Start()
    {
        GenerateMap();
    }
    // 맵 생성
    public void GenerateMap()
    {
        allTileCoords = new List<Coord>();
        for (int x = 0; x < mapSize.x; x++)
        {
            for (int y = 0; y < mapSize.y; y++)
            {
                allTileCoords.Add(new Coord(x, y)); // 좌표 리스트 추가
            }
        }
        shuffledTileCoords = new Queue<Coord>(Utility.ShuffleArray(allTileCoords.ToArray(),seed));
        mapCentre = new Coord((int)mapSize.x/2, (int)mapSize.y/2);

        string holderName = "Generated Map";
        if (transform.Find(holderName))
        {
            DestroyImmediate(transform.Find(holderName).gameObject);
        }

        Transform mapHolder = new GameObject(holderName).transform;
        mapHolder.parent = transform;

        for (int x = 0; x < mapSize.x; x++)
        {
            for (int y = 0; y <mapSize.y; y++)
            {
                Vector3 tilePosition = CoordToPosition(x, y); // 타일 위치 설정
                Transform newTile = Instantiate(tilePrefab, tilePosition, Quaternion.Euler(Vector3.right * 90)); // 타일 생성
                newTile.localScale = Vector3.one * (1 - outlinePercent) * tileSize; // 테두리 영역 설정
                newTile.parent = mapHolder; // 부모 설정
            }
        }
        bool[,] obstacleMap = new bool[(int)mapSize.x, (int)mapSize.y];

        int obstacleCount = (int)(mapSize.x * mapSize.y * obstaclePercent);
        int currentObstacleCount = 0;

        for (int i = 0; i < obstacleCount; i++)
        {

            Coord randomCoord = GetRandomCoord();
            obstacleMap[randomCoord.x, randomCoord.y] = true;
            currentObstacleCount++;

            if (randomCoord != mapCentre && MapIsFullyAccessible(obstacleMap, currentObstacleCount))
            {
                Vector3 obstaclePosition = CoordToPosition(randomCoord.x, randomCoord.y);
                Transform newObstacle = Instantiate(obstaclePrefab, obstaclePosition + Vector3.up * 0.5f, Quaternion.identity);
                newObstacle.parent = mapHolder;
                newObstacle.localScale = Vector3.one * (1 - outlinePercent) * tileSize; // 테두리 영역 설정
            }
            else
            {
                obstacleMap[randomCoord.x, randomCoord.y] = false;
                currentObstacleCount--;
            }
        }
        // 네비메쉬 마스크
        Transform maskLeft = Instantiate(navmeshMaskPrefab, Vector3.left * (mapSize.x + maxMapSize.x) / 4 * tileSize, Quaternion.identity);
        maskLeft.parent = mapHolder;
        maskLeft.localScale = new Vector3((maxMapSize.x - mapSize.x) / 2, 1, mapSize.y) * tileSize;

        Transform maskRight = Instantiate(navmeshMaskPrefab, Vector3.right * (mapSize.x + maxMapSize.x) / 4 * tileSize, Quaternion.identity);
        maskRight.parent = mapHolder;
        maskRight.localScale = new Vector3((maxMapSize.x - mapSize.x) / 2, 1, mapSize.y) * tileSize;

        Transform maskTop = Instantiate(navmeshMaskPrefab, Vector3.forward * (mapSize.y + maxMapSize.y) / 4 * tileSize, Quaternion.identity);
        maskTop.parent = mapHolder;
        maskTop.localScale = new Vector3(maxMapSize.x, 1, (maxMapSize.y - mapSize.y) / 2) * tileSize;

        Transform maskBottom = Instantiate(navmeshMaskPrefab, Vector3.back * (mapSize.y + maxMapSize.y) / 4 * tileSize, Quaternion.identity);
        maskBottom.parent = mapHolder;
        maskBottom.localScale = new Vector3(maxMapSize.x, 1, (maxMapSize.y - mapSize.y) / 2) * tileSize;

        navmeshFloor.localScale = new Vector3(maxMapSize.x, maxMapSize.y, 1) * tileSize; // 네비게이션 맵 영역 설정
    }
    // 맵 접근 가능 여부(Flood fill 알고리즘)
    bool MapIsFullyAccessible(bool[,] obstacleMap, int currentObstacleCount)
    {
        bool[,] mapFlags = new bool[obstacleMap.GetLength(0), obstacleMap.GetLength(1)];
        Queue<Coord> queue = new Queue<Coord>();
        queue.Enqueue(mapCentre);
        mapFlags[mapCentre.x, mapCentre.y] = true;

        int accessibleTileCount = 1; // 접근 가능 타일

        while (queue.Count > 0){
            Coord tile = queue.Dequeue();

            for (int x = -1; x <= 1; x++){
                for(int y = -1; y <= 1; y++){
                    int neighbourX = tile.x + x;
                    int neighbourY = tile.y + y;
                    if (x == 0 || y == 0){
                        if (neighbourX >= 0 && neighbourX < obstacleMap.GetLength(0) && 
                            neighbourY >= 0 && neighbourY < obstacleMap.GetLength(1)){
                            if (!mapFlags[neighbourX, neighbourY] && !obstacleMap[neighbourX, neighbourY]){
                                mapFlags[neighbourX, neighbourY] = true;
                                queue.Enqueue(new Coord(neighbourX, neighbourY));
                                accessibleTileCount++;
                            }
                        }
                    }
                }
            }
        }
        int targetAccessibleTileCount = (int)(mapSize.x * mapSize.y - currentObstacleCount);
        return targetAccessibleTileCount == accessibleTileCount;
    }
    // Coord를 Vector3로 변환
    Vector3 CoordToPosition(int x, int y)
    {
        return new Vector3(-mapSize.x / 2 + 0.5f + x, 0, -mapSize.y / 2 + 0.5f + y) * tileSize;
    }
    // 랜덤 좌표 반환
    public Coord GetRandomCoord()
    {
        Coord randomCoord = shuffledTileCoords.Dequeue();
        shuffledTileCoords.Enqueue(randomCoord);
        return randomCoord;
    }

    public struct Coord
    {
        public int x;
        public int y;

        public Coord(int _x, int _y)
        {
            x = _x;
            y = _y;
        }
        public static bool operator == (Coord c1, Coord c2)
        {
            return c1.x == c2.x && c1.y == c2.y;
        }
        public static bool operator !=(Coord c1, Coord c2)
        {
            return !(c1 == c2);
        }
    }
}

 

 

맵 마스크는 빈 오브젝트이고 NavMeshObstacle 정보를 가지고 있습니다.

물론 프리팹으로 만들어줘야겠죠?

 

 

유니티 슈팅

 

 

MapGenerator 정보에 해당 프리팹을 적용해 주세요.

 

 

유니티 슈팅

 

 

이제 네비게이션 마스크가 생성된 걸 확인 가능합니다.

 

 

유니티 슈팅

 

 

네비게이션도 확인해 주세요.

드디어 완성이네요ㅎㅎ

 

 

유니티 슈팅

 

 

 

2. 마무리

 

 

오늘 강의는 여기까지입니다.

탑다운 슈팅을 따라하면서 네비게이션을 적용해 보았습니다.

감사합니다.

 

 

 

수업자료: 탑다운 슈팅 따라하기 #11 네비게이션

 

 

 

 

 

 

 

 

댓글

Designed by JB FACTORY