Game Programming in C++ - Day 3

이응민·2024년 7월 15일
0

Game Programming in C++

목록 보기
3/21

Day3 오브젝트 그리기와 게임 갱신하기

도형 그리기

오브젝트를 그리기위해 GenerateOutPut 함수에서 후면 버퍼를 클리어하고 난 뒤 전면 버퍼와 후면 버퍼를 교환하기 전에 그린다. 색으로 채워진 사각형 오브젝트를 그리기 위해 SDL은 SDL_RenderFillRect 함수를 제공한다. 이 함수는 사각형의 경계를 나타내는 SDL_Rect를 파라미터로 받는다. 먼저 그리기 색상을 흰색으로 변경해야한다.

그리고 사각형을 그리기 위해 SDL_Rect 구조체에 값을 설정해야한다. 사각형은 4개의 파라미터를 갖는다.

  • 화면 상단 왼쪽 구석의 x/y 좌표
  • 사각형의 너비/높이

화면(윈도우 창)의 상단 왼쪽 구석이 (0, 0)이고 양의 값 x는 오른쪽 방향으로 증가하고 양의 값 y는 아래 방향으로 증가한다.

SDL_Rect 구조체는 위에서부터 차례대로 왼쪽 상단 x좌표, 왼쪽 상단 y좌표, 너비, 높이를 입력한다. 그리고 SDL_Rect 포인터를 받는 SDL_RenderFillRect 함수를 통해 사각형을 그린다.


퐁 게임에서 필요한 패들과 공을 그리기 위해 두 오브젝트의 중심 좌표를 저장하는 구조체를 선언한다. 그리고 Initialize 함수에서 각 구조체의 값을 초기화한다.

중심점을 이용해서 패들과 공의 사각형을 그린다.

※ static_cast 연산자
타입 변환 연산자, 컴파일 시간에 형 변환에 대한 타입 오류를 잡아준다.

실시간과 게임 시간

현실 세계의 실제 시간과 게임 세계의 게임 시간(game time)을 구별하는 것은 중요하다. 예를 들어 정지 상태의 게임을 생각해보자. 현실 세계에서는 많은 시간이 걸릴 수도 있지만 게임은 전혀 진행되지 않는다. 플레이어가 게임을 재개해야 게임도 갱신을 재개한다. 게임에서의 여러가지 기능들(원래 시간보다 느리게 갱신되는 '불릿 타임(bullet time)'이나 반대로 스포츠 게임에서 원래 시간보다 시간이 더 빠르게 흐르게 할 수도 있고, 특정 시간으로 되돌아가는 기능도 있다.) 때문에 실제 시간과 게임 시간은 다를 수 있으므로 '게임 갱신' 단계에서는 경과된 게임 시간을 고려해야 한다.

델타 시간 함수 로직

초창기 게임 프로그래머들은 특정 프로세서 속도와 특정 프레임 레이트를 가정하고 프로그래밍했다. 프로그래머는 8MHZ 프로세서를 가정하고 코드를 작성했으며, 해당 프로세서에서 제대로 작동하면 코드는 정상적으로 작동하는 것으로 간주했다. 고정된 프레임 레이트라 가정하면 적의 위치를 갱신하는 코드는 다음과 같을 것이다.

// x좌표에 5 픽셀을 더해서 갱신
enemy.mPosition.x += 5;

8MHZ 프로세서에서 해당 코드가 정상 작동한다면 16MHZ프로세서에서는 게임 루프가 두 배 빠르게 실행되므로 적들 또한 두 배 빠르게 움직일 것이다. 현대 프로세서는 훨씬 빠르므로 게임은 엄청 빨리 끝날 것이다. 이 문제를 해결하기 위해서 게임은 델타 시간(delta time)을 사용한다. 델타 시간은 마지막 프레임 이후로 경과된 게임 시간을 뜻한다. 이전의 코드를 델타 시간을 사용하도록 수정하려면 프레임마다 픽셀 이동을 생각하는 것 대신에 초당 픽셀 이동을 생각해야한다. 그래서 이상적인 이동 속도가 초당 150픽셀이라면 다음 코드가 훨씬 유연성이 있다.

// x 위치를 초당 150픽셀만큼 갱신
enemy.mPosition.x += 150 * deltaTime;

이제 프레임 레이트와 상관없이 잘 작동할 것이다. 30FPS에서 델타 시간은 ~0.033이므로 적은 프레임마다 5픽셀을 이동할 것이고 초당 150픽셀을 이동할 것이다. 60FPS에서는 적이 프레임마다 2.5픽셀을 이동할 것이다. 하지만 여전히 초당 150픽셀을 이동하게 될 것이다. 30FPS나 60FPS나 초당 속도는 동일하게 유지한다. 게임은 다양한 프레임 레이트에서 동작하므로 게임 상의 모든 것을 델타 시간의 함수로 계산해야한다. SDL은 델타 시간을 계산하는 데 도움을 주기 위해 SDL_Init 함수 호출 이후로 경과된 시간(밀리초, 1000분의 1초)을 반환하는 SDL_GetTicks 멤버 함수를 제공한다. 이전 프레임의 SDL_Ticks 결과값을 멤버 변수에 저장하고 현재 프레임에서 SDL_GetTicks을 얻으면 델타 시간을 계산할 수 있다.
먼저 mTicksCount 멤버 변수를 선언한다.

Uint32 mTicksCount;

