[Unity] 1인칭 FPS 게임 - 5. Map

홍예주·2021년 7월 17일
0

1. Map

따로 씬을 만들어서 맵 크리에이터를 생성했다.
레벨이 무한대라서, 매 레벨마다 랜덤으로 맵을 생성하기 위해 MapCreator 객체를 만들었다

랜덤 요소

  • 맵 크기
  • 장애물 개수(퍼센트로)

고정 요소

  • 시작 위치
  • 클리어 위치

- Unity Object

(Map 객체에 붙어있는 스크립트)

(Map 객체에 붙어있는 콜라이더)

- C# Code

using UnityEngine;
using System.Collections.Generic;

public class MapGenerator : MonoBehaviour
{

    public Map[] maps;
    public int mapindex;

    GameManager gm;

    public Transform tilePrefab;
    //public Vector2 mapSize;

    public Transform navmeshFloor;
    public Vector2 maxMapSize;


    //맵에 생성할 prefabs
    public Transform obstaclePrefab1;
    public Transform obstaclePrefab2;
   
    public Transform clearPrefab;
    public Transform wallPrefab;
    public Transform clearcheckPrefab;

    //장애물 밀도
    //[Range(0.1f,0.2f)]
    //public float obstaclePercent;


    List<Coord> allTileCoords; //전체 타일 위치 리스트
    Queue<Coord> shuffledTileCoords; //셔플한 타일 위치 리스트
    Queue<Coord> shuffledOpenTileCoords;
    Map currentMap;
    Transform[,] tileMap;


    private void Start()
    {
        gm = FindObjectOfType<GameManager>();
        //mapindex = gm.gameInfo.GetIndex();

        mapindex = 0;

        maps[mapindex].mapSize.x = 30;
        maps[mapindex].mapSize.y = 30;
        maps[mapindex].seed = Random.Range(-10, 200);
        maps[mapindex].obstaclePercent = Random.Range(0.1f, 0.3f);

        GenerateMap();

        gm.gameInfo.IncreaseLevel();
    }

    public void GenerateMap()
    {
        Debug.Log("Generate map");

        currentMap = maps[mapindex];
        tileMap = new Transform[currentMap.mapSize.x, currentMap.mapSize.y];
        GetComponent<BoxCollider>().size = new Vector3(currentMap.mapSize.x, 0, currentMap.mapSize.y);

        allTileCoords = new List<Coord>();

        //모든 타일 좌표 리스트에
        for (int x = 2; x < currentMap.mapSize.x-2; x++)
        {
            for (int y = 2; y < currentMap.mapSize.y-2; y++)
            {
                allTileCoords.Add(new Coord(x, y));
            }
        }

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


        string holderName = "Generated Map";

        //editor 관리용
        if (transform.Find(holderName))
        {
            DestroyImmediate(transform.Find(holderName).gameObject);
            //editor에서 호출할거라 destroy가 아닌 destroyImmediate 사용

        }

        //한 객체에 다 집어넣을려고
        Transform mapHolder = new GameObject(holderName).transform;
        mapHolder.parent = transform; //map generator의 자식으로 들어가도록


        //맵 크기만큼 타일 생성
        for(int x = 0; x < currentMap.mapSize.x; x++)
        {
            for (int y = 0; y < currentMap.mapSize.y; y++)
            {
                //일반 타일
                if (x != currentMap.mapSize.x - 2 || y != currentMap.mapSize.y - 2)
                {
                    Vector3 tilePosition = CoordToPosition(x, y);

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

                    newTile.parent = mapHolder; // 새로 생성된 타일은 모두 mapHolder로 들어감

                    tileMap[x, y] = newTile;
               }

                //clear point 지점
                else
                {
                    Vector3 clearPosition = CoordToPosition(x, y);
                    Transform clearTile = Instantiate(clearPrefab, clearPosition, Quaternion.Euler(Vector3.right * 90)) as Transform;
                    Transform clearCheck = Instantiate(clearcheckPrefab, clearPosition, Quaternion.identity) as Transform;

                    clearCheck.parent = mapHolder;
                    clearTile.parent = mapHolder;

                    tileMap[x, y] = clearTile;
                }


            }
        }

        //flood fill 알고리즘 이용해 전체 맵 접근 가능한지 확인
        //
        int obstacleCount = (int)(currentMap.mapSize.x * currentMap.mapSize.y * currentMap.obstaclePercent);


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

        //랜덤한 장애물 위치
        for(int i = 0; i < obstacleCount; i++)
        {
            Coord randomCoord = GetRandmCoord();
            Vector3 obstaclePosition = CoordToPosition(randomCoord.x, randomCoord.y);


            if (i % 2 == 0)
            {
                Transform obstacle1 = Instantiate(obstaclePrefab1, obstaclePosition, Quaternion.identity) as Transform;
                obstacle1.parent = mapHolder;
            }
            else if (i % 5 == 0)
            {
                Transform obstacle2 = Instantiate(obstaclePrefab2, obstaclePosition, Quaternion.identity) as Transform;
                obstacle2.parent = mapHolder;

                //겹치는 주변 8개 타일 빼버리기
                RemoveUseTile(randomCoord.x - 1, randomCoord.y - 1, allOpenCoords);
                RemoveUseTile(randomCoord.x - 1, randomCoord.y , allOpenCoords);
                RemoveUseTile(randomCoord.x - 1, randomCoord.y + 1, allOpenCoords);
                RemoveUseTile(randomCoord.x, randomCoord.y - 1, allOpenCoords);
                RemoveUseTile(randomCoord.x, randomCoord.y + 1, allOpenCoords);
                RemoveUseTile(randomCoord.x + 1, randomCoord.y - 1, allOpenCoords);
                RemoveUseTile(randomCoord.x + 1, randomCoord.y, allOpenCoords);
                RemoveUseTile(randomCoord.x + 1, randomCoord.y + 1, allOpenCoords);

            }
            allOpenCoords.Remove(randomCoord);
        }

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

        //벽 생성
        //세로축 벽
        for(int j = 0; j < currentMap.mapSize.y; j++)
        {
            GenerateWall(mapHolder, 0, j);
            GenerateWall(mapHolder,(int)currentMap.mapSize.x - 1, j);
        }
        //가로축 벽
        for (int i = 1; i < currentMap.mapSize.x-1; i++)
        {
            GenerateWall(mapHolder, i, 0);
            GenerateWall(mapHolder, i, (int)currentMap.mapSize.y - 1);
        }


        navmeshFloor.localScale = new Vector3(maxMapSize.x, maxMapSize.y);

    }

