[JS] 비트연산을 이용한 2차원 Map Editor 만들기

유니·2023년 1월 21일
46

JavaScript

목록 보기
9/9
post-thumbnail

2차원 지형을 표현하는 맵은 어떻게 구현할 수 있을까요?
이번 포스팅에서는 비트연산을 활용하여 2차원 Map Editor를 만드는 방법에 대해 알아보겠습니다.

준비물 : map image asset

https://cupnooble.itch.io/sprout-lands-asset-pack
저는 위의 무료 애셋을 사용했습니다.

참고로 예시처럼 sprite 이미지로 저장되어 있는 애셋을 사용하면 코드작성 및 리소스 관리측면에서 편리하고 네트워크 요청을 한번만 보내기 때문에 성능상 유리할 수 있습니다.
만일 타일 이미지가 따로 저장되어있는 애셋을 구했다면 Sprite generator(링크) 툴을 이용하여 만들어 사용하기를 권장합니다.

각 타일의 정보를 저장

맵 애셋을 준비했으면 각 타일들의 정보를 데이터로 저장해야합니다.
저는 각 타일의 데이터를 key(비트마스크) => value(sprite상의 좌표) 형식의 Map 데이터로 저장했습니다.

각 맵의 타일모양은 위와같이 3x3 그리드 형식으로 표현될 수 있습니다.
이 타일들을 비트마스크 형식의 데이터로 치환하기위해 빈 곳을 0, 땅을 1이라고 했을 때,
가장 왼쪽위의 타일은

000
011
011
이 됩니다. 그리고 sprite 좌표상으로는 0번째 행과, 0번째 열에 있습니다.
그럼 해당 타일에 대한 정보는 000011011 => [0,0] 이 되는 것입니다.


export const GRASS_COORDINATE = new Map([
  [0b000011011, [0, 0]],
  [0b000111111, [0, 1]],
  [0b000110110, [0, 2]],
  [0b000010010, [0, 3]],
  [0b000011010, [0, 4]],
  [0b000111110, [0, 5]],
  [0b000111011, [0, 6]],
  [0b000110010, [0, 7]],
  [0b000111010, [0, 8]],
  [0b110111011, [0, 9]],
  [0b011011011, [1, 0]],
  [0b111111111, [1, 1]],
  [0b110110110, [1, 2]],
  [0b010010010, [1, 3]],
  [0b011011010, [1, 4]],
  [0b111111110, [1, 5]],
  [0b111111011, [1, 6]],
  [0b110110010, [1, 7]],
  [0b111111010, [1, 8]],
  [0b011111110, [1, 9]],
  [0b011011000, [2, 0]],
  [0b111111000, [2, 1]],
  [0b110110000, [2, 2]],
  [0b010010000, [2, 3]],
  [0b010011011, [2, 4]],
	...
]);

Map의 key는 이진수 리터럴로 작성합니다. (댓글로 의견주신 superlipbalm님 감사합니다)

여기서 순수 Object가 아닌 Map 타입을 쓴 이유는, Number타입을 key로 사용하기 위해서입니다.
순수 Object는 String, Symbol값만을 key로 가질 수 있습니다. ({1:'hello'} 와 같이 Object를 선언하는것이 가능하지만 이 경우도 key가 String 타입으로 묵시적 형변환 되는 것입니다.)

🤔 타일이 이게 전부인가요?

그런데, 각 맵타일이 3*3 그리드, 즉 9비트짜리 이진수와 대응된다는 점에서 착안하면 훨씬 많은 타일이 있어야하지 않을까요?
비어있는 맵을 제외하면 000000001 ~ 111111111 에 해당하는 타일이 모두 있어 총 2^9-1개의 타일이 있어야 하는거 아닌가?
라는 의문이 들 때쯤, 가져온 map asset을 다시 살펴봅시다.

보아하니 모든 타일들은 두가지 가정을 만족합니다.

  1. 중앙 비트(5번째비트)가 1이다.
  2. 꼭지점 비트(1,3,5,7번째 비트)가 1인 경우, 해당 비트와 인접한 두개의 비트도 모두 1이다.

이는 타일들이 밟을 수 있는 땅(중앙비트가 1)만을 표현하며, 타일이 이어질때는 항상 면끼리 맞대어서 이어진다는 것을 의미합니다.

위의 두 가정을 만족하지 못하는 타일 데이터는 asset에 존재하지 않는다. 즉, 빈 땅으로 보여지는 유효하지 않은 타일이다.

