본문 바로가기

Programming Language/C, C++

[Snake Game] 3. 중간 점검, 코드 다듬기

3. 중간 점검, 코드 다듬기


1. Intro

좋은 코드는 다른 사람이 그것을 보았을 때, 그리고 나중에 개발자 본인이 읽었을 때 쉽게 이해할 수 있는 코드이다. 좋은 코드를 만들기 위해 함수와 변수 이름을 정하는 규칙을 따르고, 별도의 헤더 파일을 만들고, 상수를 선언해 사용한다. 이번 시간에는 지금까지의 우리의 코드를 보다 예쁘게 다듬어보도록 하자.


2. 상수 선언

상수란 변하지 않는 수이다. 수학에서 3.141592...를 π로 부르고, 2.718281828...을 e로 불러 유의미한 수로 취급한다. 개발을 하다 보면 비슷하게 유의미하면서 변하지 않는 수를 다루어야 하는 경우가 빈번히 발생한다. 이 때, 그 수가 상수임을 명시하기 위해 #define이나 const 등을 사용한다.

우리의 코드에서 상수로 취급해야 하는 수가 무엇인지 생각해보자. 일단, 맵의 크기가 있다. 맵의 크기는 게임 중에 변하지 않으니까 맵의 가로 세로를 각각 상수로 선언하면 중앙의 좌표를 구하거나 맵의 경계를 계산하는데 편할 것이다. 또, 상하좌우키의 아스키코드값이 있다. 72와 80이 각각 무엇을 의미하는지 값을 외우지 않는 한 알 수 없다. 이를 상수로 표현해 적용하면 다른 사람이 보았을 때 그 값이 무슨 의미인지 쉽게 이해할 수 있다.


상수를 선언할 때에는 #defineconst, enum을 사용할 수 있다. 여기에선 #define과 const를 사용해보자. 본인은 컴퓨터 시스템 상에서 미리 정의된 값은 #define, 우리의 편의를 목적으로 정의하는 값은 const를 사용한다. 이는 순전히 내 마음이다. 다른 방법을 사용하고 싶은 사람은 굳이 이것을 따르지 않아도 좋다. 상수를 선언한 부분은 아래와 같다.

#include <stdio.h>
#include <Windows.h>
#include <conio.h>
#include <time.h>
#pragma warning(disable:4996)

#define KEY_CODE_UP 72
#define KEY_CODE_DOWN 80
#define KEY_CODE_LEFT 75
#define KEY_CODE_RIGHT 77

const int MAP_SIZE_HORIZON = 30;
const int MAP_SIZE_VERTICAL = 27;
const int MAP_CENTER_HORIZON = MAP_SIZE_HORIZON / 2;
const int MAP_CENTER_VERTICAL = MAP_SIZE_VERTICAL / 2;
...

코드의 각 부분을 상수를 반영하여 고쳐보자. 다음은 main 함수 내부에 있는, 맵의 중앙에 ■을 출력하는 부분이다. 이를 MAP_SIZE_HORIZON과 MAP_SIZE_VERTICAL을 이용해 고쳐보자.

printMapBoundary();
setCursorPos(MAP_CENTER_HORIZON * 2, MAP_CENTER_VERTICAL);
printf("■");
currentPosX = MAP_CENTER_HORIZON * 2;
currentPosY = MAP_CENTER_VERTICAL;

다음은 moveSnakeHead 내부에 있는, 플레이어가 맵의 밖에 나가지 않도록 범위를 정하는 부분이다.

if (prePosX < 2 || prePosX>(MAP_SIZE_HORIZON - 2) * 2)return;
if (prePosY < 1 || prePosY>MAP_SIZE_VERTICAL)return;

다음은 플레이어의 진행 방향을 결정하는 setHeadDirection 함수이다.

int setHeadDirection(int pressedKeyData) {
	int changedHeadDirection;
	switch (pressedKeyData) {
	case KEY_CODE_UP:
		// 진행 방향을 위로 지정
		changedHeadDirection = 0;
		break;

	case KEY_CODE_DOWN:
		// 진행 방향을 아래로 지정
		changedHeadDirection = 1;
		break;

	case KEY_CODE_LEFT:
		// 진행 방향을 왼쪽로 지정
		changedHeadDirection = 2;
		break;

	case KEY_CODE_RIGHT:
		// 진행 방향을 오른쪽로 지정
		changedHeadDirection = 3;
		break;

	default:
		break;
	}
	return changedHeadDirection;
}

단순히 숫자만 쓰는 것 보다 훨씬 코드가 이해하기 편해졌다.


3. 헤더파일 만들기

