본문 바로가기

MS/DirectX

DirectDraw - SetClipper (2)

사실 Direct Draw Clipper는 상당히 쉬운 개념이지만... 나름대로 처음에 개념잡기는 힘든 편입니다.

... 일반적인 DirectX 관련 서적에 보면, Clipper 에 대한 부분은 클리퍼를 생성하는 함수만 달랑 제시해 놓고
,

이걸 불러다 쓰면 되는거야! 라고만 합니다
.

Clipper
생성에 대한 자세한 내용은 독학으로 하시는 분들은 솔직히 얻기가 힘들죠
.

... 그렇다고 해서 Clipper가 어렵다는 것은 절대로 아닙니다
.

단지 조금 난해한 구조체의 구조와 rcBound라는 것에 대한 개념이 초보 프로그래머들을 혼란스럽게 만드는 것이 가장큰 난관이죠
.

후후... 초반부터 너무 겁주기 전법으로 나가는 것 같네요
.

근데... 사실 알고보면 정말 정말 쉬워요
^^;


, 오늘은 직접 클리퍼를 생성해 보는 시간을 갖도록 할 것인데요
.

오늘 배울 프로그램은 두더지 그림을 키보드 입력을 통해서 자유 자제로 움직여보는 그런 프로그램이 되겠습니다
.

멀티 키입력 처리도 가능한 것을 볼 수 있는데요. 물론DirectInput은 사용하질 않았습니다
.

또한, 프로그램이 클리퍼를 사용할때와 클리퍼를 사용하지 않을때 어떤 차이가 있는지도 주의깊게 보시면 도움이 되겠지요
.


, 그럼 먼저 소스를 보시죠.

* Game.h

#ifndef _GAME_H_
#define _GAME_H_

#include "Bitmap.h"

#define MoleImg ".\\Img\\Mole.bmp" // 두더지 이미지 파일명.
#define Movement 5 // 이동량.

#ifndef _GAME_H_
#define _GAME_H_

#include "Common.h"
#include "ddraw.h"

class CGame
{
private:
HDC hDC;
HWND hWnd;
LPDIRECTDRAW7 lpdd;

RECT BltRect; // 블리트 할 렉트.
COORD Mole; // 두더지의 좌표.
COORD PreMole; // 두더지의 이전 좌표.
CBitmap BMole; // 두더지 그림.

DDSURFACEDESC2 ddsd;
LPDIRECTDRAWSURFACE7 lpddSPrimary;
// Primary Surface.
LPDIRECTDRAWSURFACE7 lpddSBack; // Back Buffer.
LPDIRECTDRAWSURFACE7 lpddSBuf; // Offscreen Buffer.

public:
int Init(HWND m_hWnd); // 게임 초기화.
int Main(); // 게임 메인.
int Shutdown(); // 게임 종료.

int AttachClipper(LPDIRECTDRAWSURFACE7 lpddS, int RectCnt, LPRECT ClipList);
};

#endif // end of ifndef _GAME_H.

이젠 슬슬 지겨워질만한 헤더파일입니다.

여지껏 해오던 내용과 별반 다를 것이 없죠.

단지 AttachClipper() 라는 함수가 하나 추가 되어 있는 것을 볼 수 있다는 것 외에는 설명할 것이 없네요.

* Game.cpp

#include "Game.h"

