진행 내용
에피소드 1회에서는 Cellular Automata 개념을 이용하여 타일맵을 랜덤으로 생성하는 맵 제너레이터를 구현합니다.
에피소드1 의 제목은 Cellular Automata 입니다. 이는 생소한 용어이니 간단하게 개념을 잡고 가겠습니다.
출처 https://ooz.co.kr/283 에 따르면 Cellular Automata 의 특징을 아래와 같이 정리합니다.
1. 유한개의 상태를 갖는 셀이 1차원, 2차원 등 공간에 존재합니다.
2. 예를 들어 On/Off, True/False, black/white, 1/0/-1 등으로 각 Cell은 상태를 가집니다.
3. Cell 들은 시간에 따라 현재 자신의 상태와 주변 셀들의 상태에 따라, 근거한 규칙에 의해 변화합니다.
4. 인공지능의 한 분야인 Artificial Life 에 사용된다고합니다.
5. 규칙은 셀 간의 관계를 표현함으로서 만들어지게됩니다.
위 예를 보시면 어떤 원리로 타일맵이 만들어지는지 이해할수있습니다.
그렇다면?
이번 에피소드에서 Cellular Automata 는 어떻게 쓰였을까요.
State는 0과 1이며 0은 하얀색타일, 1은 검은타일로 표현됩니다. 즉, 여기서 Cell은 타일 한개로 이해할수있습니다.
규칙은 자신을 제외한 이웃한 타일 8개에서 인접한 타일 중 벽(1)이 4개 초과면 중심타일은 검정타일(1)이 되고, 인접한 벽이 4개 미만이면 중심타일은 하얀색타일(0)이 됩니다. 이 규칙은 SmoothMap() 함수를 살펴보시면 됩니다.
마지막으로 오늘 작성한 코드를 아래에 설명합니다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MapGenerator : MonoBehaviour
{
public int width;
public int height;
public string seed;
public bool useRandomSeed;
[Range(0, 100)]
public int randomFillPercent;
int[,] map;
private void Start() {
GenerateMap();
}
private void Update() {
if (Input.GetMouseButtonDown(0)) {
GenerateMap();
}
}
private void GenerateMap() {
map = new int[width, height];
RandomFillMap();
for (int i = 0; i < 5; i++) {
SmoothMap();
}
}
void RandomFillMap() {
if (useRandomSeed) {
seed = Time.time.ToString();
}
//GetHashCode()를 하면 string 타입의 seed 에서 숫자를 뽑아내는데 렌덤한 숫자라 prng 로서 가치가 있는듯, 자세하게는 더 알아봐야할듯.
//prng는 pseudoRandomNumberGenerator 의 약자인데, 컴퓨터는 난수를 생성하지 못해서 일반적으로 난수를 위해서 seed가 필요하다.
//이때 이 난수를 위한 seed를 랜덤하게 생성해주는게 필요한데, 일반적으로 prng 라고 부르는듯하다.
System.Random prng = new System.Random(seed.GetHashCode());
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
//아래 조건문은 그림을 그려보던가 해서 이해를 해야겠다... 아마도 정가운데 3*3공간에 1값을 넣어주는 듯 하다.
if (x == 0 || x == width - 1 || y == 0 || y == height - 1) {
map[x, y] = 1;
}
else {
//랜덤퍼센트보다 낮으면 1을 반환하는데, 이는 1벽을 의미하고 0은 빈 공간을 의미한다.
map[x, y] = (prng.Next(0, 100) < randomFillPercent) ? 1 : 0;
}
}
}
}
void SmoothMap() {
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
int neighbourWallTiels = GetSurroundingWallCount(x, y);
//이웃한 타일이 4개 초과면 자신도 벽이된다..
if (neighbourWallTiels > 4) {
map[x, y] = 1;
}
//이웃한 타일이 4개 미만이면 뚫린 공간이된다..
//즉 이웃한 타일이 많은쪽은 점점 많은쪽으로 벽이되고, 없는쪽은 점점 없는쪽으로 빈공간이 되는것.
else if(neighbourWallTiels < 4) {
map[x, y] = 0;
}
}
}
}
int GetSurroundingWallCount(int gridX, int gridY) {
int wallCount = 0;
//아래 2중 for문은 gridX의 주면 3*3에 있는 이웃의 값을 가져온다.
for (int neighbourX = gridX - 1; neighbourX <= gridX + 1; neighbourX++) {
for (int neighbourY = gridY - 1; neighbourY <= gridY + 1; neighbourY++) {
//아래 이프문은 예방문인듯. 가로세로값 이내에서 이웃값을 계산하기위한 조건문
if (neighbourX >= 0 && neighbourX < width && neighbourY >= 0 && neighbourY < height) {
//이웃의 값만 가져오기 위해 자기자신의 값을 제외한다
//자기 자신을 빼고 이웃한 8개의 타일에서 벽의 개수를 카운트합니다
if (neighbourX != gridX || neighbourY != gridY) {
wallCount += map[neighbourX, neighbourY];
}
}
else {
//이웃값을 계산할때 width, height값을 포함하지 않았으므로 테두리는 모두 벽으로 인정되며 카운트된다;
wallCount++;
}
}
}
return wallCount;
}
private void OnDrawGizmos() {
if (map != null) {
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
Gizmos.color = (map[x, y] == 1) ? Color.black : Color.white;
Vector3 pos = new Vector3(-width / 2 + x + 0.5f, 0,-height / 2 + y + 0.5f);
Gizmos.DrawCube(pos, Vector3.one);
}
}
}
}
}
코드를 읽어보면 타일맵이 생성되는 과정을 이해할 수 있습니다. 이상입니다.