지금까지 수업 들은 내용을 바탕으로 전체 로직의 순서대로 각 부분을 정리하고자 한다.
Windows API는 Microsoft Windows에서 사용하는 C언어 기반의 API이다.
Application Programing Interface는 정의 및 프로토콜 집합을 사용하여 두 소프트웨어 구성 요소가 서로 통신할 수 있게 하는 매커니즘이다.
메세지 큐에서 메시지를 가져오지만, 만약 큐가 비어 있더라도 작동이 끝나지 않는다.
여기서 Get Message를 사용하지 않고 Peek Message를 사용하는 이유가 나오는데 만약 Get Message를 사용한다면 사용자가 아무런 메세지를 입력 하지 않았을때 프로그램이 자동으로 종료된다.
그렇기 때문에 Peek Message를 사용함 으로서 아무 입력이 없더라도 프로그램을 종료되게 하면 안된다.
while (true)
{
// 메세지큐에 메세지가 있다.
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (WM_QUIT == msg.message)
break;
// 메세지 처리
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// 메세지큐에 메세지가 없다.
else
{
// 게임 실행, 1 프레임
CEngine::GetInst()->Progress();
}
}
return (int) msg.wParam;
아무런 메세지가 없을 경우에도 Progress를 실행해 프로그램의 종료를 방지한다.
게임은 매 프레임마다 별의 탄생과도 같다. 게임 캐릭터가 사과를 먹는 장면을 보여주기 위해서는 매 프레임 아래와 같은 과정을 반복해야 한다.
별 탄생 -> 석시 시대 -> 중세 시대 -> 사과를 든다 -> 별 파괴
별 탄생 -> 석시 시대 -> 중세 시대 -> 사과를 입에 가져간다. -> 별 파괴
별 탄생 -> 석시 시대 -> 중세 시대 -> 입을 벌린다. -> 별 파괴
그렇기 때문에 매 프레임 메세지가 없더라도 레벨, 디버그, 충돌 검사는 항상 시켜주어야 한다.
여기서 CTimeMgr, CKeyMgr, CDbgRender 등 몇 가지 객체는 싱글톤 디자인 패턴으로 설계하였는데 그 이유는 아래에서 서술 하겠다.
void CEngine::Progress()
{
// Manager Tick
CTimeMgr::GetInst()->Tick(); // DT 계산
CKeyMgr::GetInst()->Tick(); // 각 키의 상태
CDbgRender::GetInst()->Tick();
// 레벨 실행
CLevelMgr::GetInst()->Progress();
// 충돌 검사 실행
CCollisionMgr::GetInst()->Tick();
// 렌더링
// 화면 클리어
{
SELECT_BRUSH(BRUSH_TYPE::GRAY);
Rectangle(m_hSecondDC, -1, -1, (int)m_Resolution.x + 1, (int)m_Resolution.y + 1);
}
// 레벨 렌더링
CLevelMgr::GetInst()->Render();
// 디버그 정보 렌더링
CDbgRender::GetInst()->Render();
// SecondBitmap 있는 장면을 MainWindowBitmap 으로 복사해온다.
BitBlt(m_hDC, 0, 0, (int)m_Resolution.x, (int)m_Resolution.y, m_hSecondDC, 0, 0, SRCCOPY);
// TaskMgr 동작
CTaskMgr::GetInst()->Tick();
}
싱글톤 디자인 패턴(Singleton Design Pattern)은 객체 지향 프로그래밍에서 특정 클래스의 인스턴스가 오직 하나만 존재하도록 보장하는 패턴이다. 이 패턴은 주로 전역 상태를 관리하거나 자원을 공유해야 할 때 유용하다.
Type();: 생성자를 private으로 선언하여 클래스 외부에서 인스턴스를 생성할 수 없도록 합니다.
Type(const Type& _Origin) = delete;: 복사 생성자를 삭제하여 객체의 복사를 방지합니다.
void operator=(const Type& _Origin) = delete;: 대입 연산자를 삭제하여 객체의 대입을 방지합니다.
~Type();: 소멸자를 private으로 선언합니다. 이 경우, 인스턴스가 프로그램 종료 시 자동으로 소멸되지만, 소멸자를 public으로 하지 않고 private으로 유지함으로써, 인스턴스의 소멸이 외부에서 호출되는 것을 방지합니다.
#define SINGLE(Type) public:\
static Type* GetInst()\
{\
static Type engine;\
return &engine;\
}\
private:\
Type();\
Type(const Type& _Origin) = delete;\
void operator =(const Type& _Origin) = delete;\
~Type();
SecondBitmap을 복사하는 이유는 한번에 제대로된 화면을 출력하기 위해서이다.
Win_api는 기본적로 좌측위부터 사용자에게 연산 결과를 보여준다. 하지만 이 연산이 너무나 빠른 이유로 한번에 모든 화면이 나온다고 착각하는 것이다. 만약 1연산단 1px씩 나오게 구현한다면 아무리 빠른 연산 속도를 가진다 해도 화면에 결과가 출력되는 것이 보여질 것이다.
그렇기 때문에 SecondBitmap을 만들어 첫번째 화면이 보여지는 동안 두번째 화면에 미리 구현을 다 해 놓은뒤 바꿔치기를 해 사용자에게 한번에 깔끔한 결과를 보여주는 것이다.
CLevelMgr Class 에서 처음 초기화 Init()을 살펴보면 Monster, Player 객체를 생성해 추가하는걸 확인 할 수 있다.
객체를 만들때는 여러 함수와 인자를 이용해 객체의 위치, 크기, Layer_TYPE등을 정해준다.
만약 처음 초기화 이후에 추가로 객체를 넣고 싶으면 현재 레벨을 가져오고 아래와 똑같이 AddObject를 해주면 된다.
void CLevelMgr::Init()
{
Vec2 vResolution = CEngine::GetInst()->GetResolution();
// 레벨 제작
CLevel* pLevel = new CLevel;
// Player 생성
CObj* pObject = new CPlayer;
pObject->SetPos(vResolution.x / 2.f, vResolution.y / 2.f);
pObject->SetScale(50.f, 50.f);
pLevel->AddObject(pObject, LAYER_TYPE::PLAYER);
// Monster 생성
CMonster* pMonster = new CMonster;
pMonster->SetName(L"Monster");
pMonster->SetPos(600.f, 120.f);
pMonster->SetScale(100.f, 100.f);
pMonster->SetDistance(200.f);
pMonster->SetSpeed(300.f);
pLevel->AddObject(pMonster, LAYER_TYPE::MONSTER);
pMonster = new CMonster;
pMonster->SetName(L"Monster");
pMonster->SetPos(700.f, 120.f);
pMonster->SetScale(100.f, 100.f);
pMonster->SetDistance(200.f);
pMonster->SetSpeed(300.f);
pLevel->AddObject(pMonster, LAYER_TYPE::MONSTER);
// 충돌 설정
CCollisionMgr::GetInst()->CollisionCheckClear();
CCollisionMgr::GetInst()->CollisionCheck(LAYER_TYPE::PLAYER_OBJECT, LAYER_TYPE::MONSTER);
CCollisionMgr::GetInst()->CollisionCheck(LAYER_TYPE::PLAYER, LAYER_TYPE::MONSTER);
// 생성한 레벨을 START 레벨 이자 현재 재생 중인 레벨로 설정하고
// Begin 을 호출한다.
m_CurLevel = m_arrLevel[(UINT)LEVEL_TYPE::START] = pLevel;
m_CurLevel->Begin();
}
AddObject를 따라가보자.
AddObject의 매개변수는 _Object와 Layer_Type 형 _Type 변수를 받고 있는데
여기서 LAYER_TYPE이란 각 _Object가 어디에 속하는지를 의미한다.
ex) 배경, 적, 아군, 동물 등 Layer를 다르게 배치하여 충돌 등의 상황에서 매번 오브젝트를 선언할때마다 일일히 충돌을 설정하는게 아니라 아군인지 ,적인지 분별하여 이벤트를 발생할 수 있게 한다.
후에 렌더링 할때도 Layer Type대로 진행하는 것을 알 수 있는데. 여기서 주의해야 할 점은 먼저 렌더링 되는 Object는 뒤에 배치된다는 것이다.
만약 내가 크레이지 아케이드 형 게임을 만든다고 했을때 만약 추가한 물풍선의 Layer TYPE이 Player 뒤에 있다면 Player가 현재 레벨에 그려지고 난 뒤에 물풍선이 그려지기 때문에 Player가 물풍선에 가려지게 된다.
이런 특징을 잘 고려해 LayerType을 지정해야 한다.
LayerType Enum의 마지막은 항상 end이고 end의 앞은 ui를 고정시켜 놓는 것이 보통이다.
CObj는 전체 Object클래스를 상속해주는 부모클래스이다.
레벨에 나타나는 Object가 지녀야 할 기본적인 정보들을 가지고 있다. Pos, Scale 등
위에서 충돌했을때 혹은 특정 이벤트가 발생했을때 Layer_TYPE에 따라, 그리고 Layer_TYPE의 각 객체에 따라 진행하면 된다고 설명하였는데 각 설명은 CObj를 상속받는 각 객체(Player, Monster)에서 진행해주면 된다.
COBJ를 상속받은 PlayerCpp에서 충돌을 구현하였다.
#pragma once
#include "CBase.h"
class CComponent;
class CCollider;
class CObj :
public CBase
{
private:
Vec2 m_Pos;
Vec2 m_Scale;
vector<CComponent*> m_Component;
LAYER_TYPE m_LayerType;
bool m_Dead; // 삭제 예정
public:
virtual void Begin(); // 레벨 시작할 때
virtual void Tick() = 0; // 오브젝트가 할 일
virtual void FinalTick() final; // 오브젝트가 소유한 Component 가 할 일
virtual void Render(); // 오브젝트를 그리기
virtual void BeginOverlap(CCollider* _Collider, CObj* _OtherObject, CCollider* _OtherCollider) {}
virtual void Overlap(CCollider* _Collider, CObj* _OtherObject, CCollider* _OtherCollider) {}
virtual void EndOverlap(CCollider* _Collider, CObj* _OtherObject, CCollider* _OtherCollider) {}
public:
void SetPos(Vec2 _Pos) { m_Pos = _Pos; }
void SetPos(float _x, float _y) { m_Pos = Vec2(_x, _y); }
void SetScale(Vec2 _Scale) { m_Scale = _Scale; }
void SetScale(float _x, float _y) { m_Scale = Vec2(_x, _y); }
Vec2 GetPos() { return m_Pos; }
Vec2 GetScale() { return m_Scale; }
LAYER_TYPE GetLayerType() { return m_LayerType; }
bool IsDead() { return m_Dead; }
CComponent* AddComponent(CComponent* _Component);
CComponent* GetComponent(const wstring& _Name);
CComponent* GetComponent(COMPONENT_TYPE _Type);
template<typename T>
T* GetComponent();
public:
CObj();
~CObj();
friend class CLevel;
friend class CTaskMgr;
};
template<typename T>
T* CObj::GetComponent()
{
for (size_t i = 0; i < m_Component.size(); ++i)
{
if (dynamic_cast<T*>(m_Component[i]))
return (T*)m_Component[i];
}
return nullptr;
}
충돌을 구현하기 위해서는 충돌하기 위한 선이 필요하다. 사람은 시각적으로 두 물체가 현재 겹쳐있는지, 아직 겹치기 전인지를 파악 할 수 있지만 컴퓨터는 이를 알지 못한다.
겹쳐있는지 아닌지 확인하는 방법은 아래 그림으로 설명 할 수 있다. 만약 겹쳐 있담면 두 사각형의 반지름을 더한 값과 S의 값이 같거나 작을때 두 사각형은 겹쳐있는 것으로 확인 할 수 있다.
Player의 Scale은 50, Playr Object의 Collider의 scale은 80인걸 확인 할 수 있다.
Component는 게임 오브젝트에 추가되는 구성요소이다. Collider는 Component의 한 구성요소라고 볼 수 있다.