리듬게임을 만들어보자 2. 파일 메모리 관리, 스프라이트 읽기와 그리기

Posted by 적분 ∫2tdt=t²+c
2009.11.20 16:50 프로그래밍/스페샬
화면에 그림을 그리기 위해서는 먼저 파일에서 비트맵을 읽어와야한다. 아무리 3D라고 할지라도 그 기본은 변하지 않는다. 3D프로그래밍에서는 비트맵 대신에 텍스쳐라는 말을 자주 쓴다. 그러니 앞으로 텍스쳐 얘기가 계속 나오면 그냥 비트맵을 말하는건가보다 하고 간단히 넘어가자.
Direct3D에서는 텍스쳐를 관리하는 인터페이스로 IDirect3DTexutre9를 제공한다.
근데 문제가 하나 있다. 비디오카드에서 받을 수 있는 텍스쳐는 가로 세로길이가 (2의 거듭제곱)픽셀이어야 한다. 요즘에는 가로 세로 길이에 제한이 없는 비디오카드도 있지만, 모든 카드에서 그런것이 아니다.
그러므로 우리가 사용할 텍스쳐는 어쩔 수 없이, 가로 세로 길이가 (2의 거듭제곱)픽셀이어야 한다. 그러나 화면에 출력할 그림들의 크기가 늘 (2의 거듭제곱)꼴일수는 없다.
그래서 여기에서 대안을 소개한다.
2의 거듭제곱 꼴의 크기를 갖는 텍스쳐를 불러오기는 하되, 부분부분을 쪼개서 사용하는 것이다. 이 한 조각을 스프라이트(Sprite)라고 하자.

이제, 텍스쳐와 스프라이트를 관리하는 클래스를 하나 만들겠다. 이름은 GSprites라고 하자.

그리고 스프라이트 정보를 담는 구조체를 GSpriteDat이라고 하자.
GSpriteDat은 두 필드로 이뤄져 있다.
DWORD itxt; 텍스쳐의 인덱스
RECT rect; 텍스쳐 중에서 사용할 사각형 영역

vector가 등장했다. STL에 대해 잘 모르는 사람들은 적이 당황할수 있지만, 그렇게 어려운 놈은 아니므로 안심해도 좋다. vector는 길이가 자유롭게 변하는 배열이라고 생각하면 쉽다. vector 뒤에는 <>를 쓰고 그 안에 데이터 타입을 써넣는다. 예를 들어 vector<int>라 하면 길이가 자유롭게 변하는 int 배열이 되는것이다.
vector에서 가장 중요한 함수는 push_back과 pop_back, 그리고 size이다.
push_back은 배열의 꽁지에 원소 하나를 추가하는 일을 하고,
pop_back은 꽁지에 있는 원소 하나를 빼는 일을 한다.
size는 전체 원소의 개수를 알려준다.
일반 배열처럼 []를 사용해서 원소에 접근할 수 있으니, 크기가 변하는 배열이라면 vector를 애용해주자.

이제 m_vptxt가 뭔지 쉽게 알 수 있다. 길이가 자유롭게 변하는 LPDIRECT3DTEXTURE9의 배열이다.


file에서 텍스쳐를 읽어오는 함수이다. DataInMem은 이따가 다시 살펴보도록 하고, 먼저 텍스쳐를 읽어오는 부분을 살펴보자.
D3DXCreateTextureFromFileInMemoryEx 함수를 사용했다. 후 이름 한 번 참 길다.
D3D에서는 텍스쳐를 만드는 함수를 여러가지 제공한다.

D3DXCreateTexture : 텍스쳐를 만든다.
D3DXCreateTextureFromFile : 텍스쳐를 파일로부터 만든다.
D3DXCreateTextureFromFileEx : 텍스쳐를 파일로부터 만드는데, 세부사항을 결정할 수 있다.
D3DXCreateTextureFromFileInMemory : 텍스쳐를 메모리로부터 만든다.
D3DXCreateTextureFromFileInMemoryEx : 텍스쳐를 메모리로부터 만드는데, 세부사항을 결정할 수 있다.

