본문 바로가기

IT/유니티

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

1. Map Navigation


지난번까지는 타일도 만들고 방해물도 만들어 보았다.

이제는 거의 맵의 형태를 가지고 있다고 할수 있다. 하지만 겉모습은 맵처럼 보이지만

문제는 실제로 이 맵이 맵으로써 역활을 할수 있냐라는 것이다.

맵으로써의 역활이란, 갈수 있는 곳과 가지 못하는 곳을 나누는 것이 가장 기본이라고 할수 있을것이다.

만약 유니티나 게임 엔진을 이용하지 않는다면, 아마도 귀찮고 번거러운 작업이 될거라고 생각하지만,

게임엔진들은 기본적으로 이런  기능을 제공한다.




지난시간에 사용해본 Navigation과 NavMeshAgent를 이용하면 쉽게 해결할수 있다.


지금까지 만든 맵위에 Quad를 하나 만들고 Box Collider를 삭제해준다.

우리는 어짜피 Mesh Renderer를 이용해서 Navigation에서 bake를 할꺼니까.

단순히 만들어 놓고 맵생성 스크립트에서 퍼블릭으로 선언한후 드래그해놓는다.


그리고 이 Quad를 스크립트에서 크기를 조정하면 navigation문제를 해결할수 있다.


이 이외에도 맵정보를 구조체를 만들어서 맵정보를 보다 간편하게 관리할수 있게 수정했다.

코드는 다음과 같다.


MapGenerator.cs

using UnityEngine;

using System.Collections;

using System.Collections.Generic;


public class MapGenerator : MonoBehaviour

{

public Map[] maps;

public int mapIndex;


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

    public Transform tilePrefab;

    public Transform obstaclePreFab;

public Transform navMeshFloor;

public Transform navMeshMaskPrefab;

    //지도의 크기를 결정

public Vector2 maxMapSize;


public float tileSize;


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

    [Range(0, 1)]

    public float outlinePercent;




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



        //모든 타일의 위치 정보 구조체

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


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

        //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 < currentMap.mapSize.x; x++)

        {

            for (int y = 0; y < currentMap.mapSize.y; y++)

            {

                //화면의 가운데에 타일을 만들기 위해 아래와 같이 설정한다.

                //위치 벡터값은 정 가운데를 의미한다. 

Vector3 tilePosition = CoordToPosition(x, y);

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

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

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

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

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

                newTile.transform.SetParent(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.mapCenter && 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 *( .5f *((1 - outlinePercent) * tileSize)), Quaternion.identity) as Transform;

Transform newObstacle = Instantiate(obstaclePreFab, obstaclePosition + Vector3.up * obstacleHeight/2, Quaternion.identity) as Transform;


                //사이즈및 하이어아키 조절

newObstacle.localScale = new Vector3((1 - outlinePercent) * tileSize, obstacleHeight, (1 - outlinePercent) * tileSize);

                newObstacle.SetParent(mapHolder);


Renderer obstacleRenderer = newObstacle.GetComponent<Renderer> ();

Material obstacleMaterial = new Material (obstacleRenderer.sharedMaterial);


//y축으로 멀어질수록 백그라운드 칼러에 가까워진다.

float colorPercent = randomCoord.y / (float)currentMap.mapSize.y;

obstacleMaterial.color = Color.Lerp (currentMap.foregroundColor, currentMap.backgroundColor, colorPercent);

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

maskLeft.SetParent (mapHolder);

maskLeft.localScale = new Vector3 ((maxMapSize.x - currentMap.mapSize.x)/2f, 1f, currentMap.mapSize.y) * tileSize;


Transform maskRight = Instantiate (navMeshMaskPrefab, Vector3.right * (currentMap.mapSize.x + maxMapSize.x) / 4f * tileSize, Quaternion.identity) as Transform;

maskRight.SetParent (mapHolder);

maskRight.localScale = new Vector3 ((maxMapSize.x - currentMap.mapSize.x)/2f, 1f, currentMap.mapSize.y) * tileSize;


Transform maskTop = Instantiate (navMeshMaskPrefab, Vector3.forward * (currentMap.mapSize.y + maxMapSize.y) / 4f * tileSize, Quaternion.identity) as Transform;

maskTop.SetParent (mapHolder);

maskTop.localScale = new Vector3 (maxMapSize.x, 1f, (maxMapSize.y - currentMap.mapSize.y)/2f) * tileSize;


Transform maskDown = Instantiate (navMeshMaskPrefab, Vector3.back * (currentMap.mapSize.y + maxMapSize.y) / 4f * tileSize, Quaternion.identity) as Transform;

maskDown.SetParent (mapHolder);

maskDown.localScale = new Vector3 (maxMapSize.x, 1f, (maxMapSize.y - currentMap.mapSize.y)/2f) * tileSize;


navMeshFloor.localScale = new Vector3(maxMapSize.x, maxMapSize.y) * tileSize;

    }


    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(currentMap.mapCenter);

mapFlags[currentMap.mapCenter.x, currentMap.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)(currentMap.mapSize.x * currentMap.mapSize.y - currentObstacleCount);

        return targetAccessibleTileCount == accessibleTileCount;

    }





