이번 에피소드에선 랜덤 장애물 배치 클래스를 구현했습니다. 제네릭 타입, 리스트, 큐, IEnumerable, collection, 알고리즘 등을 배울 수 있습니다.
public static T[] ShuffleArray<T>(T[] array, int seed)
{
//prng = pseudorandom number generator 의 약자 (유사난수생성기)
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;
}
랜덤 배치를 위해 배열을 셔플 했습니다. 이렇게 배열을 카드 섞듯이 하는 것을 Fisher-Yates shuffle 알고리즘이라 합니다. i번 인덱스에 배열 내 랜덤한 인덱스 값을 집어넣습니다. 이후 i++ 되고 랜덤한 값이 들어간 인덱스는 좌측으로 밀려나게 됩니다. 그러면 배열 내 랜덤한 값이 중복되어 생성되는 일을 방지할 수 있습니다.
이후에 해야할 일은 ShuffleArray에 인자로 받을 배열을 만드는 일입니다. 배열에는 각 타일의 위치값이 들어갑니다.
참고 : 제네릭에 대해서 궁금하다면
참고 : 제네릭 타입은 데이터 타입 요소를 확정하고 싶지 않을때 특정 데이타 타입을 파라미터로 받아들이도록 클래스를 정의합니다. 이렇게 정의된 제네릭 타입은 클래스 명과 함께 구체적인 데이타 타입을 함께 지정해줘야 합니다. 이렇게 되면 일부 상이한 데이타 타입때문에 여러 개의 비슷한 클래스를 따로 만드는 수고를 덜 수 있습니다. C# 제네릭은 인터페이스, 메서드 등에도 똑같이 적용됩니다.
참고 : Random 선언 시 seed는 무엇을 의미할까?
참고 : Random 선언 할 때 Seed 값을 인자로 받는데요. 이는 어떤 용도일까요? 기본적으로 컴퓨터는 난수를 생성하지 못하기 때문에 난수를 생성하는 알고리즘들이 필요합니다. 이 때 난수 생성 알고리즘을 위해 사용되는 수를 바로 seed(씨앗)이라 부릅니다. 난수 생성 알고리즘에서 사용되는 seed가 똑같다면 출력되는 난수 값이 똑같은 패턴을 가지게 됩니다. 때문에 seed값을 계속 바꿔줘야 진정한 난수 출력이 가능해 집니다. 보통 값이 계속 바뀌는 "현재 시간"과 같은 변화하는 수로 seed를 지정해줘야합니다.
public struct Coord
{
public int x;
public int y;
public Coord(int _x, int _y)
{
x = _x;
y = _y;
}
}
좌표값을 저장하는 구조체를 만들었습니다. 구조체의 이름인 Coord는 좌표라는 의미를 가집니다.
List<Coord> allTileCoords;
이 후 Coord를 타입으로 하는 List를 생성합니다. allTileCoords: List는 타일의 x,y값을 저장 할 공간입니다.
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));
}
}
allTileCoords에 List<Coord>를 생성해줍니다. 그리고 for문을 통해 타일의 x, y값을 입력해줍니다.
Queue<Coord> shuffledTileCoords;
셔플된 타일을 저장할 자료구조 큐를 선언합니다. 이 곳에는 랜덤한 타일 위치 x, y값이 들어갑니다. 여기서 리스트를 사용해도 되지만 큐를 사용하기에 더 용이한 상황이기에 큐를 사용합니다.
참고 : 리스트와 큐의 차이에 대해서 궁금하다면
리스트 : 자료들이 데이터 상에 흩뿌러져 있습니다. 리스트에는 값과 포인터가 함께 있는데, 흩뿌러진 자료들을 포인터가 연결시켜주고 있습니다. 이러한 구조때문에 리스트는 특정 데이터를 원하는 곳에 집어넣기 편합니다. 단지 포인터를 새로 들어온 값으로 연결시켜 주면 되기 때문입니다. 단점은 자료들이 데이터 상에 흩뿌러져 있는 형태이기 때문에 인덱스를 찾는 과정이 타 자료구조에 비해 상대적으로 느린 편입니다.
큐 : 선입선출의 구조를 지닌 자료구조입니다. 대기행렬이라고도 부르는 큐는 사람들이 줄을 스고 있는 모습과 흡사합니다. 먼저 들어온 사람이 먼저 들어가듯이 큐에서도 FIFO(First In First Out)이 적용됩니다. 큐에 자료를 넣고 뺄때는 inqueue, dequeue라 부릅니다.
리스트와 큐의 차이 : 이번에 큐를 사용한 shuffledTileCoords는 셔플된 타일을 받아들이기 때문에 추후 변경될 가능성이 적기 때문에 데이터를 더 효율적으로 저장하는 큐를 사용한듯합니다. *제 추측입니다.
shuffledTileCoords = new Queue<Coord>(IEnumerable < Coord > collection);
셔플타일좌표을 생성할 때 (IEnumerable<Coord>collection) 을 입력받을 수 있습니다.
참고 : IEnumerable, collection에 대해서 궁금하다면
IEnumerable : IEnumerable과 IEnumerator는 인터페이스를 구현하여 사용자 정의 '컬렉션'을 반복하는데 가장 좋은 방법입니다. 즉, 개발자가 Class를 만들 때 이 class가 사용자 정의 컬렉션이고 foreach를 쓰고 싶다면, IEnumerable과 IEnumerator 인터페이스에서 정의된 메소드들을 구현해야합니다.
collection : 컬렉션은 간단히 말해서 데이터 모음(자료구조) 입니다. 즉, 우리가 잘 아는 배열, 스택, 큐, 해쉬테이블 등을 C#에서 컬렉션이라는 이름으로 제공하고 있는 것 뿐입니다. 이를 사용하기 위해선 System.Collections; 에 접근해야합니다.
shuffledTileCoords = new Queue<Coord>(Utility.ShuffleArray(allTileCoords.ToArray(), (int)Time.time));
shuffledTileCoords에 ShuffleArray()를 이용해 뒤죽박죽된 타일좌표값을 담아줍니다.
public Coord GetRandomCoord()
{
Coord randomCoord = shuffledTileCoords.Dequeue();
shuffledTileCoords.Enqueue(randomCoord);
return randomCoord;
}
이어서 랜덤좌표를 뽑아내는 GetRandomCoord()를 만들어줬습니다. 이 메소드는 그저 큐로 부터 다음 아이템을 얻어 랜덤 좌표를 반환해줍니다. randomCoord에 셔플된 타일 좌표 큐의 첫 아이템을 가지도록 shuffledTileCoords.Dequeue()를 호출하여 할당해줍니다. 그리고 그렇게 얻은 랜덤좌표를 큐의 마지막으로 되돌려 놓습니다.
int obastacleCount = 10;
for (int i = 0; i < obastacleCount; i++)
{
Coord randomCoord = GetRandomCoord();
}
그리고 장애물을 몇개 설정할지 정하고 특정 좌표값을 입력받습니다. 남은 일은 입력받은 좌표값을 Vector3 포지션 값으로 변환하는 일입니다. Vector3 포지션 값으로 변환하는 일은 앞서 타일 맵을 생성할 때 변환식을 만들어 해결한 적 있습니다. 랜덤좌표값도 이와 똑같은 변환식을 사용하는데요. 변환식을 중복사용하기 때문에 함수를 만들어 묶어주겠습니다.
Vector3 CoordToPosition(int x, int y)
{
return new Vector3(-mapSize.x / 2 + 0.5f + x, 0, -mapSize.y / 2 + 0.5f + y);
}
깔끔한 좌표 변환식이 만들어졌습니다.
int obastacleCount = 10;
for (int i = 0; i < obastacleCount; i++)
{
Coord randomCoord = GetRandomCoord();
Vector3 obstaclePosition = CoordToPosition(randomCoord.x, randomCoord.y);
}
이어서 위와 같이 장애물 위치 값을 받습니다. 그리고 장애물로 사용할 프리팹이 필요합니다. 이어서 장애물 프리팹도 선언해줍니다.
public Transform obastaclePrefab;
장애물 프리팹을 선언합니다.
int obastacleCount = 10;
for (int i = 0; i < obastacleCount; i++)
{
Coord randomCoord = GetRandomCoord();
Vector3 obstaclePosition = CoordToPosition(randomCoord.x, randomCoord.y);
Transform newObstacle = Instantiate(obastaclePrefab, obstaclePosition, Quaternion.identity) as Transform;
}
그리고 장애물 프리펩을 랜덤한 위치에 배치시켜줍니다. 하지만 이대로 유니티 에디터 상에 가게 되면 장애물이 무수히 생성되는 것을 알 수 있는데요. 이는 제거해주지 않기 때문입니다.
newObstacle.parent = mapHolder;
장애물의 부모를 맵 홀더로 지정해준 뒤에 맵 홀더와 함께 파괴 될 수 있도록 해줍니다.
완성됐습니다.
오늘 작업한 내용을 요약해보겠습니다. 유틸리티 클래스에서 셔플 알고리즘 함수를 만들고 맵 제너레이터 클래스에서 이를 사용했습니다. 맵 제너레이터에선 Coord()라는 좌표를 저장하는 구조체를 생성한 후 이를 리스트로 넘겨주고 랜덤유틸리티 클래스의 셔플 메소드를 통해 랜덤한 좌표값을 큐에 저장했습니다. 이 자료를 가지고 장애물을 랜덤한 위치에 생성시켰습니다. 본 영상은 10분안팍이었는데 다양한 내용을 배울 수 있었습니다. 이상입니다.
다음 에피소드에선 장애물을 생성할 때 접근할 수 없도록 갇힌 영역을 만들지 않는 방법을 배워보겠습니다.