F.R.I.D.A.Y.

물리엔진 - 벽 충돌 part1. 바닥 구현 본문

DEV/Direct2D

물리엔진 - 벽 충돌 part1. 바닥 구현

F.R.I.D.A.Y. 2021. 5. 11. 23:03
반응형

 이전 시간의 중력 구현에 이어 이번에는 지나갈 수 없는 벽을 구현해봅니다.


 물리엔진을 구성하다보니 주로 게임에 빗대는 경우가 많은데, 벽이라 함은 물체가 뚫고 지나갈 수 없는 것을 말하죠. 우리가 윈도우에 사각형을 그려넣는다고 그 사각형이 바로 벽이 되지는 않습니다. 연산을 통해 벽으로 인식되는 사각형을 대상이 뚫고 움직이지 못하도록 해야하죠.

 

 이전 시간에 우리는 중력을 구현했기 때문에 중력이 적용된 시스템으로 구현해볼 것입니다. 중력을 구현한 방식과 중력을 구현하지 않은 방식은 약간의 차이가 존재하니까요.

 

접근

 대상이 벽에 접근할 때는 네가지 방향에서 접근할 수 있습니다. 그리고 네 방향을 모두 점검하는 것은 좋지 않죠. 따라서 우리는 각 방향의 접근을 알아내고 그에 맞게 코드를 작성하겠습니다. 그래야 연산이 줄어들고 그만큼 속도도 빨라지니까요.

 

 먼저, 벽이 될 사각형부터 만들어보겠습니다.

 

 이미지처럼 하단 전체를 뒤덮는 사각형 하나, 좌 우로 작은 사각형 하나씩을 구성하겠습니다. 이미지상에는 공중에 떠있는 사각형이지만, 실제로는 하단의 가장 큰 사각형 바로 위에 있는 사각형이라 가정합니다.

 

벽 구성 및 초기화

 벽은 D2D1_RECT_F 배열로 구성하겠습니다. mainWin.h 헤더 파일의 MainWindow 클래스의 private 멤버 변수로 ground 배열 멤버를 추가합니다.

D2D1_RECT_F ground[3];

 

 

 그리고 OnCreate 함수에서 초기화를 진행합니다.

	ground[0].bottom = clientSize.bottom;
	ground[0].left = 0.f;
	ground[0].right = clientSize.right;
	ground[0].top = clientSize.bottom - 40.f;

	ground[1].bottom = ground[0].top;
	ground[1].left = 100.f;
	ground[1].right = 150.f;
	ground[1].top = ground[1].bottom - 30.f;

	ground[2].bottom = ground[0].top;
	ground[2].left = 300.f;
	ground[2].right = 340.f;
	ground[2].top = ground[2].bottom - 30.f;

 이 초기화는 모양만 비슷하게 맞으면 되니 값을 정확히 따라하려 하지 않아도 됩니다.

 

벽 그리기

 초기화된 값을 기반으로 벽 이미지를 그려주도록 합니다. 이번에는 조금 색다르게 사각형 안이 채워지도록 구성해보죠.

	for (size_t i = 0; i < 3; ++i) {
		pRT->FillRectangle(ground[i], pBrush);
	}

 OnPaint 메서드 안에 넣어서 그리기 작업을 하도록 해줍니다. 이렇게 코드를 작성하면 그 결과로 아래와 같은 그림이 그려지게 됩니다.

 지금은 이미지를 그리도록만 했으니 위 사각형이 계속해서 떨어지는 것이 당연합니다. 이제 계속 떨어지지 않고 그려놓은 하얀색 사각형 위에 안착하도록 코드를 구성하겠습니다.

 

 

충돌 적용

 단일 대상으로 하는 코드는 이미 사전에 포스트하면서 언급했습니다. 그러나 여러 사각형과의 충돌을 적용하면서 중력 영향까지 주는데다 연산 부하를 줄이기 위해서는 여러 옵션을 생각해야합니다.

 

 첫째, 지금 생성하는 프로그램에서는 하얀색 테두리를 가진 사각형(이제부터 캐릭터라 언급합니다)이 위로 올라가는 경우는 존재하지 않습니다. 때문에 캐릭터의 하단부와 벽의 상단부의 충돌만 판단하면 됩니다.

 

 둘째, 하나의 벽과만 충돌 판정을 하는 것이 아닙니다. 즉, 첫 번째 옵션을 생각하면 캐릭터의 하단과 벽의 상단만 충돌 판정을 하면 된다는 안일한 생각을 할 수 있습니다. 그러나, 만일 어느 벽 하나라도 다른 벽보다 높은 곳에 위치한다면 캐릭터가 높은 곳에 위치한 벽에 닿을 수 있는지를 판단해야합니다. 즉, 캐릭터를 수직으로 내렸을 때 닿는 벽만 판단해야합니다.

 

 셋째, 현재 캐릭터의 하단부보다 높은 위치에 존재하는 벽의 상단부는 판단할 필요가 없습니다. 이미 그 벽을 넘어섰다는 말이 되니까요.

 

 넷째, 연산 부하를 줄여야 합니다.

 

연산 부하 줄이기

 연산 부하를 줄이기 위해서는 반복문 안에 분기 구문을 넣으면 안됩니다. 반복이 많아질수록 분기 구문을 위한 판정식 체크를 더 많이 하게 되니까요.

 dropSpeed로 선언된 변수는 캐릭터의 하강 속도를 나타냅니다. 즉, dropSpeed가 양수(+)인 경우에는 하강하게 된다는 뜻이 되고, 이는 곧 첫 번째 옵션인 캐릭터 하단부-벽의 상단부만 판단하면 되는 문제로 연계됩니다. 따라서 아래와 같이 if 구문을 구성합니다.

