Deeper Learning

[WinAPI] 게임 기초 시스템 완성 - 학습 후기 본문

Game Development/WinAPI

[WinAPI] 게임 기초 시스템 완성 - 학습 후기

Dlaiml 2024. 6. 16. 20:08

언리얼 엔진을 사용하여 게임 개발을 시작하고 나서 예전 게임의 소스코드를 찾아보다가 WinAPI, DirectX 등을 사용하여 작성된 코드를 많이 보았고 언리얼 엔진을 통해 배운 게임 프레임워크 설계를 WinAPI로 해보고 싶어서 간단한 WinAPI 개발을 시작하였다.

 

아래 항목의 개발을 마지막으로 간단한 기능 개발을 모두 마쳤다. 이에 대해 하나씩 소개하는 포스팅을 작성하려 한다.

  • 공격속도 추가 및 적용
  • 플레이어 능력치를 향상 시켜주는 박스 오브젝트 추가
  • 적 스폰 로직 조정, 레벨 디자인
  • Player - Enemy 오브젝트 간 충돌처리 
  • 플레이어 체력바 UI 추가
  • 게임 종료 후 Scene 생성

 

 

 

공격속도 추가 및 적용

void CAnimator::SetDuration(const wstring& _animName, float _f)
{
	CAnimation* pAnim = FindAnimation(_animName);
	if (nullptr == pAnim)
		return;

	for (size_t i = 0; i < pAnim->m_vecFrm.size(); i++)
	{
		pAnim->m_vecFrm[i].fDuration = _f;
	}
}

 

기존 코드에서는 애니메이션을 생성 이후 수정을 할 수 없었다. 공격속도가 바뀌면 애니메이션 재생 시간도 이에 따라 달라져야 하기 때문에 각 프레임 당 실행 시간은 Duration을 변경하는 코드를 Animator 클래스에 추가하였다.

 

if ((GetAnimator()->GetCurAnimation()->GetName()).find(L"ATTACK") != wstring::npos
	&& GetAnimator()->GetCurAnimation()->GetCurFrame() == 8 && m_bCanFire)
{
	Fire();
	m_bCanFire = false;
}

 

Player 클래스에서 공격을 처리하는 Fire 함수에서는 공격 애니메이션이 특정 프레임에 도달하였을 때 투사체를 발사한다. 

언리얼 엔진 애니메이션에서 공격처리를 위해 Collider 생성 시점을 특정 프레임에 지정하는 방식을 참고하였다.

 

박스 오브젝트 추가

void CBox::OnCollisionBegin(CCollider* _pOther)
{
	CObject* pOtherObj = _pOther->GetOwner();

	if (pOtherObj->GetName() == L"Player")
	{
		GetAnimator()->Play(L"BOX_OPEN_ANIM", false);
		CPlayer* pPlayer = (CPlayer*)_pOther->GetOwner();
		pPlayer->SetAttackPower(
			pPlayer->GetAttackPower() + m_fDamageUp
		);
		pPlayer->SetNumBullets(
			pPlayer->GetNumBullets() + m_iBulletCountUp
		);
		pPlayer->SetHP(
			 pPlayer->GetHP() + m_fHPUp < 100 ? pPlayer->GetHP() + m_fHPUp : 100.f
		);
		pPlayer->SetAttackSpeed(
			pPlayer->GetAttackSpeed() + m_fAttackSpeedUp
		);

		pPlayer->GetAnimator()->SetDuration(L"BOW_ATTACK_LEFT_ANIM", 1 / pPlayer->GetAttackSpeed() / 9.f);
		pPlayer->GetAnimator()->SetDuration(L"BOW_ATTACK_RIGHT_ANIM", 1 / pPlayer->GetAttackSpeed() / 9.f);
	}
}

 

Player 오브젝트와 충돌하면 상자가 열리는 애니메이션이 재생되고 Player의 능력치를 향상시켜주는 코드이다. Collider에서 Owner를 바로 가져올 수 있고 충돌 함수에는 모두 상대 Collider가 매개변수로 제공되고 때문에 충돌이 일어난 오브젝트의 로직을 쉽게 처리할 수 있다.

 

적 스폰 로직 조정

m_fTimeAcc += fDT;
if (m_fTimeAcc >= m_fEnemySpawnInterval)
{
	m_fTimeAcc -= m_fEnemySpawnInterval;

	int iRandomX = rand();
	int iRandomY = rand();
	Vec2 SpawnPoint = {
		 iRandomX % int(m_vScreenSize.x), iRandomY % int(m_vScreenSize.y)
	};
	Vec2 vToSpawnPoint = SpawnPoint - CCamera::GetInstance()->GetCurLookPos();
	float vDistToPlayer = (SpawnPoint - CCamera::GetInstance()->GetCurLookPos()).GetLength();
	Vec2 vResolution = CCore::GetInstance()->GetResolution();
	if (vDistToPlayer < (vResolution / 2.f).GetLength())
	{
		vToSpawnPoint.Normalize();
		vToSpawnPoint *= (vResolution / 2.f).GetLength();
		vToSpawnPoint += CCamera::GetInstance()->GetCurLookPos();
	}


	CEnemy* pEnemy = CEnemySpawner::SpawnEnemy(ENEMY_TYPE::NORMAL,SpawnPoint);
	AddObject(pEnemy, GROUP_TYPE::ENEMY);

	// Level Difficulty Control
	m_fEnemySpawnInterval *= 0.98;

}

 

