『Replicube』 실황: Lua 코드로 복셀 입체를 그리는 퍼즐 18문제 풀기

원문 언어 日本語

YouTube에서 열기 ↗
개요

『Replicube』는 지정된 3D 오브젝트를 재현하는 스테이지 클리어형 퍼즐 게임이다. 복셀을 손으로 놓는 것이 아니라 프로그램을 작성해 배치한다. 이 영상에서 실황자는 첫 튜토리얼 문제집 11문제와 이어서 도착하는 추가 문제집 7문제를 풀어 나간다. 모든 문제 클리어를 목표로 하는 것이 아니라, 두 문제집을 끝낸 시점에서 마무리하는 방침이다. 실황자는 전체적으로 점수를 끌어올리는 것보다 읽기 쉬운 코드를 작성하는 것을 우선한다.

7 분 분량
0:23

게임의 구조: 좌표를 받아 색을 반환한다

튜토리얼에 따르면 이 게임은 마인크래프트 같은 복셀 공간에 총 16색의 복셀을 배치해 나간다. 복셀이란 '부피가 있는 화소'라는 뜻의 조어다. 배치에는 코드를 사용하며, 항상 흰색을 반환하는 코드를 작성하면 공간 전체가 흰색으로 채워지고, 보라색을 반환하면 보라색, 파란색을 반환하면 파란색이 된다. 즉 각 복셀의 좌표를 받아 그 자리에 놓을 색의 값을 반환하는 코드를 작성하는 형식이다. 실황자는 "벌써부터 머리가 아파 오네요"라고 하면서도, 요즘은 싸우는 게임만 했으니 가끔은 이런 평화로운 게임도 좋다고 말한다.

게임 안에서는 Cubenet이라는 곳의 관리자에게서 메일이 오고, 거기에 첨부된 기초 문제집을 푸는 흐름으로 진행된다. 화면 위쪽에는 정답 형태가, 아래쪽에는 현재 코드가 그리는 형태가 표시된다. 채점에는 코드 양이나 실행 속도도 포함되는 것 같지만, 실황자는 최적화를 '나중의 디저트'로 미루고 우선은 가독성을 중시하기로 한다. 그 이유로 "코드란 기본적으로 사람을 위해 쓰는 것"이라고 말한다.

1:47

1-1~1-3: 색 바꾸기와 조건 분기

1번 문제는 이미 있는 코드의 주석을 읽으면 반환하는 색이 조금 다를 뿐이라는 것을 알 수 있다. PURPLE을 RED로 바꿔 클리어했다.

2번 문제에서는 위치에 따라 두 가지 색으로 나눠 칠한다. 좌표를 나타내는 변수 x, y, z가 주어지므로, y가 1 이상이면 DARKBLUE, 그렇지 않으면 ORANGE를 반환하는 if 문을 작성해 풀었다.

3번 문제의 주제는 비교 연산자다. 실황자는 이 언어에서 '같지 않음'을 '~='로 쓴다는 점에 주목하며, 머지않아 쓰게 될 것 같다고 예상한다. 문제 자체는 2번과 거의 같은 패턴으로, 'y가 ○○ 이상'이라는 조건으로 풀 수 있다. 비교 연산자를 쓰지 않는 다른 풀이로 return GREEN - (y + 4) // 3 * 2 같은 작성법도 있다고 소개했지만, 이런 종류의 코드는 읽는 사람을 곤란하게 만들기 쉽다며 채택하지 않았다.

3:55

1-4·1-5: 투명하게 만드는 방법

4번 문제부터는 y 좌표에 더해 x 좌표와 z 좌표도 쓰기 시작한다. 초기 코드에는 무지개색 배색이 이미 만들어져 있다. y 좌표 값을 그대로 색 코드로 활용하기 때문에, 한 단씩 올라갈 때마다 다음 색으로 바뀌는 구조다. 다만 이 문제에서 필요한 것은 오브젝트의 일부를 투명하게 만드는 것뿐이다. 주석에 따르면 아무것도 반환하지 않거나 0 또는 EMPTY를 반환하면 투명해진다. 초기 코드의 'x와 z가 2 이상일 때'라는 조건을 '0 이상일 때'로 바꿔 클리어했다.

5번 문제도 거의 같은 구조로, 'x, y, z가 모두 1 이상일 때'라는 조건을 작성해 풀었다.

5:42

1-6: 절댓값으로 선대칭 형태 그리기

