[DirectX] 2D에서의 충돌
in Study on WinAPI&DirectX
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를 반환한다. 즉 충돌 중이라는 뜻이다.