전체 씬 크기 내부 랜덤한 위치에 적을 스폰할 수 있도록 Scene에서 코드를 처리하였다. 플레이어와 너무 가까운 위치가 Spawn 위치가 될 경우 플레이어에서 스폰 포인트까지의 벡터를 활용하여 방향벡터를 추출하고 스크린 밖으로 나갈 수 있을 정도로 벡터 크기를 조정하였다.

 

게임의 난이도가 시간이 지남에 따라 지수적으로 증가하기를 원하여 적 스폰 주기는 점점 빨라지도록 하였다.

적 스폰 주기마다 업데이트 또한 이루어지기 때문에 특정 시점을 넘어가면 시간이 지나면 걷잡을 수 없도록 난이도가 높아진다. 

 

 

Player - Enemy 오브젝트 간 충돌처리

// CPlayer.cpp

else if ((L"Normal Enemy") == _pOtherColl->GetOwner()->GetName())
{
    CEnemy* pEnemy = (CEnemy*)_pOtherColl->GetOwner();
    const tEnemyInfo& tEInfo = pEnemy->GetInfo();
    m_iHP -= (int)tEInfo.fAttackPower;
    if (m_iHP <= 0)
    {
        //DeleteObject(this);
        ChangeScene(SCENE_TYPE::GAMEOVER);
    }
}

 

적과 충돌하였을 경우 적 정보가 기록된 구조체에서 공격력만큼 Player의 체력을 뺀다. 만약 체력이 0 이하로 떨어질 경우 게임오버 씬으로 바로 변경하도록 코드를 작성하였다.

 

플레이어 체력바 UI 추가

void CScene_Start::DrawHealthBar(HDC _hdc)
{
	// BackGround
	Vec2 vResolution = CCore::GetInstance()->GetResolution();
	RECT rcBackground = { 50, vResolution.y - 80, 50 + 200, vResolution.y - 80 + 20 };
	HBRUSH hBrushBackground = CreateSolidBrush(RGB(200, 200, 200));
	FillRect(_hdc, &rcBackground, hBrushBackground);
	DeleteObject(hBrushBackground);

	// Health Bar
	CPlayer* pPlayer = (CPlayer*)GetPlayer();
	int barWidth = static_cast<int>((static_cast<double>(pPlayer->GetHP()) / 100) * 200);
	RECT rcHealth = { 50, vResolution.y - 80, 50 + barWidth, vResolution.y - 80 + 20 };
	HBRUSH hBrushHealth = CreateSolidBrush(RGB(255, 0, 0));
	FillRect(_hdc, &rcHealth, hBrushHealth);
	DeleteObject(hBrushHealth);

	// Border
	HGDIOBJ hOldPen = SelectObject(_hdc, GetStockObject(BLACK_PEN));
	SelectObject(_hdc, GetStockObject(NULL_BRUSH));
	Rectangle(_hdc, 50, vResolution.y - 80, 50 + 200, vResolution.y - 80 + 20);
	SelectObject(_hdc, hOldPen);
	
}

 

UI 클래스로 따로 만들 수 있지만 간단한 기본함수로 그릴 수 있어서 씬의 멤버함수로 추가하였다. 

배경, 바, 테두리를 Rectangle을 사용하여 메인 Device context인 _hdc에 체력바를 그리는 함수 DrawHealthBar는 씬의 렌더링 마지막에 추가되어 다른 Object 보다 위(Z Order)에 올라올 수 있도록 하였다.

 

게임 종료 후 Scene 생성

void CScene_GameOver::Render(HDC _hdc)
{
	Vec2 vResolution = CCore::GetInstance()->GetResolution();
	BitBlt(
		_hdc,
		0, 0, vResolution.x, vResolution.y,
		m_pTex->GetDC(), 0, 0, SRCCOPY
	);
}

CScene_GameOver::CScene_GameOver()
	:m_pTex(nullptr)
{
	m_pTex = CResourceMgr::GetInstance()->LoadTexture(L"GameOver", L"texture\\gameover.bmp");
}

 

생성자에서 텍스쳐를 로드하고 Render 시점에 BitBlt로 메인 device context에 로드한 게임오버 화면 텍스처를 복사하는 가장 간단한 방식을 채택하였다.

 

WinAPI 주요 기능 개발 완료, 학습 목표 달성

 

위 항목의 개발을 마지막으로 간단한 기능 개발을 모두 마쳤다. 더 완성된 게임으로 만들기 위해서는 아직 많은 것들이 추가로 필요하지만, WinAPI 학습의 본 목적이 언리얼 엔진을 사용하지 않는 밑단 설계에 대해 알아보기로 당장 추가 작업 계획은 없다. 

직접 WinAPI로 게임을 만들어보니 상용엔진의 클래스 설계와, 구현 들이 얼마나 효율적이고 정교한지 깨닫게 되었다.

파이썬으로 서비스를 개발해 오면서 항상 느꼈던 것은 전체 프로젝트 코드 아키텍처를 어떻게 설계하느냐에 따라 이후 기능을 추가하거나 제거할 때 드는 시간의 차이가 매우 크게 난다는 것이다. 

언리얼 엔진처럼 거대한 엔진을 만들 때 충분히 이러한 부분이 고려되었기 때문에 이를 참고하며 WinAPI에서 개발을 할 때에도 장점을 실감할 수 있었다.

 

물론 언리얼엔진을 사용할 때 보다 저수준의 접근이 필요하고, 모든 것을 직접 관리해야 한다는 점이 어려웠지만 매우 유익한 경험이었다.

 

 

 

 

Comments