📖 [Map 템플릿 (1)]에서 맵의 충돌영역을 추출하는 기능을 만들었다면 '[Map 템플릿 (2)]'는 추출한 txt파일을 Scene에 적용시키는 단계를 구현하고자 한다.
아무것도 없는 Scene에서 맵의 식별번호같은 데이터를 넘기면 해당 맵을 랜더링하는 구조로 만들면 이후에 2개 이상의 맵이동을 구현할 때 굉장히 편하게 구현할 수 있을 것 같다.
첫번째 맵은 아래와 같이 하였는데 아래 구조라면 '벽에 막히는 것', '점프로 높은 지형에 올라가는 것', '발판이 없을 때' 등 여러가지를 시험할 수 있다.

맵을 불러오고, 소멸시키는 것과 같은 기능을 담당하는 Manager역할을 하는 클래스를 만들어 두는 것이 좋을 것 같다고 생각하는데 맵에 대한 정보를 알고 있으면 어떤 좌표가 충돌영역에 해당하는지 이동하고자하는 위치로 이동이 가능한지에 대해서 알기 쉬운 구조로 게임을 만들 수 있다.
MapManager를 만들어 놓으면 이후에 몬스터에 대한 컨트롤러를 만든다 했을 때 몬스터의 이동판정도 Mapmanager에서 처리가능하다.

[Map 템플릿 (1)]에서 Scene의 좌표영역에 대해서 얘기를 했었는데 구현할 MapManager에서 가지고 있을 충돌영역은 하나의 2차원 배열로 나타낼 수 있다. 아래 그림은 Scene과 MapManager의 좌표의 차이점을 나타낸 그림이다.

Scene의 좌표는 float로 0.3같은 실수값이 될 수 있고 MapManager의 인덱스로는 정수만 들어갈 수 있기 때문에 좌표변환과 정수변환작업이 필요하다.
정수변환과 좌표변환을 구현한 코드는 아래와 같다.
public Vector2Int FindClosePos(Vector3 pos)
{
Vector2Int ret = new Vector2Int(0, 0);
ret.x = (int)Mathf.Floor(pos.x);
ret.y = (int)Mathf.Floor(pos.y);
return ret;
}
public Vector2Int SceneToArr(Vector2Int pos)
{
Vector2Int ret = pos;
ret.x = ret.x - MinX;
ret.y = MaxY - ret.y - 1;
return ret;
}
public Vector2Int ArrToScene(Vector2Int pos)
{
Vector2Int ret = pos;
ret.x = ret.x + MinX;
ret.y = MaxY - 1 - ret.y;
return ret;
}
Floor함수는 입력한 값보다 작은 정수 중 가장 큰 정수를 반환하는 함수로 이를 이용해 실수좌표를 정수좌표로 변환할 수 있다. 예를들어 [-0.5f, 0.7f]의 좌표는 Scene의 [-1,0]에 해당하고 Floor함수를 적용하면 알맞는 좌표로 쉽게 변환할 수 있다.
Prefab으로 존재하는 Map GameObject를 Scene으로 불러오고 맵에 맞는 txt의 정보를 읽는 코드는 아래와 같이 구현할 수 있다.
public void LoadMap(int mapId)
{
DestroyMap();
string mapName = "Map" + mapId.ToString("000");
GameObject go = Managers.Resource.Instantiate($"Map/{mapName}");
go.name = "Map";
GameObject collision = Util.FindChild(go, "Collision", true);
if (collision != null)
collision.SetActive(false);
// 충돌 영역 불러오기
TextAsset txt = Managers.Resource.Load<TextAsset>($"Map/{mapName}");
StringReader reader = new StringReader(txt.text);
MinX = int.Parse(reader.ReadLine());
MaxX = int.Parse(reader.ReadLine());
MinY = int.Parse(reader.ReadLine());
MaxY = int.Parse(reader.ReadLine());
int xCount = MaxX - MinX + 1;
int yCount = MaxY - MinY + 1;
_collision = new bool[yCount - 1, xCount - 1];
for (int y = 0; y < yCount; y++)
{
string line = reader.ReadLine();
if (y == 0) continue;
for (int x = 0; x < xCount; x++)
{
if (x == xCount - 1) continue;
_collision[y - 1, x] = (line[x] == '1' ? true : false);
}
}
}
주의해야 할 점은 Collision Object여부를 저장하는 _collision의 좌표 접근이 [y,x]라는 점이다. Scene의 좌표는 오른쪽으로 갈 수록 x값이 오르지만 _collision에는 2번째 인덱스가 증가하기 때문에 x가 두번째 인덱스가 되어야 한다.
어느 지형에 서있거나 벽을 향해 움직일 때 캐릭터의 위치를 고정시킬 수 있어야 한다.

