<이 화면은 스크린샷 그림입니다>
하노이 탑 쌓기 혹은 하노이 타워라고도 불리는 이 게임은 베트남 하노이의 승려들이 했던 퍼즐게임입니다. 세 개의 장대와 크기가 다른 원판들로 구성되어 있으며 가장 왼쪽의 장대에 원판들이 크기순으로 정렬하여 가장 큰 것이 밑쪽에 가도록 쌓여 있습니다. 게임의 목표는 모든 원판을 가장 오른쪽 장대로 모두 이동하는 것이며 규칙은 다음과 같습니다.
자바스크립트와 Text Game Maker JS 라이브러리를 사용하여 이 게임을 만들어 봅시다. 게임데이터를 화면에 출력하는 부분과 키입력을 받는 부분에만 해당 라이브러리가 사용되고 나머지 모든 코드들은 네이티브 자바스크립트로 작성합니다. Text Game Maker JS 코드가 사용된 부분들은 이 강의 본문에 간단하게만 설명되니 Text Game Maker JS를 더 자세히 알고 싶은 분들은 Text Game Maker JS 튜토리얼 강의를 읽어 주세요.
상편과 하편으로 나누어서 살펴 볼텐데, 이번 상편에서는 게임에 필요한 변수들과 화면에 텍스트들을 출력하는 함수들을 만들어 보겠습니다.
우선 Text Game Maker JS를 받아서 설치합니다. https://github.com/a-mean-blogger/text-game-maker-js/releases 에 이동하면 text-game-maker-js-starter-program.zip과 text-game-maker-1.0.0.min.js.zip 두 개의 압축파일이 있는데, text-game-maker-js-starter-program.zip을 다운받아 적당한 폴더에 압축을 풀어 줍시다. 이 파일은 TM 라이브러리 파일과 간단한 데모 코드가 작성된 압축파일이고, text-game-maker-1.0.0.min.js.zip은 TM 라이브러리 파일만 들어있습니다.
text-game-maker-js-starter-program.zip의 압축을 풀고 index.html을 더블 클릭하면 웹 브라우저에 데모 프로그램이 실행됩니다. main.js의 코드를 변경하여 프로그램을 만들게 됩니다. 데모 프로그램이 문제없이 실행되는 것을 확인하면 main.js의 코드를 전부 지워서 새로운 코드를 작성할 준비를 합시다.
하노이 탑 쌓기 게임 만들기(상)의 소스 코드를 여기를 클릭하여 확인해 주세요. 전체 코드를 복사한 후 위에서 다운 받은 TM 스타터 프로그램의 main.js 파일에 붙여넣기 합니다. 그 다음 index.html을 더블 클릭하면 아래와 같이 웹 브라우저가 나타나게 됩니다.
main.js의 코드를 살펴봅시다.
var screenSetting = { // canvasId: 'tm-canvas', // frameSpeed: 40, // column: 60, // row: 20, // backgroundColor: '#151617', // webFontJsPath: 'https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js', // fontColor: '#F5F7FA', // fontFamily: 'monospace', // fontSource: null, // fontSize: 30, // zoom: 0.5, column: 56, row: 19, fontFamily: 'Consolas', }; var debugSetting = { // devMode: false, // outputDomId: 'tm-debug-output', devMode: true, };
screenSetting
과 debugSetting
은 Text Game Maker JS를 사용하기 위해 설정값을 조절하는 부분입니다. 주석 처리된 부분은 해당 항목이 없을 경우 사용될 기본값을 나타냅니다. 각 항목의 의미는 TM.defaultSettings 문서에서 확인할 수 있습니다.
var TMS = new TM.ScreenManager(screenSetting), TMI = new TM.InputManager(screenSetting.canvasId,debugSetting.devMode), TMD = new TM.DebugManager(debugSetting);
screenSetting
과 debugSetting
을 사용하여 게임 제작에 필요한 TM 메니저들을 생성합니다.
각 인스턴스의 함수 및 자세한 설명은 TM.ScreenManager, TM.InputManager, TM.DebugManager 문서에서 확인할 수 있습니다.
var NUM_OF_DISKS = 4; var NUM_OF_POLES = 3; var COLUMN_WIDTH = NUM_OF_DISKS*2+1; var COLUMN_GAP = 4; var FRAME_X = 6; var FRAME_Y = 1; var DISK_COLORS = ['#999', '#BBB', '#DDD', '#FFF']; var KEYSET = { LEFT: 37, RIGHT: 39, ENTER: 13, ESC: 27, }; var DISK_CHAR = "#"; var CURSOR_CHAR = "↓"; var GROUND_CHAR = "─"; var POLE_CHAR = "|"; var towerData; var moveCount; var cursorPosition; var cursorDiskValue; var isGameOver;
하노이 타워 게임에서 사용될 상수와 변수들입니다. 상수는 프로그램이 실행되는 동안 변하지 않는 값이며 대문자와 _를 사용하여 나타내었습니다. 변수는 프로그램이 실행되는 동안 변할 수 있는 값이며 Camel Case(띄어쓰기 대신 대문자사용)로 나타내었습니다.
**NUM_OF_DISKS
나 NUM_OF_POLES
의 값을 변경하게 되면 게임의 가로x세로 크기가 달라지게 됩니다. 이 경우 screenSetting
의 column
과 row
값을 조절하여 화면의 크기를 게임의 크기에 맞게 바꿀 수 있습니다.
TMI = new TM.InputManager(첫번째_인자, 두번째_인자)
에서 두번째_인자로 true
가 들어가게 설정) 게임 화면을 마우스로 클릭한 후에 키값을 알고자 하는 키보드 키를 누릅니다. 이렇게 하면 브라우저 콘솔에 해당 키의 값이 표시됩니다.towerData[장대_위치][원판_위치]
)로 원판의 값들을 저장하는 변수입니다. resetTowerData
함수에 의해 초기화 됩니다.true
/false
로 저장합니다.다음으로 함수들을 살펴봅시다.
function resetTowerData() { var towerData = []; for(var i=0; i<NUM_OF_POLES; i++){ towerData[i] = []; } for(var j=0; j<NUM_OF_DISKS; j++){ towerData[0].push(NUM_OF_DISKS-j); } return towerData; }
towerData
를 초기화하는 함수입니다. for(var i=0; i<NUM_OF_POLES; i++)
반복문으로 각각 장대를 초기화 하고, for(var j=0; j<NUM_OF_DISKS; j++)
반복문으로 첫번째 장대(towerData[0]
)에 원판의 값을 넣습니다. 반복이 될때마다 원판 개수(NUM_OF_DISKS
)에 반복된 횟수(j
)를 뺀 만큼을 원판의 값으로 넣어줍니다.
이렇게 초기화 된 배열은 다음과 같은 값을 가지는 이차원 배열이 됩니다.
[[4,3,2,1], [], []]
towerData[장대_위치][원판_위치]=원판의_값
가으로 사용되는데, 장대_위치는 왼쪽부터, 원판_위치는 아래쪽부터 시작되어 towerData[0][0]
이 가장 왼쪽 장대의 가장 아래쪽 원판의 값이 됩니다. 즉 가장 왼쪽 장대에 원판이 아랫쪽부터 4, 3, 2, 1의 값을 가지는 원판들이 쌓입니다. 이 값들이 실제 화면에 출력될 때는 2n+1(n은 원판의 값)으로 변화되어 9, 7, 5, 3의 길이를 가지는 원판 텍스트가 아래에서 위로 순서대로 출력됩니다.
function getDiskColor(diskValue){ var diskColor; if(diskValue>0){ diskColor = DISK_COLORS[(diskValue-1)%(DISK_COLORS.length)]; } return diskColor; }
원판의 색깔을 가져오는 함수입니다. 원판의 값을 val로 받아서 DISK_COLORS
배열을 돌면서 색깔을 가져옵니다.
이제부터 나올 함수 이름 앞에 draw가 붙은 함수들은 화면에 텍스트를 출력하는 함수들입니다.
function drawFrame(){ //draw static text TMS.insertTextAt(FRAME_X,FRAME_Y+NUM_OF_DISKS+5,"MOVES : \n\n\n"); TMS.cursor.move(FRAME_X+11,FRAME_Y+NUM_OF_DISKS+7); TMS.insertText("┌──────────────────┐\n"); TMS.insertText("│ HANOI TOWER GAME │\n"); TMS.insertText("└──────────────────┘\n"); TMS.insertText("www.A-MEAN-Blog.com"); TMS.insertTextAt(FRAME_X+5,FRAME_Y+NUM_OF_DISKS+12,"KEYS : ENTER, ←, →, ESC(restart)"); var x,y; //draw top of poles for(var i=0; i<NUM_OF_POLES; i++){ x = FRAME_X+COLUMN_GAP+NUM_OF_DISKS+i*(COLUMN_WIDTH+COLUMN_GAP); y = FRAME_Y+3; TMS.insertTextAt(x,y,POLE_CHAR); } //draw ground var width = COLUMN_WIDTH*NUM_OF_POLES+COLUMN_GAP*(NUM_OF_POLES+1); for(var j=0; j<width; j++){ x = FRAME_X+j; y = FRAME_Y+NUM_OF_DISKS+4; TMS.insertTextAt(x,y,GROUND_CHAR); } }
게임을 진행하는 동안 바뀌지 않는 텍스트를 출력하는 함수입니다. TMS.insertTextAt
함수는 x,y 위치에 텍스트를 출력하고, TMS.cursor.move
함수는 커서 위치를 옮기며, TMS.insertText
함수는 현재 커서 위치에 텍스트를 출력합니다. TMS의 모든 함수들과 자세한 설명은 TM.ScreenManager 문서에서 확인할 수 있습니다.
//draw static text은 단순히 텍스트를 출력하는 부분이므로 설명을 생략하고, //draw top of poles과 //draw ground를 아래 그림을 참고하여 살펴봅시다.
장대들의 윗쪽 끝부분 텍스트는 게임의 진행중에 절대 변하지 않습니다. 하지만 그 아래 부분들은 원판이나 장대로 바뀔 수 있는 부분들이죠. 장대의 수나 장대간의 거리는 NUM_OF_DISKS
와 NUM_OF_POLES
값을 어떻게 설정하는지에 따라 바뀌게 됩니다. 마찬가지로 바닥(ground)의 길이 역시 NUM_OF_DISKS
와 NUM_OF_POLES
값에 따라 바뀌게 됩니다.
//draw top of poles for(var i=0; i<NUM_OF_POLES; i++){ x = FRAME_X+COLUMN_GAP+NUM_OF_DISKS+i*(COLUMN_WIDTH+COLUMN_GAP); y = FRAME_Y+3; TMS.insertTextAt(x,y,POLE_CHAR); }
장대 끝부분의 수는 NUM_OF_POLES
와 같으므로 for 반복문으로 NUM_OF_POLES
수만큼 찍어주게 됩니다. x 좌표에서FRAME_X+COLUMN_GAP+NUM_OF_DISKS
가 화면 왼쪽 끝부터 첫번째 장대까지의 거리, i*(COLUMN_WIDTH+COLUMN_GAP)
가 장대 사이의 간격입니다.
//draw ground var width = COLUMN_WIDTH*NUM_OF_POLES+COLUMN_GAP*(NUM_OF_POLES+1); for(var j=0; j<width; j++){ x = FRAME_X+j; y = FRAME_Y+NUM_OF_DISKS+4; TMS.insertTextAt(x,y,GROUND_CHAR); }
바닥의 길이는 COLUMN_WIDTH
를 장대의 수만큼 곱한 것과 COLUMN_GAP
을 장대의 수+1 만큼 곱한 것의 합입니다. COLUMN_GAP
은 바닥이 시작할 때, COLUMN_WIDTH
사이에, 바닥의 끝 부분에 들어가게 되서 장대의 수+1가 됩니다.
계속해서 다음 함수를 살펴봅시다.
function drawMove(){ var x = FRAME_X+8; var y = FRAME_Y+NUM_OF_DISKS+5; TMS.insertTextAt(x,y,moveCount); }
moveCount
의 값을 출력하는 함수입니다. "MOVES :" 다음에 오도록 x, y값을 맞췄습니다.
function drawCursor(){ for(var i=0; i<NUM_OF_POLES; i++){ for(var j=0; j<COLUMN_WIDTH; j++){ var x = FRAME_X+COLUMN_GAP+i*(COLUMN_WIDTH+COLUMN_GAP)+j; var y = FRAME_Y+1; var char = " "; var charColor = null; if(i===cursorPosition){ if(cursorDiskValue === 0 && j==NUM_OF_DISKS){ char = CURSOR_CHAR; } else if(cursorDiskValue !== 0 && j>=NUM_OF_DISKS-cursorDiskValue && j<=NUM_OF_DISKS+cursorDiskValue){ char = DISK_CHAR; charColor = getDiskColor(cursorDiskValue); } } TMS.insertTextAt(x,y,char,charColor); } } }
커서를 그리는 함수입니다. for(var i=0; i<NUM_OF_POLES; i++)
을 통해 각각 장대를 반복하고, for(var j=0; j<COLUMN_WIDTH; j++)
로 COLUMN_WIDTH
만큼의 공간에 커서를 그리거나, 현재 커서에 원판이 선택된 경우(cursorDiskValue>0
) 원판을 그리거나, 이전 커서나 원판를 지우기 위해 빈칸을 출력합니다.
x, y는 텍스트를 출력할 위치이고, char는 " "(빈칸), charColor는 null
(null
인 경우 기본색 출력)으로 기본값이 지정되어 있습니다. 반복문 실행 중에 장대의 순서(i
)가 현재 커서가 위치한 장대(cursorPosition
)인 경우 (if(i===cursorPosition)
에 해당하는 경우) 다시 아래의 조건에 해당한다면 char, charColor의 값이 변경됩니다.
if(cursorDiskValue === 0 && j==NUM_OF_DISKS){ char = CURSOR_CHAR; }
커서를 그리는 조건입니다. cursorDiskValue === 0
는 커서가 원판을 가지고 있지 않음을 뜻하고, j==NUM_OF_DISKS
는 현재 텍스트 위치(j
)가 COLUMN_WIDTH
의 한가운데임을 뜻합니다. (COLUMN_WIDTH
가 2*NUM_OF_DISKS+1
이므로 NUM_OF_DISKS
의 값이 COLUMN_WIDTH
의 한가운데 값이 됩니다)
else if(cursorDiskValue !== 0 && j>=NUM_OF_DISKS-cursorDiskValue && j<=NUM_OF_DISKS+cursorDiskValue){ char = DISK_CHAR; charColor = getDiskColor(cursorDiskValue); }
원판을 그리는 조건입니다. cursorDiskValue !== 0
는 커서가 원판을 가지고 있음을 뜻하고, j>=NUM_OF_DISKS-cursorDiskValue && j<=NUM_OF_DISKS+cursorDiskValue
는 원판을 그려야 하는 위치안에 있음을 나타냅니다.
현재 예제에서 처럼 NUM_OF_DISKS
가 4인 경우 COLUMN_WIDTH
는 2n+1으로 9가 됩니다. 예를 들어 cursorDiskValue
가 2라고 하면 xx#####xx (x는 빈칸을 나타냄)를 그려야 합니다. j
가 0, 1일 때는 빈칸을, j
가 2에서6일 때는 원판(#)을, j
가 7, 8일 때는 다시 빈칸을 그려야 하는 것이죠. 위에서 설명한 것 처럼NUM_OF_DISKS
이 한가운데 값이 되므로 NUM_OF_DISKS-cursorDiskValue
이 원판의 시작부분, NUM_OF_DISKS+cursorDiskValue
이 원판의 끝부분이 됩니다.
위 조건에 들지 않는 모든 경우에는 빈칸 " "을 출력합니다. 빈칸은 만약 기존위치에 이전에 출력된 텍스트가 있었다면 그것들을 지우는 역할도 합니다.
function drawTower(){ for(var i=0; i<NUM_OF_POLES; i++){ for(var j=0; j<NUM_OF_DISKS; j++){ for(var k=0; k<COLUMN_WIDTH; k++){ var x = FRAME_X+COLUMN_GAP+i*(COLUMN_WIDTH+COLUMN_GAP)+k; var y = FRAME_Y+4+NUM_OF_DISKS-1-j; var char = " "; var charColor = null; if(!towerData[i][j] && k==NUM_OF_DISKS){ char = POLE_CHAR; } else if(towerData[i][j] !== 0 && k>=NUM_OF_DISKS-towerData[i][j] && k<=NUM_OF_DISKS+towerData[i][j]){ char = DISK_CHAR; charColor = getDiskColor(towerData[i][j]); } TMS.insertTextAt(x,y,char,charColor); } } } }
towerData
를 화면에 원판과 장대로 그리는 함수입니다. 구조는 drawCursor
와 유사한데 모든 장대 위치, 모든 원판 위치, COLUMN_WIDTH
를 돌기 때문에 3중 반복문이 되었습니다. (drawCursor
는 모든 장대 위치의 COLUMN_WIDTH
를 돌기 때문에 2중 반복문) x좌표의 값은 drawCursor와 같은 식이 사용되었고, y좌표를 좀 더 자세히 살펴봅시다.
drawFrame
에서 FRAME_Y+3
위치에 top of poles를 출렸했으므로, 바로 밑(FRAME_Y+4
)이 출력위치가 됩니다. 우리는 원판을 밑에서 부터 쌓아야 되니까 원판 높이(NUM_OF_DISKS-1
)를 더하고 거기에서 현재 반복된 값(j
)을 빼면 FRAME_Y+4+NUM_OF_DISKS-1-j
가 y 좌표의 값이 됩니다.
function drawGameOver(){ TMS.cursor.move(FRAME_X,FRAME_Y); TMS.insertText(" Completed! Your moves : "+moveCount+" \n",null,"gray"); TMS.insertText(" Press <ESC> key to start new game ",null,"gray"); }
게임이 종료된 경우 텍스트를 추가하는 함수입니다.
function reset(){ towerData = resetTowerData(); moveCount = 0; cursorPosition = 0; cursorDiskValue = 0; isGameOver = false; TMS.cursor.hide(); TMS.clearScreen(); drawFrame(); drawMove(); drawCursor(); drawTower(); }
게임의 화면과 변수들을 초기화 시키는 함수입니다. TMS.cursor.hide
는 TM의 깜박이는 커서를 숨기고, TMS.clearScreen
함수는 화면의 모든 값을 지웁니다.
reset();
지금까지는 함수와 변수를 지정하였고, reset
함수를 실행하는 부분입니다.
TMD.print('debug-data',{ moveCount: moveCount, cursorPosition: cursorPosition, cursorDiskValue: cursorDiskValue, isGameOver: isGameOver, });
마지막으로 TMD(TM.DebugManager의 인스턴스)으로 디버그용 데이터를 출력합니다. 아래 스크린샷에서 화면 오른쪽의 텍스트 부분(--debug-data--)입니다. 이 TMP.print
함수는 함수가 호출되는 순간의 해당 값들을 출력하게 됩니다. 즉 지금 상태로는 실시간으로 값을 업데이트하지 않습니다. 실시간으로 이 값들을 업데이트하는 코드는 하편에 나옵니다.
이 화면은 실제 실행중인 프로그램 화면으로 현재 페이지의 브라우저 콘솔을 연 다음 아래의 명령어들을 입력하여 프로그램을 조작할 수 있습니다.
//커서를 오른쪽으로 한칸 이동 후 업데이트하기 cursorPosition++; drawCursor(); //게임 종료 텍스트 출력해 보기 drawGameOver();
이어지는 하노이 탑 쌓기 게임 만들기 (하)편에서는 실제 데이터를 조작하는 함수를 만들고 키입력을 받아 게임을 완성해 보겠습니다.
댓글
이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기