본문 바로가기
핵심 API로 배우는 윈도우 프로그래밍

핵심 API로 배우는 윈도우 프로그래밍 연습문제 3장 9번

by 딴짓거리 2022. 9. 29.

윈도우를 생성하는 WinMain 함수의 코드는 전부 중복이므로

WndProc 함수만 적는다.

 

작성자 본인이 공부 중 푼 문제로 모범답안이 아님을 밝힙니다.

 

9.

#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <TCHAR.H>
#include <math.h>
#include <stdlib.h>
#include <time.h>
LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam);


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow)
{
	HWND		hwnd;
	MSG			msg;
	WNDCLASS	WndClass;
	WndClass.style = CS_HREDRAW | CS_VREDRAW;
	WndClass.lpfnWndProc = WndProc;
	WndClass.cbClsExtra = 0;
	WndClass.cbWndExtra = 0;
	WndClass.hInstance = hInstance;
	WndClass.hIcon = LoadIcon(NULL, IDI_QUESTION);		//윈도우 아이콘
	WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);		//커서 모양
	WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);	//배경 색
	WndClass.lpszMenuName = NULL;	//메뉴 이름
	WndClass.lpszClassName = _T("Window Class Name");	//윈도우 클래스 이름
	RegisterClass(&WndClass);
	hwnd = CreateWindow(_T("Window Class Name"),
		_T("Cobaltbru's First Window"),		//윈도우 타이틀 이름
		WS_OVERLAPPEDWINDOW,		//윈도우 스타일
		200,	//윈도우 위치 X
		300,	//윈도우 위치 Y
		1000,	//윈도우 가로
		600,	//윈도우 세로
		NULL,
		NULL,
		hInstance,
		NULL
	);
	ShowWindow(hwnd, nCmdShow);		//윈도우 기본 출력 함수
	UpdateWindow(hwnd);
	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return (int)msg.wParam;
}
struct Circle
{
	POINT p;
	int r;
	BOOL crash;
};

double LengthPts(int x1, int y1, int x2, int y2)
{
	return(sqrt((float)((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))));
}

BOOL InCircle(int x, int y, int mx, int my, int r1, int r2)
{
	if (LengthPts(x, y, mx, my) < (r1 + r2)) return TRUE;
	else return FALSE;
}

void Checker(Circle c_array[], int x, int y)
{
	for (int i = 0; i < 30; i++)
	{
		if (InCircle(c_array[i].p.x, c_array[i].p.y, x, y, c_array[i].r, 50))
		{
			c_array[i].crash = TRUE;
		}
	}
}


LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
	HDC				hdc;
	PAINTSTRUCT		ps;
	static Circle*	c_array;
	static int		x, y;
	static int		mx, my;
	static BOOL		Selection;
	HBRUSH			hBrush, oldBrush;
	static int		counter;
	static BOOL		clear;
	TCHAR			lpOut[1024];
	static clock_t	start, end;
	switch (iMsg)
	{
	case WM_CREATE:
		srand((unsigned)time(NULL));
		c_array = (Circle*)malloc(sizeof(Circle) * 30);
		for (int i = 0; i < 30; i++)
		{
			Circle c;
			c.p.x = rand() % 1000 + 1;
			c.p.y = rand() % 600 + 1;
			c.r = rand() % 80 + 10;
			c.crash = FALSE;
			c_array[i] = c;
		}
		counter = 0;
		Selection = FALSE;
		x = 0; y = 0;
		break;
	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);
		Ellipse(hdc, x - 50, y - 50, x + 50, y + 50);
		Checker(c_array, x, y);
		for (int i = 0; i < 30; i++)
		{
			if (c_array[i].crash == FALSE)
			{
				hBrush = CreateSolidBrush(RGB(0, 255, 0));
				oldBrush = (HBRUSH)SelectObject(hdc, hBrush);
				Ellipse(hdc, c_array[i].p.x - c_array[i].r, c_array[i].p.y - c_array[i].r,
					c_array[i].p.x + c_array[i].r, c_array[i].p.y + c_array[i].r);
				SelectObject(hdc, oldBrush);
				DeleteObject(hBrush);
			}
			else
			{
				hBrush = CreateSolidBrush(RGB(255, 0, 0));
				oldBrush = (HBRUSH)SelectObject(hdc, hBrush);
				Ellipse(hdc, c_array[i].p.x - c_array[i].r, c_array[i].p.y - c_array[i].r,
					c_array[i].p.x + c_array[i].r, c_array[i].p.y + c_array[i].r);
				SelectObject(hdc, oldBrush);
				DeleteObject(hBrush);
			}
		}
		if (clear == FALSE)
		{
			if (InCircle(1000, 600, x, y, 10, 50))
			{
				for (int i = 0; i < 30; i++)
				{
					if (c_array[i].crash == TRUE)
					{
						counter++;
					}
				}
				clear = TRUE;
			}
		}
		else
		{
			end = clock();
			wsprintf(lpOut, TEXT("충돌횟수: %d // 걸린시간: %d"), counter, (end - start)/CLOCKS_PER_SEC);
			TextOut(hdc, 300, 250, lpOut, lstrlen(lpOut));
		}
		
		EndPaint(hwnd, &ps);
		break;
	case WM_LBUTTONDOWN:
		mx = LOWORD(lParam);
		my = HIWORD(lParam);
		if (InCircle(x, y, mx, my, 0, 50))
		{
			start = clock();
			Selection = TRUE;
		}
		InvalidateRgn(hwnd, NULL, TRUE);
		
		break;
	case WM_MOUSEMOVE:
		mx = LOWORD(lParam);
		my = HIWORD(lParam);
		if (Selection == TRUE)
		{
			x = mx;
			y = my;
			InvalidateRgn(hwnd, NULL, TRUE);
		}
		break;
	case WM_LBUTTONUP:
		Selection = FALSE;
		InvalidateRgn(hwnd, NULL, TRUE);
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	}
	return(DefWindowProc(hwnd, iMsg, wParam, lParam));

}

어렵진 않지만 구현할 것이 많아 코드가 길어졌으므로 하나하나씩 살펴본다.

 

 

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow)
{
	HWND		hwnd;
	MSG			msg;
	WNDCLASS	WndClass;
	WndClass.style = CS_HREDRAW | CS_VREDRAW;
	WndClass.lpfnWndProc = WndProc;
	WndClass.cbClsExtra = 0;
	WndClass.cbWndExtra = 0;
	WndClass.hInstance = hInstance;
	WndClass.hIcon = LoadIcon(NULL, IDI_QUESTION);		//윈도우 아이콘
	WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);		//커서 모양
	WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);	//배경 색
	WndClass.lpszMenuName = NULL;	//메뉴 이름
	WndClass.lpszClassName = _T("Window Class Name");	//윈도우 클래스 이름
	RegisterClass(&WndClass);
	hwnd = CreateWindow(_T("Window Class Name"),
		_T("Cobaltbru's First Window"),		//윈도우 타이틀 이름
		WS_OVERLAPPEDWINDOW,		//윈도우 스타일
		200,	//윈도우 위치 X
		300,	//윈도우 위치 Y
		1000,	//윈도우 가로
		600,	//윈도우 세로
		NULL,
		NULL,
		hInstance,
		NULL
	);
	ShowWindow(hwnd, nCmdShow);		//윈도우 기본 출력 함수
	UpdateWindow(hwnd);
	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return (int)msg.wParam;
}

친숙한 WinMain 함수이다. 굳이 다시 적어 준 이유는 게임창이 어느정도 크기가 있어야 하기 때문에

윈도우 크기를 1000x600 으로 변경해 주었기 때문이다.

 

 

struct Circle
{
	POINT p;
	int r;
	BOOL crash;
};

double LengthPts(int x1, int y1, int x2, int y2)
{
	return(sqrt((float)((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))));
}

BOOL InCircle(int x, int y, int mx, int my, int r1, int r2)
{
	if (LengthPts(x, y, mx, my) < (r1 + r2)) return TRUE;
	else return FALSE;
}

