Deeper Learning

[WinAPI] Collider 구현 (2) - 충돌 판정, 충돌 상태 관리 본문

Game Development/WinAPI

[WinAPI] Collider 구현 (2) - 충돌 판정, 충돌 상태 관리

Dlaiml 2024. 6. 10. 08:58

이번 포스팅에서는 저번 포스팅에 이어서 Collider 클래스를 관리하는 매니저 클래스인 Collider Manger 클래스에 대해 소개하려 한다.

 

저번 포스팅에서 Collider 클래스를 만들어 Object의 멤버로 지정하여 Component로 이를 관리하기로 하였고 충돌 상태(Begin, Ongoing, End) 콜백함수는 Object의 충돌 로직 함수를 호출하여 Object에서 이를 처리하도록 디자인하였다.

 

이제 충돌 이벤트 발생을 판단하고 충돌이 이루어져야 하는 Object인지 기록하는 역할을 하는 등 게임에서 충돌과 관련된 기능을 관리하는 매니저 클래스인 Collider Manger 클래스에 대해 알아보자.

 

충돌 상태를 관리하고 Object 간 충돌을 판정하기 위해서는 아래 기능이 필요하다

  • 이전 프레임에서의 충돌 상태를 기록하여 충돌 상태 업데이트 (Begin -> Ongoing -> End)
  • 충돌처리가 필요한 Object 인지 판단 (Player와 Player가 발사한 투사체는 충돌 판정 X)  
  • 충돌이 일어나고 있는지 판단 및 기록

 

코드에서 각 기능의 구현을 하나씩 확인해 보자

union COLLIDER_ID
{
	struct {
		UINT iLID;
		UINT iRID;
	};
	ULONGLONG ID;
};

class CCollisionMgr
{

	// ...

private:
	map<ULONGLONG, bool> m_mapCollInfo; // Previous Frame Collision Info
	UINT m_arrCheck[(UINT)GROUP_TYPE::END]; // Collide Check Matrix between Obj Groups

public:
	void Update();
	void CheckGroup(GROUP_TYPE _eLeft, GROUP_TYPE _eRight);
	void Reset();

private:
	void UpdateCollisionGroup(GROUP_TYPE _eLeft, GROUP_TYPE _eRight);
	bool IsCollide(CCollider* _pLeftColl , CCollider* _pRightColl);
};

 

먼저 멤버변수와 전역 선언된 공용체부터 알아보자

 

Collider ID

레벨에 배치된 Object 끼리의 충돌을 관리하기 위해 Collider의 ID를 공용체로 선언하였다.

충돌하는 두 Object의 Collider의 ID를 Unsigned Int (4bytes), 두 Object의 충돌 자체의 ID을 Unsigned long long (8 bytes)로 하여 공용체를 만들었다.

 

m_mapCollInfo

Collider ID(unsigned long long) 마다 이전 프레임의 충돌 정보를 bool값으로 기록하기 위한 map이다.

충돌이 일어나고 있으면 true, 일어나지 않고 있으면 false를 저장한다.

 

m_arrCheck

Object 타입 간 충돌 처리를 할 것인지 기록하는 배열이다. Unsigned int에서 비트들이 충돌 처리 필요 여부를 나타낸다.

 

멤버 함수들은 정의 코드와 함께 하나씩 살펴보자.

 

CheckGroup

void CCollisionMgr::CheckGroup(GROUP_TYPE _eLeft, GROUP_TYPE _eRight)
{
	UINT iRow = (UINT)_eLeft;
	UINT iCol = (UINT)_eRight;

	if (iCol < iRow)
	{
		iRow = (UINT)_eRight;
		iCol = (UINT)_eLeft;
	}

	if (m_arrCheck[iRow] & (1 << iCol))
	{
		m_arrCheck[iRow] &= ~(1 << iCol);
	}
	else
	{
		m_arrCheck[iRow] |= (1 << iCol);
	}
}

 

