(현재 포스팅 시의 작업 환경은 Win32API 환경에서 이루어지고 있습니다.)
이번 글에서는 앞서 포스팅했던, 타일을 출력하는 방법을 좀 응용하여,
한가지 재미있는 주제를 구현하는 방법에 대해서 소개하겠습니다.

▲ 위 스크린샷은 유명 고전게임 프린세스메이커2 의 스크린샷입니다.
프린세스메이커2 의 스크린샷입니다. 다들 아시겠지만, 이 게임은 스케쥴을 작성하고,
해당 스케쥴로 딸을 육성하는 게임이지요. 육성과 관련하여, 날짜 개념은 필수적으로
필요하며, 달력을 통해서 이를 준비하고 있습니다.
이제 여기서 이 달력을 만들어 본다고 생각합시다.
당연하지만 달력은 관련된 몇개의 이미지를 준비하고, 내부 구현을 통해서
만들어지는 것이지, 미리 모든 달력을 이미지화 해 두고 돌려서
사용하는 것은 아닐 것입니다.
(달력 이미지가 전부 따로 있었다면, 12 * 8 개의 달력 이미지가 있었겠군요
이것도 방법이 될 수는 있겠지만, 여기서는 최소한의 이미지 리소스를 사용하여
코드를 통해서 매달 알맞은 달력의 모습을 출력하도록 만들어 봅시다.)
달력을 구현한다면, 우선적으로 달력의 특징에 대해서 분석할 필요가 있습니다.
달력의 규칙성을 찾아야 하는 것이죠.

윈도우에서 제공되는 달력입니다. 다른 달력을 봐도 마찬가지겠으나, 여기서
잡아낼 수 있는 달력의 특징은 다음과 같습니다.
1. 달력은 6행 7열의 표로 만들어져 있습니다.
- 7개의 요일로 인해 7개의 열이 필요합니다. 행과 관련해서는 5행으로 끝나는
달도 많지만, 초과분을 고려하였을때 6개의 행이 필요합니다.
2. 매월 1일은 표에서 첫번째 칸이 아니라, 몇 칸을 띄우고 시작됩니다.
- 이것은 지난달에서 넘어오는 날짜와 요일에 맞추어 달력이 구성되는 탓으로
매월 표에서 시작되는 인덱스가 달라집니다. (이 부분은 매년 매달 전부
달라지는 부분입니다만, 규칙성을 구하여 처리할 수 있습니다. )
중요한 것은, 어찌되었건 매월 1일은 몇칸을 띄우고 배치될 수 있도록
준비되어야 한다는 점입니다.
3. 매월 마치는 날이 다릅니다. (28,29,30,31)
- 월의 마지막 날 역시 매달 달라집니다. 대부분의 달은 30/31일이 마지막
날이며, 이 부분은 어느정도 규칙성도 찾을 수 있습니다.
하지만 2월은 예외입니다. 2월은 28일이 마지막 날이라는 특이점이 있으며,
4년에 한번 29일로 마치는 특징도 있습니다.
이 특징들은 모두 달력을 구현할 때 중요한 부분입니다.
지금부터 이러한 달력의 특징을 이용해서 실제 달력을 구성해 보겠습니다.
우선적으로 이미지를 벡터 컨테이너를 통해서 관리하는 방법에 대해서
알고 있어야 합니다. 이 부분에 대한 지식이 없으신 분들은, 이전에 작성한
포스트를 참고해 주세요. (이 글에서는 모두 생략합니다.)
▲ Win32API 기반에서 BMP 이미지를 다루는 법을 소개하는 포스팅입니다.
1. 리소스를 준비합니다.
실제 프린세스 메이커 2의 UI를 준비하여 출력 처리를 수행할 것입니다.
저는 현재 프린세스메이커 2 의 UI를 편집하여 사용하고 있습니다.
http://www.spriters-resource.com/
사용된 이미지들은 위 사이트에서 제공되고 있습니다. 이 사이트는
여러 2D 게임들의 리소스를 자료로써 제공하고 있습니다.
이미지가 필요하신 분들이 이곳에서 프린세스메이커를 영문으로 검색하시면
이미지 자료를 찾을 수 있을 것입니다.
더불어 당연하지만 비 영리적 목적으로만 사용가능합니다^^.
(제가 따로 올려드리지 못하는 점 양해바랍니다.)