위 규칙을 잘 활용하면, 47개의 타일로도 면으로 인접했을 때 이어지는 맵 을 충분히 구현해낼 수 있습니다.
참고로 꼭짓점 비트를 기준으로 세어보면 타일의 경우의 수를 쉽게 찾아낼 수 있습니다.

에디터에서의 타일 데이터 저장

그렇다면 현재 편집중인 map 데이터는 어떻게 저장하면 좋을까요?
저는 맵 데이터를 유효한 데이터로 변경하는 전처리를 위해 raw, real 이라는 두가지 배열을 선언하여 사용했습니다.

가령, 위처럼 맵을 그리면 데이터는 다음과 같이 변합니다. (주변의 비어있는 부분은 편의상 생략했습니다.)

raw : 땅과 주변에 이어진 땅의 유무를 모두 가지고 있는 데이터 배열
real : raw 데이터를 유효한 타일 데이터로 변경한 데이터 배열

raw, real 초기화

raw = Array.from(Array(CONSTANT_ROW), () => Array(CONSTANT_COL).fill(0))
real = Array.from(Array(CONSTANT_ROW), () => Array(CONSTANT_COL).fill(0))

원하는 맵 에디터의 행, 열만큼 0으로 채운 동일한 Array로 선언합니다.

raw 생성

  1. 셀을 클릭하면 해당 셀의 중앙비트를 1로 변경
  2. 클릭한 셀의 주변 8개 셀 중에 땅이 있으면 해당 방향의 인접비트를 1로 변경
  3. 주변 8개 셀의 마주보는 부분의 비트도 1로 변경

real 생성

raw 데이터의 선택셀과 주변 셀을 확인하여 유효 타일로 변환 후 real 배열에 반영
[유효타일 변환 과정]
- 중앙비트가 1이 아닌경우 -> 000000000
- 모든 꼭짓점을 확인하여 꼭짓점 비트가 1이면서 해당 꼭짓점과 인접한 비트가 1이 아닌경우 해당 꼭짓점비트 0으로 변경

위의 작업들을 처리하기위해 자바스크립트에서 지원하는 몇가지 비트 연산자들을 사용할 수 있었습니다.

비트연산

  • n번 비트를 1로 변경
    bitData |= 1 << n-1

  • n번 비트가 1인지 확인
    bitData & (1 << n-1)

  • n번 비트를 0으로 변경
    bitData &= ~(1 << n-1)

  • 꼭짓점 비트가 1이면서 꼭짓점 주변 비트가 1이 아닌지 확인
    (bitData & (1 << 꼭짓점인덱스)) && !(bitData & (1 << 주변1인덱스) && bitData & (1 << 주변2인덱스))

sprite 이미지 사용하기

const [coordX, coordY] = GRASS_COORDINATE.get(bitData)
...
<div style={{ background: `url(grass.png) -${coordY * GRID_SIZE}px -${coordX * GRID_SIZE}px` }} />

real 데이터 배열을 2중 map을 돌려 bitData로 뽑아내고, 위에서 정의한 GRASS_COORDINATE 를 이용하여 asset상의 좌표로 변환합니다.
이 좌표와 한 타일의 가로, 세로 픽셀크기에 해당하는 GRID_SIZE를 이용하여 해당하는 맵 이미지를 가져올 수 있습니다.
(GRID_SIZE는 본인이 사용하는 이미지에 맞게 정의해서 사용하면 됩니다.)

소스코드 및 실행화면

https://playcode.io/1083380
위의 링크를 누르면 react로 구현한 소스코드와 실행화면을 확인해 볼 수 있습니다.

profile
추진력을 얻는 중

7개의 댓글

comment-user-thumbnail
2023년 1월 25일

잘 읽고갑니다

답글 달기
comment-user-thumbnail
2023년 1월 26일

잘 읽었습니다 👍

답글 달기
comment-user-thumbnail
2023년 1월 26일

parseInt를 사용해 string을 number로 변환하는 대신 이진수 리터럴을 사용하면 가독성을 살리면서도 보다 간결해질 것 같아요!

// as-is
[convertBinaryToDecimal('000011011'), [0, 0]]
// to-be
[0b000011011, [0, 0]]

땅 연결되는거 아기자기하고 귀엽네요 ㅋㅋ
재미있게 잘 읽었습니다!

1개의 답글
comment-user-thumbnail
2023년 1월 27일

자동으로 연결되니까 고급지네요 ㄷㄷ 잘 봤습니다

데모 App.jsx 23번 라인에 오타가 있는 것 같아요~

1개의 답글
comment-user-thumbnail
2023년 1월 29일

움짤만 봐도 너무 재밌어요

답글 달기