int CGame::AttachClipper(LPDIRECTDRAWSURFACE7 lpddS, int RectCnt, LPRECT ClipList)
{
int i;
LPRGNDATA RgnData;
LPDIRECTDRAWCLIPPER lpddClipper;

// Direct Draw Clipper 생성.
if( FAILED(lpdd->CreateClipper(0, &lpddClipper, NULL)) )
return ERR;

// RgnData에 메모리 할당.
RgnData =
new RGNDATA[ sizeof(RGNDATAHEADER) + RectCnt * sizeof(RECT) ];

// RgnData의 버퍼에 매개변수로 받은 ClipList를 복사한다.
memcpy(RgnData->Buffer, ClipList,
sizeof(RECT)*RectCnt);

// 구조체에 정보를 기록한다.
RgnData->rdh.dwSize = sizeof(RGNDATAHEADER);
RgnData->rdh.iType = RDH_RECTANGLES;
RgnData->rdh.nCount = RectCnt;
RgnData->rdh.nRgnSize = RectCnt *
sizeof(RECT);

RgnData->rdh.rcBound.left = 64000;
RgnData->rdh.rcBound.top = 64000;
RgnData->rdh.rcBound.right = -64000;
RgnData->rdh.rcBound.bottom= -64000;

// 모든 클리핑 사각형들의 최대 경계점을 정한다.
for(i=0; i<RectCnt; i++)
{
if( ClipList[i].left < RgnData->rdh.rcBound.left )
RgnData->rdh.rcBound.left = ClipList[i].left;

if( ClipList[i].right > RgnData->rdh.rcBound.right )
RgnData->rdh.rcBound.right = ClipList[i].right;

if( ClipList[i].top < RgnData->rdh.rcBound.top )
RgnData->rdh.rcBound.top = ClipList[i].top;

if( ClipList[i].bottom > RgnData->rdh.rcBound.bottom )
RgnData->rdh.rcBound.bottom = ClipList[i].bottom;
}

// 이제 클리핑 리스트를 생성합니다.
if( FAILED(lpddClipper->SetClipList(RgnData, 0)) )
{
// 클리핑 리스트 생성에 실패했다면,
delete RgnData; // 반드시 할당한 메모리를 제거 시켜주고 종료한다.

return ERR;
}

// 서페이스에 클리핑 영역을 설정한다.
if( FAILED(lpddS->SetClipper(lpddClipper)) )
{
// 클리핑 영역 세팅에 실패시,
delete RgnData; // 반드시 할당한 메모리를 제거 시켜주고 종료한다.

return ERR;
}

// 여기까지 왔다면, 클리핑 영역이 제대로 설정 된 것이므로,
delete RgnData; // 반드시 할당한 메모리를 제거 시켜주고 종료한다.

SAFERELEASE( lpddClipper );

return 0;
}


int CGame::Init(HWND m_hWnd)
{
hWnd = m_hWnd;

if( FAILED(DirectDrawCreateEx(NULL, (LPVOID*)&lpdd, IID_IDirectDraw7, NULL)) )
return ERR;
if( FAILED(lpdd->SetCooperativeLevel(hWnd, DDSCL_FULLSCREEN | DDSCL_EXCLUSIVE)) )
return ERR;
if( FAILED(lpdd->SetDisplayMode(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, 0, 0)) )
return ERR;


// 두더지의 좌표 세팅.
Mole.X = 0;
Mole.Y = 0;

PreMole.X = 0;
PreMole.Y = 0;

if( BMole.Load(MoleImg) == ERR )
return ERR;


// 오프스크린 서페이스 생성.
memset(&ddsd, 0,
sizeof(ddsd));
ddsd.dwSize =
sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
ddsd.dwWidth = BMole.ReturnInfoHeader().biWidth;
ddsd.dwHeight = BMole.ReturnInfoHeader().biHeight;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_SYSTEMMEMORY;

if( FAILED(lpdd->CreateSurface(&ddsd, &lpddSBuf, NULL)) )
return ERR;

// 오프스크린 서페이스에 그림을 그려줌.
lpddSBuf->GetDC(&hDC);
BMole.Draw(hDC);
lpddSBuf->ReleaseDC(hDC);


// 프라이머리 서페이스 생성.
memset(&ddsd, 0,
sizeof(ddsd));
ddsd.dwSize =
sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_COMPLEX | DDSCAPS_FLIP;
ddsd.dwBackBufferCount = 1;

if( FAILED(lpdd->CreateSurface(&ddsd, &lpddSPrimary, NULL)) )
return ERR;

// 백버퍼 생성.
memset(&ddsd, 0,
sizeof(ddsd));
ddsd.dwSize =
sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS;
ddsd.ddsCaps.dwCaps = DDSCAPS_BACKBUFFER;

if( FAILED(lpddSPrimary->GetAttachedSurface(&ddsd.ddsCaps, &lpddSBack)) )
return ERR;

RECT ClipRgn;
ClipRgn.left = 0;
ClipRgn.top = 0;
ClipRgn.right = SCREEN_WIDTH;
ClipRgn.bottom = SCREEN_HEIGHT;

if( AttachClipper(lpddSBack, 1, &ClipRgn) == ERR )
return ERR;

return 0;
}