if(dropSpeed > 0.f){
	for(size_t i = 0; i < 3; ++i){
    
    }
}

 이 내용은 Calculate 메서드에 포함되며 기존 두 개의 if 구문은 필요하지 않습니다. 구조 자체를 뜯어 고칠 거니까 다른 if 구문은 싹다 지웁니다.

 

충돌식

if 구문 내부의 for 반복문은 벽과 관련된 판정식을 넣습니다. 먼저 해당 벽이 캐릭터보다 높은 곳에 위치하는지를 판단하고 만일 사실이라면 해당 벽은 더이상 판정할 필요가 없으므로 다음 벽으로 넘어갑니다.

if (ground[i].top < rcCenter.y + rcSize.height / 2) continue;

 윈도우 좌표계는 일반 좌표계와 Y축 방향이 반대이므로 하강은 y값 증가를 의미합니다.

 

 일상에서 우리가 땅을 밟을 때는 그 땅이 우리 다리 바로 아래 존재합니다. 프로그램에서도 벽을 바닥으로 인식하기 위해서는 캐릭터의 바로 아래에 벽이 위치하고 있어야 합니다. 즉, 벽의 x 좌표(LEFT, RIGHT) 사이에 캐릭터가 위치하고 있어야 한다는 말이 됩니다. 캐릭터가 벽을 걸치는 x 좌표를 가지는지 확인합니다.

if (!(ground[i].left < rcCenter.x + rcSize.width / 2 && 
	rcCenter.x - rcSize.width / 2 < ground[i].right)) continue;

 이 코드를 이미지로 설명하면 아래와 같습니다.

 걸쳐 있는 것을 판단해야하므로 중심점만 가지고 비교하지 않고 너비값/2를 첨가해 연산에 이용합니다.

 

 

 이제 현재 위치와 다음 프레임 위치를 판단합니다. 이 부분은 다음 프레임 위치부터 생각해야 현재 위치를 연산한게 쉬울거라 생각합니다. 먼저 다음 프레임 위치부터 판단하는 코드를 작성하겠습니다.

 

다음 프레임 위치 선별

 캐릭터의 다음 위치가 벽의 상단보다 아래에 위치한 경우에는 속도를 조정해주어야 합니다. 따라서 아래처럼 값을 분석해 이동 위치가 벽보다 낮은 곳이면 dropSpeed를 수정합니다.

			if (rcCenter.y + rcSize.height / 2 + dropSpeed > ground[i].top) {
				float gap = ground[i].top - rcCenter.y - rcSize.height / 2.f;
				dropSpeed = gap;
			}
더보기

# break 키워드 넣으면 안되나요?

 넣어보세요~ 문제 하나가 생긴답니다? 뚜렷이 보이지는 않겠지만, 두고보면 사소하면 사소하다 할 수 있는 문제가 생겨요!

 

현재 프레임 위치 선별

 위 판정식 이전에, 현재 위치를 판단하는 코드를 구현합니다. 현재 위치가 벽 바로 위에 존재하는 경우라면 이동이 필요하지 않습니다. 따라서 dropSpeed를 0으로 설정해 이동하지 않도록 수정합니다.

 주의할 점은 이 코드는 바로 위 <다음 프레임 위치 선별>의 코드보다 위에 존재해야합니다.

			if (abs(ground[i].top - rcCenter.y - rcSize.height / 2) < 0.001f) {
				dropSpeed = 0.f;
                break;
			}

 이때, 벽의 상단에 캐릭터가 존재하는 경우라면 그 이후의 벽은 확인할 필요가 없습니다. 따라서 for 반복문을 나가도록 break 키워드를 입력해줍니다.

 

 

코드 완성

 여기까지 따라했다면 Calculate 메서드는 아래와 같이 구성될 것입니다.

void MainWindow::Calculate()
{
	static float gravity = 3.f;
	static float dropSpeed = 0.f;
	dropSpeed += gravity;

	if (dropSpeed >= 0.f) {
		for (size_t i = 0; i < 3; ++i) {
			if (!(ground[i].left < rcCenter.x + rcSize.width/ 2.f && 
            	rcCenter.x - rcSize.width / 2.f < ground[i].right)) continue;

			if (ground[i].top < rcCenter.y + rcSize.height / 2) continue;

			if (abs(ground[i].top - rcCenter.y - rcSize.height / 2) < 0.001f) {
				dropSpeed = 0.f;
				break;
			}

			if (rcCenter.y + rcSize.height / 2 + dropSpeed > ground[i].top) {
				float gap = ground[i].top - rcCenter.y - rcSize.height / 2.f;
				dropSpeed = gap;
			
			}

		}
	}

	rcCenter.y += dropSpeed;
}

 이제 중간에 떨어지는 중간에 벽을 만들면 그 벽에 캐릭터가 안착하는 모습을 볼 수 있을겁니다.

 


 다음 시간에는 캐릭터의 점프와 좌/우 움직임을 구현해봅니다.

 

# index

728x90
반응형

'DEV > Direct2D' 카테고리의 다른 글

물리엔진 - 좌우 이동  (0) 2021.05.14
물리엔진 - 점프 구현하기  (0) 2021.05.13
물리엔진 - 중력 구현 안정화  (0) 2021.05.10
물리엔진 - 중력 구현  (0) 2021.05.10
Direct2D - SetTransform  (0) 2021.05.05
Comments