두 Object 타입 간 충돌 처리 여부를 기록하는 m_arrCheck에 정보를 기록하는 함수이다.

레벨을 초기화할 때 아래와 같이 호출된다. 아래 예시 레벨에서는 Player와 Enemy의 충돌, Player 투사체와 Enemy 충돌, Player와 Ground의 충돌을 제외하고는 충돌 처리를 하지 않는다.

// Check Collision between Object Type, Scene.h
CCollisionMgr::GetInstance()->CheckGroup(GROUP_TYPE::PLAYER, GROUP_TYPE::ENEMY);
CCollisionMgr::GetInstance()->CheckGroup(GROUP_TYPE::PROJ_PLAYER, GROUP_TYPE::ENEMY);
CCollisionMgr::GetInstance()->CheckGroup(GROUP_TYPE::PLAYER, GROUP_TYPE::GROUND);

 

 

UINT iRow = (UINT)_eLeft;
UINT iCol = (UINT)_eRight;

if (iCol < iRow)
{
    iRow = (UINT)_eRight;
    iCol = (UINT)_eLeft;
}

 

CheckGroup의 코드를 다시 보면 매개변수로 들어온 두 Enum을 Unsigned int로 변환하고 이를 각각 행, 열 인덱스로 지정한다.

 

Object 타입 Enum인 GROUP_TYPE은 32개로 한정되어 있으며 32개의 타입끼리의 충돌을 처리하려면 총 32x32 사이즈의 행렬이 필요하다.  A와 B의 충돌을 처리한다는 것은 B와 A의 충돌을 처리한다는 것과 동일하기 때문에, 열이 행보다 큰 경우만 처리하여도 정보를 모두 기록할 수 있다.

따라서 행이 더 클 경우 행과 열을 swap 한다.

 

부르기 편하도록 Object의 타입을 타입 0 ~ 타입 31이라고 부르겠다. 타입 1의 다른 타입과의 충돌 처리여부는 0번째 행을 확인하여 알 수 있다. 

 

타입1의 다른 타입과의 충돌 처리여부를 4 bytes(=32 Bits)인 Unsigned int의 각 비트에  기록하는 방식을 사용하였다.

 

 

 

위 그림처럼 행은 index가 위에서 아래로 열은 index가 우측에서 좌측으로 숫자가 증가하는 방식이다.

if (m_arrCheck[iRow] & (1 << iCol))
{
    m_arrCheck[iRow] &= ~(1 << iCol);
}
else
{
    m_arrCheck[iRow] |= (1 << iCol);
}

 

 

각 행은 m_arrCheck에 들어있는 하나의 Unsigned Int를 나타내고 열은 Unsigned Int의 비트가 나열되어 있는 형식이다. 

타입 0과 타입 1의 충돌 처리를 활성화 시킨다고 하면 iRow는 0, iCol은 1의 값이 들어오게 된다.

 

// m_arrCheck[0]:		0000 0000 0000 0000
// 1 : 				0000 0000 0000 0001
// 1 << 1: 			0000 0000 0000 0010
// ~(1 << 1):			1111 1111 1111 1101

 

비트 이동 연산자를 사용하여 iCol의 값인 1만큼 비트를 이동시키면 타입 인덱스 1에 해당하는 비트만 1이 되는데

이를 비트 AND 연산자 &을 사용하여 m_arrCheck과 비교한다.

 

즉 타입0과 타입1의 충돌 처리여부를 나타내는 비트를 제외하고 모두 0이 되기 때문에 m_arrCheck에서 두 타입의 충돌 처리 비트가 0이면 [0000 0000 0000 0000 = false], 1이면 [0000 0000 0000 0010 = true]가 된다.

 