    //좌표값을 실제 위치값으로 

//정수값을 넣어서 계산하면, 짝수가 아닌경우에는 화면이 삐뚤어질수 있다.

    Vector3 CoordToPosition(int x, int y)

    {

return new Vector3(-currentMap.mapSize.x / 2f + 0.5f + x, 0f, -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 foregroundColor;

public Color backgroundColor;


public Coord mapCenter{

get{

return new Coord (mapSize.x / 2, mapSize.y / 2);

}

}

}

}


쩝 보고 이해는 되는데, 다시 짜라고 하면 비슷하게도 못짤거 같다.


아래 그림은 여백을 navigation에서 제외하기 위한 마스크를 생성할때 쓰인 수식이다.

간단하지만, 이렇게 설명하고 수식으로 만들수 있는것이 능력이지 않을까?

(currentMap.mapSize.x + maxMapSize.x) / 4f  요렇게~




2. Finishing the map generator


위에서의 작업결과는 끝이라고 말할수도 있다.

하지만 우리 세바스찬에게는 아직인가 보다.ㅋㅋ

방해물의 높이를 조절한다거나, 색깔을 그라데이션을 주기도 하고

여러가지 세세한 부분까지 수정 추가 했다.


그리고 맵에디터부분도 간단히 수정했다.

맵에디터는 거의 잊고 있었다. ㅠㅠ


MapEditor.cs

using UnityEngine;

using System.Collections;

using UnityEditor;


[CustomEditor (typeof (MapGenerator))]

public class MapEditor : Editor {


    public override void OnInspectorGUI()

    {

        MapGenerator map = target as MapGenerator;


if (DrawDefaultInspector ()) {

map.GenerateMap();

}


if (GUILayout.Button ("Generate Map")) {

map.GenerateMap();

}


    }


}

DrawDefaultInspector ()는 불린값을 리턴하므로, 참일때는 인스펙터가 잘 그려졌다는 의미를 가진다.

인스펙터가 참일경우에만 맵을 생성하고, 만약을 위해서 버튼도 하나 추가 놓았다.




3. Random Spawning

지금 까지 만든 맵과 지난번에 만든 적과 플레이어 화면을 합치면 생각해야 할것이 무엇일까?

가장 시급한것은 바로 적을 랜덤하게 생성시켜야 할것이다.

걍 랜덤하게 생성하면 될까?

절대안된다. 왜냐하면 적은 반드시 비어있는 타일 위에서 나와야만 하기 때문이다.

어떻게 만들었는지는 소스코드를 보면 알것이고 단순하게 말로 풀어서 설명해보면,

방해물이 생성될때 방해물을 위치를 전체위치정보에서 하나씩 빼면, 나중에 남는 값은 빈타일이 될것이다.

이 빈타일위에 적을 만들면 문제가 없을것이고,

나오기전에 경고의 의미로 타일의 색깔을 빨간색으로 깜빡이게 한다.

이때사용하는 함수는 Mathf.pingpong함수를 이용하면 된다.


MapGenerator.cs

using UnityEngine;

using System.Collections;

using System.Collections.Generic;


public class MapGenerator : MonoBehaviour

{

public Map[] maps;

public int mapIndex;


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

    public Transform tilePrefab;

    public Transform obstaclePreFab;

public Transform navMeshFloor;

public Transform navMeshMaskPrefab;

    //지도의 크기를 결정

public Vector2 maxMapSize;


public float tileSize;


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

    [Range(0, 1)]

    public float outlinePercent;



    List<Coord> allTileCoords;

    Queue<Coord> shuffledTileCoords;

Queue<Coord> shuffledOpenTileCoords;

Transform[,] tileMap;



Map currentMap;


    void Start()

    {

        GenerateMap();

    }


    //맵생성

    public void GenerateMap()