6번 문제에서는 abs 함수가 등장한다. 주석에는 절댓값을 반환하는 함수로, 선대칭 오브젝트를 그릴 때 유용하다고 적혀 있다. 이번 목표 형태는 어느 축에 대해서도 선대칭이다.

실황자는 먼저 z축에 주목해, z의 절댓값이 2 이상이면 무(無)를 반환하도록 해서 두께를 정했다. '2 이상 또는 -2 이하'라고 쓸 수도 있지만, 절댓값 개념을 쓰는 편이 이해하기 쉬운 코드가 된다고 실황자는 생각한다. 나머지 우물 정(井) 자 형태도 절댓값을 사용해 세로 막대와 가로 막대를 만들고, 논리합 or로 이어 완성했다. 선대칭 구조는 앞으로도 자주 나올 것 같아서 절댓값을 쓸 일이 많을 것 같다고 실황자는 내다본다.

6:49

1-7: 오류 메시지를 읽고 고치기

7번 문제는 처음부터 오류가 나 있다. 실황자는 먼저 오류 메시지를 읽는다. 'LUA_ERRSYNTAX'라는 표시를 보고 이 언어가 Lua라고 추측했다. 10번째 줄의 if를 닫는 end가 와야 하는데 13번째 줄에 if가 쓰여 있다. 즉 end를 빠뜨린 것이다.

이를 고쳐도 완성은 아니었고, 빨간색이어야 할 상자가 GREY로 되어 있는 부분이 남아 있어서 그것도 바꿔 클리어했다. 실황자는 여기서 처음부터 코드를 작성할 기회는 의외로 적고, 실제로는 다른 사람의 코드를 읽는 능력이 더 중요해지는 경우가 많다고 이야기한다.

7:45

1-8: 자유 과제 'sushi?'를 체스판 거리와 테이블로 풀기

그런데 8번 문제는 처음부터 작성하게 하는 문제였다. 문제 이름은 'sushi?'로, 실황자는 '엄청난 감각'을 느낀다. 형태를 자유롭게 만들어도 되는 것 같아서, 중앙으로부터의 거리에 따라 빨강·하양·초록으로 나눠 칠하는 방침을 세웠다.

여기서 쓰는 거리는 체스판 거리라고 불리는 것이다. 8방향으로 움직일 수 있는 말이 몇 걸음 만에 도착할 수 있는가를 나타내는 거리로, 실황자는 『풍래의 시렌』 같은 로그라이크에서도 가끔 신세를 지는 개념이라고 소개한다. 이 거리는 max 함수가 있으면 구하기 쉬우므로 Documentation 탭을 열어 조사했다. 문서는 4장 구성으로, 1장이 개요, 2장이 렌더링 세부 사항이며, 3장에 찾던 정보가 있을 것 같다고 한다. 그래서 변수를 정의할 수 있다는 것, +나 - 같은 연산자를 쓸 수 있다는 것, 그리고 max 함수가 있다는 것을 확인했다.

빨강·하양·초록으로의 변환에 대해, 실황자는 편리한 기능이 없으면 if 문으로 우직하게 분기할 생각이었다. '재미있는 구현' 아이디어로는 7·1·11을 16진수로 나열한 0xb17을 거리에 따라 오른쪽으로 시프트하는 방법도 들었다.

여기서 실황자는 여담으로, 평소 영상 녹화에 쓰는 OBS의 플러그인은 Lua로도 작성할 수 있다고 이야기한다. YouTube 타임스탬프를 자동 생성하는 간단한 플러그인을 만든 적이 있는데, Lua를 접한 건 인생에서 그때뿐일지도 모른다고 한다.

구현에서는 먼저 x의 절댓값과 y의 절댓값 중 최댓값을 구해 원점으로부터의 체스판 거리를 구했다. 이번에는 0, 1, 2 중 하나가 되므로, 이를 0→7(빨강), 1→1(하양), 2→11(초록)으로 변환하는 구조가 필요하다. 문서에 'table'이라는 글자가 언뜻 보였기 때문에 배열 같은 구조를 쓸 수 있으리라 추측했다. 많은 언어에서 쓸 수 있는 대괄호 배열 리터럴을 써 봤지만 Lua에서는 쓸 수 없었다. 그래서 'Table'이라는 명칭으로 보아 중괄호 형식일 거라 생각해 고쳐 쓰자 거의 잘 작동했다. 다만 색이 하나씩 어긋나 있었고, Lua의 인덱스가 1부터 시작한다는 사실이 밝혀졌다. 이에 맞춰 고쳐서 초밥이 완성되었다.