충돌 처리가 이미 활성화되어있다면 비트 NOT 연산을 하여 ~(1<<1)과 m_arrCheck[0]의 비트 AND 연산 값을 m_arrCheck[0]으로 할당한다. 타입 0, 타입 1의 충돌 처리 담당 비트를 제외하고는 그대로 유지하며, 해당 부분의 값은 0과의 AND 연산이므로 항상 0 값이 나와 충돌처리가 비활성화된다.

 

충돌 처리가 비활성화되어있다면 비트 OR 연산을 하여 마찬가지로 두 타입 충돌처리 담당 비트를 제외하고 유지시키며, 충돌 처리 담당 비트는 1과의 OR 연산 값은 항상 1이므로 충돌처리가 활성화된다.

 

IsCollide

두 Collider의 위치와 크기값을 기준으로 충돌이 일어나는지 확인하는 함수이다.

WinAPI로 간단한 2D 플랫포머나 슈팅게임을 만들 예정이라 정사각형의 Collider를 기준으로 작성된 코드이다. 두 Collider의 중점의 X축 간 거리가 두 Collider의 가로변의 합을 2로 나눈 값 보다 작거나 같고 Y축에 대해서도 동일하면 겹쳐있다고 판단이 가능하기 때문에 true값을 반환한다. (두 원에서 중점, 반지름을 가지고 겹쳐있는지 판단하는 예시를 생각하면 수비다)

bool CCollisionMgr::IsCollide(CCollider* _pLeftColl, CCollider* _pRightColl)
{
	Vec2 vLeftPos = _pLeftColl->GetCollPos();
	Vec2 vLeftScale = _pLeftColl->GetScale();
	Vec2 vRightPos = _pRightColl->GetCollPos();
	Vec2 vRightScale = _pRightColl->GetScale();

	if (abs(vRightPos.x - vLeftPos.x) <= (vLeftScale.x + vRightScale.x) / 2.f &&
		abs(vRightPos.y - vLeftPos.y) <= (vLeftScale.y + vRightScale.y) / 2.f)
	{
		return true;
	}

	return false;
}

 

 

UpdateCollisionGroup

 

Update 시점(프레임마다 게임 정보를 업데이트)에 호출되는 함수로 두 Object 타입에 속하는 모든 객체에서 충돌을 확인하고 Collider의 충돌 이벤트를 발생시키는 함수이다.  

함수 내부 동작은 간단하지만 코드가 길어 코드를 잘라 각 부분을 순서대로 살펴보려 한다.