달력의 특징에 맞추어 6행 7열의 표를 준비하여...
이렇게 준비된 숫자 이미지를 달력이라 생각하고, 몇월이라는 정보를 지정해 주면,
그에 맞추어 알맞은 칸에 숫자가 배치되도록 하는 것이 이번에 소개할 내용인 것이지요.
2. 리소스를 관리할 컨테이너를 준비하고, 이미지를 로드합니다.
일단 저는 Texture 형의 클래스를 다루는 벡터(Vector) 컨테이너를 준비하여 리소스를
관리할 것입니다. 필요한 이미지의 분류가, 일반 UI/월/일 등으로 구분될 수 있으므로,
이를 컨테이너 배열로 구분하였습니다. Texture 형이나, 컨테이너를 다루어 이미지를
관리하는 방식에 대해서는 이전 포스팅에서 다루고 있으므로 생략합니다.
(위에 링크시켜둔 글입니다.)

더불어 이때, 컨테이너 참조를 쉽게 하기 위해서 관련된 열거형도 준비해 두었습니다.
일단 컨테이너의 대분류인 일반 UI/월/일 을 기본적으로 열거형으로 분류해 두었고,
이외에도 일반 UI 의 경우 실제 컨테이너 내부를 참조할 인덱스 값에 대해서도 열거형을
준비해 두었습니다.
(월/일 에 대해서는 따로 열거형이 필요가 없는데, 이유는 이들은 상수로 참조하여도
무슨 리소스인지 아는데 지장이 없기 때문입니다. 0~31 까지의 숫자를 찾는데 굳이
Ont,Two... 하는 식으로 열거형을 준비할 필요는 없겠죠? ^^)
이후, 필요한 리소스 이미지들을 로드해 주면 됩니다. 제가 사용한 방식에 대해서는
저의 이전 포스팅에 소개되어 있으며, 위에서 링크를 통해 연결시켜 두었으므로,
필요하신 분들은 참고하시길 바랍니다.
3. 표현할 달력의 자료를 정의합니다.
위에서도 언급하였지만, 매월 끝나는 날은 달마다 다릅니다. 또한 매달 시작시에
1일이 위치하는 칸이 몇칸 띄우고 배치되기 시작하는가에 대한 데이터도 다릅니다.
따라서 실제 달력 리소스를 알맞게 배치하기 위해서는 먼저 이 2가지에 대한 데이터를
산출해 둘 필요가 있습니다.
위에서도 언급한 부분입니다만, 달력은 1일이 시작되는 것이 첫 칸에 시작되는 것이 아니라,
몇 칸의 공백을 두고 시작됩니다. 더불어 모든 칸을 채워나가는 것도 아니고, 해당 달에 맞는
마지막 날까지만 배치되는 것 역시 특징이라고 할 수 있겠습니다.

