윈도우API)18.충돌-1
충돌
충돌은 게임에서 많은 역할을 수행한다. 예를 들자면 물리적인 충돌을 막을 수도 있고 충돌을 판단하여 이벤트를 수행할수도 있다. 이러한 충돌은 상속구조보다는 컴포넌트를 이용하는것이 게임제작에 편리하다. 본 포스팅에서는 이러한 충돌을 컴포넌트를 이용하여 구현해보고자 한다.
콜리전 생성
먼저 생성할 콜리전의 타입을 정의해준다. 일반적으로 2D게임에서는 박스와 스피어 두종류의 콜리전을 사용한다.
enum class CollisionType
{
Box,
Sphere,
};
타입을 정의하였으면 콜리전의 베이스들이 될 콜리전 클래스를 정의해준다.
class Collision: public Component
{
public:
Collision(CollisionType CType);
virtual ~Collision()override;
public:
virtual bool CheckCollision(Collision* other);
void SetShowDebug(bool show) { showDebug = show; }
CollisionType GetCollisionType() { return _CType; }
static bool CheckBox2Box(BoxCollision* box1, BoxCollision* box2);
static bool CheckSphere2Sphere(SphereCollision* sp1, SphereCollision* sp2);
static bool CheckSphere2Box(SphereCollision* sp, BoxCollision* box);
protected:
CollisionType _CType;
bool showDebug=true;
};
베이스 클래스를 정의했다면 사용할 박스콜리전과 스피어 콜리전을 정의해준다.
class SphereCollision :public Collision
{
public:
virtual bool CheckCollision(Collision* other);
float GetRadius() { return _radius; }
void SetRadius(float radius) { _radius = radius; }
private:
float _radius=0.f;
};
class BoxCollision :public Collision
{
public:
virtual bool CheckCollision(Collision* other);
Vec2 GetSize() { return _size;}
void SetSize(Vec2 size) { _size = size; }
private:
Vec2 _size = {};
};
사용할 콜리전을 정의했다면 디버깅시 확인 할수 있도록 랜더링 해준다.
void BoxCollision::Render(HDC hdc)
{
Super::Render(hdc);
if (showDebug == false)return;
Vec2 camera = GET_SINGLE(SceneManager)->GetCameraPos();
Vec2 pos = GetOwner()->Getpos();
pos.x -= camera.x - GWinSizeX / 2;
pos.y -= camera.y - GWinSizeY / 2;
HBRUSH myBrush = (HBRUSH)::GetStockObject(NULL_BRUSH);
HBRUSH oldBrush = (HBRUSH)::SelectObject(hdc, myBrush);
Utils::DrawRect(hdc, pos, _size.x, _size.y);
::SelectObject(hdc, oldBrush);
::DeleteObject(myBrush);
}
void SphereCollision::Render(HDC hdc)
{
Super::Render(hdc);
if (showDebug == false)return;
Vec2 camera = GET_SINGLE(SceneManager)->GetCameraPos();
Vec2 pos = GetOwner()->Getpos();
pos.x -= camera.x - GWinSizeX / 2;
pos.y -= camera.y - GWinSizeY / 2;
HBRUSH myBrush = (HBRUSH)::GetStockObject(NULL_BRUSH);
HBRUSH oldBrush = (HBRUSH)::SelectObject(hdc, myBrush);
Utils::DrawCircle(hdc, pos, _radius);
::SelectObject(hdc, oldBrush);
::DeleteObject(myBrush);
}
구현한 콜리전을 Scene에 추가한다. 스피어콜리전은 플레이어에 박스콜리전은 특정 좌표에 위치시킨다.
{//플레이어
Player* player = new Player();
SphereCollision* SCollision = new SphereCollision();
SCollision->SetRadius(100.f);
player->AddComponent(SCollision);
GET_SINGLE(CollisionMananger)->AddCollision(SCollision);
AddActor(player);
}
{//테스트 충돌물체
Actor* player = new Actor();
BoxCollision* BCollision = new BoxCollision();
BCollision->SetSize({100,100});
player->Setpos({400,200});
player->AddComponent(BCollision);
GET_SINGLE(CollisionMananger)->AddCollision(BCollision);
AddActor(player);
}
위 코드들을 실행시키면 다음과 같은 결과가 나온다.
충돌 판별
1.콜리전 매니저 생성
게임내에서는 수많은 콜리전들이 존재할것이다. 그렇다면 이것을 관리해야할 것이 필요하게 되는데, 이것을 수행하는 것이 콜리전 매니저이다. 콜리전 매니저는 게임안에 있는 콜리전들을 삽입/삭제하고 틱마다 충돌여부를 판별한다.
class CollisionMananger
{
public:
void AddCollision(Collision* collision);
void RemoveCollision(Collision* collision);
private:
vector<Collision*>_Collisions;
};
void CollisionMananger::Update()
{
vector<Collision*>& collisions = _Collisions;
for (int32 i = 0; i < collisions.size(); i++)
{
for (int32 j = i + 1; j < collisions.size(); j++)
{
Collision* dest1 = collisions[i];
Collision* dest2 = collisions[j];
if (dest1->CheckCollision(dest2))//충돌했을경우
{
//추가예정
}
else//충돌하지 않았을경우
{
}
}
}
}
void CollisionMananger::AddCollision(Collision* collision)
{
_Collisions.push_back(collision);
}
void CollisionMananger::RemoveCollision(Collision* collision)
{
auto it = find(_Collisions.begin(),_Collisions.end(),collision);
if (it == _Collisions.end())return;
_Collisions.erase(it);
}
2.충돌여부 판단
충돌여부 체크는 대상의 콜리전 타입에 따라 다르게 동작한다. 충돌여부는 (Box2Box),(Sphere2Sphere),(Sphere2Box)세종류가 있다.
bool BoxCollision::CheckCollision(Collision* other)
{
switch (other->GetCollisionType())
{
case CollisionType::Box:
return CheckBox2Box(this, static_cast<BoxCollision*>(other));
case CollisionType::Sphere:
return CheckSphere2Box(static_cast<SphereCollision*>(other),this);
}
return false;
}
bool SphereCollision::CheckCollision(Collision* other)
{
switch (other->GetCollisionType())
{
case CollisionType::Box:
return CheckSphere2Box(this, static_cast<BoxCollision*>(other));
case CollisionType::Sphere:
return CheckSphere2Sphere(this, static_cast<SphereCollision*>(other));
}
return false;
}
충돌여부는 콜리전베이스에서 수행한다.
Box2Box:각각의 사각형에서 각변의 중심좌표를 얻고 두 사각형의 x축y축이 겹치는지를 판별하는 함수이다.
bool Collision::CheckBox2Box(BoxCollision* box1, BoxCollision* box2)
{
Vec2 p1 = box1->GetOwner()->Getpos();
Vec2 s1 = box1->GetSize();
Vec2 p2 = box2->GetOwner()->Getpos();
Vec2 s2 = box2->GetSize();
float minX_1 = p1.x - s1.x / 2;
float maxX_1 = p1.x + s1.x / 2;
float minY_1 = p1.y - s1.y / 2;
float maxY_1 = p1.y + s1.y / 2;
float minX_2 = p2.x - s2.x / 2;
float maxX_2 = p2.x + s2.x / 2;
float minY_2 = p2.y - s2.y / 2;
float maxY_2 = p2.y + s2.y / 2;
if (maxX_2 < minX_1)
return false;
if (maxX_1 < minX_2)
return false;
if (maxY_1 < minY_2)
return false;
if (maxY_2 < minY_1)
return false;
return true;
}
Sphere2Sphere: 각 원의 중심좌표의 길이가 반지름의 길이의 합보다 작으면 충돌이 일어난 것으로 판단하는 함수이다.
bool Collision::CheckSphere2Sphere(SphereCollision* sp1, SphereCollision* sp2)
{
Vec2 p1 = sp1->GetOwner()->Getpos();
Vec2 p2 = sp2->GetOwner()->Getpos();
float r1 = sp1->GetRadius();
float r2 = sp2->GetRadius();
Vec2 dir = p1 - p2;
float dist = dir.Length();
return dist<=(r1+r2);
}
3.충돌여부 전달
충돌 여부가 판별되었으면 충돌 사실을 플레이어에게 알려야한다. 이러한 역할을 수행하는 함수가 OnComponentBeginOverlap/OnComponentEndOverlap이다.
class Actor
{
public:
virtual void OnComponentBeginOverlap(Collision* collider, Collision* other);
virtual void OnComponentEndOverlap(Collision* collider, Collision* other);
};
class Player :public FlipbookActor
{
public:
virtual void OnComponentBeginOverlap(Collision* collider, Collision* other)override;
virtual void OnComponentEndOverlap(Collision* collider, Collision* other)override;
}
콜리전 매니저를 통해 OnComponentBeginOverlap/OnComponentEndOverlap를 전달한다.
void CollisionMananger::Update()
{
vector<Collision*>& collisions = _Collisions;
for (int32 i = 0; i < collisions.size(); i++)
{
for (int32 j = i + 1; j < collisions.size(); j++)
{
Collision* dest1 = collisions[i];
Collision* dest2 = collisions[j];
if (dest1->CheckCollision(dest2))
{
dest1->GetOwner()->OnComponentBeginOverlap(dest1,dest2);
dest2->GetOwner()->OnComponentBeginOverlap(dest2,dest1);
}
else
{
dest1->GetOwner()->OnComponentEndOverlap(dest1, dest2);
dest2->GetOwner()->OnComponentEndOverlap(dest2, dest1);
}
}
}
}
다만 위 코드의 문제점은 충돌/비충돌 했을 경우 매틱마다 OnComponentBeginOverlap/OnComponentEndOverlap가 계속 호출된다는 점이다. 이럴 경우 이미 충돌한 콜리전들을 저장해놓고 다음 충돌은 무시하는 방식으로 구현할수 있다. 충돌여부 저장의 경우 탐색 속도가 빠른 컨테이너로 구성하는 것이 좋다.
class Collision: public Component
{
public:
unordered_set<Collision*>_collisionSet;//이미 충돌한 콜리전들을 저장
};
충돌할 콜리전 컨테이너를 생성했다면 이전에 충돌 여부를 판별한후 이전에 충돌하지 않은 콜리전이라면 컨테이너에 넣어서 반복적인 알림을 차단하고 충돌에서 벗어날 경우 지워준다.
//CollisionManager.cpp
if (dest1->CheckCollision(dest2))
{
if (dest1->_collisionSet.contains(dest2) == false)
{
dest1->GetOwner()->OnComponentBeginOverlap(dest1,dest2);
dest2->GetOwner()->OnComponentBeginOverlap(dest2,dest1);
dest1->_collisionSet.insert(dest2);
dest2->_collisionSet.insert(dest1);
}
}
else
{
if (dest1->_collisionSet.contains(dest2))
{
dest1->GetOwner()->OnComponentEndOverlap(dest1, dest2);
dest2->GetOwner()->OnComponentEndOverlap(dest2, dest1);
dest1->_collisionSet.erase(dest2);
dest2->_collisionSet.erase(dest1);
}
}