개요
개인 프로젝트의 주제인 Javascript(Node.js)로 로그라이크 게임 만들기를 실시하였다.
나는 이를 응용하여 textRPG를 만드는 시간을 가졌고, 나의 textRPG에는 맵 생성을 비롯한 이동하기 로직이 포함된다. 이번 시간에는 맵 생성 시에 고난을 겪었던 이야기를 하고자 한다! 🤣🤣🤣
트러블 슈팅
배경
처음 주제를 선정했을 때 내 머릿속 흐름은 다음과 같았다...
공격, 방어... → 전투 → RPG → 미니맵...
아, 미니맵을 생성하자...!
const map = Array.from({ length: height }, () => Array(width).fill('.'));
위와 같이 맵을 생성하는 데에는 딱 한 줄이면 충분했다!!!
하지만 RPG 게임에 텅 비어있는 맵이 어디있겠는가!?
if (player.x === x && player.y === y) {
// 플레이어의 위치
return chalk.blue('P');
}
if (boss && boss.x === x && boss.y === y) {
// 보스 몬스터의 위치
return chalk.magenta('B');
}
플레이어 위치와 보스 위치(랜덤 생성)도 표시해주고,
// 장애물 추가 (연결된 경로를 방해하지 않도록)
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// 처음(플레이어) 지점과 마지막(보스) 지점 제외
if((y === 0 && x === 0) || (x === bossX && y === bossY))
continue;
// 30% 확률로 장애물 생성
if (map[y][x] === '.' && Math.random() > 0.7) {
// 장애물 추가
map[y][x] = 'X';
}
}
}
플레이어가 진입할 수 없는 장애물도 만들었다.
발단
그렇게 순조롭게 맵 생성이 진행되던 중...
그림과 같은 맵이 생성되었다. 그런데 예상치 못한 버그가 발생해버렸다.
사방이 꽉 막힌 맵이 생성되어 버린 것이다!!! → 보스방은 어떻게 진입하지...?
전개
그럼 어떻게 해야 플레이어와 보스 사이의 길을 뻥 뚫어줄 것인가?
구글링과 자료 조사, 웹서핑을 진행한 결과, 수많은 방법이 있다는 걸 알 수 있었다. 그 중 BFS(너비 우선 탐색) 알고리즘을 이용하여 플레이어와 보스 사이의 최단 거리를 알아내기로 결정하였다.
먼저 초기화부터 진행한다.
const queue = [[startX, startY]];
visited[startY][startX] = true;
queue 는 경로 진행 시 방문할 노드를 저장하는 큐이다. 선언과 동시에 플레이어가 시작하는 노드를 삽입해준다. 2차원 배열 visited 는 해당 노드의 방문 여부를 기록한다. 이후 queue 가 텅 빌때까지 루프를 돌린다.
const [x, y] = queue.shift();
...
const directions = [
[0, 1], [1, 0], [0, -1], [-1, 0]
];
...
큐에서 노드를 꺼내 (x, y)로 저장하고, 상하좌우로 이동 방향을 선언한다.
for (const [dx, dy] of directions) {
// 각 방향으로 이동한 노드
const newX = x + dx;
const newY = y + dy;
// 유효한 범위 체크
if (newX >= 0 && newX < width && newY >= 0 && newY < height && !visited[newY][newX]) {
// 방문 업데이트
visited[newY][newX] = true;
// 방문했으니 queue에 추가
queue.push([newX, newY]);
...
}
}
이를 활용하여 플레이어가 보스방으로 직행하는 길이 뻥 뚫릴 줄... 알았다...
위기
하지만 여전히 플레이어와 보스방 사이를 가로막는 장애물이 등장하였다.
이 때 멘탈 나가는 줄... 🤬🤬🤬
절정
많은 고난과 시련 끝에 나는 한 가지 방법을 알아냈다. 바로 부모 노드를 통해 경로 추적을 하는 것!!!
const parent = {};
parent는 경로 추적을 위해 각 노드의 부모노드를 저장한다.
// 부모 노드 업데이트
parent[`${newX},${newY}`] = `${x},${y}`;
상하좌우 네 방향을 향해 새로운 노드로 이동할 때, 기존의 노드를 부모노드로 업데이트 해줬다.
결말
...
if (newX === endX && newY === endY) {
// 경로 재구성
let [px, py] = [endX, endY];
while (parent[`${px},${py}`]) {
[px, py] = parent[`${px},${py}`].split(',').map(Number);
addPath(px, py);
}
return;
}
그렇게 이동하던 노드가 목표 노드(보스방 위치)에 도달하면 부모 노드를 타고 올라가 해당 경로의 모든 노드에 . 을 표시한다. 함수 addPath 는 플레이어와 보스의 최단 경로를 저장하고 장애물이 없는 일반 노드로 . 을 할당하는 함수다.
이후 장애물 생성 시에 플레이어와 보스방의 최단 경로에 장애물을 생성하지 못하도록 만들면 끝!
if (map[y][x] === '.' && Math.random() > 0.7 && !path.has(`${x},${y}`)) {
// 장애물 추가
map[y][x] = 'X';
}
path 가 addPath 를 통해 최단 경로를 담은 자료구조(Set - 중복방지)다.
마무리
이로써 모험을 위한 맵생성을 끝마칠 수 있었다. 그 후에도 업적 생성, 상점 만들기 등 다양한 시련이 찾아왔지만...
그건 다음 시간에 얘기하도록 하자...
'Side Projects' 카테고리의 다른 글
ORM / Low-Level Query (0) | 2024.10.29 |
---|---|
Protocol Buffers? (4) | 2024.10.24 |
모듈 시스템에서 변수 바인딩 방식 (0) | 2024.10.11 |
Chrome dino 모작 - 트러블 슈팅 (1) | 2024.10.07 |
Prisma 다시 돌아보기 (2) | 2024.09.25 |