[유니티]탑다운 슈팅 따라하기 #13 랜덤 소환

안녕하세요 유랑입니다.

 

 

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

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

 

 

 

1. 탑다운 슈팅 따라하기

 

 

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

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

 

유튜브 사이트 => 유튜브 

 

 

 

유니티 슈팅

 

 

 

1-1) 랜덤 소환

 

 

이번 시간에는 랜덤 소환을 해보겠습니다.

말그대로 랜덤한 위치에서 적을 생성해 주며,

생성되기 전에 해당 타일을 빨간색으로 점멸시켜주며 알려줍니다.

 

 

 

유니티 슈팅

 

 

 

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

 

 

MapGnerator 스크립트에 적이 생성될 타일 정보와

타일 위치에 대한 내용을 추가해 주겠습니다.

 

 

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; // 셔플된 좌표 리스트
    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);

        // 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;
                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.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;

                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);
        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 Transform GetTileFromPosition(Vector3 position)
    {
        int x = Mathf.RoundToInt(position.x / tileSize + (currentMap.mapSize.x - 1) / 2f);
        int y = Mathf.RoundToInt(position.z / tileSize + (currentMap.mapSize.y - 1) / 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 foregroundColour; // 전반부 색
        public Color backgroundColour; // 후반부 색

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

    }
}

 

 

유니티 슈팅

 

 

맵 생성이 될 때 타일맵 위치값 정보를 넣어주고

 

 

유니티 슈팅

 

 

장애물이 생성될 때 마다 allOpenCoords 리스트에서 해당 부분을 삭제 후

최종적으로 적이 생성될 수 있는 타일 리스트 정보를 만들어 줍니다.

 

 

유니티 슈팅

 

 

그리고 현재 플레이어가 있는 타일 정보와

랜덤 위치 값 정보를 출력할 수 있도록 구성해 줍니다.

이제 Spanwer 스크립트에서 해당 코드를 사용해 줄게요!!

 

 

유니티 슈팅

 

 

 

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

 

 

Spanwer 스크립트에서는 플레이어 정보를 토대로 적을 소환시킵니다.

캠핑이라는 부분을 이용해 플레이어 앞에 소환할 수도,

멀리서 소환할 수도 있답니다.

 

 

 

using UnityEngine;
using System;
using System.Collections;

public class Spawner : MonoBehaviour
{
    public Wave[] waves;
    public Enemy enemy;

    LivingEntity playerEntity;
    Transform playerT; // 플레이어 트랜스폼

    Wave currentWave; 
    int currentWaveNumber; // 현재 웨이브

    int enemiesRemainingToSpawn; // 남은 소환할 적의 수
    int enemiesRemainingAlive; // 살아있는 적의 수
    float nextSpawnTime; // 다음번 소환 시간

    MapGenerator map;
    /*Camping : 게임에서 유리한 위치에 죽치고 않아있는 비매너 플레이*/
    float timeBetweenCampingChecks = 2; // 캠핑 간격 체크
    float campThresholdDistance = 1.5f; // 캠프 최소 한계거리
    float nextCampCheckTime; // 다음 캠핑 검사 예정 시간
    Vector3 campPositionOld; // 이전 캠프 위치
    bool isCamping; // 캠핑 여부

    bool isDisabled; // 사망 여부

    void Start()
    {
        playerEntity = FindObjectOfType<Player>();
        playerT = playerEntity.transform;

        nextCampCheckTime = timeBetweenCampingChecks + Time.time;
        campPositionOld = playerT.position;
        playerEntity.OnDeath += OnPlayerDeath;

        map = FindObjectOfType<MapGenerator>();
        NextWave();
    }

    void Update()
    {
        if (!isDisabled)
        {
            // 캠핑 체크
            if (Time.time > nextCampCheckTime)
            {
                nextCampCheckTime = Time.time + timeBetweenCampingChecks;

                isCamping = (Vector3.Distance(playerT.position, campPositionOld) < campThresholdDistance);
                campPositionOld = playerT.position;
            }
            // 적 소환 
            if (enemiesRemainingToSpawn > 0 && Time.time > nextSpawnTime)
            {
                enemiesRemainingToSpawn--;
                nextSpawnTime = Time.time + currentWave.timeBetweenSpawns;

                StartCoroutine(SpawnEnemy());
            }
        }
    }

    IEnumerator SpawnEnemy()
    {
        float spawnDelay = 1;
        float tileFlashSpeed = 4;

        Transform spawnTile = map.GetRandomOpenTile();
        if (isCamping)
        {
            spawnTile = map.GetTileFromPosition(playerT.position);
        }
        Material tileMat = spawnTile.GetComponent<Renderer>().material;
        Color initialColour = tileMat.color;
        Color flashColour = Color.red;
        float spawnTimer = 0;

        while (spawnTimer < spawnDelay)
        {

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

            spawnTimer += Time.deltaTime;
            yield return null;
        }

        Enemy spawnedEnemy = Instantiate(enemy, spawnTile.position + Vector3.up, Quaternion.identity) as Enemy;
        spawnedEnemy.OnDeath += OnEnemyDeath;
    }

    void OnPlayerDeath()
    {
        isDisabled = true;
    }

    void OnEnemyDeath()
    {
        enemiesRemainingAlive--;

        if (enemiesRemainingAlive == 0)
        {
            NextWave();
        }
    }

    void NextWave()
    {
        currentWaveNumber++;
        print("Wave: " + currentWaveNumber);
        if (currentWaveNumber - 1 < waves.Length)
        {
            currentWave = waves[currentWaveNumber - 1];

            enemiesRemainingToSpawn = currentWave.enemyCount;
            enemiesRemainingAlive = enemiesRemainingToSpawn;
        }
    }
    [Serializable]
    public class Wave
    {
        public int enemyCount; // 소환할 적의 수
        public float timeBetweenSpawns; // 소환 딜레이
    }
}

 

 

유니티 슈팅

 

 

캠핑 체크를 통해 플레이어가 유리한 위치에 가만히 있는걸 방지할 수 있겠네요.

 

 

유니티 슈팅

 

 

적이 소환될 때는 타일 머티리얼 색도 바꾸고,

점멸시켜 소환 위치를 알려줍니다.

 

 

유니티 슈팅

 

 

웨이브 마다 적이 생성될 수와 간격을 조절해 주고

 

 

유니티 슈팅

 

 

게임을 시작해 봅니다.

어떤가요? 플레이어가 캠핑하면 적이 앞에서 소환되네요ㅎㅎ

 

 

유니티 슈팅

 

 

 

2. 마무리

 

 

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

탐다운 슈팅을 따라하면서 랜덤 소환을 해보았습니다.

감사합니다.

 

 

 

수업자료: 탑다운 슈팅 따라하기 #13 랜덤 소환

 

 

 

 

 

 

 

 

 

댓글

Designed by JB FACTORY