모든 코드를 하나의 c파일에 모아놓으면 코드를 확인하는데 대단히 불편하다. 그렇기 때문에 별도의 c파일을 만들어 함께 빌드하거나, 헤더파일을 만들어 가장 기본적인 코드를 그곳에 포함하기도 한다. 사용자 정의 헤더파일을 만들어, 게임의 기본적인 시스템에 대한 코드(맵 출력, 상수들)를 모두 옮겨보자.

이렇게 만든 사용자 정의 헤더파일인 game.h는 아래와 같다.

// FILE NAME : game.h
#include <Windows.h>
#pragma once
#pragma warning(disable:4996)

#define KEY_CODE_UP 72
#define KEY_CODE_DOWN 80
#define KEY_CODE_LEFT 75
#define KEY_CODE_RIGHT 77

const int MAP_SIZE_HORIZON = 30;
const int MAP_SIZE_VERTICAL = 27;
const int MAP_CENTER_HORIZON = MAP_SIZE_HORIZON / 2;
const int MAP_CENTER_VERTICAL = MAP_SIZE_VERTICAL / 2;

void setCursorPos(int x, int y) // 콘솔 좌표 위치 지정
{
	COORD pos = { x,y };
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}


void printMapBoundary()
{
	printf("■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■                                                        ■\n");
	printf("■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n");
}

setCursorPos는 외부 파일에 있는 것이 아무래도 좋을 것 같다. 맵을 출력하는 함수도 굳이 기본 파일에 있을 필요가 없다. 그리고 상수 선언 부분도 다 헤더파일로 옮겨보았다. setCursorPos를 사용하기 위해서는 Windows.h가 필요하기 때문에 game.h내부에서 Windows.h를 include 하였다.


기본 파일인 SnakeThis.cpp 파일은 아래와 같다.

// FILE NAME : SnakeThis.cpp

#include <stdio.h>
#include <conio.h>
#include <time.h>
#include "game.h"

// ■ ▲ ▼ ◀ ▶ ♥

int headDirectionWeight[4][2] = {
	{0, -1}, // 위로 이동
	{0, 1}, // 아래로 이동
	{-2, 0}, // 왼쪽으로 이동
	{2, 0} // 오른쪽으로 이동
};

int setHeadDirection(int pressedKeyData);
void moveSnakeHead(int &posX, int &posY, int headDirection);

int main()
{
	int currentPosX;
	int currentPosY;
	int currentHeadDirection = 0;
	printMapBoundary();
	setCursorPos(MAP_CENTER_HORIZON * 2, MAP_CENTER_VERTICAL);
	printf("■");
	currentPosX = MAP_CENTER_HORIZON * 2;
	currentPosY = MAP_CENTER_VERTICAL;
	while (true) {
		int refTime = clock();
		int currentTime=0;
		while (true) {
			if (kbhit()) {
				if (getch() == 224) {
					int pressedKey = getch();
					currentHeadDirection = setHeadDirection(pressedKey);
				}
			}
			currentTime += clock()-refTime;
			if (currentTime > 300000) {
				currentTime = 0;
				break;
			}
		}
		moveSnakeHead(currentPosX, currentPosY, currentHeadDirection);
	}
	return 0;
}

void moveSnakeHead(int &posX, int &posY, int headDirection) {
	int prePosX = posX + headDirectionWeight[headDirection][0];
	int prePosY = posY + headDirectionWeight[headDirection][1];
	if (prePosX < 2 || prePosX>(MAP_SIZE_HORIZON - 2) * 2)return;
	if (prePosY < 1 || prePosY>MAP_SIZE_VERTICAL)return;
	setCursorPos(posX, posY);
	printf("  ");
	posX = prePosX;
	posY = prePosY;
	setCursorPos(posX, posY);
	printf("■");
}

int setHeadDirection(int pressedKeyData) {
	int changedHeadDirection;
	switch (pressedKeyData) {
	case KEY_CODE_UP:
		// 진행 방향을 위로 지정
		changedHeadDirection = 0;
		break;

	case KEY_CODE_DOWN:
		// 진행 방향을 아래로 지정
		changedHeadDirection = 1;
		break;

	case KEY_CODE_LEFT:
		// 진행 방향을 왼쪽로 지정
		changedHeadDirection = 2;
		break;

	case KEY_CODE_RIGHT:
		// 진행 방향을 오른쪽로 지정
		changedHeadDirection = 3;
		break;

	default:
		break;
	}
	return changedHeadDirection;
}

사용자 정의 헤더파일을 불러올 때에는 <>대신 ""을 사용한다.

이렇게 상수를 사용하고 코드를 몇 개의 파일로 분리하면서 코드를 보기 좋게 다듬을 수 있다. 가독성 좋은 코드일수록 나중에 코드를 고치고 바꿀 때 들어가는 노력과 시간이 눈에 띄게 감소하기 때문에 항상 좋은 코드를 만들도록 노력하자.