게임에서 Frame rate 에 관한 문제가 크게 2가지가 있다.
- 게임을 일정한 속도로 진행시켜야 한다는 문제.
- 게임의 Frame rate 제한을 두어 CPU를 불태우지 않도록 하는 문제.
이 중 1번 문제에 대해서는 아래의 유명한 글에서 다룬다.
https://gafferongames.com/post/fix_your_timestep/
이 글에서는 위 링크된 글을 토대로 1번 문제를 그림과 함께 살펴보도록 한다.
2번 문제에 대해서는 간단히 살펴보고, 추후에 글을 업데이트 하든 해야겠다.
- 여기서 timestep 이란, delta time 을 의미한다.
- 예시 코드 스니펫은 C++20, SFML을 사용한다.
필요성
// 게임 루프
while (game.isRunning()) {
game.processEvents();
game.update();
game.render();
}
위 게임 루프의 경우, 컴퓨터의 성능에 따라 게임 루프가 다른 속도로 실행될 것이다.
그 결과, 성능이 빠른 컴퓨터에서는 게임이 빠르게 진행되고,
게임이 환경에 따라 서로 다른 속도로 진행되는 문제가 있으니, 뭔가 조치가 필요하다.
1. Variable delta time
코드
sf::Clock clock;
while (game.isRunning()) {
// sf::Clock::restart() 는 해당 타이머의 경과 시간을 반환하고, 재시작
sf::Time frameTime = clock.restart();
game.processEvents();
game.update(frameTime); // 경과 시간에 맞는 상태 업데이트 진행
game.render();
}
설명
한 번의 게임 루프를 도는 동안, 경과 시간을 frameTime
으로 계산해서 Game::update(frameTime)
으로 전달한다.
전달받은 경과 시간을 토대로 적은 시간이 흘렀으면 조금 이동하고, 많은 시간이 흘렀으면 많이 이동하는 식으로 게임을 업데이트 하면 될 것이다.
문제점
게임 루프가 엄청 느리게 실행되는 바람에, frameTime
이 5초나 걸렸다고 해보자.
이동거리 = 속도 * frameTime
으로 이동 처리를 한다면, 위치가 너무 많이 달라져서
중간에 벽이 있어도 무시하고 그대로 뚫고 넘어갈 수 있는 문제가 발생할 수 있다.
(그림)
저런 극단적인 경우가 아니더라도, 경과 시간에 따라 미묘하게 게임 물리가 다르게 느껴질 상황이 많을 것이다.
2. Semi-fixed timestep
아이디어
한 번의 게임 루프 진행시에,
게임의 논리적 상태를 일정 단위(SEC_PER_FRAME
)로 나눠서 업데이트한다.
예를 들어 이번 게임 루프에 frameTime == 2.7/60
만큼 진행시켜야 하면, 아래처럼 3번 나눠서 업데이트를 수행한다.
deltaTime == 1/60 (SEC_PER_FRAME)
주고 업데이트deltaTime == 1/60
주고 업데이트deltaTime == 0.7/60
주고 업데이트
그 뒤에 화면을 1번 그린다.
(N-1)번까지는 SEC_PER_FRAME
으로 timestep 이 고정되지만,
마지막 1번은 그것보다 적은 timestep 으로 업데이트가 수행되므로 반쪽짜리 고정 타임스텝 Semi-fixed timestep 이라 부르는 듯하다.
코드
const int FPS = 60;
const sf::Time SEC_PER_FRAME = sf::seconds(1.f / FPS);
sf::Clock clock;
while (game.isRunning()) {
sf::Time frameTime = clock.restart();
// 죽음의 소용돌이 임시방편 해결책, 아래 주석을 해제해 frameTime 상한을 둔다.
// frameTime = std::min(frameTime, sf::seconds(0.25f));
while (frameTime > sf::seconds(0.f)) {
sf::Time deltaTime = std::min(frameTime, SEC_PER_FRAME);
frameTime -= deltaTime;
game.processEvents();
game.update(deltaTime);
}
game.render();
}
장단점
이제 각 update()
에 전달되는 deltaTime
의 상한이 생겼다.
게임 상태를 1번 업데이트 할 때는 아무리 오래 걸려도 SEC_PER_FRAME
를 초과하지 않는다는 말.
대신에 1번의 게임 루프 실행 시에 update()
가 여러 번 호출될 수 있다.
만일 update 가 render 보다 무거운 작업이라면, 성능이 안 좋아질 수 있다.
심할 경우 아래에서 설명할 죽음의 소용돌이 spiral of death 문제가 발생할 수 있다.
문제점과 해결
죽음의 소용돌이
1번의 게임 루프를 처리한다고 하자.
여러 번의 update 를 통해 게임의 물리 상태를 X
만큼 진행시켰다.
그런데 그 모든 물리 업데이트를 처리하는 데 X
보다 더 긴 시간 Y
가 걸린다면?
다음 번 루프에는 Y
시간의 물리 업데이트를 처리해야 할 것이다.
마찬가지로 그걸 처리하는 데도 Y
보다 더 긴 시간이 걸릴 것이고…
이런 식으로 해야 할 물리 처리 frameTime
이 매 루프마다 점점 불어나면
결국 무한히 지연될텐데, 이를 죽음의 소용돌이 spiral of death 라고 부른다.
해법
이걸 해결하는 궁극적인 방법은 X
만큼의 게임 물리 상태 업데이트하는데
X
보다 훨씬 적은 시간이 들도록 최적화를 잘 하는 것이다.
그러면 어쩌다 순간적인 랙이 발생하더라도 금방 물리 업데이트가 쫓아갈테니 괜찮다.
임시방편의 해결책도 있다.
frameTime
에 상한을 둬서 최대 지연 시간을 제한하는 것이다.
게임이 아예 멈추는 것보다야 잠깐씩 멈칫거리는 게 나을테니…
위 코드에서 주석 처리 해놓은 코드를 해제하면 이 임시방편 해결책이 적용된다.
3. Free the physics (Hard-fixed timestep)
필요성
위의 Semi-fixed timestep 도 실제로 자주 쓰이지만, 아쉬운 점이 있다.
마지막 1번의 update 의 시간이 일정하지가 않아서, 같은 입력에 대해 항상 같은 결과를 보장하지 못한다는 점이다.
만일 네트워크 동시 플레이를 지원해야 한다면, 물리 상태를 동기화하기 난해할 수 있다.
아이디어
위의 Semi-fixed timestep 에서 Semi- 에 해당하는 부분을 제거한다.
(N-1)번 업데이트 후 마지막 1번의 update 를 처리하기엔 잔여시간이 SEC_PER_FRAME
보다 적을 것이다.
이 잔여시간을 Semi- 에서는 전부 소모했다면, 이번 Hard- 에서는 남겨놓고 다음 루프에 누적하여 처리하게 한다.
간단히 말해 애매한 잔여시간을 다음 루프로 넘겨버리는 것이다.
같은 예시를 들면, frameTime == 2.7/60
를 진행시킬 때, 아래처럼 2번만 업데이트를 수행한다.
deltaTime == 1/60 (SEC_PER_FRAME)
주고 업데이트deltaTime == 1/60
주고 업데이트
남은 0.7/60
초는 이번엔 진행을 포기하고, 다음 게임 루프로 누적해 넘겨서 처리하게 한다.
코드
const int FPS = 60;
const sf::Time SEC_PER_FRAME = sf::seconds(1.f / FPS);
sf::Clock clock;
// 지연시간을 누적할 변수
sf::Time timeSinceLastUpdate = sf::Time::Zero;
while (game.isRunning()) {
timeSinceLastUpdate += clock.restart();
// 죽음의 소용돌이 임시방편 해결책
timeSinceLastUpdate = std::min(timeSinceLastUpdate, sf::seconds(0.25f));
while (timeSinceLastUpdate > SEC_PER_FRAME) {
timeSinceLastUpdate -= SEC_PER_FRAME;
game.processEvents();
game.update(SEC_PER_FRAME);
}
game.render();
}
위 코드를 보면 game.update(SEC_PER_FRAME)
처럼, 완전히 고정된 Hard-fixed 델타 타임으로 업데이트 됨을 알 수 있다.
The final touch
필요성
위의 Hard-fixed timestep 을 그대로 써도 무방하나 여전히 아쉬운 점이 있다.
덜덜거리는 움직임 stutter
(수정 중)
아이디어
코드
참고 자료
마지막 수정 : 2022-11-16 12:00:00 +0900