사용한 요소는 다소 까다로웠지만, 코드를 훑어보면 의외로 간결해 보인다고 실황자는 말한다. 다만 그렇게 생각하는 건 대개 작성한 본인뿐이라고 스스로 덧붙인다.

11:25

1-9·1-10: '칠하지 않는 곳'과 중첩 if

9번 문제는 마우스로 시점을 움직여 형태를 잘 확인해 보자는, 조작 튜토리얼에 가까운 문제다. 위가 열린 상자 형태로, x나 z가 ±3일 때 또는 y가 -3일 때 BROWN을 반환하면 된다. y가 +3인 경우를 제외함으로써 위쪽 뚜껑이 열린 형태가 된다. 여기서도 절댓값을 사용했다. 주석은 '어디를 칠할지'가 아니라 '어디를 칠하지 않을지'로 표현하게 하고 싶은 것 같았지만, 실황자는 어느 쪽이든 상관없다고 판단한다. 앞의 초밥에 너무 공을 들인 탓에 다소 맥이 빠졌다고 한다.

10번 문제는 실황자 말로는 Google Chrome 같은 배색이지만, 색 배열에 특징적인 규칙성은 보이지 않는다. 주석도 중첩 if를 기대하는 것 같아서 순순히 조건 분기를 작성했다. y가 1 이상일 때는 x가 1 이상이면 GREEN, 그렇지 않으면 ORANGE를 반환한다. y가 1 이상이 아닐 때는 z가 1 이상이면 BLUE, 그렇지 않으면 RED를 반환한다. 실황자는 축이 잘 보이도록 시점을 회전시키면서 작성해 나갔다.

13:34

1-11: 이중 철자 실수와 X자의 원리

튜토리얼 마지막인 11번 문제도 처음부터 문제를 안고 있다. 오류 메시지에 따르면 31번째 줄에서 'colo'라는 알 수 없는 변수를 다루고 있다. 이는 'color'의 입력 실수라고 추측해 수정했다. 하지만 그것만으로는 계단이 일부만 그려진다. 코드를 더 읽어 보니 영국 영어 철자인 'colour'로 된 부분이 있어서, 이를 고쳐 완성했다.

이 코드가 X자를 그릴 수 있는 이유에 대해, 실황자는 x==y나 x==-y 같은 식을 쓰고 있기 때문이라고 해석했다. 일차함수를 등식으로 쓰면 이런 형태를 그릴 수 있다는 것이다. 실황자는 여기서 "뭐, 저와는 분명 무관한 지식이겠죠"라고 말한다.

14:40

2-1~2-3: 등식으로 대각선 그리기

튜토리얼을 마치면 커뮤니티로부터 환영 메일과 총 7문제의 추가 문제집이 도착한다.

추가 문제집의 1번 문제에서는 방금 '무관하다'고 했던 일차함수 문제가 곧바로 나왔다. y = -x 직선이므로 Lua에서는 x == -y로 쓸 수 있다. ==에 의한 비교는 true나 false를 반환하는데, Replicube에서는 이것이 흰색과 무(無)로 그려지기 때문에 비교 결과를 그대로 반환하기만 해도 클리어할 수 있었다.

2번 문제도 대각선 직선이지만, z 좌표로 범위를 더 좁혀야 한다. 먼저 x == y로 평면을 그리고, 논리곱 and로 x == z 조건을 이어 형태를 완성했다. 반환값을 true/false가 아니라 GREEN이나 무(無)로 만들기 위해 끝에 and GREEN을 붙여 단락 평가를 활용했다.

3번 문제는 2번 문제를 선대칭으로 만든 형태라서, 식을 절댓값 버전으로 바꾸기만 하면 풀 수 있었다. abs(x)를 두 번 계산하는 것은 다소 낭비지만, 느린 처리가 아니므로 신경 쓰지 않기로 했다.

17:00

2-4~2-6: 나머지와 덧셈으로 무늬 만들기

4번 문제에서는 나눗셈의 나머지를 반환하는 % 연산자가 도입된다. 초기 코드는 y를 3으로 나눈 나머지로 0·1·2에 차례대로 나눠 배정하고 있다. 정답 형태를 보니 x 값을 0이나 1로 변환하는 것처럼 보여서, 2로 나눈 나머지를 쓰면 된다고 추측했다. 한 칸만큼 어긋나 있었기 때문에 x에서 1을 빼서 조정해 클리어했다.

