본문 바로가기

카테고리 없음

Win32 API - SINGLETON(싱글톤)

1. singleton의 개념

해당 클래스가 생성할 수 있는 개체(인스턴스)를 한 개로 제한하는 디자인 패턴

 

구현 방법

- 힙 메모리 영역(동적할당)을 이용한 구현

- 데이터 영역을 이용한 구현

- 템플릿 프로그래밍을 이용한 구현

 

2. 폴더 구조 정리

해당 프로젝트에 engine폴더 생성 및 engine 폴더 하위 폴더로  header폴더와 core 폴더 생성

- Core 클래스

생성하고자 하는 하나의 객체의 자료형이 되는 클래스

 

2. 데이터 영역을 이용한 구현

-> 정적 맴버 함수 안에 정적 맴버 변수의 객체를 생성하고 그 주소 값을 반환한다. ( 정적 맴버 함수 밑 정적 맴버 변수는 아래에 설명)

-> 이 때, core1 은 데이터 영역에 존재 하므로 프로그램이 끝날 때까지 사라지지 않는다. 즉 GetInstance를 재호출 한다고 할지라고  원래 존재하는 정적 변수인 core1의 주소값만을 반환하는 것이기에 하나의 클래스에 하나의 객체 만을 갖고 있게 된다.

-> 하지만 이는 사용자가 원할 때( 예를 들어 더이상 사용하지 않아 객체가 쓸모가 없어졌을 때) 객체를 삭제 할 수 없다는 문제가 있어 프로그램이 끝날 때까지 데이터 영역의 일부분을 차지하고 있는다는 단점이 있다. 이를 보완하기 위해서 데이터 영역이 아닌 힙영역을 사용하는 방법이 있다.

 

2. 동적할당( 힙 데이터 역영 사용 )으로 singleton 구현

 

하나인 개체는 어디에서든 접근할 수 있어야 한다.

extern 개념으로 객체를 만들경우 개체를 함개만 만들 수 있다는 강제성이 없음

-> 생성자와 소멸자를 private로 정의

MyCore.h

-> 개체를 만들 수도 없게 된다. 따라서 단 하나의 객체를 만들어 줄 수 있는 함수를 따로 만든다. ( 해당 클래스의 맴버 함수는 private 접근 지정자에 속한 맴버에 접은 할 수 있다는 것을 이용한다. )

-> 맴버 함수에 어떻게 접근 하는지 생각해보자.

-> 우선 객체가 있어야 그 객체에 속한 맴버 함수에 접근하고 그 객체의 메모리 주소가 맴버함수에 this 로 전해지지 않았던가?

-> 따라서 객체가 없어도 호출이 가능한 맴버 함수를 선언해야 한다.

-> 정적 맴버 함수

정적 맴버 함수는 객체 없이 호출이 가능하다. -> this 가 없다.

-> 위의 코드를 통해서 GetInstance() 함수를 호출해야만 객체를 생성할 수 있게 되었지만 재호출하면 주소값을 계속 할당한다는 문제점이 있다.

-> 이를 해결하기 위해 객체의 주솟값이 되는 myCorePtr 을 정적변수로 선언한다.

GetInstance가 재호출 되어도 myCorePtr이 정적 변수이기 때문에 재할당이 발생하지 않고 원래의 주솟값을 반환한다.

-> 이 동적 할당 메모리를 결국 해제 해야 하는데 MyCore 클래스의 소멸자가 private 이라 외부에서 호출이 불가능하다.

(소멸자를 public 으로 설정하고 외부에서 객체의 주소값이 담긴 포인터를 호출하여 삭제 한다고 가정 하자. 삭제 후에 GetInstance()를 호출하면 myCorePtr은 삭제된 주소값을 가지고 있고 그 주솟값을 반환하게 된다. -> 객체를 삭제한뒤 새로운 객체를 사용하고 싶었는데 여전히 GetInstance() 함수는 삭제된 주솟값을 반환하니 문제!!)

 

-> 메모리 해제용 정적 맴버 함수를 따로 생성한다. (Release() 함수)

-> 이 함수에서 GetInstance() 호출로 객체의 주솟값을 가져와 지역변수에 대입하고 그것을 delete한다.

