테트리스 탈추우우울
네… 지난 2주간 테트리스 지옥(?)에 갇혀있던 Summer, 생존 신고 합니다. dimanche의 지난글에서 알 수 있다시피 저희 초보자들은 2.5주전 4번째 미션으로 “테트리스 만들기”를 받았습니다. 오늘은 그 2.5주간을 뒤돌아보며, “초보가 도전적인 과제를 받았을 때의 자세”와 “초보가 테트리스를 어떻게 만들었는가”에 대해 이야기해볼까 합니다.
마인드 컨트롤
테트리스 미션을 받은 것은 10/19 수요일이었습니다. 당시 저는 To do list 프로그램을 만든 경험을 블로그로 쓰고 있던 때였습니다. 블로그 글쓰기가 끝나지 않았기 때문에 테트리스 프로젝트를 바로 착수하지 못했고, 일정이 조금씩 밀린다는 것을 깨닫자 불안감이 스멀 스멀 올라오기 시작했습니다.
게다가 테트리스라니. 테트리스 게임도 못하는데 테트리스를 만들어야한다니!!! 내가 게임을 만든다고오오옹? 어떻게????? 가능해???란 부담감도 점점 커져갔죠. 그렇다보니 부담과 조급함이 섞여 저를 얼어붙게 만들었습니다. 그렇다보니 머리가 돌아가질 않고, 조급함은 부주의함을 낳았습니다. 에러가 하나둘 씩 생깁니다. 머리는 더 패닉상태로 갑니다. 패닉상태의 저는 근본없는, 잘못된 디버깅을 하기 시작했고 더 많은 에러가 생겼습니다. 내가 할 수 있을까? 나 이렇게 엉망인데 개발자 할 수 있을까?란 생각까지 들기도 했습니다. 그렇게 멘붕의 삼각지대에 2주간 빠져있었습니다.
이것이 빠져나가기 그렇게 힘들다는 버그 삼각지대…!
신기하게도(?) 코드는 저의 정신과 마음 상태를 그대로 반영합니다. 그렇기 때문에 마인드 컨트롤에 실패한 저의 정신세계가 그대로 코드에 드러나게 되었고, 결과는 역시나 좋지 않았습니다. 자신감과 침착함을 유지할 수 있도록 마인드 컨트롤을 해야합니다.
기본은 탄탄한 문서작성에서부터
제가 버그 삼각지대에서 허우적댔던 가장 큰 이유중 하나는 문서작성을 꼼꼼히 하지 않아서였습니다. 그렇게 ‘문서작성이 중요하다’를 블로그 글에도 쓰고, 회사에서 지시도 “문서작성을 포함한 테트리스 구현”이라고 받았거늘…. 저는 후자에 초점을 맞춰 전자를 ‘적당히 문서 작성을 하고 바로 테트리스를 만들려고 했습니다. 마감에 대한 급한 마음때문에 그랬다고 했지만, 오히려 그것이 독이 되어 논리적, 물리적 오류의 늪에서 빠져나올 수가 없었습니다. 처음 1주일이 지나갈 때쯔음에서야 문서 작성을 소홀히 했다는 것을 깨닫고 다시 문서 작성을 했습니다.
이번에는 한국어로 코딩을 한다고 생각하며 아주 꼼꼼하게 문서를 작성했습니다. (확실히 프로그램이 복잡해진 만큼 문서의 양도 많아지더군요.)
다음은 제가 작성한 설계 문서 중에서 랭킹화면에 대한 부분입니다.
화면도 진짜 화면처럼!
한줄 한줄 자세하게!
플로우차트는 더 자세하게!
테트리스 화면 그리기
테트리스를 만드는데 가장 큰 고민은 도대체 테트리스 화면을 어떻게 그릴 것인가였습니다. 아니, 더 정확하게 말하자면 여러가지 CSS의 조합으로 테트리스 창은 그릴 수 있지만, 테트리스 칸에 블록이 들어왔는지 안들어왔는지를 체크할 수 있는 건 어떡하지?에서 막혔습니다.
해결방법은 바로 “2차원배열”이었습니다. 2차원배열은 표 형식 데이터를 취급할 수 있는 배열을 의미합니다. 두개의 배열을 ‘행’과 ‘열’로 만들어 데이터 값을 나타냅니다.
배열의 데이터에 접근하려면 어떻게 할까요? 배열이 한개일 때 루프문을 통해 접근했던 것 처럼, 배열이 두개이므로 이중 루프문을 통해 데이터에 접근합니다. 즉, 세로 인덱스 1개를 돌 때 가로축 인덱스를 모두 돌고 그 세로축을 반복하는 방법을 통해 모든 정보에 접근할 수 있는 것입니다.
자 그렇다면 이제 테트리스의 블럭들도 2차원 배열로 보이시나요?
코드로 나타내면 다음처럼 나타낼 수 있습니다.
//테트리스 정보 var data = { curr:undefined, //현재 선택된 블록의 종류 rotate:0, //현재 선택된 블록이 몇번이나 회전한 모양인지 pos:[4,0], // 현재 블록의 위치 next: this.getBlock, //다음 블록의 종류 grid:[ [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0] ], speed:700, //틱 스피드 lines:0, // 지운 줄 수 blocks:0, //지금까지 나온 블록의 수 time:"0:0",  //게임한 시간 score:0,  // 점수 unitSize:20,  //블록 1개의 크기 getBlock: function(){return Math.floor((Math.random() * block.length))} //다음 블록을 정하는 메소드 }; //위에 있는 정보를 바탕으로 화면을 그려줍니다. //1. 테트리스 전체 화면 그리기 var unitSize = data.unitSize, textOffset = 25; var grid, i, j; getEl('grid','id').innerHTML =""; //2차원 배열 중 세로 루프를 돌립니다. for(i = 0; i < data.grid.length; i++){ //2차원 배열 중 가로 루프를 돌립니다. for(j = 0; j < data.grid[i].length; j++){ //각 값마다 div 요소를 만들고, 그 div가 제자리에 위치할 수 있도록 위치값과 색상값을 지정합니다. grid = document.createElement('div'); grid.class = "gridBlock"; grid.style.cssText = "width:" + unitSize + "px;height:"+ unitSize + "px;position:absolute;top:" + (i * unitSize) + "px;left:" + (j * unitSize) + "px;background-color:"+_colors[data.grid[i][j]]; getEl('grid','id').appendChild(grid); } } //현재 움직이고 있는 블록은 더이상 움직일 수 없을때(아래칸에 블록이 있을 때)까지는 테트리스 grid에 저장되지 않습니다. 그러므로 따로 함수를 만들어 현재 블록을 화면에 보여줍니다. 보여주는 방식은 위와 비슷합니다. var curr, i, j, k, l, x, y, div, unitSize = data.unitSize; curr = block[data.curr].rotate[data.rotate]; for(i = 0,j = curr.length; i < j; i++){ for(k = 0, l = curr[0].length; k < l; k++){ x = data.pos[0] + k; y = data.pos[1] - curr.length + i; div = document.createElement('div'); div.class = "gridBlock"; div.style.cssText = "position:absolute;width:" + unitSize + "px;height:" + unitSize + "px;top:" + (y * unitSize) + "px;left:" + (x * unitSize) + "px;background-color:" + _colors[curr[i][k]] + ";border:1px solid #333"; if(y < 0) div.style.cssText = "display:none"; getEl('grid','id').appendChild(div); } }
결국, 테트리스 화면이란 테트리스 2차원 배열의 값을 예쁘게 나타낸 것이고, 사용자가 게임을 키보드를 눌러 블럭을 옮기고 돌리는 것은 배열의 데이터 값을 조정하고, 갱신한다는 의미를 가지게 됩니다.
테트리스 이렇게 보니 참 별거 없는거 같죠…^^?
하지만 말처럼 쉽지 않습니다. 실제로 저같은 경우 2차원 배열을 다룰 때 인덱스는 0부터 시작한다는 점, 인덱스와 길이값은 차이가 난다는 점, 반복문의 카운터와 인덱스를 잘 맞춰야한다는 점 등을… 꼼꼼하게 챙기지 못해 마지막까지 헤맸습니다.
테트리스 동작 시키기
자, 이제 우리는 테트리스 게임이라는 것은 테트리스 화면 데이터를 조작하고, 그것을 예쁘게 나타내는 것임을 알게 되었습니다. 화면을 그렸으니 이제 움직여 봐야겠죠? 테트리스의 움직임에는 어떤 것이 있는지 생각해 봅시다.
- 키보드 좌/우키를 눌러 좌우로 옮기기
- 키보드 위 키를 눌러 시계방향 90도로 회전시키기
- 키보드 스페이스바를 눌러 맨 아래로 옮기기
- 일정한 시간 간격으로 블록을 아래로 한칸씩 옮기기
크게 보면 이렇게 나눌 수 있습니다. 그럼 저 분류대로 함수를 만들어서 구현을 하면 될것 같은데… 잠깐! 아래의 개념을 생각해보고 만들어 봐야합니다.
틱
그 전까지의 프로젝트는 이벤트 중심의 프로그래밍으로 사용자가 어떤 행위를 하는가를 기점으로 프로그램의 동작을 나눌 수 있었습니다. 그리고 그 동작을 만드는데 역할을 인식해 나누면 되었지만, 위 4번처럼 테트리스는 사용자가 어떠한 동작을 하지 않아도 블럭이 움직이고, 줄이 계산되고, 테트리스 상의 시간이 흐르게 됩니다.
그럼 이 시간은 어떻게 결정이 되는걸까요? 인간의 시간처럼 자연스럽게 흘러가지 않으니 정해줘야 하겠죠? 이렇게 프로그래밍에서 작게 쪼갠 시간을 ‘틱’이라고 합니다. 테트리스 같은 경우에는 한 틱마다 블록이 내려오고, 줄이 계산되고, 시간이 지나는 것처럼 진행이 됩니다. 이렇게 사용자가 어떤 행위를 하기까지 기다리지 않고, 프로그래밍에 마치 시간이 있는 것처럼 계속 흐르는 것을 반영한 프로그래밍을 시간이 계속 흐르는 것을 반영한 프로그램을 리얼타임 프로그래밍이라고 합니다.
따라서 다음과 같이 나눠서 생각해보면 좋습니다.
- 사용자의 입력에 따라 뭔가 작동이 되는가?
- 그렇지 않은 경우 틱을 어떻게 나누고, 그 틱마다 어떤 일들이 일어나는가?
제 테트리스같은 경우 위의 1,2,3이 1번에 해당하고 4가 2에 해당한다고 볼 수 있겠네요.
실제로, 제 테트리스에선 스피드의 간격으로 틱이 실행되는데 틱마다 다음과 같은 일들이 일어납니다.
지금은 잘 정리가 되었지만, 처음에는 저렇게 쪼개는것이 매우 어려웠습니다. 특히 “반복”이 되기 때문에 반복되는 것들의 시작과 순서를 어떻게 정할것인지도 매우 어려웠는데 이건 마치 계란이 먼저냐 닭이 먼저냐를 결정할때의 헷갈림과 같았습니다.
그룹핑 vs 스코핑
dimanche의 글 후반부에서도 나온 중요한 개념, 스코핑! 저는 만들기 전, 실장님께서 스코핑에 대해 설명을 들었습니다. 그래서 저 나름의 스코핑을 하려고 했죠. 하지만 처음부터 스코프까지 생각하며 코드를 짜려니 어려워서 “우선 필요한 함수/변수를 다 만들어 놓고 나중에 스코핑의 개념을 도입해 정리하자!!”라고 생각했습니다. 그래서 다 만들어놓고 정리를 했는데 자꾸만 발생하는 버그…..
왜그랬을까요? 제가 했던 방식은 스코핑이 아닌 바로 “그룹핑”이이었기 때문입니다. 그룹핑은 비슷한 특징을 기준으로 묶어 나눈 1차원적인 분리입니다. 따라서, 상-하 관계가 존재하지 않으며, 줌인/줌아웃도 불가능합니다. 스코핑으로 나눠야 ‘레이어 층’이 생기고, 서로 완전히 분리가 가능합니다. 그런데 그룹핑을 해서 이곳저곳 섞여있으니 안될 수 밖에…
그럼 도대체 스코핑은 어떻게 할 수 있을까요? 스코핑은 앞에서 말씀드렸다시피 상-하위 관계가 존재합니다. 그 상-하위 관계는 스코프, 즉 누가 어떤 정보에 얼만큼 접근할 수 있을까? 누구만 그 정보를 알면 될까?를 고민해보면 됩니다.
예를들어, 테트리스에서 타이틀 화면에서 게임화면으로 전환하거나, 게임이 끝난 후 타이틀로 돌아오거나, 또는 타이틀 화면에서 랭킹을 확인할 수 있는 곳으로 가는 기능은 가장 먼저, 상단에서 필요하죠. 하지만 플레이 화면이 뜨고 실제 게임이 진행되어 실행되는 내용들은 타이틀 화면이나 랭킹에서 알 필요가 없습니다. 그러므로 게임을 진행하는 play은 화면전환 아래에 존재하는 스코프인 것 이죠. 반면, 랭킹 화면에 들어가면 시작될 rank, 또는 타이틀 화면으로 왔을 때 시작되는 start와 같은 경우 play와 같은 층에 있는 개념인 것을 알 수 있습니다.
이렇듯 프로그래밍은 완성된 큰 것을 쪼개서 스코프를 나누고, 각 스코프마다 어떤 것이 필요한지 쪼개는 과정으로 진행되어야 합니다. 저처럼 테트리스를 모든 조각으로 나눠 분리하게 된다면 어떤 것이 어디로 가는지 알기 위해서는 모든 경우의 수로 돌려봐야 알 수 있을 것입니다. 그만큼 완성 또한 멀고 험난한 길이 되겠죠..^^
완성, 그래서 소감은?
많이 헤매고 어려워했지만 어쨌든 테트리스를 완성하긴했습니다. (감격!)
마지막으로 한가지 아쉬웠던 점은 만드는 과정에 있어 제가 어떤 것을 모르는지 확실하게 알고 질문을 적극적으로 하지 못했던 점이네요… 다음 미션은 좀 더 침착하게, 그리고 배운 것을 잘 활용하여 정복할 수 있길 바라며..!!
summer| bsidesoft 신입사원
디자인을 공부했던 섬머는 개발까지 해버리겠다는 욕심으로 개발자의 세계에 입문하게 되었습니다. 개발왕이 되어 멋진 제품을 만들어내는 꿈을 꾸고 있습니다. 코드, 디자인을 포함한 세상의 모든 아름다운 것들과 미드, 그리고 달리기를 좋아합니다.
recent comment