본문 바로가기

IT/유니티

16. 유니티 교육 (Sebastian Lague 강의-3DShooter #3)

1. Tile Map


오늘은 타일 맵을 만들어 보자.

우선 타일이 될 Quad를 하나 준비한다. 

Quad는 얼핏보면 Plane과 비슷하지만 Quad는 한쪽 면만 보이기 때문에 코스트가 적다.

아무튼 이 Quad를 Prefab으로 저장해 놓는다. 


그리고 빈객체를 만들고 Map이라고 정한다. 

이 Map객체에 MapGenerator스크립트는 넣어준다.

스크립트 소스를 보자.


MapGenerater.cs

using UnityEngine;

using System.Collections;


public class MapGenerator : MonoBehaviour {


    //Prefab를 지정하기 위해서 Public으로 

    public Transform tilePrefab;

    //지도의 크기를 결정

    public Vector2 mapSize;


    //인스펙터에서 타일의 크기를 슬라이드로 조정

    [Range(0,1)]

    public float outlinePercent;


    void Start()

    {

        GenerateMap();

    }


    //맵생성

    public void GenerateMap()

    {

        //생성할때마다 맵을 만든다면 메모리 소비가 커지기때문에

        //holder를 만들고 그밑에 모든 타일을 자식으로 가지게 한다.

        //그리고 다시 생성할때마다 모든 타일을 삭제하고 다시 생성한다.

        string holderName = "Generated Map";

        if (transform.FindChild(holderName))

        {

            DestroyImmediate(transform.FindChild(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 = new Vector3(-mapSize.x/2 + 0.5f + x, 0, -mapSize.y/2 + 0.5f + y);

                //화면에 맵처럼 보이기 위해서 Quaternion.Euler(Vector3.right*90)을 이용해서 뉘어놓는다.

                Transform newTile = Instantiate(tilePrefab, tilePosition, Quaternion.Euler(Vector3.right*90)) as Transform;

                //타일의 크기를 조정한다.

                newTile.localScale = Vector3.one * (1-outlinePercent);

                //관리를 위해서 맵홀더를 부모로 한다.

                newTile.transform.SetParent(mapHolder);

            }

        }

    }

}


설명은  주석으로 대신한다.

하지만 맵생성이라는게 할때마다 다를텐데 확인을 할때마다 실행하고 중지하고를 반복해야 한다면,
굉장히 피곤할거 같다.
그래서 우리는 mapEditor를 준비해보자.

단순히 스크립트를 하나 만들고 다음과 같이 코딩해보자.

using UnityEngine;
using System.Collections;
using UnityEditor;

[CustomEditor (typeof (MapGenerator))]
public class MapEditor : Editor {

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        MapGenerator map = target as MapGenerator;

        map.GenerateMap();
    }

}

커스텀 에디터로써 mapGenerator스크립트를 제어해주는 역활을 해준다.
스크립트를 따로 인스펙터등에서 지정하지 않아도 알아서 된다.
신기한데 이해는 안된다. 왜 되는지...ㅠㅠ


2. Obstacle

이제 장애물을 만들어 보자.
지난번에 시뮬레이션으로 가볍게 겜을 했을때에도 느꼈지만, 단순이 적을 죽이는것보다
엄폐물이나 장애물을 이용해서 적을 따돌리는게 재미가 있었다.
그럼 이런 장애물을 어떻게 만들수 있는지 한번 알아보자.

위에서 생성한 맵위에 단순히 충돌체크하는 큐비를 올려놓으면 끝이라고 할수 있다.
하지만 문제는 랜덤으로 올리는게 중요할것이다.

우선 단순히 랜덤한 위치에 큐브를 올리는것은 간단하다.
타일맵이 생성될때마다 랜덤으로 큐브를 그위에 생성시키면 되기때문이다.

하지만 레벨디자인등을 고려해서 랜덤값을 생성하고 그 랜덤값이 겹치지 않게 방해물을 만들어 보자.
수정한 mapGenerator.cs를 확인해 보자.

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

public class MapGenerator : MonoBehaviour
{

    //Prefab를 지정하기 위해서 Public으로 
    public Transform tilePrefab;
    public Transform obstaclePreFab;
    //지도의 크기를 결정
    public Vector2 mapSize;

    //인스펙터에서 타일의 크기를 슬라이드로 조정
    [Range(0, 1)]
    public float outlinePercent;

    List<Coord> allTileCoords;
    Queue<Coord> shuffledTileCoords;

    public int seed = 10;

    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));

        //생성할때마다 맵을 만든다면 메모리 소비가 커지기때문에
        //holder를 만들고 그밑에 모든 타일을 자식으로 가지게 한다.
        //그리고 다시 생성할때마다 모든 타일을 삭제하고 다시 생성한다.
        string holderName = "Generated Map";
        if (transform.FindChild(holderName))
        {
            DestroyImmediate(transform.FindChild(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 = new Vector3(-mapSize.x / 2 + 0.5f + x, 0, -mapSize.y / 2 + 0.5f + y);
                //화면에 맵처럼 보이기 위해서 Quaternion.Euler(Vector3.right*90)을 이용해서 뉘어놓는다.
                Transform newTile = Instantiate(tilePrefab, tilePosition, Quaternion.Euler(Vector3.right * 90)) as Transform;
                //타일의 크기를 조정한다.
                newTile.localScale = Vector3.one * (1 - outlinePercent);
                //관리를 위해서 맵홀더를 부모로 한다.
                newTile.transform.SetParent(mapHolder);
            }
        }

        int obstacleCount = 10;
        for (int i = 0;i<obstacleCount;i++)
        {
            //셔플한 위치정보중 가장 앞의 정보를 가져온다.
            Coord randomCoord = GetRandomCoord();
            //가져온 위치정보를 벡터 값으로 변환해 놓는다.(좌표값을 실제 위치값으로 변환) 
            Vector3 obstaclePosition = CoordToPosition(randomCoord.x, randomCoord.y);
            //실제 위치 벡터로 장애물을 생성
            Transform newObstacle = Instantiate(obstaclePreFab, obstaclePosition + Vector3.up* .5f, Quaternion.identity) as Transform;

            //사이즈및 하이어아키 조절
            newObstacle.localScale = (Vector3.right+Vector3.forward) * (1 - outlinePercent) + Vector3.up;
            newObstacle.SetParent(mapHolder);
        }

    }

    //좌표값을 실제 위치값으로 
    Vector3 CoordToPosition(int x, int y)
    {
        return new Vector3(-mapSize.x / 2 + 0.5f + x, 0, -mapSize.y / 2 + 0.5f + y);
    }


    //큐에서 가장 앞에 있는 놈을 리턴하고 
    //떼온 맨 앞의 값을 큐에 가장 마지막에 넣는다.
    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;
        }
    }

}
그리고 셔플을 위한 유틸리티를 추가했다.
단순히 배열의 길이만큰 셔플하는 기능을 가지고 있다.
utility.cs
using System.Collections;

