*** 공부 방법 ***
1. 코딩을 해야 하는 부분은 첫 부분에 변수나 함수, 메소드에 대한 선언이 코드블럭으로 표시되어 있다. //ex) MakeFunction(); 2. 코드블럭 하단에는 해당 선언에 대한 구현 로직이 작성되어 있다. 처
hyrule.tistory.com
조금이나마 성능 최적화를 해보고자 코드를 개선해보았다.
[ 개선 내용 ]
enum class ECollision_Interaction
{
Ignore,
Collision
};
-> 해당 열거체 제거. 충돌 여부 bool로 대체
struct CollisionProfile
{
std::string Name;
ECollision_Channel Channel;
bool Enable;
std::vector<ECollision_Interaction> vecCollisionInteraction;
CollisionProfile() :
Enable(true)
{
}
};
-> 위 구조체를 아래와 같이 변경함.
struct CollisionProfile
{
ECollisionChannel Channel;
bool Enable;
//상호작용여부를 bool타입 변수 배열로 변경
bool Interaction[(int)ECollisionChannel::Max];
CollisionProfile():
Enable(true)
{
//미리 기본 충돌 설정을 false로 해놓는다.
for (int i = 0; i < (size_t)ECollisionChannel::Max; ++i)
{
Interaction[i] = false;
}
}
};
- 이제 각 채널 별 프로필을 생성 시 기본값이 모두 false로 설정된다.
- 충돌시키고 싶은 충돌체만 바꿔주면 됨
class CCollisionManager
private:
//각 채널별로 하나의 프로파일 보유. 초기화 때 각 프로파일을 설정
CollisionProfile* m_vecProfile[(int)ECollisionChannel::Max];
CCollisionManager은 m_vecProfile을 보유. 인덱스 번호가 각 채널에 소속된 충돌체의 프로파일을 저장
bool CCollisionManager::Init()
//0: Default 설정 -> 모두와 충돌
m_vecProfile[0] = new CollisionProfile;
SetProfileAll(ECollisionChannel::Default);
//1: Player 설정 -> Default, Monster, MonsterAttack과 충돌
m_vecProfile[1] = new CollisionProfile;
SetProfile(ECollisionChannel::Player, ECollisionChannel::Default);
SetProfile(ECollisionChannel::Player, ECollisionChannel::Monster);
SetProfile(ECollisionChannel::Player, ECollisionChannel::MonsterAttack);
//2: Monster 설정 -> Default, Player, PlayerAttack과 충돌
m_vecProfile[2] = new CollisionProfile;
SetProfile(ECollisionChannel::Monster, ECollisionChannel::Default);
SetProfile(ECollisionChannel::Monster, ECollisionChannel::Player);
SetProfile(ECollisionChannel::Monster, ECollisionChannel::PlayerAttack);
//3: PlayerAttack 설정 -> Default, Monster과 충돌
m_vecProfile[3] = new CollisionProfile;
SetProfile(ECollisionChannel::PlayerAttack, ECollisionChannel::Default);
SetProfile(ECollisionChannel::PlayerAttack, ECollisionChannel::Monster);
//4: MonsterAttack 설정 -> Default, Player과 충돌
m_vecProfile[4] = new CollisionProfile;
SetProfile(ECollisionChannel::MonsterAttack, ECollisionChannel::Default);
SetProfile(ECollisionChannel::MonsterAttack, ECollisionChannel::Player);
- CCollisionManager의 초기화 함수에서 채널 별로 충돌 관계를 사전설정해준다.
[ 사전 지식 ]
std::bind(Func, Obj, std::placeholders::_1, std::placeholders::_2);
- 인자가 있는 함수 포인터(std::function) 등록의 경우 bind를 활용하여 등록할 때, 인자 정보도 넘겨주어야 한다.
- 이 때 사용되는 것이 std::placeholders이다.
** placeholders 번호 순서를 뒤집어서 넣으면 인자도 바뀌어서 들어가지만, 그렇게는 잘 쓰지 않는다.
- 충돌체는 다 만들었는데, 실제 충돌 관리는 어디서 해야할까?
- 사실 아직 모든 충돌체들을 모아놓고 관리를 시작하지 않았다.
-- 충돌도 사실 CScene을 넘어갈 일은 없으므로 CScene 별로 관리를 해 주면 된다.
--- CScene에 충돌체를 모아놓고, 한 충돌체에 대해 나머지 모든 충돌체와 충돌이 있는지 검사하는 일을 반복한다.(이중 for문)
class CSceneCollision
- 한 씬의 충돌체들을 리스트로 모아놓을 클래스이다.
- 외부 접근을 제한하고, CScene에서만 자유롭게 접근할 수 있도록 해준다.
class CScene
< std::vector<class CCollider*> m_vecCollider[(int)ECollisionChannel::Max] >
- 위 클래스를 동적 할당하여 들고 있고, 소멸자에서 제거시켜준다.
- 해당 클래스 주소를 반환해주는 < GetCollision() > 메소드도 생성해 준다.
class CSceneCollision
- 이 클래스에서는 매 프레임마다 모든 충돌체를 배열에 모아서 충돌처리를 진행한다. 매 프레임마다 새로 채우고, 매 프레임마다 전부 비운다.
< m_vecCollider >
- 매 프레임마다 현재 Active && Enable 상태인 충돌체를 전부 모아서 저장할 배열
- 배열 재할당이 일어나면 성능이 크게 감소할 것이므로 reserve() 함수를 통해 미리 공간을 500개 정도 만들어 놓자.
< void AddCollider() >
- CCollider 포인터를 받아서 m_vecCollider에 pushback 해주는 메소드
- 들어온 충돌체가 소속된 충돌 채널을 확인하고, 해당 채널에 맞는 인덱스 번호로 넣어주어야 한다.
void CSceneCollision::AddCollider(CCollider* Col)
{
//자신이 소속된 Collision Channel 배열 에 pushback 해준다.
m_vecCollider[(int)Col->GetProfile()].push_back(Col);
}
< class CCollisionManager* m_CollManager >
- 매 프레임 CollManager의 주소를 참조해야 하기 때문에 아예 주소를 초기화 단계에서 미리 받아놓는다.
class CGameObject
< Update(), PostUpdate(), Render() >
- 자신이 들고 있는 m_ColliderList를 순회 돌며 각각의 Update(), PostUpdate(), Render() 메소드를 호출한다.
< PostUpdate() >
- 이 메소드에서 최종적으로 충돌체 상태에 대한 Update() 과정이 끝나면, AddCollider() 메소드를 통해 해당 충돌체를 CSceneCollider의 m_vecCollider에 등록시킨다.
< ~CGameObject() >
- 게임오브젝트가 SetActive() 등으로 인해 제거될 경우
- 혹시나 들고있는 충돌체가 있으면 해당 충돌체까지 깨끗하게 제거해주어야 한다.
- 또한 해당 충돌체가 충돌하고 있던 충돌체들에도 자신을 지워달라고 요청해야 한다.
--> 해당 충돌체에서 ClearCollisionList() 메소드를 호출하면 처리 된다.
class CSceneCollision
< void CollisionUpdate() >
1. 일단 첫 번째 비교 Source배열의 순회를 돌아준다.
- 만약 원소가 없다면 continue를 통해 스킵
//일단 i만큼 순회를 돌아야 한다.
for (int i = 0; i < (int)ECollisionChannel::Max; ++i)
{
//비교할 채널의 충돌체가 비어있으면 스킵한다.
if (m_vecCollider[i].empty())
continue;
2. 두 번쨰 비교 Dest 배열의 순회를 돌아준다. 여기서도 마찬가지로 원소가 비어있으면 continue로 스킵한다.
for (int j = i + 1; j < (int)ECollisionChannel::Max; ++j)
{
//비교 대상 채널의 충돌체가 비어있으면 스킵한다.
if (m_vecCollider[j].empty())
continue;
3. Src와 Dest 배열 모두 원소가 있다면 두 배열 모두 원소에 대한 순회를 돌아준다.
- 혹시나 두 배열이 서로 충돌하지 않도록 설정 되어있다면 순회 k와 l 모두 빠져나와야 하므로
noCollision 변수를 만들어 둔다.
size_t size_i = m_vecCollider[i].size();
for (size_t k = 0; k < size_i; ++k)
{
//다음 반복문도 빠져나오기 위한 변수
//첫번쨰 원소끼리 전혀 관계없는 충돌체이면
//다음 원소들도 각각 같은 충돌 그룹이기 때문에
//전혀 관련없으므로 반복문 전체 중단
bool noCollision = false;
size_t size_j = m_vecCollider[j].size();
for (size_t l = 0; l < size_j; ++l)
{
4. 두 배열의 충돌 프로필을 확인한다. 둘 중 하나라도 서로에 대한 충돌 판정이 false일 경우 충돌 확인 과정을 스킵한다.
이 경우 k에 대한 반복문과 j에 대한 반복문 모두를 빠져나와야 하므로 noCollision 변수를 true로 바꿔준다.
ECollisionChannel SrcProfile = m_vecCollider[i][k]->GetProfile();
ECollisionChannel DestProfile = m_vecCollider[j][l]->GetProfile();
//둘중 한 채널이라도 상대방 채널과 충돌 판정이 없다면
//두 채널은 관계없는 충돌 판정 라인이므로 break를 통해 스킵
if (m_CollManager->GetProfile(SrcProfile)->Interaction[(int)DestProfile] == false ||
m_CollManager->GetProfile(DestProfile)->Interaction[(int)SrcProfile] == false)
{
noCollision = true;
break;
}
.
.
.
}
if(noCollision)
break;
5. 통과했으면 이제 충돌여부를 확인한다.
//통과했으면 충돌 처리를 시작한다.
CCollider* Src = m_vecCollider[i][k];
CCollider* Dest = m_vecCollider[j][l];
//충돌 여부를 확인해서 충돌이면
if (Src->Collision(Dest))
{
//근데 만약 첫 충돌이면
//->Src의 충돌중인 충돌체 리스트에 Dest가 없으면
if (!Src->CheckCollisionList(Dest))
{
//서로의 리스트에 서로를 삽입한다.
Src->AddCollisionList(Dest);
Dest->AddCollisionList(Src);
//그리고 각자의 충돌 호출 함수에 상대를 넣어 호출한다.
Src->CallCollisionBegin(Dest);
Dest->CallCollisionBegin(Src);
}
}
//만약 충돌 상태가 아닌데 자신의 충돌중 리스트 안에
//상대 오브젝트가 있을 경우
//-> 충돌중이었는데 다시 떨어졌다는 의미이다!
else if (Src->CheckCollisionList(Dest))
{
//서로에게 자신을 리스트에서 지우도록 요청
Src->DeleteCollisionList(Dest);
Dest->DeleteCollisionList(Src);
//그리고 충돌 종료 함수를 호출
Src->CallCollisionEnd(Dest);
Dest->CallCollisionEnd(Src);
}
}
6. 모든 충돌 처리 과정이 끝나면 다음 프레임 처리를 위해 배열을 비워준다.
for (int i = 0; i < (int)ECollisionChannel::Max; ++i)
{
m_vecCollider[i].clear();
}
}
- 메소드의 전체 코드는 다음과 같다.
void CSceneCollision::CollisionUpdate()
{
//일단 i만큼 순회를 돌아야 한다.
for (int i = 0; i < (int)ECollisionChannel::Max; ++i)
{
//비교할 채널의 충돌체가 비어있으면 스킵한다.
if (m_vecCollider[i].empty())
continue;
for (int j = i + 1; j < (int)ECollisionChannel::Max; ++j)
{
//비교 대상 채널의 충돌체가 비어있으면 스킵한다.
if (m_vecCollider[j].empty())
continue;
size_t size_i = m_vecCollider[i].size();
for (size_t k = 0; k < size_i; ++k)
{
//다음 반복문도 빠져나오기 위한 변수
//첫번쨰 원소끼리 전혀 관계없는 충돌체이면
//다음 원소들도 각각 같은 충돌 그룹이기 때문에
//전혀 관련없으므로 반복문 전체 중단
bool noCollision = false;
size_t size_j = m_vecCollider[j].size();
for (size_t l = 0; l < size_j; ++l)
{
ECollisionChannel SrcProfile = m_vecCollider[i][k]->GetProfile();
ECollisionChannel DestProfile = m_vecCollider[j][l]->GetProfile();
//둘중 한 채널이라도 상대방 채널과 충돌 판정이 없다면
//두 채널은 관계없는 충돌 판정 라인이므로 break를 통해 스킵
if (m_CollManager->GetProfile(SrcProfile)->Interaction[(int)DestProfile] == false ||
m_CollManager->GetProfile(DestProfile)->Interaction[(int)SrcProfile] == false)
{
noCollision = true;
break;
}
//통과했으면 충돌 처리를 시작한다.
CCollider* Src = m_vecCollider[i][k];
CCollider* Dest = m_vecCollider[j][l];
//충돌 여부를 확인해서 충돌이면
if (Src->Collision(Dest))
{
//근데 만약 첫 충돌이면
//->Src의 충돌중인 충돌체 리스트에 Dest가 없으면
if (!Src->CheckCollisionList(Dest))
{
//서로의 리스트에 서로를 삽입한다.
Src->AddCollisionList(Dest);
Dest->AddCollisionList(Src);
//그리고 각자의 충돌 호출 함수에 상대를 넣어 호출한다.
Src->CallCollisionBegin(Dest);
Dest->CallCollisionBegin(Src);
}
}
//만약 충돌 상태가 아닌데 자신의 충돌중 리스트 안에
//상대 오브젝트가 있을 경우
//-> 충돌중이었는데 다시 떨어졌다는 의미이다!
else if (Src->CheckCollisionList(Dest))
{
//서로에게 자신을 리스트에서 지우도록 요청
Src->DeleteCollisionList(Dest);
Dest->DeleteCollisionList(Src);
//그리고 충돌 종료 함수를 호출
Src->CallCollisionEnd(Dest);
Dest->CallCollisionEnd(Src);
}
}
//마찬가지로 상관없는 라인이므로 여기도 빠져나와준다.
if (noCollision)
break;
}
}
}
for (int i = 0; i < (int)ECollisionChannel::Max; ++i)
{
m_vecCollider[i].clear();
}
}
class CCollider
< Update(), PostUpdate(), Render() >
- 자식 클래스에서 재정의해서만 사용할 수 있도록 순수가상함수로 만들어 준다.
< SetCollisionBeginFunction() >
< SetCollisionEndFunction() >
- 충돌이 일어났을 경우 / 끝났을 경우 호출할 함수를 등록해주는 메소드.
- 어떤 클래스가 들어올 지 알 수 없으므로 템플릿을 사용한다.
- T포인터 타입과
- Collider* Src, Collider* Destf를 인자로 받는 T포인터 안의 void 멤버함수 포인터를 인자로 받는다.
void SetCollisionBeginFunction(T* Obj, void(T::* Func)(CCollider*, CCollider*));
- 해당 함수를 m_CollisionBegin과 m_CollisionEnd 변수에 등록해준다.
** bind를 할 떄 인자까지 사전 지식에 있는 내용을 이용하여 넘겨주어야 한다 **
class CColliderBox
< virtual void Collision() >
- Dest의 EColliderType를 확인하고, 자신과 상대방 충돌체의 모양에 따라 다른 충돌 확인 메소드를 호출한다.
(Dest가 사각형인지 원인지 점인지 선인지에 따라 처리가 달라져야 하기 떄문)
-- switch문으로 작성해준다.
switch (Dest->GetColliderType())
{
case ECollider_Type::Box:
return CCollisionManager::GetInst()->CollisionBoxToBox(m_HitPoint, this, (CColliderBox*)Dest);
case ECollider_Type::Circle:
break;
}
class CCollsionManager
< bool CollisionBoxToBox >
* 두개 타입으로 오버로딩
- Vector2& HitPoint, CColliderBox* Src, CColliderBox* Dest
- Vector2& HitPoint, BoxInfo& Src, BoxInfo& Dest
- 위의 Collision() 메소드에서 어떤 타입의 충돌인지 체크하고, CCollisionManager에서 적합한 충돌 메소드를 호출해주는 방식이다.
class CColliderBox
< Vector2 m_HitPoint >
- 충돌이 발생할 지점을 저장할 변수를 하나 새로 만들어준다.
< CCollision() >
- 다시 돌아와서, Switch문을 통해 자신과 충돌한 상대방의 타입에 따라 CCollisionManager에서 적합한 함수를 호출해준다.
class CCollisionManager
< bool CollisionBoxToBox(Vector2& HitPoint, CColliderBox* Src, CColliderBox* Dest) >
- 이 함수에 진입하면,
해당 메소드 안에서 다시 CColliderBox* 안의 m_Info를 받아와서(GetInfo()),
오버로딩된 BoxInfo& 인자 두개를 받는 메소드에 넣어준다.
- 호출한 함수에서 true를 반환하면 true를 반환한다.
- 그렇지 않다면 false를 반환한다.
< bool CollisionBoxToBox(Vector2& HitPoint, const BoxInfo& Src, const BoxInfo& Dest) >
- 이 함수에서 충돌 판정을 한다.
위의 충돌하지 않는 4 경우를 모두 걸러낸다. 만약 이 4 경우를 모두 통과했다면 충돌이 된다.
- 이제 Hit Point를 구할 차례이다. 이 지점은 히트 이펙트 등을 표시하기 위해 사용될것이다.
- Left, Top, Right, Bottom을 각각 구해주어야 한다. 한 변씩 살펴보자
-- Left: 서로 교집합인 두 사각형 중 "LeftTop.x 값이 큰" 녀석이 Left가 된다.
-- Top: 서로 교집합인 두 사각형 중 "LeftTop.y 값이 큰" 녀석이 Top이 된다.
-- Right: 서로 교집합인 두 사각형 중 "RightBottom.x 값이 작은" 녀석이 Right가 된다.
-- Bottom: 서로 교집합인 두 사각형 중 "RightBottom.y값이 작은"녀석이 Bottom이 된다.
- 이제 구한 값을 통해 HitPoint를 구하고, 인자로 들어온 HitPoint 주소에 대입한다.
- true를 반환하여 충돌이 일어났음을 알린다.
class CSceneCollision
< Collision() >
- 다시 여기로 돌아와서, 위에서 만든 충돌 함수를 이용하여
- Src의 Collision() 메소드에 인자로 Dest를 넣어 호출한다.
-- 만약 충돌이 일어났다면 true가 반환될 것이다.
-- 충돌이 일어났다면, 이제 처음 충돌한것인지 계속 충돌하고 있던것인지를 확인해봐야 한다.
--- CheckCollisionLIst() 메소드를 통해 Src의 m_CollisionList 안에 Dest의 주소가 있는지 확인해본다.
---- 이 리스트 안에 m_CollisionList가 없다면 충돌이 방금 처음 일어났다는 뜻이다. -> AddCollisionList() 메소드를 통해 서로의 m_CollisionList 안에 주소를 넣어준다.
class CCollider
- 이제 충돌 여부를 알았으니 충돌이 발생했을 경우 함수를 호출하는 메소드를 만들어야 한다.
< void CallCollisionBegin() >
< void CallCollisionEnd() >
- 인자로는 상대 충돌체의 주소를 받는다.
- m_CollisionBegin 변수에 함수가 '등록되어 있을 경우'에만 호출한다. (등록은 SetCollisionBegin 메소드)
m_CollisionBegin 함수에 Src = this, Dest = 상대 충돌체를 넣어서 호출한다.
- 두 메소드 모두 동일하다.
class CScene
< PostUpdate() >
- 모든 처리가 끝나고 마지막 부분에서 CSceneCollider의 CollisionUpdate()를 호출한다.
- 처리 과정은 이렇게 된다.
1. CGameManager에서 현재 CScene의 Update(), PostUpdate(), Render() 메소드(이하 로직) 호출
2. CScene에서 현재 생성되어 있는 모든 CGameObject 리스트를 순회 돌면서 로직 호출
3. 각각 CGameObject에서는 자신의 로직을 처리함과 동시에 자신이 가지고 있는 충돌체에 대한 로직 호출
4. CGameObject의 로직 처리 마지막에서 제거되지 않은 CCollider를 CSceneCollision에 있는 m_vecCollider에 등록
5. CScene의 로직 마지막 부분에서 등록된 m_vecCollider에 대한 충돌여부 처리
6. 반복
- 이제 충돌 설계는 끝났다. 실제 게임오브젝트들에 충돌체를 생성하여 작동하는지 확인하면 된다.
//충돌체 설정
CColliderBox* Coll = AddCollider<CColliderBox>("PlayerBox");
Coll->SetCollisionProfile(ECollisionChannel::Player);
Coll->SetExtent(m_Size.x, m_Size.y);
Coll->SetOffset(Vector2(10.f, 10.f));
이런 식으로, Player과 Monster, 그리고 Bullet에 충돌체를 생성했다.
- CBullet에서는 충돌했을 시 파괴되는 콜백 함수를 생성하여 등록했다.
- 이외에도 현재 생성되는 모든 CGameObject에 모두 충돌체를 입혀 준다.
'WIN32API FrameWork > 한단계씩 직접 구현' 카테고리의 다른 글
48. 스케일 조정 (0) | 2022.06.01 |
---|---|
47. 이펙트 (0) | 2022.06.01 |
45. 충돌 처리 5 - 실제 충돌체 생성 및 관리 (0) | 2022.05.30 |
44. 충돌 처리 4 - 사각형 충돌체 (Box Collider) (0) | 2022.05.30 |
43. 충돌 처리 3 - 충돌체 2 (0) | 2022.05.30 |