-> 하지만 이 방법도 앞서 문제 생긴 외부에서 포인터 호출해서 삭제와 다를 바가 없다. ( 만약 프로그램 중간, Release()가 호출되었을 때 다시 MyCore형의 객체가 필요해서  GetInstance()를 호출하면 해제된 메모리 시작 주소가 반환된다.)

 

-> 이를 함수 밖에서 객체의 포인터를 선언하는 것으로 해결해보자.

MyCore.h
MyCore.cpp

-> 위의 코드에서 ㄱ과 같이 맴버 변수를 static 으로 선언한 것을 정적 맴버 변수라고 한다.

- 정적 맴버 변수는 클래스에는 속하지만, 객체 별로 할당되지 않고 클래스의 모든 객체가 공유하는 맴버를 말한다.  맴버 변수가 정적(static)으로 선언되면, 해당 클래스의 모든 객체에 대해 하나의 데이터 만이 유지 관리된다.

- 정적 맴버 변수에도 클래스 맴버의 접근 제한 규칙이 적용되므로, 클래스의 맴버 함수나 프렌드만이 접근이 가능하다.

- 정적 맴버 변수를 외부에서도 접근할 수 있게 하려면 정적 맴버 변수를 public 영역안에 선언하면 된다.

- 이렇게 선언된 정적 맴버 변수는 객체를 생성하지 않고도, 클래스의 이름만으로 호출할 수 있다.

- 클래스 외부에서 정적 맴버 변수를 초기화 해주어야 한다. 그렇지 않으면 컴파일 에러가 발생한다.

 

3. 미리 컴파일된 헤더

지금부터는 싱글톤을 이용하여 사각형 움직임을 조작하는 기능을 구현한다.

그를 위해 main.cpp 뿐만 아니라 Mycore파일에서도 윈도우 핸들에 접근해야할 필요가 있다.

이때 첫번째 방법은 main.cpp 에서 extern변수로 윈도우 핸들을 선언하여 사용하는 것이다. 하지만 이는 윈도우 핸들을 어디에서나 접근할 수 있게 하기 때문에 객체 지향과 맞지 않는다.

두번째는 MyCore 파일에서 <Windows.h>를 참조하는 것이다.

이를 위해서 미리 컴파일된 헤더를 만들고자 한다.

 

미리 컴파일된 헤더(PreCompiled Header File)란 자주 변경되지 않는 용량이 큰 소스파일을 미리 컴파일하여 바이너리 파일로 저장하여 사용하는 것이다. 원래라는 처음부터 컴파일을 해주어야 하지만 미리 컴파일된 헤더를 사용하면 컴파일 속도가 크게 향상된다.

 

3-1 프로젝트를 우클릭한 뒤 속성 클릭

3-2 구성을 모든 구성으로 바꾸고 c/c++ 의 미리 컴파일된 헤더 항목에서 ' 미리 컴파일된 헤더 사용 안 함' 을 '사용' 혹은 '만들기'로 전환.

만들기 부분을 클릭하면 우측에 화살표가 뜨니 그것을누르면 선택사항이 나타난다.

3-3 헤더 파일에 stdafx.h 추가 ( 이는 위의 속성페이지의 미리 컴파일된 헤더 파일 의 이름과 같아야 한다.)

이와 동시에 프로그램의 모든 cpp 파일은  #include "stdafx.h" 를 해줘야 한다.

 

3-4 MyCore.h 에서 HWND 인식

 필자는 윈도우 핸들을 MyCore.h 에서 사용하기 위해 미리 컴파일된 헤더를 사용하게 되었다. 3-1~3-3 까지의 과정을 거치니 그전에 MyCore.h 에서 HWND 를 사용하면 에러가 나던 것이 사라졌다. MyCore.h는 어떻게 HWND를 인식한 것일까.

MyCore.cpp

미리 컴파일러 선언 뒤에 MyCore.h가 선언되었다.

stdafx.h

미리 컴파일러 된 헤더에는 framework.h가 선언되어 있다.

framework.h

framework.h 는 windows.h를 참조하고 있다.

이렇듯 거슬러 올라가서 MyCore.h 는 windows.h 를 참조 받고 있기에 HWND를 인식할 수 있었던 것이다.