    void RemoveUseTile(int x, int y,List<Coord> allOpenCoords)
    {
        Coord extraCoord = new Coord(x, y);
        allOpenCoords.Remove(extraCoord);
    }

    void GenerateWall(Transform mapHolder, int x, int y)
    {

        Vector3 wallPosition = CoordToPosition(x, y);
        Transform wall = Instantiate(wallPrefab, wallPosition, Quaternion.identity) as Transform;
        wall.parent = mapHolder;

    }

    //좌표를 벡터로 전환
    Vector3 CoordToPosition(int x, int y)
    {
        return new Vector3(-currentMap.mapSize.x / 2 + 0.5f + x, 0, -currentMap.mapSize.y / 2 + 0.5f + y);
    }

    //큐에서 다음 아이템 얻어 랜덤 좌표 반환
    public Coord GetRandmCoord()
    {
        Coord C = new Coord(-1,-1);

        Coord randomCoord = shuffledTileCoords.Dequeue();
        shuffledTileCoords.Enqueue(randomCoord);

        //입구&출구 근처에서 생성되지 못하게
        if ((randomCoord.x < 4 && randomCoord.y < 4) || (randomCoord.x >currentMap.mapSize.x-4 && randomCoord.y >currentMap.mapSize.y-4))
        {
            C = GetRandmCoord();
        }

        if(C.x != -1)
        {
            randomCoord = C;
        }

        return randomCoord;
    }
    
    //몬스터 랜덤 소환 위치
    public Transform GetRandomOpenTile()
    {

        Coord C = new Coord(-1, -1);

        if (shuffledOpenTileCoords.Equals(null))
        {
            //Debug.Log("no opentile");
            return null;
        }
        else
        {
            Coord randomCoord = shuffledOpenTileCoords.Dequeue();
            shuffledOpenTileCoords.Enqueue(randomCoord);

            if ((randomCoord.x < 4 && randomCoord.y < 4) || (randomCoord.x > currentMap.mapSize.x - 4 && randomCoord.y > currentMap.mapSize.y - 4))
            {
                return GetRandomOpenTile();
            }



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


    [System.Serializable]
    public class Map
    {
        public Coord mapSize;


        public float obstaclePercent;
        public int seed;
        

        
    }
}

2. Map Editor

코드 수정할 때 내가 에디터로 편하게 보기 위해 작성.
맵을 구성하는 요소 값이 바뀌거나, Generate Map 버튼을 누를 때 에디터 상에 뜨는 맵이 갱신된다.

- C# Code

using System.Collections;
using UnityEditor;
using UnityEngine;

//editor에서 맵 설정하게
[CustomEditor (typeof(MapGenerator))]
public class MapEditor : Editor
{
    public override void OnInspectorGUI()
    {
        //base.OnInspectorGUI();
        MapGenerator map = target as MapGenerator;
        if (DrawDefaultInspector())
        {
            map.GenerateMap();
        }

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

3. Utility - Shuffle

Map에서 랜덤으로 위치를 뽑아내기 위해 사용한 함수. 다른 곳에 쓸 일이 있을지도 몰라 Utility 클래스로 따로 만들어두었다.

using System.Collections;

public static class Utility
{
    //seed값을 기준으로 배열 내부 요소 섞기
    public static T[] ShuffleArray<T>(T[] array, int seed)
    {
        System.Random prng = new System.Random(seed);


        //the fisher-yates shuffle
        //마지막 루프 생략해도 돼서
        for(int i = 0; i < array.Length - 1; i++)
        {
            //i와 배열 끝 사이 중 하나 렌덤으로 선택
            int randomIndex = prng.Next(i,array.Length);

            //i와 랜덤 선택 원소 바꿈
            T temp = array[randomIndex];
            array[randomIndex] = array[i];
            array[i] = temp;
        }

        return array;
    }
}

4. 환경(Obstacle, tile, wall)

모두 Box Collider를 가지고 있다.

- Obstacle1

- OBstacle2

- Wall

- Tile

5. 발생했던 문제점

1. 콜라이더 문제

맵과 플레이어 모두에 콜라이더가 있음에도 불구하고 벽에 플레이어를 비비면 플레이어가 맵 밖으로 떨어지는 문제가 생겼다.
원인은 플레이어의 자식들에 콜라이더가 없어서 해당 물체와 벽이 부딪혔을 때 플레이어가 밖으로 떨어지는 것이었다.
플레이어의 자식들에 콜라이더를 붙여 문제를 해결했다.

2. 건물 겹침

좌표값이랑 건물 크기가 일치하지 않아서 발생하는 문제다.
근데 건물이 겹치는게 화면 상으로 더 다채로워 보여서 굳이 수정하지 않았다.

profile
기록용.

0개의 댓글