[DirectX] 2D에서의 충돌


2D 상에서 충돌 감지기능 구현하기

CollisionMgr 클래스


// 헤더파일

#pragma once
#include "CSingleton.h"

class CLayer;
class CCollider2D;


union CollisionID
{
	struct
	{
		UINT LeftID;
		UINT RightID;
	};

	UINT_PTR id;
};


class CCollisionMgr :
    public CSingleton<CCollisionMgr>
{
	SINGLE(CCollisionMgr);
private:
	UINT					m_matrix[MAX_LAYER];
	map<UINT_PTR, bool>		m_mapColID;

public:
	void LayerCheck(UINT _left, UINT _right);
	void LayerCheck(const wstring& _strLeftLayer, const wstring& _strRightLayer);

	void Clear()
	{
		memset(m_matrix, 0, sizeof(UINT) * MAX_LAYER);
	}

public:
	void tick();

private:
	void CollisionBtwLayer(CLayer* _LeftLayer, CLayer* _RightLayer);
	void CollisionBtwObject(CGameObject* _LeftObject, CGameObject* _RightObject);
	bool CollisionBtwCollider(CCollider2D* _pLeft, CCollider2D* _pRight);
};
// cpp 파일

#include "pch.h"
#include "CCollisionMgr.h"

#include "CLevelMgr.h"
#include "CLevel.h"
#include "CLayer.h"
#include "CGameObject.h"
#include "CCollider2D.h"

CCollisionMgr::CCollisionMgr()
    : m_matrix{}
{

}


CCollisionMgr::~CCollisionMgr()
{

}



void CCollisionMgr::tick()
{
	CLevel* pLevel = CLevelMgr::GetInst()->GetCurLevel();

	for (UINT iRow = 0; iRow < MAX_LAYER; ++iRow)
	{
		for (UINT iCol = iRow; iCol < MAX_LAYER; ++iCol)
		{
			if (!(m_matrix[iRow] & (1 << iCol)))
				continue;

			// iRow 레이어와 iCol 레이어는 서로 충돌검사를 진행한다.
			CollisionBtwLayer(pLevel->GetLayer(iRow), pLevel->GetLayer(iCol));
		}
	}
}

void CCollisionMgr::CollisionBtwLayer(CLayer* _Left, CLayer* _Right)
{
	const vector<CGameObject*>& vecLeft = _Left->GetObjects();
	const vector<CGameObject*>& vecRight = _Right->GetObjects();

	if (_Left == _Right)
	{
		for (size_t i = 0; i < vecLeft.size(); ++i)
		{
			for (size_t j = i + 1; j < vecRight.size(); ++j)
			{
				CollisionBtwObject(vecLeft[i], vecRight[j]);				
			}
		}
	}
	else
	{
		for (size_t i = 0; i < vecLeft.size(); ++i)
		{
			for (size_t j = 0; j < vecRight.size(); ++j)
			{
				CollisionBtwObject(vecLeft[i], vecRight[j]);				
			}
		}
	}
}

void CCollisionMgr::CollisionBtwObject(CGameObject* _LeftObject, CGameObject* _RightObject)
{
	if (!(_LeftObject->Collider2D() && _RightObject->Collider2D()))
		return;

	// 충돌체 ID 생성
	CollisionID id = {};
	id.LeftID = _LeftObject->Collider2D()->GetID();
	id.RightID = _RightObject->Collider2D()->GetID();
	
	// ID 검색
	map<UINT_PTR, bool>::iterator iter = m_mapColID.find(id.id);
	if (iter == m_mapColID.end())
	{
		m_mapColID.insert(make_pair(id.id, false));
		iter = m_mapColID.find(id.id);
	}

	// 두 충돌체가 지금 충돌 중인지 확인
	if (CollisionBtwCollider(_LeftObject->Collider2D(), _RightObject->Collider2D()))
	{
		if (iter->second)
		{
			// 계속 충돌 중
			_LeftObject->Collider2D()->OnOverlap(_RightObject->Collider2D());
			_RightObject->Collider2D()->OnOverlap(_LeftObject->Collider2D());
		}
		else
		{
			// 이번 프레임에 충돌
			_LeftObject->Collider2D()->BeginOverlap(_RightObject->Collider2D());
			_RightObject->Collider2D()->BeginOverlap(_LeftObject->Collider2D());
			iter->second = true;
		}
	}

	else
	{
		// 충돌 해제
		if (iter->second)
		{
			_LeftObject->Collider2D()->EndOverlap(_RightObject->Collider2D());
			_RightObject->Collider2D()->EndOverlap(_LeftObject->Collider2D());
			iter->second = false;
		}
	}


}