int CGame::Main()
{
if( KEYDOWN(VK_ESCAPE) )
return ERR;


// 방향키 입력 처리 멀티키 입력도 가능.
if( KEYDOWN(VK_LEFT) )
Mole.X -= Movement;
if( KEYDOWN(VK_RIGHT) )
Mole.X += Movement;
if( KEYDOWN(VK_UP) )
Mole.Y -= Movement;
if( KEYDOWN(VK_DOWN) )
Mole.Y += Movement;

BltRect.left = Mole.X;
BltRect.top = Mole.Y;
BltRect.right = Mole.X + BMole.ReturnInfoHeader().biWidth;
BltRect.bottom = Mole.Y + BMole.ReturnInfoHeader().biHeight;

// Blt BltFast를 비교해보잣!
lpddSBack->Blt(&BltRect, lpddSBuf, NULL, DDBLT_WAIT, NULL);
//lpddSBack->BltFast(Mole.X, Mole.Y, lpddSBuf, NULL, DDBLTFAST_WAIT); // BltFast는 클리퍼 적용 불가능.

lpddSPrimary->Flip(lpddSBack, DDFLIP_WAIT);

Sleep(50);

////////////////////////////////
//
이전 위치의 그림을 지워준다. //
////////////////////////////////

lpddSBack->GetDC(&hDC);

SelectObject(hDC, GetStockObject(BLACK_BRUSH));
Rectangle(hDC, PreMole.X-10, PreMole.Y-10, PreMole.X+BMole.ReturnInfoHeader().biWidth+10, PreMole.Y+BMole.ReturnInfoHeader().biHeight+10);

lpddSBack->ReleaseDC(hDC);

// 공의 이전위치 기억.
PreMole.X = Mole.X;
PreMole.Y = Mole.Y;

return 0;
}

int CGame::Shutdown()
{
SAFERELEASE( lpddSBuf );
SAFERELEASE( lpddSBack );
SAFERELEASE( lpddSPrimary );
SAFERELEASE( lpdd );

BMole.Unload();

return 0;
}

... Game.cpp 역시 예전과 별 다를 것이 없습니다.

그럼 제일 먼저 새롭게 추가된 AttachClipper() 함수를 먼저 볼까요?

지난시간에 분명히 Clipping 영역은 한군데가 아니라 여러군데가 될 수 있다고 말씀 드렸습니다.

AttachClipper()
함수에서는 클리핑 영역을 ClipList라는 RECT 배열로 받아서 여러군데의 클리핑 영역을 지정할수가 있습니다.

* RGNDATA 메모리 할당.

// RgnData에 메모리 할당.
RgnData =
new RGNDATA[ sizeof(RGNDATAHEADER) + RectCnt * sizeof(RECT) ];


자 이부분은 RGNDATA 라는 구조체에 메모리를 할당 시켜주는 작업입니다.

new 라는 연산자는 C++에 새롭게 추가된 메모리 할당 연산자인데요.

new 변수타입[ 바이트 or 갯수 ]

와 같은 형식을 지닙니다.

, 위의 내용은 RGNDATA
sizeof(RGNDATAHEADER) + RectCnt * sizeof(RECT) 의 사이즈로 메모리에 할당한다는 뜻이죠.

sizeof()가 바이트 단위의 메모리 사이즈를 리턴한다는 것은 알고 계시죠?

ClipList라는 RECT 배열에 5개의 RECT가 저장되어 있다면,RECT 구조체의 5배의 크기를 지정해 주어야 하는 것이죠.

다시 말해, RECT 배열에 X개의 Rect가 저장 되어 있을때, RGNDATA의 메모리 할당 후의 모습은 다음과 같죠.

사용자 삽입 이미지


 

그 다음에는 RGNDATAHEADER 다음에 클리핑 영역을 대입해 주어야 하겠지요?

물론, 일일이 대입해도 좋겠지만, 여기서는 직접 메모리를 복사하는 방식을 택하고 있습니다.

* ClipList
RGNDATA에 복사.