void Checker(Circle c_array[], int x, int y)
{
	for (int i = 0; i < 30; i++)
	{
		if (InCircle(c_array[i].p.x, c_array[i].p.y, x, y, c_array[i].r, 50))
		{
			c_array[i].crash = TRUE;
		}
	}
}

이번 문제에서 계속 쓸 구조체와 함수이다.

편의를 위해 화면에서 장애물로 표시될 원을 구조체로 만들어 저장했다.

구조체에는 원의 중점, 반지름, 그리고 충돌하였을때 색깔을 변하게 만들기 위해 충돌 여부도 저장하게 해 주었다.

 

LengthPts() 와 InCircle은 원의 중짐과 마우스 포인터의 위치를 받아 마우스 포인터가 원 내부에 있는지 검사하는 함수로써 이제까지 유용하게 써왔다. 하지만 이번 문제에서는 원과 원의 충돌이므로 내가 움직이는 원의 반지름과 필드에 존재하는 원의 반지름을 더한 값과 비교해 주었다.

 

Checker 함수를 한번 호출하면 윈도우 내에 생성된 모든 원이 유저가 움직이는 원과 충돌 했는지 검사한다.

다만 이렇게 되면 프레임단위로 Checker 함수가 계속 호출되고 호출될 때 마다 30개의 원을 루프돌며 충돌 검사를 하기에 그리 효과적인 방법은 아니다..

 

LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
	HDC				hdc;
	PAINTSTRUCT		ps;
	static Circle*	c_array; // 윈도우 내에 생성될 원 구조체를 동적할당해서 저장할 배열
	static int		x, y; // 유저가 움질일 원의 중심
	static int		mx, my; // 마우스 포인터의 좌표
	static BOOL		Selection; // 유저가 움직일 원을 클릭하였는지 여부를 저장할 변수
	HBRUSH			hBrush, oldBrush;
	static int		counter; // 총 몇개의 원과 충돌했는지 저장할 변수
	static BOOL		clear; // 게임을 clear 했는지 상태를 저장할 변수
	TCHAR			lpOut[1024]; // 결과 메시지를 출력하기 위한 버퍼
	static clock_t	start, end; // 게임을 클리어 하는데 걸린 시간을 재기 위한 변수

WndProc 에서 사용한 변수의 목록이다.

 

 

case WM_CREATE:
		srand((unsigned)time(NULL));
		c_array = (Circle*)malloc(sizeof(Circle) * 30);
		for (int i = 0; i < 30; i++)
		{
			Circle c;
			c.p.x = rand() % 1000 + 1;
			c.p.y = rand() % 600 + 1;
			c.r = rand() % 80 + 10;
			c.crash = FALSE;
			c_array[i] = c;
		}
		counter = 0;
		Selection = FALSE;
		x = 0; y = 0;
		break;

윈도우가 생성되자마자 일단 맵을 구성해주어야 한다.

필드에 원 30개를 만들예정이므로(내가 적당히 정한 값이다.) c_array 배열에 Circle 구조체 30개만큼의 저장공간을 할당해준다.

원의 위치와 크기는 랜덤으로 생성해주고 싶다. 다만 화면 밖으로 완전히 나가버리거나 보이지 않을만큼 작거나, 무지하게 크거나 하면 곤란하므로 최대 최소값을 잘 생각해 랜덤함수를 사용한다.

 