// 두 충돌체의 충돌 검사 진행
bool CCollisionMgr::CollisionBtwCollider(CCollider2D* _pLeft, CCollider2D* _pRight)
{
    // 분리축 테스트

    return false;
}



void CCollisionMgr::LayerCheck(UINT _left, UINT _right)
{
	UINT iRow = (UINT)_left;
	UINT iCol = (UINT)_right;

	if (iRow > iCol)
	{
		UINT iTemp = iCol;
		iCol = iRow;
		iRow = iTemp;
	}

	m_matrix[iRow] |= (1 << iCol);
}

void CCollisionMgr::LayerCheck(const wstring& _strLeftLayer, const wstring& _strRightLayer)
{
	CLevel* pCurLevel = CLevelMgr::GetInst()->GetCurLevel();

	CLayer* pLeft = pCurLevel->FindLayerByName(_strLeftLayer);
	CLayer* pRight = pCurLevel->FindLayerByName(_strRightLayer);

	LayerCheck(pLeft->GetLayerIndex(), pRight->GetLayerIndex());
}

WinAPI랑 했던 것과 거의 비슷하다.

  • CollisionBtwLayer함수에서 같은 레이어끼리 충돌하는 경우는 방지해준다.
  • 충돌하는 경우는 아래와 같은 경우들이 있다.

  • 충돌이 시작되는 경우
  • 충돌 중인 경우
  • 충돌이 끝난 경우
  • 충돌 중이지 않은 경우

그래서 충돌하는 경우를 따라서 충돌 아이디를 형성하여 준다.

CollisionBtwObejct함수에서 두 오브젝트가 모두 충돌체를 가지고 있을 경우에만 충돌체 아이디를 생성한다.

CollisionID id = {};
id.LeftID = _LeftObject->Collider2D()->GetID();
id.RightID = _RightObject->Collider2D()->GetID();

그리고 map에서 아이디를 검색한다.

map<UINT_PTR, bool>::iterator iter = m_mapColID.find(id.id);

만약에 iter가 존재하지 않았다면

if (iter == m_mapColID.end())
{
    m_mapColID.insert(make_pair(id.id, false));
    iter = m_mapColID.find(id.id);
}

그리고 iter->second가 true였다면 직전 프레임에 충돌하고 있었다는 것이고, false였다면 충돌하고 있지 않았다는 것이다.

반면에, CollisionBtwCollider는 지금 충돌 중인지 검사하는 것이다.

그래서 CollisionBtwCollider는 참이면서 iter->second 값이 참인 경우는 지금 충돌 중이라는 것이다.

그래서 분기를 아래와 같이 나눌 수 있다.

  • CollisionBtwCollider : 참 / iter->second : 참 -> 지금 충돌 중 (OnOverlap)
  • CollisionBtwCollider : 참 / iter->second : 거짓 -> 충돌 시작 (BeginOverlap)
  • CollisionBtwCollider : 거짓 / iter->second : 참 -> 충돌 끝 (EndOverlap)
  • CollisionBtwCollider : 거짓 / iter->second : 거짓 -> 충돌하지 않음


LayerCheck 함수


void CCollisionMgr::LayerCheck(UINT _left, UINT _right)
{
	UINT iRow = (UINT)_left;
	UINT iCol = (UINT)_right;

	if (iRow > iCol)
	{
		UINT iTemp = iCol;
		iCol = iRow;
		iRow = iTemp;
	}

	m_matrix[iRow] |= (1 << iCol);
}

void CCollisionMgr::LayerCheck(const wstring& _strLeftLayer, const wstring& _strRightLayer)
{
	CLevel* pCurLevel = CLevelMgr::GetInst()->GetCurLevel();

	CLayer* pLeft = pCurLevel->FindLayerByName(_strLeftLayer);
	CLayer* pRight = pCurLevel->FindLayerByName(_strRightLayer);

	LayerCheck(pLeft->GetLayerIndex(), pRight->GetLayerIndex());
}

어떤 레이어끼리 충돌시킬지 결정하는 함수이다.

Collider2D 클래스


Collider2D 클래스에서 충돌체의 World 행렬을 계산해준다.

// 헤더파일

#pragma once
#include "CComponent.h"

