본문으로 건너뛰기
뒤로가기

[WinAPI] 메탈슬러그 모작: 충돌 이벤트와 지형 보정

TL;DR — 충돌이 필요한 오브젝트 그룹 조합만 등록하고, 일반 박스는 축별 거리 비교로, 회전 박스는 네 축에 대한 분리축 검사로 판정했습니다. 충돌 결과는 이전 프레임과 비교해 진입·유지·종료 이벤트로 바꾸고, 지형과 만난 캐릭터는 선분의 높이에 맞춰 위치와 수직 속도를 보정했습니다.

Table of contents

Open Table of contents

들어가며

2D 슈팅 게임에서는 플레이어, 지형, 적, 투사체, 탑승물이 서로 다른 규칙으로 충돌합니다. 모든 오브젝트를 같은 방식으로 검사하면 불필요한 조합이 늘고, 단순한 겹침 여부만 반환하면 피격과 지형 접촉처럼 지속 시간이 다른 동작을 구분하기 어렵습니다.

이 프로젝트에서는 충돌 처리를 다음 단계로 나눴습니다.

  1. 씬에서 검사할 오브젝트 그룹 조합을 등록합니다.
  2. 콜라이더 모양에 맞춰 축 정렬 박스 또는 회전 박스를 판정합니다.
  3. 현재 결과와 이전 프레임 결과를 비교해 충돌 이벤트를 만듭니다.
  4. 지형 충돌 이벤트에서 캐릭터 위치와 중력 상태를 보정합니다.

화면 예시는 비상업적 학습용 모작에서 직접 구현한 충돌 디버그 출력을 보여주며, 포함된 원작 그래픽의 권리는 각 권리자에게 있습니다.

사전 지식: 2차원 벡터의 내적과 축 투영에 대한 기본 이해를 전제로 합니다.


1. 검사할 충돌 그룹을 먼저 선택했습니다

스테이지를 시작할 때 PLAYER-GROUND, PLAYER-ENEMY, PLAYER_WEAPON-ENEMY, ENEMY_WEAPON-PLAYER처럼 실제 게임 규칙에 필요한 그룹 조합만 등록했습니다.

ColliderManager::GetInstance()->checkGroup(
    GROUP_TYPE::PLAYER,
    GROUP_TYPE::GROUND);

ColliderManager::GetInstance()->checkGroup(
    GROUP_TYPE::PLAYER_WEAPON,
    GROUP_TYPE::ENEMY);

ColliderManager::GetInstance()->checkGroup(
    GROUP_TYPE::ENEMY_WEAPON,
    GROUP_TYPE::PLAYER);SceneStage01.cpp

ColliderManager는 그룹별 비트 마스크를 순회하며 등록된 조합만 collisionUpdate()에 전달합니다.