public class Utility {

public static T[] ShuffleArray<T>(T[] array, int seed)
    {
        System.Random prng = new System.Random(seed);

        for(int i=0; i < array.Length - 1; i++)
        {
            int randomIndex = prng.Next(i, array.Length);
            T tempItem = array[randomIndex];
            array[randomIndex] = array[i];
            array[i] = tempItem;
        }

        return array;
    }
}

3. map connectivity

길이 끊어지지 않게 이어주기 위한 로직이 들어 있는데 너무 어렵다.
시간을 내어서 더 공부해 보자.

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

public class MapGenerator : MonoBehaviour
{

    //Prefab를 지정하기 위해서 Public으로 
    public Transform tilePrefab;
    public Transform obstaclePreFab;
    //지도의 크기를 결정
    public Vector2 mapSize;

    //인스펙터에서 타일의 크기를 슬라이드로 조정
    [Range(0, 1)]
    public float outlinePercent;

    [Range(0, 1)]
    public float obstaclePercent;

    List<Coord> allTileCoords;
    Queue<Coord> shuffledTileCoords;

    public int seed = 10;
    Coord mapCenter;

    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));
        mapCenter = new Coord((int)mapSize.x/2, (int)mapSize.y/2);

        //생성할때마다 맵을 만든다면 메모리 소비가 커지기때문에
        //holder를 만들고 그밑에 모든 타일을 자식으로 가지게 한다.
        //그리고 다시 생성할때마다 모든 타일을 삭제하고 다시 생성한다.
        string holderName = "Generated Map";
        if (transform.FindChild(holderName))
        {
            DestroyImmediate(transform.FindChild(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 = new Vector3(-mapSize.x / 2 + 0.5f + x, 0, -mapSize.y / 2 + 0.5f + y);
                //화면에 맵처럼 보이기 위해서 Quaternion.Euler(Vector3.right*90)을 이용해서 뉘어놓는다.
                Transform newTile = Instantiate(tilePrefab, tilePosition, Quaternion.Euler(Vector3.right * 90)) as Transform;
                //타일의 크기를 조정한다.
                newTile.localScale = Vector3.one * (1 - outlinePercent);
                //관리를 위해서 맵홀더를 부모로 한다.
                newTile.transform.SetParent(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 != mapCenter && MapIsFullyAccessible(obstacleMap, currentObstacleCount))
            {
                //가져온 위치정보를 벡터 값으로 변환해 놓는다.(좌표값을 실제 위치값으로 변환) 
                Vector3 obstaclePosition = CoordToPosition(randomCoord.x, randomCoord.y);
                //실제 위치 벡터로 장애물을 생성
                Transform newObstacle = Instantiate(obstaclePreFab, obstaclePosition + Vector3.up * .5f, Quaternion.identity) as Transform;

                //사이즈및 하이어아키 조절
                newObstacle.localScale = (Vector3.right + Vector3.forward) * (1 - outlinePercent) + Vector3.up;
                newObstacle.SetParent(mapHolder);
            }else
            {
                obstacleMap[randomCoord.x, randomCoord.y] = false;
                currentObstacleCount--;
            }

            
        }

    }

    bool MapIsFullyAccessible(bool[,] obstacleMap, int currentObstacleCount)
    {
        //이차원배열은 행과 열로 나누어질수 있는데,
        //GetLength(0)은 행의 갯수를 반환하고 GetLength(1)은 열의 갯수를 반환한다.
        //예를 들어서 bool map[3,6]라고 한다면,
        //GetLength(0)은 3을 GetLength(1)은 6을 반환한다.

        //결국 mapFlags는 obstacleMap과 같은 크기의 맵을 만든다.
        bool[,] mapFlags = new bool[obstacleMap.GetLength(0), obstacleMap.GetLength(1)];
        Queue<Coord> queue = new Queue<Coord>();

        queue.Enqueue(mapCenter);
        mapFlags[mapCenter.x, mapCenter.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;
    }




    //좌표값을 실제 위치값으로 
    Vector3 CoordToPosition(int x, int y)
    {
        return new Vector3(-mapSize.x / 2 + 0.5f + x, 0, -mapSize.y / 2 + 0.5f + y);
    }


    //큐에서 가장 앞에 있는 놈을 리턴하고 
    //떼온 맨 앞의 값을 큐에 가장 마지막에 넣는다.
    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);
        }
    }

}