이때  MyCore.cpp 에서  #include "stdafx.h"가  #include "MyCore.h" 뒤에 선언 되면 MyCore.h 가 stdafx.h 를 인식하지 못해 MyCore.h 의 HWND 부분에서 에러가 발생한다.

 

4. 동적 할당 방식의 싱글톤으로 사각형 움직이기 구현

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_WIN32APIPRACTICE, szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

   
    MyCore::GetInstance()->init(); // ... ㄱ

    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WIN32APIPRACTICE));

    MSG msg;

    // 기본 메시지 루프입니다:
    while (true)
    {
        if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
        {
            if (WM_QUIT == msg.message)
            {
                break;
            }

            if (!TranslateAcceleratorW(msg.hwnd, hAccelTable, &msg))
            {
                TranslateMessage(&msg);
                DispatchMessageW(&msg);
            }
        }
        
        else
        {
            // ... ㄴ
            MyCore::GetInstance()->tick();
        }
        
    }
    MyCore::Release(); // ... ㄷ

    return (int) msg.wParam;
}

위의 #include "MyCore.h" 한 main.cpp 의 wWinMain 함수이다.

ㄱ이 싱글톤의 초기화. ㄴ 이 실제 게임이 실행되는 코드부분(매 프레임마다 발생). ㄷ이 프로그램 종료전 싱글톤을 삭제하는 부분이다. -> 여기에서 프레임이란 wWndMain함수의 while 문이 한번 순환하는 주기를 말한다.

tick()과 init()을 추가한 MyCore.h 는 다음과 같다.

#pragma once

class MyCore
{
private:
    static MyCore* m_myCorePtr;

public:
    static MyCore* GetInstance()
    {
        if (m_myCorePtr == nullptr)
        {
            m_myCorePtr = new MyCore;
        }
        return m_myCorePtr;
    }

    static void Release()
    {
        if (nullptr != m_myCorePtr)
        {
            delete m_myCorePtr;
            m_myCorePtr = nullptr;
        }
    }

    void init();
    void tick();

private:
    MyCore();
    ~MyCore();
};

4-1 MyCore.h 에 윈도우 핸들 맴버 변수로 선언 -> 싱글톤에 윈도우 핸들이 맴버 변수로 선언됨에 따라 어디에서든 윈도우 핸들 사용가능.

<MyCore.h>
#pragma once

class MyCore
{
private:
    static MyCore* m_myCorePtr;
    HWND            m_hMainWmd;  // ...ㄱ

public:
    static MyCore* GetInstance()
    {
        if (m_myCorePtr == nullptr)
        {
            m_myCorePtr = new MyCore;
        }
        return m_myCorePtr;
    }

    static void Release()
    {
        if (nullptr != m_myCorePtr)
        {
            delete m_myCorePtr;
            m_myCorePtr = nullptr;
        }
    }

    HWND GetMainHWnd() { return m_hMainWmd; } // ...ㄷ

    void init(HWND m_hMainWmd); // ...ㄴ

    void tick();

private:
    MyCore();
    ~MyCore();
};

ㄱ. 윈도우 핸들을 맴버 변수로 선언.

ㄴ. 싱글톤 초기화 함수에 윈도우 핸들을 인자로 전달.

ㄷ. 단순히 윈도우 핸들을 반환하는 함수.

<MyCore.cpp>
#include "stdafx.h"
#include "MyCore.h"

MyCore* MyCore::m_myCorePtr = nullptr;

MyCore::MyCore()
: m_hMainWmd(0) // ...ㄱ
{}

MyCore::~MyCore(){}

void MyCore::tick(){}

void MyCore::init(HWND _hWnd) // ...ㄴ
{
	m_hMainWmd = _hWnd; // ...ㄷ
}

ㄱ. 윈도우 핸들 함수 초기화

ㄴ, ㄷ. mian.cpp 에서 Initinstance 의 CreateWindowW의 반환값을 main cpp 의 전역변수에 대입하고 그 전역변수를  MyCore::GetInstance->init()의 인자로 넣어주는 것이다.

 

4-2 tick 구현

게임에서는 프레임이라는 개념이 존재한다.