class CCollider2D :
    public CComponent
{
private:
    Vec3            m_vOffsetPos;
    Vec3            m_vOffsetScale;
    bool            m_bAbsolute;
    COLLIDER2D_TYPE m_Shape;
    Matrix          m_matCollider2D;    // Collider 의 월드행렬

    int             m_iCollisionCount;  // 충돌 횟수


public:
    virtual void finaltick() override;

public:
    void SetOffsetPos(Vec2 _vOffsetPos){ m_vOffsetPos = Vec3(_vOffsetPos.x, _vOffsetPos.y, 0.f); }
    void SetOffsetScale(Vec2 _vOffsetScale) { m_vOffsetScale = Vec3(_vOffsetScale.x, _vOffsetScale.y, 1.f); }
    void SetAbsolute(bool _bSet) { m_bAbsolute = _bSet; }
    void SetCollider2DType(COLLIDER2D_TYPE _Type) { m_Shape = _Type; }

public:
    void BeginOverlap(CCollider2D* _Other);
    void OnOverlap(CCollider2D* _Other);
    void EndOverlap(CCollider2D* _Other);



    CLONE(CCollider2D);
public:
    CCollider2D();
    ~CCollider2D();
};


// cpp 파일

#include "pch.h"
#include "CCollider2D.h"

#include "components.h"


CCollider2D::CCollider2D()
	: CComponent(COMPONENT_TYPE::COLLIDER2D)
	, m_Shape(COLLIDER2D_TYPE::RECT)
	, m_bAbsolute(false)
{
}

CCollider2D::~CCollider2D()
{
}


void CCollider2D::finaltick()
{
	// 충돌 회수가 음수인 경우
	assert(0 <= m_iCollisionCount);

	m_matCollider2D = XMMatrixScaling(m_vOffsetScale.x, m_vOffsetScale.y, m_vOffsetScale.z);
	m_matCollider2D *= XMMatrixTranslation(m_vOffsetPos.x, m_vOffsetPos.y, m_vOffsetPos.z);

	const Matrix& matWorld = Transform()->GetWorldMat();

	if (m_bAbsolute)
	{
		Matrix matParentScaleInv = XMMatrixInverse(nullptr, Transform()->GetWorldScaleMat());
		m_matCollider2D = m_matCollider2D * matParentScaleInv * matWorld;
	}
	else
	{
		// 충돌체 월드 * 오브젝트 월드
		m_matCollider2D *= matWorld;
	}
	
	// DebugShape 요청
	Vec4 vColor = Vec4(0.f, 1.f, 0.f, 1.f);
	if (0 < m_iCollisionCount)
		vColor = Vec4(1.f, 0.f, 0.f, 1.f);

	if (COLLIDER2D_TYPE::CIRCLE == m_Shape)
		DrawDebugCircle(m_matCollider2D, vColor, 0.f);	
	else	
		DrawDebugRect(m_matCollider2D, vColor, 0.f);	
}



void CCollider2D::BeginOverlap(CCollider2D* _Other)
{

	m_iCollisionCount += 1;
}

void CCollider2D::OnOverlap(CCollider2D* _Other)
{

}

void CCollider2D::EndOverlap(CCollider2D* _Other)
{
	m_iCollisionCount -= 1;
}

CollisionMgr에서 검사한 결과에 따라서 OnOverlap, BeginOverlap, EndOverlap함수를 구동시킨다.

그리고 몇 개체와 충돌 중인지 카운트를 할 수 있도록 m_iCollisionCount 함수를 설정해준다.

assert(0 <= m_iCollisionCount);

위 코드를 통해서 충돌체의 개수가 음수인 경우를 체크해준다.

이런 경우는 말이 되지 않기 때문.

CLevelMgr 클래스


// cpp 파일 중에서

// Monster
	CGameObject* pMonster = new CGameObject;
	pMonster->SetName(L"Monster");

	pMonster->AddComponent(new CTransform);
	pMonster->AddComponent(new CMeshRender);
	pMonster->AddComponent(new CCollider2D);

	pMonster->Transform()->SetRelativePos(Vec3(0.f, 250.f, 100.f));
	pMonster->Transform()->SetRelativeScale(Vec3(200.f, 200.f, 1.f));
	
	pMonster->MeshRender()->SetMesh(CResMgr::GetInst()->FindRes<CMesh>(L"RectMesh"));
	pMonster->MeshRender()->SetMaterial(CResMgr::GetInst()->FindRes<CMaterial>(L"Std2DMtrl"));

	pMonster->Collider2D()->SetAbsolute(true);
	pMonster->Collider2D()->SetOffsetScale(Vec2(100.f, 100.f));

	m_pCurLevel->AddGameObject(pMonster, L"Monster", false);



	// 충돌 시킬 레이어 짝 지정
	CCollisionMgr::GetInst()->LayerCheck(L"Player", L"Monster");

그리고 init 함수 중에서 몬스터를 만들어서 실험할 수 있도록 init함수를 위와 같이 바꾸어준다.

회전시 충돌처리


