이번엔 화투이미지를 이용해 짝맞추는 게임을 만들어봅시다.
화투 이미지는 뒷면 1 , 앞면 18개로 총 19장입니다.
대화상자형식으로 프로젝트를 만든 후 우선 헤더파일로 가서 카드 갯수를 define해줍니다.
#define MAX_CARD_COUNT 19//카드 갯수
#define REAL_CARD_COUNT 18//실제 카드 게임에 활용하는 카드 갯수
그리고 카드 이미지를 불러오기 위해 CImage로 선언합니다.
private:
CImage m_card_image[MAX_CARD_COUNT];
%% wm_creat VS wm_initDialog
일반적으로 윈도우가 만들어질 때 운영체제가 wm_creat 메세지를 줍니다. 따라서 만들고 바로 쓰지 말고 wm_creat 메세지를 받은 뒤 써야합니다. 하지만 대화상자는 wm_creat가 발생하는 때가 자기 자신만(아무 기능없이 윈도우만) 만들어졌을 때 이고, 리소스를 포함한 모든게 만들어졌을때 wm_initDialog가 발생합니다. 따라서 대화상자는 이 메세지가 발생한 뒤 사용해야하고 creat메세지를 사용하면 안됩니다. 반대로 윈도우도 init메세지를 사용해선 안됩니다.
%%on_initDialog
wm_initDialog 메세지를 처리하는 함수입니다. 여기서 작업하는 것은 대화상자가 생기기 전으로 눈에 보이지 않습니다. 이 함수가 끝나고 나면 대화상자가 나타납니다. 여기서 대화상자의 크기를 바꾸거나 위치를 바꾸는 것은 불가능했었는데 init다음에 생성되는 것이 있었고 거기서 설정이 또 바뀌었기 때문입니다. 하지만 VS2013 기준으로 사용자가 지정한 설정이 그대로 유지되도록 하였는데 이전 버전과의 호환성을 위해 위치, 활성화, 포커스 등은 여기서 설정하지 않는 것이 좋습니다.
이미지를 불러오기 위해 on_initDialog 함수에 반복문을 사용해줍니다.
CString str;
for (int i = 0; i < MAX_CARD_COUNT; i++){
str.Format(L"c:\\work\\card_image\\%03d.bmp",i);// 화면으로 내보내는 것이 아니라 CString 클래스가 문자열을 가진다.
m_card_image[i].Load(str);
}
%%CString
C언어에서는 sprintf라는 함수가 화면으로 문자열을 내보내는 것이 아닌 프로그램(내부)으로 보내는 역할을 합니다. MFC에서 제공하는 클래스 CString도 마찬가지의 역할을 하지만 느리기 때문에 프로그래밍을 배운 초반에만 사용하고 나중에는 다른 걸로 대체하는 것이 좋습니다. 내부적으로는 포인터로 되어있지만 배열처럼 사용할 수 있습니다.
%%연산자 오버로딩
C++은 연산자 오버로딩을 통해 연산자 명도 함수이름으로 쓸 수 있습니다. 연산자 오버로딩을 통해 연산자도 재정의 할 수 있으며, 연산자 오버로딩을 갖고 있으면 연산자 오버로딩을 따로 써주지 않아도 적용됩니다.
CString 클래스는 연산자 오버로딩을 여러개 갖고 있는데 그중에 += 등이 있고 그다음으로 LPCTSTR타입에 대해 연산자 오버로딩이 되어있습니다. 그래서 따로 형변환을 해주지 않아도 str만 적어도 가능합니다.
카드 이미지를 띄우기 위해 OnPaint를 수정해줍니다.
else
{
//CDialogEx::OnPaint();
CPaintDC dc(this);//CDC클래스가 부모 클래스인데 HDC에 대해 연산자 오버로딩을 갖고 있어서
밑에 그냥 dc라도 적어도 오류가 나지 않음
for (int i = 0; i < MAX_CARD_COUNT; i++){
m_card_image[i].Draw(dc, i*20, 0);//dc.m_hDC 명시적으로 표현
}
}
가로x세로 6개씩 카드를 띄우고 랜덤하게 섞기 위해 우선 36개의 배열을 만듭니다.
private:
CImage m_card_image[MAX_CARD_COUNT];
char m_card_table[REAL_CARD_COUNT * 2];//2장씩 써야하므로 *2
현재 사용하는 카드 18장을 2장씩 나오게 해서 같은 2장을 맞추는 게임입니다. 따라서 36개의 배열을 만들고 그 배열에 인덱스 0~17을 두개 넣어서 비교하는 식으로 하게됩니다.
난수를 발생시켜 대입하는 것이 아닌 36개 숫자를 먼저 만들어놓고 배열의 인덱스를 만들어 자리 바꿈을 하는 것입니다.
36개의 배열에 0~17값을 대입해줍니다.
or (int i = 0; i < REAL_CARD_COUNT; i++){
m_card_table[i] = i;//0~17까지만 가능
m_card_table[18+i] = i;//따라서 18개부터 돌게 하기 위해
}//m_card_table은 0~17,0~17 총 36개 값이 들어간다.
출력해봅니다.
else
{
//CDialogEx::OnPaint();
CPaintDC dc(this);
for (int i = 0; i < REAL_CARD_COUNT*2; i++){
m_card_image[m_card_table[i]+1].Draw(dc, i*20, 0);
}
} //m_card_table에 0~17,0~17을 넣었기 때문에 *2
위의 좀 더 쉬운 표현, 배열안의 상수를 index로 처리
else
{
//CDialogEx::OnPaint();
CPaintDC dc(this);
int index;
for (int i = 0; i < REAL_CARD_COUNT*2; i++){
index = m_card_table[i] + 1;
m_card_image[index].Draw(dc, (i%6)*36, (i/6)*56);//dc.m_hDC 명시적으로 표현//dc,x,y
}
}
출력되는 그림이 카드가 6개씩 일렬로 나열되게 하기 위해서 나눗셈과 나머지 연산으로 조정해줍니다.
그림의 크기가 가로 36, 세로 56이므로, 배열이 36만큼 돌때 가로에 6개의 카드를 놓기 위해 %6을 해줍니다. 그러면 0~5까지는 나머지가 0,1,2,3,4,5 6~11까지도 나머지가 0,1,2,3,4,5 가 됩니다. 이때 이 나머지에 36씩 곱해주면 36,72,108.. 이 순으로 크기가 증가하게 됩니다. 반대로 y의 경우도 마찬가지 입니다. /6을 하면 0~5까지는 몫이 0, 6~11은 몫이 1,..이 되는데 여기에 *56을 해주니 이미지 세로 크기만큼 배열이 되게 됩니다.
이제 카드를 랜덤하게 섞어야하는데, 랜덤하게 섞기 위해 난수를 이용합니다. 난수를 발생시키기 위해 rand를 사용합니다. rand를 쓰려면 srand를 통해 난수의 기준을 정해주어야합니다. 난수는 타임시드로 결정이 납니다. 즉 타임시드를 고정 값을 쓰면 알아내서 예측할 수 있게 됩니다. 그러므로 타임시드를 랜덤으로 바꿔가면서 해야합니다.
우선 이 헤더파일을 포함합니다.
#include <stdlib.h>
#include <time.h>
%%time.h
1970년 1월 1일부터 0초로 시작해서 현재까지 초로 환산한 값, 시간이 계속 흘러가므로 계속 변한다. 즉 타임시드로 적합. 하지만 이것도 간파당하면 바로 찾아낼 수 있다.
타임시드를 설정해줍니다.
srand(time(NULL));
for (int i = 0; i < 50; i++){
rand();
}
그리고 카드를 두장 뽑아 섞어야 하므로 변수를 선언해줍니다.
char first_index, second_index,temp;
카드를 섞는 반복문을 설정해줍니다.
for (int i = 0; i < 100; i++){
first_index = rand() % 36;//(1)
second_index = rand() % 36;
temp = m_card_table[first_index];//(2)
m_card_table[first_index] = m_card_table[second_index];
m_card_table[second_index] = temp;
}
우선 (1) rand를 통해 랜덤한 인덱스를 뽑고 (2) 그 인덱스 값을 서로 바꿔줍니다. 같은 값이 나오는 경우 바꾸지 말라는 조건문을 넣어야 정확히 100번이 돌지만, 값이 같을 경우 교환이 안되는 것과 같고 조건문을 안쓰는게 더 낫기 때문에 그냥 이렇게 작성합니다.
앞면을 잠깐 보여주고 뒷면으로 뒤집기 위해서 타이머를 이용합니다.
SetTimer(1, 3000, NULL);//wm_timer메세지가 발생할때 어떤 타이머에 의해 발생하는지 구분하기 위해 아이디가 필요
//밀리초단위이기 때문에 3000
//함수의 포인터가 넘어감, 함수를 적으면 3초 뒤에 함수가 실행됨,
NULL을 쓰면 3초 뒤에 wm_timer 메세지가 발생
//killtimer가 발생할때까지 3초에 한번씩 계속 발생한다.
wm_timer는 settimer로 만들고 killtimer로 죽입니다. wm_timer는 wm_paint와 같은 flag성 메세지 이기 때문에 우선순위가 밀리고 시스템이 바쁘면 발생하지 않기 때문에 정확한 시간을 요구하는데에는 사용하지 않는 것이 좋습니다.
클래스 마법사에 가서 타이머를 추가해줍니다.(wm_timer 검색 -> OnTimer)
void CExamCardDlg::OnTimer(UINT_PTR nIDEvent)
{
CDialogEx::OnTimer(nIDEvent);
}
%%OnTime
wm_timer메세지가 발생할 때 실행되는 함수로, 타이머 아이디 값이 인자로 넘어옵니다. 만약 사용자가 타이머를 하나만 사용한다고 해서 타이머에 if문을 넣지 않으면 문제가 발생할 수 도 있습니다. 클래스 자체에서 타이머를 쓰기도 합니다.
앞면을 보여주다가 3초뒤 뒷면을 보여주도록 하려면 앞면인지 뒷면인지 상태를 보여주는 변수 하나를 추가한 뒤 타이머에서 3초뒤 화면을 갱신하고 wm_paint에서 조건문을 통해 뒷면인 경우를 설정해주면 됩니다.
우선 변수를 하나 추가합니다.
private:
CImage m_card_image[MAX_CARD_COUNT];
char m_card_table[REAL_CARD_COUNT * 2];
char m_hint_flag = 1;//1 : 앞면, 0: 뒷면
타이머를 설정해줍니다.
void CExamCardDlg::OnTimer(UINT_PTR nIDEvent)
{
if (nIDEvent == 1){
KillTimer(1);//1번 타이머를 죽임
m_hint_flag = 0;
Invalidate();
}else CDialogEx::OnTimer(nIDEvent);
}
조건문을 수정합니다.
if (m_hint_flag == 1){
int index;
for (int i = 0; i < REAL_CARD_COUNT * 2; i++){
index = m_card_table[i] + 1;
m_card_image[index].Draw(dc, (i % 6) * 36, (i / 6) * 56);//dc.m_hDC 명시적으로 표현//dc,x,y
}
}
else{//m_hint_flag==0인 경우
for (int i = 0; i < REAL_CARD_COUNT; i++){
m_card_image[0].Draw(dc, (i % 6) * 36, (i / 6) * 56);//카드의 뒷면만 보인다
}
}
%%invalidate
=1이라고 뜬 것은 ()인자를 적어주지 않으면 1값이 넘어가는 것. 현재 이 윈도우를 무효화하는 함수 invalide->valide상태로 바뀌면 wm_paint가 발생되기 때문. wm_paint가 발생되고 onpaint함수에서 else문이 실행된다.
이제 두장의 카드를 선택해서 같으면 사라지고 다르면 뒤집어지도록 해봅시다.
마우스를 클릭했을 때 조건을 설정하기 위해 LBUTTONDOWN을 추가합니다.
우선 헤더파일에 가서 선택할 카드 인덱스에 대한 변수를 추가합니다.
private:
CImage m_card_image[MAX_CARD_COUNT];
char m_card_table[REAL_CARD_COUNT * 2];//2장씩 써야하므로 *2
char m_hint_flag = 1;//1이 앞면, 0이 뒷면
char m_first_selected_index = -1, m_second_selected_index = -1;//현재 index값이 -1이면 선택되지 않았다는 뜻
OnButtonDown내에 조건을 설정합니다.
void CExamCardDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
if (point.x >= 0 && point.x <= 6 * 36 && point.y >= 0 && point.y <= 6 * 56){//카드가 있는 범위 내에만 클릭이 가능하도록//유효범위
int x = point.x / 36;//인덱스가 나옴
int y = point.y / 56;
CClientDC dc(this);
int select_index = y * 6 + x;//y가 하나 증가할때마다 6증가하기 때문 //내가 클릭한 위치
if (m_first_selected_index = -1){
m_first_selected_index = select_index;
m_card_image[m_card_table[select_index] + 1].Draw(dc, x * 36, y * 56);//클릭한 카드를 뒷면에서 앞면으로 보여줌
} else{//m_second_selected_index
if (select_index != m_first_selected_index){//같은 카드를 두번 선택하지 않도록 조건.
m_second_selected_index = select_index;
m_card_image[m_card_table[select_index] + 1].Draw(dc, x * 36, y * 56);
if (m_card_table[m_first_selected_index] == m_card_table[m_second_selected_index]){
//선택한 두개의 카드의 인덱스 값(카드테이블)을 비교, 같은 그림 카드를 선택했다는 뜻
m_card_table[m_first_selected_index] = -1;//맞힌 카드를 사라지게 함,인덱스 -1은 사라지는 것으로 가정
m_card_table[m_second_selected_index] = -1;
}
else{
}
m_first_selected_index = -1;
m_second_selected_index = -1;
//여기에 타이머 설정
}
}
}
CDialogEx::OnLButtonDown(nFlags, point);
}
이제 인덱스가 -1일때 카드가 사라지도록 wm_paint에 코드를 설정합니다.
else
{
CPaintDC dc(this);
if (m_hint_flag == 1){
int index;
for (int i = 0; i < REAL_CARD_COUNT * 2; i++){
index = m_card_table[i] + 1;
if (index) m_card_image[index].Draw(dc, (i % 6) * 36, (i / 6) * 56);
}//index가 0이면 출력x +1을 해주기 때문에 -1이 나올 수 없다 , 0, 굳이 비교문 하나 더 적는 것보다 이 표현이 좋음
}
else{
for (int i = 0; i < REAL_CARD_COUNT * 2; i++){
if (m_card_table[i] != -1){//맞추면 뒷면도 표시하지 않는다, -1이 들어간것은 카드가 사라진걸로 간주
m_card_image[0].Draw(dc, (i % 6) * 36, (i / 6) * 56);
}
}
}
}
이렇게 하고 실행을 시켜보면 잘 동작하지만 동작이 너무 빠르게 이루어져서 사용자가 보기 어려우므로 약간 딜레이가 생기도록 합니다. SetTimer를 이용합니다.
SetTimer(1, 1000, NULL);
마지막으로 딜레이를 줬을 때 딜레이동안 무한정 클릭할 수 있는 버그가 있습니다. 이를 해결하기 위해 조건을 하나 걸어줍니다.
void CExamCardDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
if (m_hint_flag == 2) return;//무한정 클릭하지 못하게함
…
———————
…
}
m_first_selected_index = -1;
m_second_selected_index = -1;
m_hint_flag = 2;//0아니면 다 무효, 1일때만 카드를 보여줌, 기존 변수 활용
SetTimer(1, 1000, NULL);
}
}
카드 두개를 선택한 후 두 카드의 인덱스를 -1로 초기화한 후에 상태 변수를 2로 바꾸고 클릭할 때 적용되는 lbuttondown에서 상태변수가 2일때는 클릭하지 못하도록하면 두번이상 클릭할 수 없게 됩니다.
1초뒤 wm_time 메세지가 발생하게 settimer해놓은 상태에서 그 안에 클릭을 계속하면 onlbuttondown함수가 실행되서 플래그 변수가 2니까 동작을 하지않는다. 그리고 1초가 지나면 기존에 설정된 대로 타이머가 죽고 플래그 변수값이 바뀌고 다시 클릭할 수 있게 된다.
프로그램을 실행하면
처음에는 앞면을 보여줬다가 3초 뒤에 뒤집어집니다.
그리고 짝맞추기에 성공하면 카드가 사라집니다.
'Programming > Win32 API & MFC' 카테고리의 다른 글
MFC 07 : 대화상자 컨트롤 (0) | 2015.08.15 |
---|---|
MFC 06 : 짝맞추기 게임 2 (2) | 2015.08.13 |
MFC 04 : 간단한 오목 프로그램 만들기 (2) | 2015.08.09 |
MFC 03 : 도형 그리는 프로그램 만들기 2 (0) | 2015.08.06 |
MFC 02 : 도형 그리는 프로그램 만들기 1 (0) | 2015.08.06 |