Tip. Dequeue와 Enqueue






Tip. Fisher–Yates shuffle


아~ 위의 그림을 보라~ 아름답지 않은가 ㅋㅋ

피셔 예이츠 셔플이라는 것인데, 난 정말 아무 생각도 없이 개발을 한거 같다.

하긴 겜개발책이라고는 단 한권도 읽은적이 없으니 어쩌면 당연하다.

아무튼 이 셔플은 아주 간단하지만 강력하고 유용한다.


게임 제작을 하다보면 아주 중요한 것이 바로 랜덤이다.

적의 생성이나 맵을 만들때에도 랜덤은 필수라고 할수 있다.

이 랜덤중에 정해진것에도 중복되지 않게 모든 것을 다 보여줘야 할때 어떻게 해야 할까?


전에 인디로 겜만들때 이게 항상 고민이였다. 

뭐 대충 만들다 보니 그냥 겹치겠지 하고  대충 넘겼지만 위의 셔플방식을 쓰면 아주 간단하게 해결할수 있을거 같다.

따로 라이브러리로 만들어 놓아도 좋을거 같다.


원래는 쉽다

배열에 원하는 값들을 다 넣어 놓는다. 정렬은 필요 없다.

다 넣어 놓고, 랜덤을 돌리고 나온값을 배열의 맨 앞에 두고 다시 랜덤을 돌리는데,

여기서 중요한것은 랜덤의 범위의 최소값을 1증가 시켜서 돌린다

결국 이런식으로 값을 옮기고, 랜덤의 최소값을 조정하면서 랜덤값을 얻어내면

아주 쉽게 겹치지 않는 랜덤값을 모두 얻어 낼수 있다.

대단하다!!






Tip. 스프라이트 이미지를 쪼개보자


에디터에 그림을 올릴려면 반드시 스프라이트형태이어야 한다.

텍스쳐 타잎을 스프라이트 형태로 하면 스프라이트 모드가 있다.

여기서 멀티플을 선택하고, 이미지를 자기가 원하는 만큰 쪼개면 된다.

비율로도 쪼깰수도 있다.

slice와 trim의 방식으로 쪼갤수 있다.

 

그리고 옵션중 Pixels per Unit이라는 옵션이 있다.

우선 작으면 크게 보이고, 이 수치가 높으면 작게 보인다.

펙셀을 유니티의 크기 단위인 unit에 맞쳐서 크기를 조절할수 있다.