    {

currentMap = maps[mapIndex];

tileMap = new Transform[currentMap.mapSize.x, currentMap.mapSize.y];

System.Random prng = new System.Random (currentMap.seed);


GetComponent<BoxCollider> ().size = new Vector3 (currentMap.mapSize.x * tileSize, .05f, currentMap.mapSize.y * tileSize);



        //모든 타일의 위치 정보 구조체

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


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

        //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 < currentMap.mapSize.x; x++)

        {

            for (int y = 0; y < currentMap.mapSize.y; y++)

            {

                //화면의 가운데에 타일을 만들기 위해 아래와 같이 설정한다.

                //위치 벡터값은 정 가운데를 의미한다. 

Vector3 tilePosition = CoordToPosition(x, y);

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

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

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

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

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

                newTile.transform.SetParent(mapHolder);

tileMap [x, y] = newTile;

            }

        }


        

        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;

List<Coord> allOpenCoords = new List<Coord> (allTileCoords);



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

        {

            //셔플한 위치정보중 가장 앞의 정보를 가져온다.

            Coord randomCoord = GetRandomCoord();


            obstacleMap[randomCoord.x, randomCoord.y] = true;

            currentObstacleCount++;


if (randomCoord != currentMap.mapCenter && 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 *( .5f *((1 - outlinePercent) * tileSize)), Quaternion.identity) as Transform;

Transform newObstacle = Instantiate(obstaclePreFab, obstaclePosition + Vector3.up * obstacleHeight/2, Quaternion.identity) as Transform;


                //사이즈및 하이어아키 조절

newObstacle.localScale = new Vector3((1 - outlinePercent) * tileSize, obstacleHeight, (1 - outlinePercent) * tileSize);

                newObstacle.SetParent(mapHolder);


Renderer obstacleRenderer = newObstacle.GetComponent<Renderer> ();

Material obstacleMaterial = new Material (obstacleRenderer.sharedMaterial);


//y축으로 멀어질수록 백그라운드 칼러에 가까워진다.

float colorPercent = randomCoord.y / (float)currentMap.mapSize.y;

obstacleMaterial.color = Color.Lerp (currentMap.foregroundColor, currentMap.backgroundColor, colorPercent);

obstacleRenderer.sharedMaterial = obstacleMaterial;


allOpenCoords.Remove (randomCoord);

            }else

            {

                obstacleMap[randomCoord.x, randomCoord.y] = false;

                currentObstacleCount--;

            }


            

        }



shuffledOpenTileCoords = new Queue<Coord>(Utility.ShuffleArray(allOpenCoords.ToArray(), currentMap.seed));



Transform maskLeft = Instantiate (navMeshMaskPrefab, Vector3.left * (currentMap.mapSize.x + maxMapSize.x) / 4f * tileSize, Quaternion.identity) as Transform;

maskLeft.SetParent (mapHolder);

maskLeft.localScale = new Vector3 ((maxMapSize.x - currentMap.mapSize.x)/2f, 1f, currentMap.mapSize.y) * tileSize;


Transform maskRight = Instantiate (navMeshMaskPrefab, Vector3.right * (currentMap.mapSize.x + maxMapSize.x) / 4f * tileSize, Quaternion.identity) as Transform;

maskRight.SetParent (mapHolder);

maskRight.localScale = new Vector3 ((maxMapSize.x - currentMap.mapSize.x)/2f, 1f, currentMap.mapSize.y) * tileSize;


Transform maskTop = Instantiate (navMeshMaskPrefab, Vector3.forward * (currentMap.mapSize.y + maxMapSize.y) / 4f * tileSize, Quaternion.identity) as Transform;

maskTop.SetParent (mapHolder);

maskTop.localScale = new Vector3 (maxMapSize.x, 1f, (maxMapSize.y - currentMap.mapSize.y)/2f) * tileSize;


Transform maskDown = Instantiate (navMeshMaskPrefab, Vector3.back * (currentMap.mapSize.y + maxMapSize.y) / 4f * tileSize, Quaternion.identity) as Transform;

maskDown.SetParent (mapHolder);

maskDown.localScale = new Vector3 (maxMapSize.x, 1f, (maxMapSize.y - currentMap.mapSize.y)/2f) * tileSize;


navMeshFloor.localScale = new Vector3(maxMapSize.x, maxMapSize.y) * tileSize;

    }


    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(currentMap.mapCenter);

mapFlags[currentMap.mapCenter.x, currentMap.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)(currentMap.mapSize.x * currentMap.mapSize.y - currentObstacleCount);

        return targetAccessibleTileCount == accessibleTileCount;

    }





    //좌표값을 실제 위치값으로 

//정수값을 넣어서 계산하면, 짝수가 아닌경우에는 화면이 삐뚤어질수 있다.

    Vector3 CoordToPosition(int x, int y)

    {

return new Vector3(-currentMap.mapSize.x / 2f + 0.5f + x, 0f, -currentMap.mapSize.y / 2f + 0.5f + y) * tileSize;

    }



