/* 전체 소스코드는 본문 하단에 위치해 있습니다 */
「개발 분류」
#winapi #c #c++ #간단한_게임
「프로그램 개요」
이름: Do Not Hit the Ball
분류: winapi 실습, 게임
배우고있는 winapi를 깊게 팔 의지는 없지만 과제하다가 재밌게 만든게 있어서 첫글로 올린다.
왼쪽 위에 생성된 원을 마우스로 제어하여 오른쪽 아래까지 이동하여야 하는데, 지나가는 공간에는 무작위 크기의, 겹치지 않은 원들이 자리하고 최대한 그 원들과 충돌하지 않아야 한다.
그저 내용을 구현할 뿐이라 게임이라고 하기에는 뭐하지만 어느정도 미션이 있으니 미니게임이라고 퉁치자.
제공받은 프로그램 내용 그대로는 이렇다.
"프로그램을 시작하면 화면에 장애물로 사용할 초록색 작은 원들이 나타난다. 주인공은 왼쪽 위 구석에서 시작한다. 마우스로 클릭하여 드래그하는 것을 통해 주인공 원을 이동시키며 이동 중간에 부딪힌 초록색 원은 빨간색으로 변한다. 오른쪽 아래 끝까지 도달하면 게임을 끝내고 화면 정중앙에 부딪힌 원의 갯수를 출력한다."
「주요 내용」
모든 내용에 대해 서술하지는 않고 재미있는 부분 몇몇만 담았다.
전체 소스코드는 게시글 하단에 있으니 뜯어보고 싶으신 분은 환영입니다.
- 자료구조
- 원-점 충돌처리
- 원-원 충돌처리
- 30개의 무작위, 겹치지 않는 원 초기화
- 원 제어
□ 01 자료구조
평면위에 위치를 편히 제어하기 위해 x좌표, y좌표를 저장할 구조체를 하나 만든다.
struct vec2
{
int x;
int y;
};
원과 같이 무언가를 그릴때 brush에 전달되는 색상값 구조체이다.
이 부분은 내부 구현을 어떻게 하느냐에 따라서 사용하지 않거나, 대체될 수도 있다.
struct cRGB
{
int r, g, b;
};
마지막으로 원을 쉽게 제어하기 위한 구조체를 만든다. 원의 중심좌표를 가질 vec2, 반지름 radius, 원의 색을 지정할 변수를 담는다.
struct Ball
{
vec2 pos;
int radius;
cRGB color;
};
□ 02 원-점 충돌처리
원과 점에 대한 충돌처리는 사각형이나 삼각형과 같은 도형보다 훨 쉽다.
1. 원의 중심점과 대상 좌표사이의 거리 계산
2. 계산된 거리값이 원의 반지름보다 작거나 같을경우 둘은 충돌(=겹침)
충돌처리에 관한 내용은 자주 사용하므로 함수로 만들었다.
bool isHit(Ball ball, vec2 point)
{
float length = sqrt(((point.x - ball.pos.x) * (point.x - ball.pos.x)) + ((point.y - ball.pos.y) * (point.y - ball.pos.y)));
if (length <= ball.radius)
{
return true;
}
return false;
}
□ 03 원-원 충돌처리
위의 원-점 충돌처리 방식과 거의 동일하다.
1. 두 원의 원점끼리의 거리를 계산
2. 거리값이 두 원의 반지름 합보다 작거나 같을경우 둘은 충돌(=겹침)
bool isHit_btb(Ball ball, Ball ball2)
{
float length = sqrt(((ball2.pos.x - ball.pos.x) * (ball2.pos.x - ball.pos.x)) + ((ball2.pos.y - ball.pos.y) * (ball2.pos.y - ball.pos.y)));
if (length <= ball2.radius + ball.radius)
{
return true;
}
return false;
}
□ 04 30개의 무작위, 겹치지 않는 원 초기화
해당 내용은 함수를 따로 만들고 그 함수를 윈도우 프로시저 함수의 WM_CREATE이벤트에서 호출하였다.
함수 내용은 다음과 같다.
void InitActor(RECT area, Ball* ActorList, int Count)
{
srand((unsigned int)time(NULL));
for (int i = 0; i < Count; i++)
{
ActorList[i].pos.x = rand() % (area.right - 19) + 20;
ActorList[i].pos.y = rand() % (area.bottom - 19) + 20;
ActorList[i].radius = rand() % 81 + 20;
reFining:
for (int a = 0; a <= i; a++)
{
if (a != i)
{
if (isHit_btb(ActorList[i], ActorList[a]) || isHit_btb({ { 20, 20 }, 20, {255, 255, 255} }, ActorList[a]))
{
ActorList[i].pos.x = rand() % (area.right - 19) + 20;
ActorList[i].pos.y = rand() % (area.bottom - 19) + 20;
ActorList[i].radius = rand() % 81 + 20;
goto reFining;
}
}
}
ActorList[i].color = { 0, 255, 0 };
}
}
지금보니 if (a != i)는 왜 넣었나 싶다. for문 부호하나 바꾸면 되는거니 여러분은 그러지 마세요.
간단히 설명하자면 하나의 원을 생성할 때마다 지금까지 생성된 원들과 겹치는지 확인한다. 그 과정에서 원 겹침이 발생하면 다시 랜덤시키고 goto문을 사용하여 겹침을 재확인한다. 이 과정을 겹치지 않을때 까지 반복할 것이며 성공적으로 배치되면 다음 원을 그리게 된다.
한가지 집고 넘어가자면 goto문을 사용하는 것은 코드의 흐름을 파악하는데 매우 어려움을 줄수 있으니 되도록 잊어버리고 코딩하자.
그러나 지금처럼 사용해도 흐름을 완전히 파악할 수 있고, 어느정도 코드를 간결하게 만들어줄 수 있다면 사용해도 된다고 생각한다.
결론은 goto문을 사용할땐 매우 주의하라.
□ 05 원 제어
원래는 WM_LBUTTONDOWN이 호출되면 WM_MOUSEMOVE 이벤트 통해서 원의 위치를 제어할 생각이었다만, 한 이벤트에 부하가 커지니 brush가 흰색으로 뭉개지는 현상이 발생했다. 고로 WM_LBUTTONDOWN 발생시 타이머를 발생시켜 구현하였다.
아래 코드는 마우스 좌측 클릭에 대한 처리이다.
case WM_LBUTTONDOWN:
if (isHit(pawn, { LOWORD(lParam), HIWORD(lParam) }))
{
bActive = true;
SetTimer(hwnd, 1, 70, NULL);
}
break;
case WM_LBUTTONUP:
if (bActive)
{
KillTimer(hwnd, 1);
bActive = false;
}
break;
아래 코드는 드래그 이동, 충돌체크, 게임 끝을 판정하는 WM_TIMER이벤트 내부이다.
case WM_TIMER:
POINT pt;
RECT WindowPos;
GetCursorPos(&pt);
GetWindowRect(hwnd, &WindowPos);
if (bActive)
{
pawn.pos.x = pt.x - WindowPos.left - 10;
pawn.pos.y = pt.y - WindowPos.top - 30;
}
for (int i = 0; i < ActorCount; i++)
{
if (isHit_btb(pawn, Actor[i]))
{
Actor[i].color = { 255, 0, 0 };
}
}
if (isHit(pawn, { area.right, area.bottom }))
{
bEnd = true;
KillTimer(hwnd, 1);
}
InvalidateRgn(hwnd, NULL, true);
break;
「전체 소스코드」
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <TCHAR.H>
#include <time.h>
#include <math.h>
#include <iostream>
#include <string>
#define ActorCount 30
LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow)
{
HWND hwnd; // handle window
MSG msg; // message
WNDCLASS WndClass; // window class
WndClass.style = CS_HREDRAW | CS_VREDRAW; // 출력 형태
WndClass.lpfnWndProc = WndProc; // 프로시저 함수 등록
WndClass.cbClsExtra = 0; // 클래스 여분 메모리
WndClass.cbWndExtra = 0; // 윈도우 여분 메모리
WndClass.hInstance = hInstance; // 윈도 인스턴스
WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION); // 아이콘
WndClass.hCursor = LoadCursor(NULL, IDC_ARROW); // 커서
WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); // 윈도우 초기 배경색 지정
WndClass.lpszMenuName = NULL; // 메뉴 이름
WndClass.lpszClassName = _T("WindowClass_1"); // 클래스 이름
RegisterClass(&WndClass);
hwnd = CreateWindow(_T("WindowClass_1"), _T("WindowProgramming Study"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
struct vec2
{
int x;
int y;
};
struct cRGB
{
int r, g, b;
};
struct Ball
{
vec2 pos;
int radius;
cRGB color;
};
bool isHit(Ball ball, vec2 point)
{
float length = sqrt(((point.x - ball.pos.x) * (point.x - ball.pos.x)) + ((point.y - ball.pos.y) * (point.y - ball.pos.y)));
if (length <= ball.radius)
{
return true;
}
return false;
}
bool isHit_btb(Ball ball, Ball ball2)
{
float length = sqrt(((ball2.pos.x - ball.pos.x) * (ball2.pos.x - ball.pos.x)) + ((ball2.pos.y - ball.pos.y) * (ball2.pos.y - ball.pos.y)));
if (length <= ball2.radius + ball.radius)
{
return true;
}
return false;
}
void InitActor(RECT area, Ball* ActorList, int Count)
{
//srand((unsigned int)time(NULL));
//for (int i = 0; i < Count; i++)
//{
// ActorList[i].pos.x = rand() % area.right;
// ActorList[i].pos.y = rand() % area.bottom;
// ActorList[i].radius = rand() % 81 + 20;
// ActorList[i].color = { 0, 255, 0 };
//}
srand((unsigned int)time(NULL));
for (int i = 0; i < Count; i++)
{
ActorList[i].pos.x = rand() % (area.right - 19) + 20;
ActorList[i].pos.y = rand() % (area.bottom - 19) + 20;
ActorList[i].radius = rand() % 81 + 20;
reFining:
for (int a = 0; a <= i; a++)
{
if (a != i)
{
if (isHit_btb(ActorList[i], ActorList[a]) || isHit_btb({ { 20, 20 }, 20, {255, 255, 255} }, ActorList[a]))
{
ActorList[i].pos.x = rand() % (area.right - 19) + 20;
ActorList[i].pos.y = rand() % (area.bottom - 19) + 20;
ActorList[i].radius = rand() % 81 + 20;
goto reFining;
}
}
}
ActorList[i].color = { 0, 255, 0 };
}
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
static Ball pawn;
static Ball Actor[ActorCount];
static RECT area;
static HBRUSH brush;
static bool bActive;
static bool bEnd;
switch (iMsg)
{
case WM_CREATE:
GetClientRect(hwnd, &area);
pawn = { {20, 20}, 20, {255, 255, 255} };
InitActor(area, Actor, ActorCount);
bActive = false;
bEnd = false;
break;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
for (auto actor : Actor)
{
brush = CreateSolidBrush(RGB(actor.color.r, actor.color.g, actor.color.b));
SelectObject(hdc, brush);
Ellipse(hdc, actor.pos.x - actor.radius, actor.pos.y - actor.radius, actor.pos.x + actor.radius, actor.pos.y + actor.radius);
}
brush = CreateSolidBrush(RGB(pawn.color.r, pawn.color.g, pawn.color.b));
SelectObject(hdc, brush);
Ellipse(hdc, pawn.pos.x - pawn.radius, pawn.pos.y - pawn.radius, pawn.pos.x + pawn.radius, pawn.pos.y + pawn.radius);
if (bEnd)
{
unsigned int count = 0;
TCHAR EndText[20] = _T("충돌한 공의 개수: ");
for (auto actor : Actor)
{
if (actor.color.r == 255) { count++; }
}
static char a[2];
sprintf(a, "%d", count);
static TCHAR b[2];
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, a, strlen(a), b, 256);
EndText[11] = b[0];
EndText[12] = b[1];
DrawText(hdc, EndText, _tcslen(EndText), &area, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
}
DeleteObject(brush);
EndPaint(hwnd, &ps);
break;
case WM_LBUTTONDOWN:
if (isHit(pawn, { LOWORD(lParam), HIWORD(lParam) }))
{
bActive = true;
SetTimer(hwnd, 1, 70, NULL);
}
break;
case WM_LBUTTONUP:
if (bActive)
{
KillTimer(hwnd, 1);
bActive = false;
}
break;
case WM_TIMER:
POINT pt;
RECT WindowPos;
GetCursorPos(&pt);
GetWindowRect(hwnd, &WindowPos);
if (bActive)
{
pawn.pos.x = pt.x - WindowPos.left - 10;
pawn.pos.y = pt.y - WindowPos.top - 30;
}
for (int i = 0; i < ActorCount; i++)
{
if (isHit_btb(pawn, Actor[i]))
{
Actor[i].color = { 255, 0, 0 };
}
}
if (isHit(pawn, { area.right, area.bottom }))
{
bEnd = true;
KillTimer(hwnd, 1);
}
InvalidateRgn(hwnd, NULL, true);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, iMsg, wParam, lParam);
}
「피드백」
- 코드정리 필요
- 게임이 끝나고 충돌개수 출력위해 형변환 하는 방식 변화 필요
- 화면의 깜빡임 (=> 더블버퍼링 or 이벤트 프로시저 구조 개선으로 해결가능)
- Brush 흰색으로 뭉개지는 현상 여전히 있음: WM_TIMER를 사용하는 방식으로 개선하여 얌전히 마우스를 움직이면 괜찮으나, 격한 움직임을 할경우 여전히 발생함.
- 타이머 통한 프레임 방식으로 구현하여 마우스를 빠르게 움직이면 그 사이에 겹친 내용은 계산에 포함이 안됨
: 타이머 호출 시간을 줄이거나, 프레임간 변위 선분을 만들어 충돌판정을 구현하면 해결가능
그 외에도 개선점이나 해결책, 또다른 피드백이 있으신 분의 댓글을 환영합니다.
'개발 > 게임\그래픽스' 카테고리의 다른 글
2019_Under the Chamber (0) | 2019.10.26 |
---|---|
2018_2D 로그라이크 게임 (0) | 2019.10.26 |
winapi_도형 복사붙여넣기 프로그램 (0) | 2019.10.25 |
2017_게임 공모전 입상작 (0) | 2019.10.25 |
2017_자동차 게임 프로젝트 (0) | 2019.10.25 |