[유니티]탑다운 슈팅 따라하기 #12 맵 구성

안녕하세요 유랑입니다.

 

 

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

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

 

 

 

1. 탑다운 슈팅 따라하기

 

 

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

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

 

유튜브 사이트 => 유튜브 

 

 

 

유니티 슈팅

 

 

 

1-1) 맵 구성

 

 

이번 시간에는 맵 구성을 해보겠습니다.

지금까지 했던 것들을 이용해 다양한 맵을 만들어 볼께요.

 

 

 

유니티 슈팅

 

 

 

1-2) 스크립트 작성 -㉠MapGenerator

 

 

 

MapGenerator 스크립트의 내용을 재구성해보겠습니다.

맵의 정보를 클래스를 만들어 구성하고,

기존의 정보들은 이 클래스로 교체해 줄게요.

맵 클래스를 Serializable을 통해서 인스펙트에서 조정도 가능하답니다^^

 

 

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

public class MapGenerator : MonoBehaviour
{
    public Map[] maps; // 맵들 
    public int mapIndex;

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

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

    public float tileSize; // 타일 사이즈
    List<Coord> allTileCoords; // 모든 좌표 리스트
    Queue<Coord> shuffledTileCoords; // 셔플된 좌표 리스트

    Map currentMap;

    void Start()
    {
        GenerateMap();
    }
    // 맵 생성
    public void GenerateMap()
    {
        currentMap = maps[mapIndex];
        System.Random prng = new System.Random(currentMap.seed);
        GetComponent<BoxCollider>().size = new Vector3(currentMap.mapSize.x * tileSize, .05f, currentMap.mapSize.y * tileSize);

        // Coord 생성
        allTileCoords = new List<Coord>();
        for (int x = 0; x < currentMap.mapSize.x; x++)
        {
            for (int y = 0; y < currentMap.mapSize.y; y++)
            {
                allTileCoords.Add(new Coord(x, y));
            }
        }
        shuffledTileCoords = new Queue<Coord>(Utility.ShuffleArray(allTileCoords.ToArray(), currentMap.seed));

        // mapHolder 생성
        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 < currentMap.mapSize.x; x++)
        {
            for (int y = 0; y < currentMap.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)currentMap.mapSize.x, (int)currentMap.mapSize.y];

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

        for (int i = 0; i < obstacleCount; i++)
        {
            Coord randomCoord = GetRandomCoord();
            obstacleMap[randomCoord.x, randomCoord.y] = true;
            currentObstacleCount++;

            if (randomCoord != currentMap.mapCentre && MapIsFullyAccessible(obstacleMap, currentObstacleCount))
            {
                float obstacleHeight = Mathf.Lerp(currentMap.minObstacleHeight, currentMap.maxObstacleHeight, (float)prng.NextDouble());
                Vector3 obstaclePosition = CoordToPosition(randomCoord.x, randomCoord.y);

                Transform newObstacle = Instantiate(obstaclePrefab, obstaclePosition + Vector3.up * obstacleHeight / 2, Quaternion.identity);
                newObstacle.parent = mapHolder;
                newObstacle.localScale = new Vector3((1 - outlinePercent) * tileSize, obstacleHeight, (1 - outlinePercent) * tileSize);

                Renderer obstacleRenderer = newObstacle.GetComponent<Renderer>();
                Material obstacleMaterial = new Material(obstacleRenderer.sharedMaterial);
                float colourPercent = randomCoord.y / (float)currentMap.mapSize.y;
                obstacleMaterial.color = Color.Lerp(currentMap.foregroundColour, currentMap.backgroundColour, colourPercent);
                obstacleRenderer.sharedMaterial = obstacleMaterial;
            }
            else
            {
                obstacleMap[randomCoord.x, randomCoord.y] = false;
                currentObstacleCount--;
            }
        }
        // 네비메쉬 마스크 생성
        Transform maskLeft = Instantiate(navmeshMaskPrefab, Vector3.left * (currentMap.mapSize.x + maxMapSize.x) / 4f * tileSize, Quaternion.identity);
        maskLeft.parent = mapHolder;
        maskLeft.localScale = new Vector3((maxMapSize.x - currentMap.mapSize.x) / 2f, 1, currentMap.mapSize.y) * tileSize;

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

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

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