이런 특징을 반영하기 위해서, 2개의 배열을 준비했습니다. 월은 12월까지 있으므로, 배열의
갯수는 각각 12개씩입니다. 각각의 역할은 이름에서 알 수 있습니다. 달력의 최초 공백의 갯수를
나타낼 배열과, 마지막 날을 나타낼 배열이지요.
실제 값의 초기화는 임시로 준비한 InitCalendar 초기화 함수에서 이루어지게 됩니다.
아래에 해당 함수의 코드를 첨부해 두겠습니다.
HRESULT CCore::InitCalendar()
{
memset(Month_Vacuum,0,sizeof(int) * 12);
memset(Month_Last_Day,0,sizeof(int) * 12);
//memset을 통해 배열의 값을 초기화 합니다.
Month_Vacuum[0] = 0; //2017.1 월 기준
//첫번째 공백을 기록합니다.
Month_Last_Day[0] = 31;
Month_Last_Day[1] = 28;
for (int i = 0; i < 2; ++i)
{
for(int j = 0; j < 5; ++j)
{
int Index = 2 + (5 * i) + j;
int LastDay = Index % 2;
Month_Last_Day[Index] = (Index < 7) ? 31 - LastDay : 30 + LastDay;
}
}
//12달의 Last_Day 를 결정합니다.
//3월부터 12월까지는 30,31이 어느정도 규칙을 이루며 마지막 날로 결정되므로,
//이 부분에 대한 반복문을 준비하였습니다. (1월/2월은 그냥 대입했습니다.)
//사실 이 부분은 반복문을 쓰는게 연산 비용이 오히려 더 비효율적일 수 있으므로,
//0~11까지 배열에 직접 대입하는 것도 괜찮습니다. 배열의 크기가 100,1000칸씩
//되는 상황은 아니니까요.
for(int i = 0; i < 11; ++i)
Month_Vacuum[i + 1] = (Month_Vacuum[i] + Month_Last_Day[i]) % 7;
//12달의 Vacuum을 결정합니다.
//다음달의 공백의 갯수는 어디까지나 요일에 의해서 결정됩니다.
//(공백 + 마지막 날) 의 결과를 7로 나눈 나머지를 구하게 되면,
//마지막줄의 날짜 갯수가 나오며, 이 갯수는 마지막 날의 요일을 알아내는
//역할을 할 수 있습니다. 따라서, 이 결과는 다음 달의 공백의 갯수가 됩니다.
//이 기능을 반복문을 통해 수행하여 1년간의 정보를 구하게 됩니다.
return S_OK;
}
기본적으로 이미지는 준비되었다는 가정에서 출발하므로, 준비할 내용은
12달에 대한 공백(Vacuum)과 마지막 날(Last_Day) 입니다.
소개한 함수는 이러한 값을 결정짓는 역할을 합니다.
4. 준비된 정보를 토대로 달력을 출력합니다.
이제 필요한 기본적인 준비가 되었으므로, 이 정보들을 토대로 달력을
구성합니다. 달력은 기본적으로 달력의 패널(배경) 을 출력하고
그 위에 알맞은 칸에 숫자에 해당되는 이미지를 출력하는 식으로 구성합니다.
달력은 7 x 6 사이즈의 표를 사용하므로 기본적으로 2중 for문으로 처리합니다.
아래에는 해당 부분에 대한 코드를 첨부합니다. 참고로 여기서
이전에 소개한 타일(Tile)을 배치하는 방식을 응용해 사용하게 됩니다.
void CCore::RenderCalendar()
{
int Calendar_X = 90; //달력 패널 좌표 X
int Calendar_Y = 120; //달력 패널 좌표 Y
int NumX = Calendar_X + 28; //달력의 숫자 시작 좌표 X
int NumY = Calendar_Y + 68; //달력의 숫자 시작 좌표 Y
//달력의 패널 좌표와, 첫번째 칸에 대한 좌표를 준비합니다.
//보다 발전된 형태로, 패널을 부모 UI 로 삼고, 종속된 자식 UI를
//구성하는 방식으로 개선 및 UI 객체를 따로 클래스화 하는 편이
//좋습니다만. 일단은 예시로써 생각해 주십시오.
int NumberSize = 20; //숫자 이미지의 픽셀 사이즈
int NumberDiviSize = 40; //다음 숫자로 넘어갈때 띄울 픽셀 사이즈
//***** 달력의 패널을 출력한다. *****//
BitBlt(m_VecBmp[VEC_BMP_UI][UI_BACKGROUND]>GetTextureDC(),
Calendar_X,Calendar_Y,300,300,
m_VecBmp[VEC_BMP_UI][UI_CALENDAR_PANEL]->GetTextureDC(),
0,0,SRCCOPY);
int Month = 2; //구성하고 싶은 월을 고릅니다. (현재 2월)
int Index = 1; //시작 인덱스. (몇일인가를 나타냅니다.)
//*** 달력의 월(Month) 를 출력한다 ***//
BitBlt(m_VecBmp[VEC_BMP_UI][UI_BACKGROUND]->GetTextureDC(),
Calendar_X + 150,Calendar_Y + 10,60,20,
m_VecBmp[VEC_BMP_MONTH][Month - 1]->GetTextureDC(),0,0,SRCCOPY);
//***** 달력의 날짜(Day)를 출력한다. *****//
for(int i = 0; i < 6; ++i)
{
for(int j = 0; j < 7; ++j)
{
if(Index > Month_Last_Day[Month - 1])
break;
//해당 달의 마지막날인 경우 루프를 탈출합니다.
if( i * 7 + j < Month_Vacuum[Month - 1])
{
NumX += NumberDiviSize;
continue;
}
//최초의 공백 갯수까지는 숫자를 출력하지 않아야 합니다
//공백처리를 하고 continue 처리합니다. 당연하지만 날짜
//인덱스도 증가시키지 않습니다.
//(다만 공백처리는 초반에 끝나는 것임에도 불구하고 현 구조로는
//날짜가 끝날때까지 매번 체크하도록 되어있어, 비효율성이 남아있습니다.
//이 부분은 필요한 처리를 추가하여 보다 개선하시길 바랍니다.)
BitBlt(m_VecBmp[VEC_BMP_UI][UI_BACKGROUND]->GetTextureDC(),
NumX,NumY,NumberSize,NumberSize,
m_VecBmp[VEC_BMP_DAY][Index]->GetTextureDC(),0,0,SRCCOPY);
//현재 날짜 인덱스에 해당된 이미지를 출력합니다.
//배치할 좌표는 반복문 연산을 통해서 수정됩니다.
++Index; //다음 인덱스(날짜)로
NumX += NumberDiviSize;
}
NumX -= NumberDiviSize * 7; //가로 칸을 다시 첫번째칸으로 되돌립니다.
NumY += NumberDiviSize;
}
//기본적으로 6 x 7 로 42번 수행할 수 있는 for문입니다.
//이때 날짜 인덱스가 LastDay가 되는 순간 루프를 탈출하게 되므로
//실제 연산횟수는 줄어들게 됩니다.
}
이 코드는 출력을 담당하는 Render 함수입니다. 편의상 함수 내에서 변수를 할당하고
있습니다만, 제대로 작업한다면 이런 식으로 할당해서는 안됩니다. 출력은 매번 이루어지는
처리인데, 날짜를 표현하기 위한 변수를 매번 할당하고, 관련된 연산을 할 필요는 없기
때문입니다. 일단은 소개를 위한 참고용 코드 정도로 생각해 주시길 바랍니다.
뿐만 아니라, 이 코드는 아직 개선할 사항이 몇가지 남아있는 상태입니다. 이러한 부분에
대해서는 보다 체계화된 구조를 준비하는 사전 작업이 선행되어야 하므로, 일단 생략해
두었습니다. 어디까지나 이번 글의 목적에 맞는 정도의 정보만 제공하기 위해서입니다.
여기까지의 결과로 다음 출력결과를 볼 수 있습니다. 좀 횡해 보인다는게 흠이지만 어디까지나
달력을 구현하는 것이 목적이었던 것이므로, 양해 바랍니다^^;
(추가로 년도는 출력하지 않았는데, 2017년 달력으로 알고 봐 주시길 바랍니다.)
현재 기준은 2017년으로 잡아두었습니다. 2017년 1월 달력의 특징은 공백이 없다는 점입니다.
또한 마지막 날은 31일 입니다. 최초에 공백을 지정할때 1월의 공백만 지정해주면,
나머지 달은 알아서 설정되도록 구현하였으므로, 다른 년도를 지정하고 싶다면, 다른 년도의
1월 공백을 넣어주면 알아서 해당 년도에 맞는 정보를 구성하게 됩니다.