5번 문제는 체커보드다. 먼저 평면으로 만들기 위해 0이 아니면 무(無)를 반환하는 처리를 작성하려다가, 처음에는 z로 조건을 써 버려서 y로 고쳤다. RGB 화살표가 XYZ에 대응하지만, 가끔 잊어버려서 헷갈린다고 한다. 무늬에 대해서는 2번 문제의 초기 코드에 있던 return x+y로 더하면 대각선 줄무늬가 된다는 것을 '몰래 슬쩍' 봐 두었다. 이것을 %로 정리하면 체커보드가 된다. 처음에는 분홍색이 나와서, 검은색을 반환하도록 고쳐 클리어했다. 조건 분기를 피하고 싶다면 나머지 0과 1을 흑백의 1과 3으로 변환하는 식으로도 구현할 수 있다고 보여 줬지만, 색으로의 변환이 매우 알아보기 어려워진다고 실황자는 평한다.

6번 문제는 대각선 무지개색 무늬다. 덧셈으로 대각선 줄무늬를 그릴 수 있다는 점을 이용해, 먼저 x와 y를 더했다. 이것으로 대체로 비슷한 모양이 되었다. 다만 실황자의 코드에서는 원점이 0을 반환하는 데 비해 정답에서는 LIGHTGREEN이었기 때문에, LIGHTGREEN을 더해 맞췄다. 마지막으로 z가 0이 아닐 때 무(無)를 반환해 두께를 조정하고 완성했다.

19:55

2-7: 네 다리 테이블과 함수를 이용한 정리

마지막 문제는 전형적인 네 다리 테이블로, 재질은 모두 BROWN이다. 형태만 생각하면 되므로, 실황자는 현실의 조립 공정을 본떠 상판과 다리를 나눠 처리했다. --로 주석을 쓸 수 있다는 것을 알게 되어 주석도 달았다. 상판은 y가 1일 때 BROWN을 반환하기만 하는 평면이다. 다리는 먼저 그리는 범위를 y가 0 이하로 제한해 높이를 정했다. 위치는 선대칭이므로 늘 그렇듯 절댓값을 써서, x와 z의 절댓값이 모두 2일 때만 그린다. 이 조건들을 and로 이어, 모두 만족할 때 BROWN을 반환하도록 해서 완성했다.

다만 실황자는 조금 아쉬워서 좀 더 손을 보기로 했다. 문서에 따르면 자체 함수를 정의할 수 있었을 테니 그것을 써 보고 싶었다고 한다. 지금 코드는 재질이 전부 BROWN인데도 return BROWN을 두 번 쓰고 있어서, "역시 WHITE로 바꿔 줄래?"라는 말을 들으면 일이 두 번이 된다. 그래서 형태만 다루는 함수를 만들고, 색은 한 곳에서 정하는 형태로 다시 작성했다.

구체적으로는 해당 좌표가 상판인지를 반환하는 isTopPlate 함수와 다리인지를 반환하는 isLeg 함수를 정의하고, '상판 또는 다리라면 BROWN을 반환한다'고 작성했다. Lua의 함수 이름은 아마 snake_case가 일반적이지만, 이번에는 화면 폭이 극단적으로 좁아서 만일을 위해 camelCase로 했다고 한다. 함수 호출에는 일정한 비용이 들지만, 정지된 물체를 그리는 데는 그다지 성능이 요구되지 않으므로 문제없다고 실황자는 판단한다. 한편 이 작품에서는 애니메이션도 만들 수 있는 것 같으니, 그런 경우에는 다소 신경 쓰는 편이 좋을지도 모른다고도 말한다.

22:52

실황자의 평가

두 문제집을 마치고 실황자는 『Replicube』를 다음과 같이 평가한다. 처음에는 다가가기 어려운 인상이 있었지만, UI도 도입부도 매우 친절하고, 해 보면 무척 즐겁다. 다양한 관점과 풀이가 있는 퍼즐 게임으로서 훌륭하고, 프로그래밍에 흥미를 갖는 계기로서도 즐길 수 있다. 수학적인 분야와의 연결점도 있어, 여러 방면으로 즐길 수 있는 작품이라고 느꼈다며 마무리한다.