        navmeshFloor.localScale = new Vector3(maxMapSize.x, maxMapSize.y) * 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(currentMap.mapCentre);
        mapFlags[currentMap.mapCentre.x, currentMap.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)(currentMap.mapSize.x * currentMap.mapSize.y - currentObstacleCount);
        return targetAccessibleTileCount == accessibleTileCount;
    }
    // Coord를 Vector3로 변환
    Vector3 CoordToPosition(int x, int y)
    {
        return new Vector3(-currentMap.mapSize.x / 2f + 0.5f + x, 0, -currentMap.mapSize.y / 2f + 0.5f + y) * tileSize;
    }
    // 랜덤 좌표 반환
    public Coord GetRandomCoord()
    {
        Coord randomCoord = shuffledTileCoords.Dequeue();
        shuffledTileCoords.Enqueue(randomCoord);
        return randomCoord;
    }
    [System.Serializable]
    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);
        }
    }
    [System.Serializable]
    public class Map
    {

        public Coord mapSize; // 맵 크기
        [Range(0, 1)]
        public float obstaclePercent; // 장애물 영역
        public int seed;
        public float minObstacleHeight; // 장애물 최소 높이
        public float maxObstacleHeight; // 장애물 최대 높이
        public Color foregroundColour; // 전반부 색
        public Color backgroundColour; // 후반부 색

        public Coord mapCentre
        {
            get
            {
                return new Coord(mapSize.x / 2, mapSize.y / 2);
            }
        }

    }
}

 

 

 

1-3) 스크립트 작성 -㉡MapEditor

 

 

MapEditor 스크립트에서는 "Generate Map" 버튼을 만들어주어,

다른 씬에서 편하게 적용해 주겠습니다.

 

 

using UnityEngine;
using System.Collections;
using UnityEditor;

// 커스텀 에디터(맵 설정)
[CustomEditor (typeof (MapGenerator))]
public class MapEditor : Editor
{
    // 유니티가 인스펙터를 GUI로 그려주는 메소드
    public override void OnInspectorGUI()
    {
        MapGenerator map = target as MapGenerator;

        if (DrawDefaultInspector())
        {
            map.GenerateMap();
        }

        if (GUILayout.Button("Generate Map"))
        {
            map.GenerateMap();
        }
    }
}

 

 

간단히 2개의 맵 정보만 입력해 주겠습니다.

다르게 하셔도 되지만 저는 우선 이렇게 설정하였습니다.

 

 

유니티 슈팅

 

 

그리고 콜라이더 컴포넌트도 붙여주세요.

 

 

유니티 슈팅

 

 

어떤가요? 

이제 그럴듯하죠ㅎㅎ

 

 

유니티 슈팅

 

 

 

1-4) 맵 설정 -㉠복사

 

 

우리가 만든 맵 정보를 Main 씬에다가 적용해줄텐데요.

Map을 복사하여 옮겨줄게요.

 

 

 

유니티 슈팅

 

 

Main씬에는 플레이어와 카메라, 라이트, 그리고 스포너의 정보만 남겨주고

나머지는 지워주겠습니다.

이렇게 복사해서 붙여주고, Generator Map 버튼을 클릭하여 맵을 생성해 주세요!!

 

 

유니티 슈팅

 

 

 

1-5) 맵 설정 -㉡플레이어 위치

 

 

장애물에 가려지지 않도록 플레이어 위치를 옮겨줄게요.

 

 

 

유니티 슈팅

 

 

 

1-6) 맵 설정 -㉢네비게이션

 

 

여기서도 네비게이션 설정을 해주어야겠죠?!

플레이어 크기를 고려하여 Agent Radius도 조정해 주겠습니다.

그리고 Bake 해줄게요ㅎㅎ

 

 

 

유니티 슈팅

 

 

네비게이션 적용도 완료!!

 

 

유니티 슈팅

 

 

 

1-7) 맵 설정 -㉣Map

 

 

맵 설정도 끝내고,

카메라 위치도 조정해 주고

 

 

유니티 슈팅

 

 

Spawner 값도 조정하고 시작해보겠습니다.

 

 

유니티 슈팅

 


이제 게임답네요.

따라란~~~

 

 

유니티 슈팅

 

 

 

2. 마무리

 

 

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

탑다운 슈팅을 따라하면서 맵 구성을 해보았습니다.

감사합니다.

 

 

 

수업자료: 탑다운 슈팅 따라하기 #12 맵 구성

 

 

 

 

 

 

댓글

Designed by JB FACTORY