void CCollisionMgr::UpdateCollisionGroup(GROUP_TYPE _eLeft, GROUP_TYPE _eRight)
{
	CScene* pCurScene = CSceneMgr::GetInstance()->GetCurScene();
	const vector<CObject*>& vecLeft = pCurScene->GetGroupObject(_eLeft);
	const vector<CObject*>& vecRight = pCurScene->GetGroupObject(_eRight);

	map<ULONGLONG, bool>::iterator iter;

	for (size_t i = 0; i < vecLeft.size(); ++i)
	{
		if (nullptr == vecLeft[i]->GetCollider()) continue;
		for (size_t j = 0; j < vecRight.size(); ++j)
		{
			if (nullptr == vecRight[j]->GetCollider() ||
				vecLeft[i] == vecRight[j]) continue;


			CCollider* pLeftColl = vecLeft[i]->GetCollider();
			CCollider* pRightColl = vecRight[j]->GetCollider();


// ...

 

우선 현재 레벨(씬)을 가져오고 매개변수로 주어진 두 타입의 Object 객체가 모두 저장된 vector를 각각 불러온다.

_eLeft를 투사체 타입, _eRight를 적 타입이라고 하면 레벨에 배치된 모든 투사체, 적 객체의 포인터를 담은 vector를 불러온 것이다.

 

이중 for문으로 투사체 객체, 적 객체를 모두 순회하는데 만약 두 Object 객체 중 하나라도 Collider를 가지고 있지 않다면 충돌처리를 할 필요가 없기 때문에 continue로 충돌 처리를 건너뛴다. 두 객체가 같은 객체여도 마찬가지로 충돌 처리를 건너뛴다. 

 

// for (size_t i = 0; i < vecLeft.size(); ++i)
// for (size_t j = 0; j < vecRight.size(); ++j)
    
// Collision ID
COLLIDER_ID ID = {};
ID.iLID = pLeftColl->GetID();
ID.iRID = pRightColl->GetID();

iter = m_mapCollInfo.find(ID.ID);

// Collide status check
if (m_mapCollInfo.end() == iter)
{
    m_mapCollInfo.insert(make_pair(ID.ID, false));
    iter = m_mapCollInfo.find(ID.ID);
}

 

앞서 살펴본 Collider ID 공용체에 두 Collider의 ID를 입력하고 고유한 Collider ID 마다 이전 프레임에서의 충돌 정보가 저장된 m_mapCollInfo에서 ID값이 있나 확인한다.

 

만약 없다면 ID와 충돌이 일어나지 않고 있다는 false값을 추가하고 추가한 pair를 가리키도록 iter를 세팅해 둔다

 

// for (size_t i = 0; i < vecLeft.size(); ++i)
// for (size_t j = 0; j < vecRight.size(); ++j)

if (IsCollide(pLeftColl, pRightColl))
{
	// Collide Ongoing
	if (iter->second)
	{
		if (vecLeft[i]->IsDead() || vecRight[j]->IsDead())
		{
			pLeftColl->OnCollisionEnd(pRightColl);
			pRightColl->OnCollisionEnd(pLeftColl);
			iter->second = false;
		}
		else
		{
			pLeftColl->OnCollision(pRightColl);
			pRightColl->OnCollision(pLeftColl);
		}

	}
	// Collide Begin Tick
	else
	{
		// Ignore Collide if one of Objects supposed to delete
		if (!vecLeft[i]->IsDead() && !vecRight[j]->IsDead())
		{
			pLeftColl->OnCollisionBegin(pRightColl);
			pRightColl->OnCollisionBegin(pLeftColl);
			iter->second = true;
		}
	}
}
else // No Collision
{
	if (iter->second) // Collision End
	{
		pLeftColl->OnCollisionEnd(pRightColl);
		pRightColl->OnCollisionEnd(pLeftColl);
		iter->second = false;
	}
}

.

이제 두 Collider의 충돌 여부를 IsCollide 함수로 확인하고 두 Collider가 겹치거나 접하고 있어 충돌이 일어나고 있다면 iter->second로 이전 프레임에서의 충돌 여부를 확인한다. 이전 프레임에서도 충돌이 일어났다면 충돌이 계속 일어나고 있는 상황(Ongoing)이다.

 

만약 이번 프레임에 삭제 예정인 객체가 있다면 충돌 상태를 false로 바꾸고 충돌이 끝났다는 OnCollisionEnd를 두 Collider에서 모두 호출한다. 그렇지 않다면 충돌이 계속 진행 중이라는 OnCollision을 호출한다.

 

이전 프레임에서 충돌이 일어나지 않았으면 이번이 첫 충돌이 발생한 프레임으로 두 객체가 삭제 예정이 아니라면 충돌 여부를 true로 설정하고 충돌이 시작하였다는 OnCollisionBegin 함수를 호출한다.

 

IsCollide의 값이 false로 충돌이 일어나지 않고 있다면 두 Object의 이전 프레임 충돌 정보를 확인하고 만약 충돌이 이전 프레임에 일어났으면 충돌이 끝났다는 OnCollisonEnd를 호출하고 충돌 여부를 false로 마킹한다.

 

 

 

 

 

코드는 [1]의 유튜브 영상을 예시로 참고하였다.

Reference

[0] https://learn.microsoft.com/ko-kr/windows/win32/api/

[1] https://www.youtube.com/playlist?list=PL4SIC1d_ab-ZLg4TvAO5R4nqlJTyJXsPK

Comments