지금까지는 Collider 자체가 회전하지 않았고, 이에 따라 충돌체의 가로축과 세로축이 각각 x축과 y축에 평행했기 때문에 편리하게 충돌처리를 할 수 있었다.

하지만 사각형 충돌체가 회전을 하게 된다면 그 때는 어떻게 충돌처리를 해야 할까?

분리축 테스트


// CollisionMgr.cpp 중에서

// 두 충돌체의 충돌 검사 진행
bool CCollisionMgr::CollisionBtwCollider(CCollider2D* _pLeft, CCollider2D* _pRight)
{
	// 0 -- 1
	// |    |
	// 3 -- 2
	Vec3 arrLocal[4] =
	{
		Vec3(-0.5f, 0.5f, 0.f),
		Vec3(0.5f, 0.5f, 0.f),
		Vec3(0.5f, -0.5f, 0.f),
		Vec3(-0.5f, -0.5f, 0.f),
	};

	// 두 충돌체의 각 표면 벡터 2개씩 구함
	Vec3 arrProj[4] = {};

	arrProj[0] = XMVector3TransformCoord(arrLocal[1], _pLeft->GetColliderWorldMat()) - XMVector3TransformCoord(arrLocal[0], _pLeft->GetColliderWorldMat());
	arrProj[1] = XMVector3TransformCoord(arrLocal[3], _pLeft->GetColliderWorldMat()) - XMVector3TransformCoord(arrLocal[0], _pLeft->GetColliderWorldMat());
	
	arrProj[2] = XMVector3TransformCoord(arrLocal[1], _pRight->GetColliderWorldMat()) - XMVector3TransformCoord(arrLocal[0], _pRight->GetColliderWorldMat());
	arrProj[3] = XMVector3TransformCoord(arrLocal[3], _pRight->GetColliderWorldMat()) - XMVector3TransformCoord(arrLocal[0], _pRight->GetColliderWorldMat());
	
	// 두 충돌체의 중심점을 구함
	Vec3 vCenter = XMVector3TransformCoord(Vec3(0.f, 0.f, 0.f), _pRight->GetColliderWorldMat()) - XMVector3TransformCoord(Vec3(0.f, 0.f, 0.f), _pLeft->GetColliderWorldMat());
	

	// 분리축 테스트
	for (int i = 0; i < 4; ++i)
	{
		Vec3 vProj = arrProj[i];
		vProj.Normalize();

		// 4개의 표면백터를 지정된 투영축으로 투영시킨 거리의 총합 / 2
		float fProjDist = 0.f;
		for (int j = 0; j < 4; ++j)
		{
			fProjDist += fabsf(arrProj[j].Dot(vProj));
		}
		fProjDist /= 2.f;

		float fCenter = fabsf(vCenter.Dot(vProj));

		if (fProjDist < fCenter)
			return false;
	}


    return true;
}

투영 대상이 되는 벡터를 구한 다음에 나머지 벡터들을 투영 대상 벡터에 투영시켜서 중심점간의 거리와 투영벡터의 길이를 비교하여 충돌처리를 한다.

여기서 중심점은 로컬 좌표계에서는 원점 즉 (0,0,0)에 있을 것이므로, XMVector3TransformCoord 함수에 인자를 (0,0,0)을 넣어서 중심점을 구해준다.

분리축 테스트를 총 4번 진행한다.

그렇다면 분리축 테스트는 어떻게 하느냐?

좌측 충돌체의 벡터 2개, 우측 충돌체의 벡터 2개, 중심점 간 벡터를 알고 있는 상태에서

  • 중심점 간 벡터를 제외한 나머지 벡터 중 하나를 투영 대상 벡터로 정한다.
  • 그리고 그 벡터를 정규화(Normalize)한다.
  • 그리고 Dot함수를 이용해서 내적을 한다.
  • 내적의 결과가 양수가 나오도록 절대값(fabsf)을 사용한다.
  • 4개의 표면벡터를 지정된 투영축으로 투영시킨 거리의 총합을 구한다.
  • 그리고 2를 나눈다. (여기서 나온 결과 값을 A라 하자.)
  • 중심점 간의 벡터를 투영 대상 벡터에 투영한 거리를 구한다. (이 값을 B라 하자)
  • 이 때 A와 B를 비교한다. 이 때 B가 A보다 크면 실패이다. 즉, 충돌하지 않는다는 것이다.

이 과정을 4번 반복한다.
이 4번의 과정을 모두 통과하면 함수는 true를 반환한다. 즉 충돌 중이라는 뜻이다.


© 2022.07. by Wookey_Kim

Powered by Hydejack v7.5.2