등록된 그룹 조합
  +-- PLAYER x GROUND       --> 이동·점프 보정
  +-- PLAYER x ENEMY       --> 접촉 처리
  +-- PLAYER_WEAPON x ENEMY --> 피격 처리
  `-- ENEMY_WEAPON x PLAYER --> 플레이어 피격

충돌 규칙을 개별 오브젝트의 update()에 흩어 놓지 않고 스테이지가 필요한 그룹 관계를 먼저 선언하도록 했습니다. ColliderManager는 후보 조합을 찾고, 실제 반응은 각 오브젝트의 충돌 콜백이 담당합니다.


2. 축 정렬 박스와 회전 박스를 분리해 판정했습니다

회전하지 않는 박스는 두 중심 사이의 거리가 두 박스 반너비의 합보다 작은지 x축과 y축에서 각각 확인했습니다.

cAxcBxwA+wB2,cAycByhA+hB2|c_{Ax} - c_{Bx}| \leq \frac{w_A + w_B}{2}, \qquad |c_{Ay} - c_{By}| \leq \frac{h_A + h_B}{2}

두 조건을 모두 만족하면 x축과 y축에서 겹치므로 충돌로 판단합니다. 플레이어처럼 회전하지 않는 일반 오브젝트에는 이 계산만으로 충분했습니다.

기울어진 지형처럼 회전값을 판정에 반영해야 하는 충돌체에는 같은 비교를 그대로 사용할 수 없습니다. 각 박스의 위쪽·오른쪽 방향을 회전시켜 네 개의 검사 축을 만들고, 두 박스를 각 축에 투영했습니다.

한 축 nn에서 두 중심 사이의 투영 거리는 다음과 같습니다.

dn=(cAcB)nd_n = |(c_A - c_B) \cdot n|

두 박스의 반쪽 변 벡터를 eie_i라고 할 때, 해당 축에서 두 박스가 차지하는 반지름의 합은 다음처럼 계산했습니다.

rn=ieinr_n = \sum_i |e_i \cdot n|

dn>rnd_n > r_n인 축이 하나라도 있으면 두 박스 사이에 빈 간격이 있으므로 충돌하지 않습니다. 네 축에서 모두 dnrnd_n \leq r_n이면 분리할 축이 없으므로 충돌로 판단했습니다.

for (Vec2& d : dirs)
{
    float dist = fabsf((lf - rf).dot(d.normal()));
    float sum = 0.0f;

    for (int i = 0; i < (int)dirs.size(); i++)
    {
        sum += fabsf(dirs[i].dot(d.normal()));
    }

    if (dist > sum)
    {
        return false;
    }
}ColliderManager.cpp

단순 오브젝트는 축 정렬 박스로 처리하고, 회전값이 실제 판정에 필요한 오브젝트만 회전 박스로 처리했습니다. 모양의 목적에 맞춰 계산 경로를 나눈 선택입니다.


3. 이전 프레임과 비교해 충돌 이벤트를 만들었습니다

충돌 쌍마다 두 콜라이더 ID를 하나의 키로 묶고, 이전 프레임에 충돌 중이었는지를 collisionInfo에 저장했습니다. 현재 판정과 이전 상태를 비교하면 세 종류의 이벤트를 구분할 수 있습니다.

이전 false + 현재 true  --> onCollisionEnter
이전 true  + 현재 true  --> onCollision
이전 true  + 현재 false --> onCollisionExit

처음 겹친 프레임에는 onCollisionEnter()를 호출하고 상태를 true로 바꿉니다. 다음 프레임에도 겹쳐 있으면 onCollision()을 호출하며, 더 이상 겹치지 않으면 onCollisionExit()을 호출하고 상태를 다시 false로 바꿉니다.

이 구분을 이용해 투사체 피격처럼 한 번만 처리할 동작은 진입 이벤트에 두고, 지형 위 위치 보정처럼 접촉하는 동안 계속 필요한 동작은 유지 이벤트에 둘 수 있었습니다. 지형에서 벗어났을 때는 종료 이벤트에서 중력을 다시 활성화했습니다.


4. 라인 맵을 얇은 회전 지형으로 구성했습니다

스테이지 지형은 배경 이미지에 직접 충돌을 넣지 않고, 별도 라인 맵의 점 배열로 저장했습니다. 인접한 두 점을 하나의 Ground로 만들고, 선분의 중점·길이·기울기로 위치와 회전을 계산했습니다.

라인 맵 점 배열
  P0 ---- P1 ---- P2 ---- P3
   `지형 0` `지형 1` `지형 2`

Ground는 길이가 선분 길이이고 높이가 1인 얇은 회전 박스로 구성했습니다. 캐릭터와 지형의 회전 박스가 충돌하면 캐릭터의 x좌표에서 선분의 y값을 계산합니다.

y=m(xx1)+y1y = m(x - x_1) + y_1

계산한 지형 높이에서 캐릭터 콜라이더의 절반 높이와 오프셋을 반영해 캐릭터 위치를 지형 위로 맞췄습니다. Gravity::setGround(true)는 수직 속도를 0으로 만들고, 지형에서 벗어나면 setGround(false)로 바꿔 다음 프레임부터 중력 가속도를 다시 적용했습니다.

회전 박스와 라인 지형 충돌을 화면에 표시한 디버그 장면

빨간 선은 충돌 중인 회전 박스, 초록 선은 지형 콜라이더를 표시합니다.

이 방식은 배경 픽셀을 직접 검사하지 않고 별도 지형 데이터만으로 이동 가능한 표면을 구성합니다. 배경 이미지가 바뀌더라도 라인 맵을 다시 조정하면 같은 충돌 처리 코드를 사용할 수 있었습니다.


5. 충돌 판정과 게임 반응을 분리했습니다

한 프레임의 충돌 흐름은 다음과 같습니다.

  1. 각 오브젝트의 finalUpdate()가 콜라이더의 최종 위치를 계산합니다.
  2. ColliderManager가 등록된 그룹 쌍을 순회합니다.
  3. 콜라이더 유형에 맞는 겹침 검사를 실행합니다.
  4. 이전 프레임 상태와 비교해 진입·유지·종료 콜백을 호출합니다.
  5. 플레이어, 투사체, 지형이 각자의 게임 반응을 처리합니다.
오브젝트 위치 갱신
  -> 충돌 후보 그룹 선택
  -> 박스 겹침 판정
  -> Enter / Stay / Exit 분류
  -> 피격·삭제·지형 보정·중력 상태 변경

ColliderManager는 어떤 오브젝트가 피해를 받는지 알지 못하고, 오브젝트는 박스 투영 계산을 직접 수행하지 않습니다. 판정과 반응을 나눠 같은 충돌 흐름을 플레이어, 적, 투사체, 탑승물, 지형에 적용할 수 있었습니다.


정리하며

핵심 요약:

이 프로젝트에서는 충돌 알고리즘 하나만 구현하는 데 그치지 않고, 오브젝트의 위치 갱신부터 이벤트 분류와 지형 보정까지 한 프레임의 흐름으로 연결했습니다. 이를 통해 충돌이 게임 상태와 이동에 어떻게 반영되는지 직접 구성해 볼 수 있었습니다.

참고 자료


이 게시물은 학습한 내용을 바탕으로 초안을 작성한 뒤, LLM의 도움을 받아 내용을 검수하고 다듬어 완성되었습니다.


공유하기:

이전 글
[C++] IOCP 1: OVERLAPPED I/O와 Completion Port
다음 글
[WinAPI] 메탈슬러그 모작: 프레임 루프와 버퍼 합성