충돌영역에 대한 정보를 MapManager가 가지고 있기 때문에 이동하고자 하는 위치로 이동이 가능한지를 알아내기 위해 아래와 같은 함수를 정의할 수 있다. 입력으로는 실수 좌표를 받고 가장 가까운 정수좌표를 구한 뒤 _collision배열에 장애물 존재 여부를 반환한다.
public bool CanGo(Vector3 Pos)
{
if (Pos.x < MinX || Pos.x >= MaxX) return false;
if (Pos.y < MinY || Pos.y >= MaxY) return false;
Vector2Int pos = FindClosePos(Pos);
// 좌표 변환
pos = SceneToArr(pos);
return !_collision[pos.y, pos.x];
}
위의 CanGo를 적용해 캐릭터의 이동을 적용하는 ApplyMove함수는 아래와 같이 바꿀 수 있다. 여기서 x축과 y축을 따로 적용하는 이유는 예를들어 벽 방향으로 이동하면서 점프한다 했을 때 x축으로는 이동하면 안되지만 y축으로는 이동을 해야하기 때문이다.
void ApplyMove()
{
Vector3 nextPos = transform.position;
// X 축
nextPos += Time.deltaTime * new Vector3(_speedVec.x, 0, 0);
if (!Managers.Map.CanGo(nextPos))
{
// 이동 적용
}
transform.position = nextPos;
// Y 축
nextPos += Time.deltaTime * new Vector3(0, _speedVec.y, 0);
if (!Managers.Map.CanGo(nextPos))
{
// 이동 적용
}
transform.position = nextPos;
}
캐릭터나 몬스터와 같은 GameObject가 지형위에 서있는 판정은 위에 구현한 CanGo함수를 이용하면 된다. y축으로 매우 작은 거리만큼 떨어진 곳에 충돌 Object가 있으면 어딘가에 서있다는 판단을 할 수 있다.
밑의 함수는 현재 위치에 서있는 판정을 하는 IsStand함수로 CanGo함수와 다르게 입력값이 현재 위치임을 주의해야 한다.
public bool IsStand(Vector3 currentPos)
{
if (currentPos.x < MinX || currentPos.x >= MaxX) return false;
if (currentPos.y < MinY || currentPos.y >= MaxY) return false;
Vector3 standingPos = new Vector3(currentPos.x, currentPos.y - 0.01f, 0);
return !CanGo(standingPos);
}
위의 IsStand를 사용하려면 캐릭터가 점프하고 내려올 때 멈춘 지점이 0.01f이내에 있어야 한다는 조건이 필요하다. 하지만, 높은곳에서 떨어진 경우와 같이 떨어지는 속도가 빠를 경우 멈추는 지점과 땅과의 거리가 0.01f이내라는 보장을 할 수 없다. 그렇기 때문에 특정 두 지점을 이은 선과 장애물이 만나는 지점을 구한다음 구한 위치로 보정을 해야한다.
위치보정을 해야하는 상황을 나타낸 그림은 아래와 같다. 위치보정을 해야하는 상황은 위에서 떨어지거나 빠른 속도로 이동하는 상황 등 여러가지 상황에서 쓰일 수 있다.

시작점에서 도착점까지의 보정위치를 찾는 방법으로는 '이진탐색'을 사용하였다. 중앙점(mid)이 장애물에 포함되었는지 계산해가면서 빠른 시간내에 보정위치를 구할 수 있다. 하지만, 이진탐색 특성상 입력값인 to가 충돌영역안에 있어야 한다는 단점이 있다.
public Vector3 GetCorrectionPos(Vector3 from, Vector3 to)
{
Vector3 ret = from;
while (true)
{
Vector3 mid = (from + to) / 2;
if ((mid - ret).magnitude < 0.01f)
{
return ret;
}
if (CanGo(mid))
{
ret = mid;
from = mid;
}
else
{
to = mid;
}
}
}
Map 템플릿을 구현하는데 생각지도 못하게 플레이어의 이동 방식까지 변경해야 했는데 큰 어려움 없이 잘 구현한 것 같다.
원래 다음 단계는 서버를 붙이는 작업인데 서버를 붙이기 전에 몬스터와 몬스터의 공격판정까지 싱글게임으로 구현하고 그 다음으로 서버를 붙어볼 예정인데 게임로직적으로는 거의 같을 것이라고 생각하지만 나중에 구현할 때 직접 느끼는게 나을것이다. 아마도...

Map 템플릿 (3)으로 뒤의 배경에 대한 설정을 다루고 Map 템플릿을 마무리 할 예정이다.