결과를 몇가지 더 출력해 보았습니다. 실제 2017년 달력과 비교해보면 일치하는 것을
확인할 수 있습니다. 달력을 구현하는 것에 대한 소개는 이정도로 마치도록 하겠습니다.

▲ 통합 객체의 소멸자에서 컨테이너의 원소에 관한 메모리를 해제합니다.
해제 후에도 컨테이너 내부에는 널 포인터 변수가 남게 되므로, Clear를 통해
컨테이너를 비우도록 합니다.
마지막으로 프로그램을 마칠때, 벡터 컨테이너에 보관된 텍스쳐 객체를 해제해 주어야 합니다.
벡터 컨테이너는 인덱스로 원소에 접근할 수 있으며, 이러한 특성을 이용하여, 별도의 이터레이터
없이 해제 작업을 수행하도록 작성했습니다.
(객체의 Release 함수 내부에는 이미지 처리에 사용되었던 DC 등을 해제하는 내용이 추가되어
있습니다. 이 정보 역시, 위에 첨부해 둔 링크를 통해 소개한 글을 참고할 수 있습니다.)
더불어 벡터 컨테이너를 배열의 형태로 사용하고 있으므로, 대괄호 연산자를 두번 붙여
접근하고 있습니다. 첫번째 대괄호 연산자는 컨테이너 배열에서 접근할 컨테이너를 가르키며,
두번째 대괄호 연산자는 선택된 컨테이너 내부의 원소에 직접적으로 접근하는 과정입니다.
이 점에 유의하여 해제 작업을 수행하시길 바랍니다.
사실, 글에서는 프린세스메이커 2 의 리소스를 사용하였습니다만, 내용상으로 이번
글의 내용은 어디까지나 범용적인 달력을 구현하는 방법을 소개하였다고 생각합니다.
프린세스메이커의 달력은 하나의 예시로 생각해 주시고, 새로운 이미지로 새로운
달력을 만들어 게임을 만들때 적용하는 등, 응용의 여지는 많다고 생각됩니다.
아무쪼록, 글의 내용이 여기까지 읽어주신 분들께 도움이 되길 바랍니다.
글은 여기까지입니다.





덧글