ISSUE 게임 후처리 관련 이슈 해결
카테고리: issue
트러블 이슈 → 후처리 관련.
오늘 새벽에 트러블 하나를 이슈했는데, 내 스스로 꽤 만족스러운 해결이었기에 호다다닥… 올리고 있다.
일반 간단하게 문제를 보자.
기본적으로 객체를 delete하는데서 오는 문제인데.
일단 Missile의 충돌 코드를 보자.
void CMissile::OnCollisionEnter(CCollider* _pOther) { CObject* pOtherObj = _pOther->GetObj();
if (pOtherObj->GetName() == L"Monster") {
//파라미터 세팅.
Vec2 objRenderScale = pOtherObj->GetRenderScale();
wstring damage = std::to_wstring(m_iDamage);
//DamageUI 오브젝트 만들기.
CDamageUI* damageText = new CDamageUI;
damageText->SetPos(CCamera::GetInstance()->GetRenderPos(pOtherObj->GetPos()));
damageText->SetPivotPos();
damageText->SetDuration(1.5f);
if (!m_bIsCritial) {
damageText->CreateTextUI(damage, Vec2(-20.f, -objRenderScale.y - 10.f), Vec2(20.f, -objRenderScale.y - 5.f)
, 16, D2D1::ColorF::White
, true
, 1.f, D2D1::ColorF::Black
, FONT_TYPE::KR
, TextUIMode::TEXT, 0);
}
else {
damageText->CreateTextUI(damage, Vec2(-20.f, -objRenderScale.y - 10.f), Vec2(20.f, -objRenderScale.y - 5.f)
, 16, D2D1::ColorF::Yellow
, true
, 1.f, D2D1::ColorF::Black
, FONT_TYPE::KR
, TextUIMode::TEXT, 0);
}
CreateObject(damageText, GROUP_TYPE::IMAGE);
//CreateObject
DeleteObject(this);
}
}
솔직히 다른 거 다 필요 없고, 맨 밑에 DeleteObject로, 스스로를 삭제한다! 그 부분만 보면 된다. 이 부분은 직접 삭제하는 게 아니라, 이벤트 매니저를 통해 객체를 삭제한다.
void DeleteObject(CObject* _pObj)
{
tEvent deleteEvn = {};
deleteEvn.eEven = EVENT_TYPE::DELETE_OBJECT;
deleteEvn.lParam = (DWORD_PTR)_pObj;
CEventMgr::GetInstance()->AddEvent(deleteEvn);
}
이렇게 이벤트를 등록해주고.
void CEventMgr::Excute(const tEvent& _eve)
{
switch (_eve.eEven) {
case EVENT_TYPE::CREATE_OBJECT:
{
// lParam : Object Address
// wParam : Group Type
CObject* pNewObj = (CObject*)_eve.lParam;
GROUP_TYPE eType = (GROUP_TYPE)_eve.wParam;
CSceneMgr::GetInstance()->GetCurScene()->AddObject(pNewObj, eType);
}
break;
case EVENT_TYPE::DELETE_OBJECT:
{
// lParam : Object Address
// wParam : NULL
// Object를 Dead 상태로 변경하고 -> 삭제 예정 오브젝트들을 모아둔다.
CObject* pDeleteObj = (CObject*)_eve.lParam;
pDeleteObj->SetDead();
m_vecDeadScheduled.insert(pDeleteObj);
}
break;
case EVENT_TYPE::SCENE_CHANGE:
{
// lParam : Next Scene Type
// wParam : NULL
CSceneMgr::GetInstance()->ChangeScene((SCENE_TYPE)_eve.lParam);
//이전 Scene에 대한 UI를 가리키고 있었기에 그거 해제.
CUIMgr::GetInstance()->SetFocusedUI(nullptr);
}
break;
case EVENT_TYPE::CHANGE_AI_STATE:
{
// lParam : AI PTR
// wParam : Next Type
AI* pAI = (AI*)_eve.lParam;
MON_STATE eNextState = (MON_STATE)_eve.wParam;
pAI->ChangeState(eNextState);
}
break;
}
}
등록된 이벤트는 다음 프레임에 이렇게 이벤트 타입에 따라 처리를 해준다. 여기서 중요한 건 Delete관련이니, 그 쪽만 보자.
void CEventMgr::update()
{
//==================================================
//이전 프레임에서 등록해준 Dead Object들을 삭제한다.
//==================================================
////임시조치///
for (auto deadObjectPtr : m_vecDeadScheduled) {
delete deadObjectPtr;
}
m_vecDeadScheduled.clear();
///////
// ===============
// Event 처리.
// ===============
for (const auto& event_node : m_vecEvent) {
Excute(event_node);
}
m_vecEvent.clear();
}
그리고 update(1프레임)마다 m_vecDeadScheduled에서 for문을 돌려서 삭제해준다.
자, 그러면 CollisionManager의 코드를 보자.
void CCollisionMgr::CollisionGroupUpdate(GROUP_TYPE _eLeft, GROUP_TYPE _eRight)
{
CScene* pCurScene = CSceneMgr::GetInstance()->GetCurScene();
//우리가 하려는 의도랑 조금 어긋나는 부분이 있음.
//반환타입이 참조이니, 우리가 레퍼런스를 가져온 것.
//vecLeft는 주소값의 복사본(지역변수)임. 따라서 &로 받아야함.
const vector<CObject*>& vecLeft = pCurScene->GetGroupObject(_eLeft);
const vector<CObject*>& vecRight = pCurScene->GetGroupObject(_eRight);
unordered_map<ULONGLONG, bool>::iterator colliderMapIter;
for (auto leftIDX = 0; leftIDX < vecLeft.size(); leftIDX++) {
//충돌체를 보유하지 않는 경우
if (nullptr == vecLeft[leftIDX]->GetCollider()) continue;
for (auto rightIDX = 0; rightIDX < vecRight.size(); rightIDX++) {
//충돌체를 보유하지 않는 경우. or 자기 자신과의 충돌인 경우.
if (nullptr == vecRight[rightIDX]->GetCollider() || vecLeft[leftIDX] == vecRight[rightIDX]) continue;
CCollider* pLeftCollider = vecLeft[leftIDX]->GetCollider();
CCollider* pRightCollider = vecRight[rightIDX]->GetCollider();
//충돌체들 간의 조합 ID
COOLIDER_ID ID;
ID.Left_id = pLeftCollider->GetID();
ID.Right_id = pRightCollider->GetID();
colliderMapIter = m_mapColInfo.find(ID.ID);
//충돌 정보가 미 등록 상태인 경우 등록(충돌하지 않았다. 로)
if (colliderMapIter == m_mapColInfo.end()) {
//두 조합의 검사를 최초로 했을 경우.
m_mapColInfo.insert(make_pair(ID.ID, false));
colliderMapIter = m_mapColInfo.find(ID.ID);
}
//현재 충돌한 경우.
if (IsCollision(pLeftCollider, pRightCollider)) {
if (colliderMapIter->second == false) {
//이번 프레임에 막 충돌한 경우.
//하필 이제 삭제될 때, 충돌할 때. 그 충돌은 없었던 것이 됨.
if (!vecLeft[leftIDX]->IsDead() && !vecRight[rightIDX]->IsDead()) {
pLeftCollider->OnCollisionEnter(vecRight[rightIDX]->GetCollider());
pRightCollider->OnCollisionEnter(vecLeft[leftIDX]->GetCollider());
colliderMapIter->second = true;
}
}
else if (colliderMapIter->second == true) {
//이전 프레임에도 충돌하고 있었을 경우.
//이제 죽는 애는 충돌에서 벗어났다고 명시적으로 해줌.
if (vecLeft[leftIDX]->IsDead() || vecRight[rightIDX]->IsDead()) {
pLeftCollider->OnCollisionExit(vecRight[rightIDX]->GetCollider());
pRightCollider->OnCollisionExit(vecLeft[leftIDX]->GetCollider());
colliderMapIter->second = false;
}
else {
pLeftCollider->OnCollision(vecRight[rightIDX]->GetCollider());
pRightCollider->OnCollision(vecLeft[leftIDX]->GetCollider());
}
}
}
else {
//충돌하지 않은 경우.
if (colliderMapIter->second == true) {
//막 지금 충돌에서 벗어난 경우.
pLeftCollider->OnCollisionExit(vecRight[rightIDX]->GetCollider());
pRightCollider->OnCollisionExit(vecLeft[leftIDX]->GetCollider());
colliderMapIter->second = false;
}
}
}
}
}
일단 충돌은 사각형 기반의 AABB알고리즘으로 되어있다. 여튼 그게 중요한 건 아니고, 중요한 건 부딪힌 서로의 오브젝트에 OnCollision함수를 불러준다는거다. 즉, 서로서로 1개씩. 총 2개의 함수를 호출한다.
자, 문제 상황을 이야기 해보면, 완전히 겹친 2개의 Monster오브젝트가 있다. 그리고 그걸 향해 발사되는 미사일이 있고 부딪혔다!!
- A몬스터에서 충돌하여 A, M(미사일)의 충돌 함수가 발생한다.
- B몬스터에서 충돌하여 B, M(미사일)의 충돌 함수가 발생한다.
- 미사일 기준에서 한 프레임안에 2개의 충돌 함수가 발생하여.
- 한 프레임에 두 번의 DeleteObject가 호출된다.
- 따라서 이벤트 벡터에 똑같은 메모리를 Delete하는 명령이 2개 들어간다.
- 한 번 Delete를 하고 다음으로 넘어가면, 이미 해제된 메모리를 해제하는데 여기서 에러가 난다.
그러면 이걸 어떻게 해결해야할까? 생각을 했는데.
- 미사일에 Flag변수를 넣어, 한 번에 한 번만 Delete를 호출할 수 있게 한다.
- 명령어가 중복되지 않도록 한다.
1번은 솔직히 너무 짜친다. 게다가 update마다 초기화도 해야하고, 여러가지로 너무 짜치고 근본적인 해결이 아니다.
그래서 2번을 기준으로 잡고 생각했다.
처음에는 m_vecDeadScheduled가 벡터라서 push_back할 때, 이미 있는가 없는가. 확인을 for문으로 돌면서 해야한다.
vector<CObject*> deadlist;
for (auto deadObjectPtr : m_vecDeadScheduled) {
bool flag = false;
for (size_t i = 0; i < deadlist.size(); ++i)
{
if (deadlist[i] == deadObjectPtr)
{
flag = true;
break;
}
}
if (flag) continue;
deadlist.push_back(deadObjectPtr);
delete deadObjectPtr;
}
deadlist.clear();
m_vecDeadScheduled.clear();
이러면 O(N^2)방식이라 최적화적으로 문제가 있다. 오브젝트가 수천 개가 되면 그에 대한 이미지를 다 검사하는데 꽤 많은 낭비가 될거라고 생각이 되었다. 그래서 이걸 어떻게 해야할까 생각을 하다가…
중요한 건 이 점이라는 걸 생각했다.
- 메모리는 단 한 번만 delete한다.
- 따라서 1프레임에 삭제할 메모리는 ‘유일’하다.
여기서 든 생각은 유일한 값을 가지고 있는 자료구조. 그러면서 검색이 빠른 자료구조가 뭐가 있을까 생각을 하였더니 unordered_set이 있다.
해시구조라서 O(1)이기도 하고 바로 적용했더니, 당연하다면 당연히 잘 되었다.
이래서 자료구조에 대한 지식이 잘 있어야 한다, 기초가 중요하다는 것을 다시 한 번 느꼈고, 깔끔히 문제를 해결해낸 내 스스로에게 기분이 좋아졌다.
댓글 남기기