// RgnData의 버퍼에 매개변수로 받은ClipList를 복사한다.
memcpy(RgnData->Buffer, ClipList, sizeof(RECT)*RectCnt);


, 위와 같이 RECT 배열을 RGNDATAHEADER 뒷부분에 붙여 넣음으로써 그림과 같은 형식을 만들어 줄수 있게 됩니다^^


, 이제 다음 부분 부터는 RGNDATA 구조체안의 rdh라는 구조체에 값을 대입해 주고 있는데요.

다른 것은 지난시간에 설명드린 내용과 같지만, rcBound라는 내용이 눈에 띕니다.

* rcBound
세팅.

RgnData->rdh.rcBound.left = 64000;
RgnData->rdh.rcBound.top = 64000;
RgnData->rdh.rcBound.right = -64000;
RgnData->rdh.rcBound.bottom= -64000;

// 모든 클리핑 사각형들의 최대 경계점을 정한다.
for(i=0; i<RectCnt; i++)
{
if( ClipList[i].left < RgnData->rdh.rcBound.left )
RgnData->rdh.rcBound.left = ClipList[i].left;

if( ClipList[i].right > RgnData->rdh.rcBound.right )
RgnData->rdh.rcBound.right = ClipList[i].right;

if( ClipList[i].top < RgnData->rdh.rcBound.top )
RgnData->rdh.rcBound.top = ClipList[i].top;

if( ClipList[i].bottom > RgnData->rdh.rcBound.bottom )
RgnData->rdh.rcBound.bottom = ClipList[i].bottom;
}


... rcBound에 초기값을 -64000~64000씩 줬다가 나중에 밑에서 또 if 문으로 값을 변경 하고 있습니다.

rc
바운드란, 여러개의 클리핑 영역들을 효율적으로 관리하기 위해서 필요한 개념인데요.

쉽게 말씀 드리자면
클리핑 영역을 총괄적으로 포함하는 부모 사각형이라고 말씀 드릴 수 있습니다.

다음 그림을 또 보죠.

사용자 삽입 이미지


 

클리핑 영역을 3군데를 지정했습니다.

region #1, region #2, region #3
이렇게 3군데를 지정했는데요.

클리핑 영역을 위와 같이 정하게 되면, 클리핑 영역이 아닌 곳에서는 전혀 블리팅을 할 수가 없습니다.

region #1, region #2, region #3 위에서만 블리팅 작업이 가능 하다는 것이죠.

그런데 위와 같은 것을 DirectX에서는 비트(bit)연산을 통해서 하게 됩니다.

그래픽 카드는 픽셀 단위로 이미지를 블리팅 한다고 말씀 드렸었지요?

따라서, 픽셀에 이미지를 찍을때, 그영역이 클리핑 영역 내부라면 찍고, 아니라면 찍어주질 않는 것이죠.

그런데, Direct Draw에서 VRAM 전체를 검사를 하려면 엄청난 오버헤드가 발생합니다.

우리가 800*600으로 화면 해상도를 지정했다고 해서,DirectX 800*600 만을 검사하는 것이 절대 아니죠.

VRAM
에서 현재 사용중인 모든 메모리를 검사하고 있는 것입니다.

굳이 그럴 필요가 있을까요?

그렇기 때문에 rcBound라는 것을 지정해서, "너는 rcBound 안만 검사를 하면 된다."라고 DirectX에가 알리는 것입니다.


그리고 처음에 64000을 준 이유는 별로 중요한 것이 아닙니다. 그냥 임의로 큰 값을 넣어준 것이고 다른값이 와도 상관은 없습니다.

단지, 여기서 당연히 left -64000 이어야 하는데, 64000이라는 값을 주었고, right 64000 이어야 하는데 -64000을 주었다는 것을 유심히 보시길 바랍니다.

왠지 거꾸로 된 느낌이 오시죠?

위와 같은 값을 준 것은 그 다음의 if 문들을 위한 것입니다.

그 다음에 나오는 for 문의 안에 if 문들은 클리핑 영역들을 for 문으로 루프를 돌면서 비교해서 가장 작은 값을left top으로 지정하고 가장 큰 값을 right bottom으로 지정합니다.