FromFile과 FromFileInMemory와는 파일에서 읽어오느냐, 메모리에서 읽어오느냐의 차이밖에 없고 나머지는 동일하다. 그러니까 하나만 제대로 알고 있으면 나머지도 알고 있는거나 마찬가지다.

HRESULT D3DXCreateTextureFromFileInMemoryEx( LPDIRECT3DDEVICE9 pDevice, LPCVOID pSrcData, UINT SrcDataSize, UINT Width, UINT Height, UINT MipLevels, DWORD Usage, D3DFORMAT Format, D3DPOOL Pool, DWORD Filter, DWORD MipFilter, D3DCOLOR ColorKey, D3DXIMAGE_INFO * pSrcInfo, PALETTEENTRY * pPalette, LPDIRECT3DTEXTURE9 * ppTexture );
pDevice: D3D장치
pSrcData, SrcDataSize: 메모리의 시작 주소와, 그 크기

Width, Height: 만들 텍스쳐의 가로와 세로 길이, 0이나 D3DX_DEFAULT이면 원본 크기를 그대로 사용함.
- 원본크기를 그대로 사용한다지만, 2의 거듭제곱 꼴이 아닐 경우에는, 제일 가까운 2의 거듭제곱꼴로 올려버린다. 그러므로, 2의 거듭제곱 꼴이 아닌 것을 사용하면 그림이 원치 않는 비율로 늘어날 수 있다.

Pool: 텍스쳐가 위치할 메모리의 종류
 D3DPOOL_DEFAULT : 기본
 D3DPOOL_MANAGED : D3D가 처음부터 끝까지 다 관리해줍니다.
 D3DPOOL_SYSTEMMEM : 시스템메모리에 저장된다.
등등

- 흔히 D3DPOOL_DEFAULT를 사용하면 될것 같지만, 그러면 굉장히 성가시게 된다. D3DPOOL_DEFAULT로 만들어진 텍스쳐는 장치를 잃어버렸을 경우, 텍스쳐를 해제했다가, 장치가 복구되면 다시 만들어줘야 한다.
- D3DPOOL_MANAGED는 정말로 D3D가 처음부터 끝까지 다 관리해준다. 해제했다가 다시 만들어주는 일 따위는 신경 쓰지 않아도 된다. (하지만 RenderTarget등으로는 쓸 수 없다는 단점이 있다.) 우리는 앞으로 이 방식을 사용할 것이다.
- D3DPOOL_SYSTEMMEM은 지금 몰라도 아무 문제 없으므로 패스.


ColorKey: 투명색으로 사용할 색깔.
- bmp나 jpg같은 이미지 포맷은 알파채널이 없다. 만약 이런 이미지들을 사용할때, 투명색을 지정해 주려면 사용하면 된다. 그러나 우리는 png포맷을 사용할것이므로 신경 쓸 필요가 없다.

ppTexture: 우리가 얻고자하는 LPDIRECT3DTEXTURE9의 주소.

다시 본론으로 돌아가서,
텍스쳐를 성공적으로 만들었다면 m_vptxt.push_back(ptxt) 로 m_vptxt에 꽁지에 넣어놓는다.


텍스쳐를 해제해주는 함수이다. GSprites 소멸자에서 반드시 이 함수를 호출해줘야 한다. 그렇지 않으면 해제되지 않은 텍스쳐 때문에 메모리가 줄줄 새게 될 것이다.
m_vptxt.back()이라는 것은 m_vptxt에 마지막 원소를 내놓으라는 얘기이다. 그것을 SAFE_RELEASE한 뒤에 m_vptxt.pop_back()하여서 배열의 원소에서 빼버리는 것이다.

이제 DataInMem에 대해서 설명하겠다.

파일에서 데이터를 읽어오는 함수를 열심히 짜놓았다고 해보자. 근데 나중에 그 파일이 리소스로 포함되어버렸다. 그래서 이제 메모리 상에서 데이터를 읽어와야 한다. 그러면 전에 짜놓았던 함수 말고 또 다른 함수를 다시 짜야한다. 상당히 귀찮은 일이다. 데이터가 메모리 상에 있던지 파일에 있던지 상관하지 않고 일률적으로 입출력하기 위해서 DataInMem클래스를 만들었다.