한 번의 프레임 마다 게임을 구성하는 여러 상태의 업데이트와 그 업데이트된 내용을 렌더링 하는 작업이 존재 한다.

현재 이 프로젝트 에서는 wWinMain의 while 문이 한번 순환하는 것을 1프레임으로 정하고 그에 따라 tick()이 한번 호출되는 것이 1프레임이라고 여기는 것이 된다.

MainWindow 핸들을 담을 전역 변수 선언
ㄱ. InitInstance의 CreateWindowW의 반환값을 전역벽수 g_hWnd 에 대입 및 그에 따른 ShowWindow함수와 UpdateWindow함수의 인자도 바꿔준다.
wWinMain함수의 기본 메세지 루프 전에 MyCore 초기화 함수에  g_hWnd 인자로 전달
MyCore.cpp 의 초기화 함수에 MyCore 클래스의 맴버 함수인 HWND m_hMainWnd에 매개변수로 받은 g_hWnd 대입.

 

tick()의 렌더링 부분에  Rectangle() 함수를 이용하여 사각형을 화면에 출력하려고 할때 DC 가 필요하다.

전에는 WM_PAINT 메세지를 사용하여. BeginPaint 함수로 DC를 생성하고  EndPaint 함수로 DC 를 해제 했다면, 지금은 GetDC() 함수로 DC 를 생성하고  ReleaseDC() 함수로 DC 를 해제할 것이다.

MyCore.h

<MyCore.h>
#include "stdafx.h"
#include "MyCore.h"

MyCore* MyCore::m_myCorePtr = nullptr;

MyCore::MyCore()
	: m_hMainWnd(0)
	, m_hDC(0) // ...ㄱ
{}

MyCore::~MyCore()
{
	ReleaseDC(m_hMainWnd, m_hDC); // ...ㄷ
}


void MyCore::init(HWND _hWnd)
{
	m_hMainWnd = _hWnd;

	m_hDC = GetDC(m_hMainWnd); // ...ㄴ
}


void MyCore::tick()
{
	// 사각형의 좌표 위치 및 크기
	static float x = 100, y = 100;
	static float sizex = 100, sizey = 100;
    
	// 업데이트
	if (GetAsyncKeyState(VK_LEFT) & 0x8000)
	{
		x -= 0.01f;
	}

	if (GetAsyncKeyState(VK_RIGHT) & 0x8000)
	{
		x += 0.01f;
	}

	if (GetAsyncKeyState(VK_UP) & 0x8000)
	{
		y -= 0.01f;
	}

	if (GetAsyncKeyState(VK_DOWN) & 0x8000)
	{
		y += 0.01f;
	}
    
	// 렌더링
	Rectangle(m_hDC
		, (int)(x - sizex / 2.f)
		, (int)(y - sizey / 2.f)
		, (int)(x + sizex / 2.f)
		, (int)(y + sizey / 2.f));
}

ㄱ. DC저장용 맴버 변수 초기화

ㄴ. DC 생성

ㄷ. DC 해제

 

- 사각형의 위치 및 크기

객체가 될 사각형의 좌표와 크기를 하드코딩으로 입력해 준다

 

- 렌더링

DC와 하드코딩한 사각형의 데이터를 기준으로  사각형을 출력한다.

 

- 업데이트

GetAsynckeyState(int vkey) 함수는 입력을 멀티로 받을 수 있다.

인자값으로는 키보드의 키(ex: VK_LEFT) 같은 키값이 온다.

반환값

키의 상태에 따라 위와 같이 반환값이 비트 형태로 리턴된다.

이 반환값을 활용하기 위해 아래와 같이 비트 연산자를 활용한다.

if (GetAsyncKeyState(VK_LEFT) & 0x8000) -> 지금 키가 눌림
if (GetAsyncKeyState(VK_LEFT) & 0x8001) -> 이전과 지금사이에 키가 눌림
if (GetAsyncKeyState(VK_LEFT)) -> 둘다 가능

* GetAsyncKeyState() 함수는 비동기 처리한다 -> 호출된 시점에서 키 상태를 조사하여, 메세지 큐를 거치지 않고 리턴한다. 반면에 GetKeyState() 함수는 호출된 시점에서 메시지 큐를 거친다.