case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);
		Ellipse(hdc, x - 50, y - 50, x + 50, y + 50);
		Checker(c_array, x, y);
		for (int i = 0; i < 30; i++) //원 30개를 그리는 루프
		{
			if (c_array[i].crash == FALSE) // 충돌하지 않은 원은 초록색으로
			{
				hBrush = CreateSolidBrush(RGB(0, 255, 0));
				oldBrush = (HBRUSH)SelectObject(hdc, hBrush);
				Ellipse(hdc, c_array[i].p.x - c_array[i].r, c_array[i].p.y - c_array[i].r,
					c_array[i].p.x + c_array[i].r, c_array[i].p.y + c_array[i].r);
				SelectObject(hdc, oldBrush);
				DeleteObject(hBrush);
			}
			else		//충돌한 원은 빨간색으로 그려야 함
			{
				hBrush = CreateSolidBrush(RGB(255, 0, 0));
				oldBrush = (HBRUSH)SelectObject(hdc, hBrush);
				Ellipse(hdc, c_array[i].p.x - c_array[i].r, c_array[i].p.y - c_array[i].r,
					c_array[i].p.x + c_array[i].r, c_array[i].p.y + c_array[i].r);
				SelectObject(hdc, oldBrush);
				DeleteObject(hBrush);
			}
		}
		if (clear == FALSE) // 게임도중에만 클리어 검사를 해주면 충분함
		{
			if (InCircle(1000, 600, x, y, 10, 50))
			{	//클리어조건을 일정 픽셀로 하면 사람손으로 클리어하기 어려우므로
            	//우측하단에 임의의 원의 범위에 사용자의 원이 충돌하면 클리어하는 것으로 했다.
				for (int i = 0; i < 30; i++)
				{ // 결과값을 표시하기 위해 충돌한 원의 갯수를 새 준다.
					if (c_array[i].crash == TRUE)
					{
						counter++;
					}
				}
				clear = TRUE;
			}
		}
		else
		{
			end = clock(); // 후술할 시간을 잴 때 필요한 과정이다.
			wsprintf(lpOut, TEXT("충돌횟수: %d // 걸린시간: %d"), counter, (end - start)/CLOCKS_PER_SEC);
			TextOut(hdc, 300, 250, lpOut, lstrlen(lpOut));
		}
		
		EndPaint(hwnd, &ps);
		break;
case WM_LBUTTONDOWN:
		mx = LOWORD(lParam);
		my = HIWORD(lParam);
		if (InCircle(x, y, mx, my, 0, 50))
		{
			start = clock();
			Selection = TRUE;
		}
		InvalidateRgn(hwnd, NULL, TRUE);
		
		break;

마우스 좌클릭 신호를 받으면 클릭한 좌표가 사용자가 움직여야 할 원 내부의 좌표인지 검사한다.

맞으면 클릭을 시작한 시점의 클럭값을 받는다.

이 값과 게임을 종료할때의 클럭값을 계산하면 게임을 클리어하는데 걸린 시간을 알 수 있다.

 

case WM_MOUSEMOVE:
		mx = LOWORD(lParam);
		my = HIWORD(lParam);
		if (Selection == TRUE)
		{
			x = mx;
			y = my;
			InvalidateRgn(hwnd, NULL, TRUE);
		}
		break;

사용자의 원이 클린 된 상태이면 계속해서 사용자의 원의 위치를 마우스 포인터의 현재 위치로 이동시켜준다.

case WM_LBUTTONUP:
		Selection = FALSE;
		InvalidateRgn(hwnd, NULL, TRUE);
		break;
	case WM_DESTROY:
		free(c_array);
		PostQuitMessage(0);
		break;
	}
	return(DefWindowProc(hwnd, iMsg, wParam, lParam));

마우스 좌클릭을 때면 Selection 변수에 FALSE 값을 줌으로 써 클릭을 하지 않은 상태임을 알려준다.

 

윈도우를 종료할때 c_array에 동적할당한 메모리를 free 함수로 꼭 해제해주어 메모리 낭비를 막는다.

 

문제점

1.각종 예외처리가 안되어있어 통제되지 않은 상황에 매우 취약하다. 

2. 마우스를 움직일때마다 WM_PAINT가 호출되어 원을 출력하는 구성의 한계로 엄청나게 많은 루프를 돌리기 때문에 화면 깜박임이 심하다. 이는 수시로 원의 충돌 상태를 검사해야 하는 특성상 계속해서 충돌체크 루프를 돌려줘야 했기때문에 어쩔 수 없이 택한 방식이지만 이보다 더 좋은 방법이 생각나지 않았다...