현재 rcBound left, top, right, bottom과 입력받은 클리핑 영역의 left, top, right, bottom을 비교해서 가장 작거나 큰값을 얻어 오는 것이죠.

그러면 위의 그림에서 제시한 것과 같은 rcBound를 얻어 올수 있게됩니다.

그 다음 부분들은 지난시간에 이미 말씀 드렸던 내용이므로 별다른 설명이 필요 없을 듯 하네요.


그러나 여기서 가장 중요한 내용이 하나 숨겨져 있는데요.

우리가 AttachClipper() 함수의 제일 처음 부분에서,
new 연산자로 메모리 할당을 했었지요?

new 연산자로 메모리 할당을 했을때는 반드시 delete 연산자로 할당된 메모리를 제거 시켜주어야 합니다.

게임 프로그램에서 delete가 제대로 되지 않을 때는 얼마나 심각한 문제를 발생하는지...

나중에
new delete를 이용한 메모리 할당을 제대로 공부하면서 직접 체험해 보는 기회를 갖도록 하죠-_-ㅋㅋ


, 그럼 이제 Init() 함수에서 어떻게 AttachClipper() 함수를 호출했는지 한번 살펴보도록 하죠.

* AttachClipper()
함수의 사용 예

RECT ClipRgn;
ClipRgn.left = 0;
ClipRgn.top = 0;
ClipRgn.right = SCREEN_WIDTH;
ClipRgn.bottom = SCREEN_HEIGHT;

if( AttachClipper(lpddSBack, 1, &ClipRgn) == ERR )
return ERR;

이번 예제에서는 우리가 사용할 클리핑 영역은 1군데이므로, 굳이 배열을 만들지 않고, RECT 구조체 하나만을 만들어서 AttachClipper() 함수에 넘겨주었습니다.

그런데 여기서 주의깊게 보셔야 할 부분은 lpddSBack, 즉 백버퍼에 Clipper를 세팅 했다는 점입니다.

대부분의 경우 Clipper 는 백버퍼나 오프스크린 표면에 많이 사용되며 주표면에는 거의 사용되질 않습니다.


그리고 마지막으로 Main() 함수를 보시면, 키입력 처리를 해주고 있는데요.

* 키입력 처리

// 방향키 입력 처리 멀티키 입력도 가능.
if( KEYDOWN(VK_LEFT) )
Mole.X -= Movement;
if( KEYDOWN(VK_RIGHT) )
Mole.X += Movement;
if( KEYDOWN(VK_UP) )
Mole.Y -= Movement;
if( KEYDOWN(VK_DOWN) )
Mole.Y += Movement;


... 사실 말이 멀티 키이지... 결국엔 하나 하나 처리해 주는 것이라는 것을 알 수 있죠.

예를 들어 왼쪽 키와 위쪽 키를 함께 누르면 그림은 분명히 왼쪽 위로 움직입니다.

그러나 사실 내부에서 알고 보면, 왼쪽으로 한번 위쪽으로 한번 움직여 준 것이라는 것을 알수 있는 것이죠.


그리고 사실상 이 점은 DirectInput을 사용해도 마찬가지입니다.

컴퓨터에게 있어서
"동시"라는 개념은 없으니까요.


그러면... 우리는 이걸로 멀티키 입력을 구현을 해버렸군요. 엄청나게 간단한 코드로....

그렇다면 DirectInput을 배울 필요가 없을까요?

흐흐... 글쎄요. DirectInput이 좋고 API는 안좋다 이런 것은 없지만...

다음시간에 좀더 자세히 DirectInput이 왜 필요한지를 알아보도록 하죠^^


뭐 그 다음에 나오는 내용들은 두더지를 그려주고, 서페이스를 뒤집어주고, 이전 위치의 두더지를 지워주고...

뭐 이전시간에 이미다 해보았던 내용들이죠.

 

출처 : http://aptinfo.tistory.com/88

'MS > DirectX' 카테고리의 다른 글

D3DFont 성능  (0) 2012.07.10
DX Sprite 사용법  (0) 2012.06.26
D3DXFont::DrawText 완전 느림...  (0) 2012.06.26
DirectDraw - SetClipper (1)  (0) 2012.06.01