그런 다음 SDL_GetTicks를 사용해서 UpdateFrame의 첫 번째 버전을 만든다/

델타 시간은 현재 프레임의 틱값과 이전 프레임 틱값의 차(양의 값)다. 그리고 1000.0f를 나눠서 초 단위의 델타 시간을 얻는다. 그러나 물리(점프류의 플랫폼 게임)에 의존하는 게임은 프레임 레이트에 따라 동작에 차이가 발생할 수 있다. 가장 간단한 해결 책은 프레임 제한(frame limit)을 구현하는 것이다. 프레임을 제한하면 게임 루프를 목표 델타 시간이 경과할 때까지 기다리도록 한다. 예를 들어 목표 프레임이 60FPS라고 가정하자. 프레임이 단 15ms만에 완료되면 게임 루프는 목표 델타 시간 16.6ms를 충족하기 위해 ~1.6ms를 추가로 기다린다. SDL은 프레임 제한을 위한 방법을 제공한다. 위 소스 코드에서 보이듯 적어도 프레임 간 16ms가 경과함을 보장하고 싶다면 SDL_TICK_PASSED 함수와 while문을 이용하면 된다. 또한 너무 큰 델타 시간에 주의해야한다. 예를 들어 프로세스를 정지시키면 프로그램은 매우 큰 델타 시간을 가지게 돼 예상치를 초과해 훨씬 더 앞으로 점프하게 될 것이다. 따라서 델타시간을 0.05와 같은 최대값으로 고정하면 된다. 위 소스코드에서 if문에서 최대 델타 시간 값을 고정하는 것이다.

패들의 위치 갱신하기

패들을 W키를 누르면 위로 S키를 누르면 아래로 이동시켜본다. 키를 누르지 않거나 동시에 누르면 움직이지 않도록 한다. mPaddleDir 정수형 멤버 변수를 사용해서, 패들이 움직이지 않으면 0, 위로 움직이면 -1(음수 y) 아래로 움직이면 1(양수 y)로 설정하면 패들의 움직임을 구현할 수 있다. ProcessInput 함수에서 mPaddleDir를 갱신하는 코드가 필요하다.

이러한 방법은 플레이어가 동시에 키를 눌렀을때 mPaddleDir 값이 0임을 보장한다. 다음으로 UpdateGame에서 델타 시간 값으로 패들을 갱신하는 코드를 추가한다.

위 코드에서는 패들의 방향과 초당 300.0f픽셀의 속도, 그리고 델타 시간으로 패들의 y 좌표를 갱신한다. mPaddleDir가 -1이라면 패들이 위로 1이라면 패들이 아래로 이동한다.

아래 if문을 추가해서 패들이 너무 위나 아래로 가서 화면 밖으로 벗어나는 것을 방지한다.

공 위치 갱신하기

공의 위치를 갱신하는 것은 패들의 위치를 갱신하는 것보다 좀 더 복잡한데 공이 x, y방향으로 이동하고, 패들이나 벽에 부딪히며 그때마다 이동 방향을 바꾸기 때문이다. 그래서 공의 속도(velocity, 속력과 방향)이 필요하며 충돌 감지(collision detection)가 필요하다. 공의 속도를 저장하기 위해 mBallVel 이름으로 Vector2 멤버 변수를 추가한다. 그리고 mBallVel을 (-200.0f, 235.0f)로 초기화한다. 초당 x방향으로 -200픽셀, y방향으로 235픽셀 이동한다는 것을 의미한다. 속도로 공의 위치를 갱신하기 위해 UpdateGame에 다음과 같이 두 줄의 코드를 추가한다.

다음으로 벽과 충돌했을 때 튀어나오게 하는 코드가 필요하다. 그리고 충돌했을 때 무엇을 해야 하냐가 중요하다. 예를 들어 공이 벽에 충돌하기 전에 위쪽, 오른쪽 방향으로 이동한다고 가정했을 때 공이 위쪽 벽과 부딪혔다면 아래쪽, 오른쪽 방향으로 이동해야 할 것이다. 따라서 위쪽 벽에 충돌했을때 공의 방향을 바꾸는 코드를 작성해야한다.

if (mBallPos.y <= thickness) {
	mBallVel.y *= -1;
}

위 코드에는 한 가지 큰 문제가 있다. 공과 벽이 충돌했을때 공이 벽과 충분히 멀어지지 않아서 다시 공의 속도의 y값이 다시 음수로 바뀌었다가 다시 양수로 바뀌는 것을 반복할 것이다. 그래서 공은 영원히 위쪽 벽에 붙어있게 될 것이다. 그래서 위쪽 벽과 부딪혔을때 추가적으로 y 속도 값이 음수인지를 확인한다.

if (mBallPos.y <= thickness && mBallVel.y < 0.0f) {
	mBallVel.y *= -1;
}

패들과 공의 충돌은 약간 더 복잡하다 패들과 공의 x 좌표가 나란해야하고 공의 y 좌표가 패들의 y 좌표 내에 있어야한다. 그래서 공의 중심의 y 위치와 패들의 중심의 y 위치 사이의 차 절댓값을 계산해서 이 차이가 패들의 높이 절반보다 크다면 공이 패들의 범위 밖에 있는 것이다. 또 공이 패들에 가까워지는지(x의 속도가 음수인지)도 확인해야한다.

이제 게임의 Hello World인 퐁 게임을 완성했다.

0개의 댓글