이론/디자인패턴

게임 루프

tsyang 2022. 1. 31. 18:50

게임 루프


게임 루프는 게임 시간 진행을 유저 입력, 프로세서 속도(!)와 디커플링 한다.

 

유저 입력은 알겠는데 프로세서 속도와 디커플링 한다는게 무슨 말일까?

 

굉장히 간단한 게임 루프를 보자.

while(true)
{
	processInput();
	update();
	render();
}

processInput에서 유저의 입력을 처리하고, update에서 게임을 시뮬레이션 하고, render에서는 화면을 그린다.

 

당연한 얘기지만, 컴퓨터가 좋을 수록 1초에 돌릴 수 있는 루프의 횟수가 증가한다. 즉, 게임이 빨리 돌아가게 된다.

 

즉, 게임의 속도가 프로세서의 속도와 커플링 되어있는 것이다. 

 

https://youtu.be/r4EHjFkVw-s

144hz로 게임을 플레이하면 속도가 빨라지는 폴아웃4

 


 

어떻게 디커플링?


그럼 어떻게 프로세서 속도와 게임 진행을 디커플링 할 수 있을까?

 

 

빨리 처리하고 쉬기(X)

while(true)
{
	double startTime = Time::getCurrentTime();
	
	processInput();
	update();
	render();

	double deltaTime = Time::getCurrentTime() - startTime;
    
	// MS_PER_FRAME : 한 프레임에 사용할 시간 60fps라면 16ms)
	sleep(MS_PER_FRAME - deltaTime);
}

게임을 60FPS로 돌리는게 목표라면 한 프레임을 16ms(MS_PER_FRAME) 동안 진행해야 한다. 만약 16ms보다 루프를 빨리 돌았으면 16ms가 될 때 까지 기다린다.

 

이런 방법은 게임이 빨리 진행되는걸 막을 수 있지만 느리게 진행되는 것은 막지 못한다

 

 

 

시간 차이를 넘겨주기(X)

이전 프레임과 현재 프레임의 시간 차이(deltaTime)을 update()에 넘겨주어 시뮬레이션 할 수 있다. 

while(true)
{
	double curTime = Time::getCurrentTime();
	double deltaTime = curTime - lastTime;
	lastTime = curTime;
	
	processInput();
	update(deltaTime);
	render();
}

update()에 시간을 넘겨준다. 따라서 게임 오브젝트를 시뮬레이션 할 때는 정해진 시간 간격만큼만 업데이트를 하면 된다.  deltaTime은 그때그때 달라질 수 있다. 즉, 시간 간격은 가변적이다.

 

그러나 이런 방법에는 치명적인 문제가 있는데, 바로 게임이 비결정적이게 된다는 것이다. 즉, 시뮬레이션 할 때 마다 다른 결과가 나올 수 있다. 왜 그럴까?

 

똥컴과 좋은 컴퓨터가 있다. 이 컴퓨터들을 게임을 각각 10fps, 100fps로 돌린다. 그 말은 좋은 컴퓨터에서는 1초에 100번을 업데이트 하는 반면, 똥컴에서는 10번밖에 업데이트 하지 못한다. 

 

게임을 시뮬레이션 할 때는 오차가 발생하기 마련이다. 대표적으로는 부동 소수점 연산을 통한 오차와, 물리 엔진에서 더 빠른 연산을 위해 근사값을 사용하여 발생하는 오차가 있다. 당연히 이런 오차는 좋은 컴퓨터에서 더 많이 누적된다. 날아가는 총알을 예로 들자면, 결국 PC에 따라 총알의 위치가 달라질 수 있다는 것이다. 

 

 

 

고정된 시간을 넘겨주기(O)

앞선 해결책에서는 시간 간격이 가변적이기 때문에 게임이 비결정적이게 되었다. 이를 수정하기 위해 update()에 고정된 시간(FIXED_DELTA_TIME)을 넘겨준다.

 

double accumulated_time = 0.0;
while(true)
{
	double curTime = Time::getCurrentTime();
	double deltaTime = curTime - lastTime;
	lastTime = curTime;
	accumulated_time += deltaTime;
	
	processInput();
	while(accumulated_time >= FIXED_DELTA_TIME)
	{
		update(FIXED_DELTA_TIME);
		accumulated_time -= FIXED_DELTA_TIME;
	}
	render();
}

위 코드에서는 시간 차이를 누적한다. 누적된 시간이 FIXED_DELTA_TIME보다 크거나 같은 동안 게임을 FIXED_DELTA_TIME만큼 시뮬레이션 한다.

 

이제 모든 컴퓨터에서는 동일하게 고정된 시간 간격만큼 게임을 시뮬레이션 한다. 하드웨어에 따라 게임의 상태가 달라지는 일이 없다. 즉, 게임이 결정적(deterministic)이게 된다.

 

그러나 만약 update()를 수행하는데 드는 시간이 FIXED_DELTA_TIME보다 오래 걸린다면? 게임은 계속 뒤쳐지고 누적 시간은 폭발적으로 증가한다. 그렇기 때문에 시간 간격은 충분히 여유롭게 설정해야 한다.

 

 


 

렌더링은?


이제 고정된 시간 간격을 사용함으로써 게임을 여러 하드웨어에서 잘 돌아가게 만들었다. 렌더링은 가능할 때 마다 호출된다. 만약 컴퓨터가 좋다면 초당 100프레임 이상을 렌더링 할 것이다. 만약 고정된 시간 간격이 여유롭게 1/30초 정도로 설정되어 있다면 어떨까? 렌더링이 아무리 자주 일어나도 플레이어의 눈에는 30fps로 보일 것이다.

 

이를 해결하기 위해, 렌더링을 할 때 게임 상태가 업데이트 되고 얼마나 지났는지를 알려줄 수 있다.

//지금 업데이트와 다음 업데이트 사이의 어디에 있는지를 넘겨준다.
render(accumulated_time / FIXED_DELTA_TIME);

 

 

render()에서는 넘겨받은 인자를 통해 게임 객체들이 어디에 있을지 예측한다. 이런 예측이 틀리면 움직임이 튀는 현상이 일어날 수 있지만, 더 부드러운 게임 화면을 볼 수 있다는 장점이 존재한다.

 


참고 : 로버트 나이스트롬, 게임 프로그래밍 패턴, 한빛미디어, [161-177p]

Glenn Fiedler - 'Fix your Timestep' (https://web.archive.org/web/20210224113810/https://gafferongames.com/post/fix_your_timestep/)

John Austin - 'FIX YOUR (UNITY) TIMESTEP!' (https://johnaustin.io/articles/2019/fix-your-unity-timestep)

Koen Witters - 'deWiTTERS Game Loop' (https://dewitters.com/dewitters-gameloop/)

 

'이론 > 디자인패턴' 카테고리의 다른 글

업데이트 메서드  (0) 2022.02.20
타입 객체  (0) 2022.02.10
이중 버퍼  (0) 2022.01.26
서비스 중개자  (0) 2022.01.16
이벤트 큐 패턴  (1) 2022.01.03