ISSUE 게임 후처리 관련 이슈 해결

Date:     Updated:

카테고리:

태그:

트러블 이슈 → 후처리 관련.

오늘 새벽에 트러블 하나를 이슈했는데, 내 스스로 꽤 만족스러운 해결이었기에 호다다닥… 올리고 있다.

일반 간단하게 문제를 보자.

기본적으로 객체를 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오브젝트가 있다. 그리고 그걸 향해 발사되는 미사일이 있고 부딪혔다!!

  1. A몬스터에서 충돌하여 A, M(미사일)의 충돌 함수가 발생한다.
  2. B몬스터에서 충돌하여 B, M(미사일)의 충돌 함수가 발생한다.
  3. 미사일 기준에서 한 프레임안에 2개의 충돌 함수가 발생하여.
  4. 한 프레임에 두 번의 DeleteObject가 호출된다.
  5. 따라서 이벤트 벡터에 똑같은 메모리를 Delete하는 명령이 2개 들어간다.
  6. 한 번 Delete를 하고 다음으로 넘어가면, 이미 해제된 메모리를 해제하는데 여기서 에러가 난다.

그러면 이걸 어떻게 해결해야할까? 생각을 했는데.

  1. 미사일에 Flag변수를 넣어, 한 번에 한 번만 Delete를 호출할 수 있게 한다.
  2. 명령어가 중복되지 않도록 한다.

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)이기도 하고 바로 적용했더니, 당연하다면 당연히 잘 되었다.

이래서 자료구조에 대한 지식이 잘 있어야 한다, 기초가 중요하다는 것을 다시 한 번 느꼈고, 깔끔히 문제를 해결해낸 내 스스로에게 기분이 좋아졌다.

issue 카테고리 내 다른 글 보러가기

댓글 남기기