DataInMem함수는 일반 파일 입출력과 비슷하게 읽고(read), 쓰고(write), 포인터이동(seek)도 할 수 있다.
하지만, DataInMem함수는 메모리상의 데이터에 대해서만 작동한다. 파일 상의 데이터를 위해서 약간 손을 봐야한다.


DataInMem클래스를 상속받아서 DataInFile 클래스를 만들었다. 이 클래스는 파일을 메모리에 매핑하여 DataInMem클래스로 주소를 넘겨주는 역할을 한다.

-파일을 메모리에 매핑한다?
파일을 메모리 주소에 연결시켜서 그 주소에 읽기/쓰기를 하면, 파일에 읽기/쓰기를 할 수 있는 기법을 말한다.
CreateFileMapping함수를 사용하여서 먼저 FileMapping핸들을 얻고,
MapViewOfFile함수를 사용하여서 메모리 주소를 얻는다.
그리고 다 쓴 후에는 CloseHandle을 이용해서 닫아준다.

그리고 다시 DataInFile을 상속받아서 DataInFileDirect 클래스를 만들었다. DataInFileDirect 클래스는 파일 주소를 가지고 파일을 열어서(CreateFile함수) DataInFile에게 그 핸들을 넘겨준다.


다시 GSprites로 돌아와서 화면에 출력하는 함수를 살펴보자.
GGame에서 ID3DXSprite 인터페이스를 얻어와서 Draw해주고 있다.
SetTransform함수는 변환행렬을 설정해주는 부분인데, 단위행렬을 그냥 설정했다. 이 것에 대해서는 다음 강좌에서 자세히 설명하겠다.

HRESULT
Draw( LPDIRECT3DTEXTURE9 pTexture, CONST RECT * pSrcRect, CONST D3DXVECTOR3 * pCenter, CONST D3DXVECTOR3 * pPosition, D3DCOLOR Color );
pTexture: 화면에 그릴 텍스쳐.
pSrcRect: 텍스쳐의 어느 부분을 그릴것인가? NULL일 경우 전체 다 그린다.
pCenter: 텍스쳐의 중심을 어디로 잡을까? NULL일 경우 (0, 0)을 중심으로 잡는다.
pPosition: 어느 지점에 그릴것인가? NULL일 경우 (0, 0)에다가 그린다.
Color : 텍스쳐에 곱할 색깔.


OnCreate에서 텍스쳐를 읽고, 스프라이트를 3개 추가하고,
OnDraw에서 그리고 있다.


이 댓글을 비밀 댓글로
    • ssd
    • 2009.11.20 22:09
    프갤에서 왔스빈다. 적분횽 강좌계속 부탁드링미
    • jyh
    • 2011.09.06 07:17
    ID3DXSprite::Flush() 이거 안하고, blend가 안먹혀서 반나절 삽질하고, 여기서 답을 찾았네요.
    • 메모리 반환 문제 발견
    • 2013.02.01 03:26
    01.GRET GSprites::ReleaseTexture()
    02.{
    03. for(size_t i=0;i<m_vptxt.size();i++) <--- 메모리 누수가 발생하는 코드 발견
    04. {
    05. SAFE_RELEASE(m_vptxt.back());
    06. m_vptxt.pop_back();
    07. }
    08. return GRET_OK;
    09.}



    For 문에서 조건절에 m_vptxt.size() 함수 사용시

    size 범위 까지
    메모리 반환을 하는
    함수 인데

    반복문에서
    i 는 증가하지만
    m_vptxt.size() 는 감소하므로

    1/2 까지
    메모리 반환후

    반복문이 종료돼므로

    나머지 1/2 그림이 메모리 반환이 돼지 않게 됀다.





    해결 필요


    for(size_t i=0;i<m_vptxt.size();i++)
    {
    메모리 반환();
    }

    ->

    int size = m_vptxt.size();
    for(size_t i=0;i<size;i++)
    {
    메모리 반환();
    }
    • 완성됀 리듬게임좀
    • 2013.02.01 03:28
    완성됀 리듬게임 소스를 보고 싶네요 !! 부탁드려요~!!
    • 처음에 구축한 프레임웍에 제한이 생각보다 많아서 프로젝트를 버리고 새로 작업하고 있습니다. 죄송합니다.