public Transform GetTileFromPosition(Vector3 position){

int x = Mathf.RoundToInt (position.x / tileSize + (currentMap.mapSize.x - 1f) / 2f);

int y = Mathf.RoundToInt (position.z / tileSize + (currentMap.mapSize.y - 1f) / 2f);

x = Mathf.Clamp(x, 0, tileMap.GetLength (0)-1);

y = Mathf.Clamp(y, 0, tileMap.GetLength (1)-1);

return tileMap[x, y];

}


    //큐에서 가장 앞에 있는 놈을 리턴하고 

    //떼온 맨 앞의 값을 큐에 가장 마지막에 넣는다.

    public Coord GetRandomCoord()

    {

        Coord randomCoord = shuffledTileCoords.Dequeue();

        shuffledTileCoords.Enqueue(randomCoord);

        return randomCoord;

    }


public Transform GetRandomOpenTile(){

Coord randomCoord = shuffledOpenTileCoords.Dequeue();

shuffledOpenTileCoords.Enqueue(randomCoord);

return tileMap[randomCoord.x, randomCoord.y]; 

}



    //셔플할때 필요한 위치정보 구조체

[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 foregroundColor;

public Color backgroundColor;


public Coord mapCenter{

get{

return new Coord (mapSize.x / 2, mapSize.y / 2);

}

}

}

}





spawner.cs

using UnityEngine;

using System.Collections;


public class Spawner : MonoBehaviour {


    public Wave[] waves;

    public Enemy enemy;


LivingEntitiy playerEntity;

Transform playerT;


    Wave currentWave;

    int currnetWaveNumber;


    int enemyRemainingToSpawn;

    int enemyRemainingAlive;

    float nextSpawnTime;


MapGenerator map;


//플레이어가 한곳에 오래 머물르면 근처에 적을 스폰하기 위한 변수

float timeBetweenCampingChecks =2;

float campThresholdDistance = 1.5f;

float nextCampCheckTime;

Vector3 campPositionOld;

bool isCamping;


bool isDisabled;


    [System.Serializable]

    public class Wave

    {

        public int enemyCount;

        public float timeBetweenSpawns;

    }

    

    void Start()

    {

playerEntity = FindObjectOfType<Player> ();

playerT = playerEntity.transform;


nextCampCheckTime = timeBetweenCampingChecks + Time.time;

campPositionOld = playerT.position;

//죽은 다음에는 적들을 스폰하지 않기 위해서 플레이어가 죽으면 스포너에게 알린다.

playerEntity.OnDeath += OnPlayerDeath;


map = FindObjectOfType<MapGenerator> ();

        NextWave();

    }

   

// Update is called once per frame

void Update () {

if(!isDisabled){

//일정시간(timeBetweenCampingChecks) 안에 일정한 반경(campThresholdDistance)안에서

//벗어나지 않으면 캠핑하고 있다고 판단한다.

if(Time.time > nextCampCheckTime){

nextCampCheckTime = Time.time + timeBetweenCampingChecks;

isCamping = (Vector3.Distance (playerT.position, campPositionOld) < campThresholdDistance);

campPositionOld = playerT.position;

}


if(enemyRemainingToSpawn > 0  && Time.time > nextSpawnTime)

{

enemyRemainingToSpawn--;

nextSpawnTime = Time.time + currentWave.timeBetweenSpawns;


StartCoroutine (SpawnEnemy());

}

}


}


IEnumerator SpawnEnemy(){

float spawnDelay = 1;

float tileFlashSpeed = 4;


Transform randomTile = map.GetRandomOpenTile ();

//만약 플레이어가 캠핑중이라면, 스폰 위치는 플레이어 바로 옆으로 한다.

if (isCamping) {

randomTile = map.GetTileFromPosition (playerT.position);

}

Material tileMat = randomTile.GetComponent<Renderer> ().material;

Color initialColor = tileMat.color;

Color flashColor = Color.red;

float spawnTimer = 0;


while(spawnTimer < spawnDelay){


tileMat.color = Color.Lerp (initialColor, flashColor,Mathf.PingPong(spawnTimer*tileFlashSpeed, 1));


spawnTimer += Time.deltaTime;

yield return null;

}

Enemy spawnedEnemy = Instantiate(enemy, randomTile.position + Vector3.up, Quaternion.identity) as Enemy;

spawnedEnemy.OnDeath += OnEnemyDeath;

}


void OnPlayerDeath(){

isDisabled = true;

}



    void OnEnemyDeath()

    {

        enemyRemainingAlive--;


        if (enemyRemainingAlive == 0)

        {

            NextWave();

        }

    }


    void NextWave()

    {

        currnetWaveNumber++;


        print("Wave: " + currnetWaveNumber);

        if(currnetWaveNumber -1 < waves.Length)

        {

            currentWave = waves[currnetWaveNumber - 1];

            enemyRemainingToSpawn = currentWave.enemyCount;

            enemyRemainingAlive = enemyRemainingToSpawn;

        }

    }

}



UI정리