본문 바로가기

CS/프밍 잡지식

컴퓨터의 시간측정 응용 - FixedUpdate

https://basaeng.tistory.com/28

 

컴퓨터의 시간측정

개요코드의 성능 측정을 위해 시작 끝 지점의 시간을 구해서 사용하거나, n초 후에 어떤 로직을 실행해야 하는 등 코드를 만들 때 시간을 측정해야 하는 경우가 있다. 이 때 시간을 어떻게 받아

basaeng.tistory.com

 

개요

Unity같은 엔진에서는 FixedUpdate같은 프레임 보정 함수들이 있다. 

"간단하게는 항상 일정한 시간 간격으로 호출된다.(0.02s = 50fps)" 라고 말하지만 실제 0.02초마다 한번 보다는 1초에 50번을 보장한다 같은 느낌이 더 가깝다. 

https://docs.unity3d.com/6000.1/Documentation/ScriptReference/MonoBehaviour.FixedUpdate.html

 

이번에는 이러한 개념을 이해해보고, 이전의 시간측정 함수를 통해 간단하게 구현해보자.


게임에서의 프레임

게임에서 한 프레임은 입력 처리 -> 로직 처리 -> 렌더링 처리 같은 흐름이 한번 실행되는 것을 '한 프레임' 이라고 한다.

 

또한 FPS(Frames Per Second)는 '1초에 프레임이 몇번 실행되는가' 를 뜻한다.

일반적인 50fps 라면 약 20ms 마다 한번의 프레임이 실행되는 것과 같다.

    for (;;)
    {
        Sleep(20);
    }

위의 루프는 '50fps(Sleep 함수로 인해 실제로는 더 느림) 로 도는 루프' 라고 볼 수 있다.

 

그렇다면 루프에서 기본적으로 로직이 한번 실행된다면 렌더링은 한번 되고, 로직이 열번 실행되면 렌더링은 열번 실행될 것이다.

 

여기서 생길 수 있는 문제가 뭘까?

 

렌더링 작업으로 인해 프레임이 드랍된다면 로직도 같이 느려지는 문제가 생긴다.

이러한 상황이 발생하면, 오브젝트 좌표 이동 등의 상황에서 항상 프레임에 따라 속도가 바뀌는 문제가 생길 것이다.

 

이 문제를 어떻게 해결하면 좋을까?


Frame Skip, FixedUpdate

보통 렌더링보다는 로직 작업이 우선시 되기 때문에 아래와 같은 아이디어를 떠올 릴 수 있다.

1. 프레임이 드랍되어, 한 프레임 내에서 로직이 실행되지 않을 수 있다면, 렌더링을 스킵한다.

2. 프레임이 드랍되어, 한 프레임 내에서 로직이 실행되지 않았다면, 다음에 로직을 몰아서 더 실행한다.

 

1번을 Frame Skip, 2번을 Fixed Update 방식이라고 한다.

 

프레임 스킵의 경우 프레임 드랍이 심해지면, 유저는 화면이 멈춘 것 처럼 보일 것이다.

아래는 실제 프레임 스킵 기능이 있는 테라리어에서 유저들이 나눈 얘기이다.

https://forums.terraria.org/index.php?threads/what-is-the-frame-skip.39882/

 

Terraria Community Forums

Official Terraria Community Forums

forums.terraria.org

 

고정 업데이트의 경우 프레임 드랍이 심해지면 몰아서 실행할 로직이 많아지고, 이로 인해 다시 프레임이 회복하지 못하는 악순환이 발생할 수 있다.

 

이번에는 FixedUpdate를 구현해보겠다.


FixedUpdate 간단 구현해보기

단계를 나눠서 구현해보겠다.

 

1. 50fps 만들기?

1초에 루프가 50번 돌게 하려면 어떻게 해야할까? 위의 코드 처럼 Sleep(20)을 통해 루프마다 20ms씩 휴식하기 때문에 로직의 시간이 매우 짧다면 큰 문제가 없을 것이다.

 

void fps()
{
	static int fpsCnt;
	static unsigned long fpsTime = timeGetTime();
	++fpsCnt;

	unsigned long curTick = timeGetTime();
	if (curTick - fpsTime >= 1000)
	{
		printf("FPS: %d\n", fpsCnt);
		fpsCnt = 0;
		fpsTime = curTick;
        //fpsTime += 1000; //(다음 프레임에 포함된 시간)
	}
}

int main() {
    timeBeginPeriod(1);

    for (;;)
    {
		fps();
        Sleep(20);
    }


    timeEndPeriod(1);
    return 0;
}

함수 fps는 fps를 측정하기 위해 static fpsCnt와 fpsTime을 사용한다.

fpsCnt는 해당 함수를 실행하면 1증가한다. static이기 때문에 상태는 유지될 것이다. 

fpsTime은 함수를 처음 호출했을 때 시간을 초기화 한다.

이후에는 함수에 진입했을 때의 시간 curTick과 비교해 1000(1s)가 지나면, fpsCnt가 몇인지 출력할 것이다.

 

if 문 안에서는 1초가 지났기 때문에, fpsCnt는 초기화 시키고, curTick을 fpsTime에 대입해 다음 프레임을 위한 계산 값에 사용한다.

이 경우 실행시키면 위와 같이 50과 49의 숫자가 나오는 것을 확인할 수 있다. 현재 프레임은 49~50인 것일까?

 

 

그런데 주석이 있는 것을 보아 눈치챘듯이 위의 코드에는 큰 문제가 있다. 바로 curTick을 fpsTime에 넣는 부분이다.

printf는 입출력 함수로 매우 큰 시간을 소요하기에, curTick을 측정했던 시점과, 대입하는 시점은 다르다.

 

결정적으로, 만약에 curTick - fpsTime이 1000이 아니라 1003과 같은 값이라면, 그대로 대입하기 때문에 오차가 계속해서 누적될 것이다.

이것은 우리가 원하는 것이 아니기 때문에 이 코드 대신에 주석을 풀고 진행하자.

 

fpsTime += 1000을 해서 고정시간을 더한다면 다음 목표가 정확하게 1초 뒤로 설정될 것이다.

막상 주석으로 코드를 바꾸면, 프레임이 이전보다 더 떨어져보인다.

측정자체는 알맞게 바꿨기 때문에, fps자체가 50이 아니라는 것이다.

 

이러한 이유는 Sleep(20)이 20ms를 정확하게 쉬지 않는 것 때문이다.

 

그렇다면, 이것또한 fps함수의 측정처럼, 고정시간을 활용한다면 해결될 것이다.

고정시간에 로직에 소요된 시간을 뺀만큼 Sleep한다면, 20ms를 지킬 수 있을 것이다.

 

void fps()
{
	static int fpsCnt;
	static unsigned long fpsTime = timeGetTime();
	++fpsCnt;

	unsigned long curTick = timeGetTime();
	if (curTick - fpsTime >= 1000)
	{
		printf("FPS: %d\n", fpsCnt);
		fpsCnt = 0;
		//fpsTime = curTick;
		fpsTime += 1000; //(다음 프레임에 포함된 시간)
	}
}

int main() {
    timeBeginPeriod(1);
	int FRAME_DURATION = 20;
	int next = timeGetTime();
    for (;;)
    {
		next += FRAME_DURATION;
		fps();
		int now = timeGetTime();
		int sleepTime = next - now;
		if (sleepTime > 0)
		{
			Sleep(sleepTime);
		}
    }


    timeEndPeriod(1);
    return 0;
}

이제 fps가 50으로 정상 출력된다.

 

추가로, 만약 로직 처리 시간이 너무 길어 next-now가 0 이하가 된다면, Sleep 하지 않는다.

 

2. FixedUpdate

이제 위의 코드를 기반으로 fixedUpdate를 만들어보자.

fixedUpdate의 기본 개념은 위에서 언급했듯이, 만약 프레임 드랍(누적오차가 증가)이 생기면 이를 위해 이후에 로직을 추가로 더 실행시키는 방법이다.

 

그렇다면 기본적으로 누적오차를 계산해야 한다.

만약 누적오차가 한 프레임(20ms)보다 크다면 20씩 감소시키며 계속해서 실행시키면 될 것이다.

 

int main() {
    timeBeginPeriod(1);
	int FRAME_DURATION = 20;
	DWORD prev = timeGetTime();
	DWORD next = timeGetTime();
	DWORD accumulatedTime = 0;
	for (;;)
	{
		DWORD now = timeGetTime();
		accumulatedTime += now - prev;
		prev = now;

		while (accumulatedTime >= FRAME_DURATION)
		{
			fps();
			accumulatedTime -= FRAME_DURATION;
		}

		Sleep(rand() % 10);

		DWORD afterLoop = timeGetTime();
		DWORD sleepTime = FRAME_DURATION - (afterLoop - now);
		if (sleepTime > 0)
		{
			Sleep(sleepTime);
		}
	}

    timeEndPeriod(1);
    return 0;
}

이제 누적 시간을 계산하고, 누적 시간이 프레임 이상인 경우 루프를 돌며 실행하는 것을 볼 수 있다. 

만약 40이 누적되었다면 2번 60이 누적되었다면 3번을 몰아 실행할 것이다.

 

변화를 확인하기 위해 Sleep을 랜덤하게 하는 코드를 넣었다. 이 코드가 렌더링이라고 생각하면 된다.

 

왼쪽은 fps함수를 FixedUpdate 로직 안에 넣은 것, 오른쪽은 fps함수를 밖으로 옮긴 것이다.

 

FixedUpdate 로직 안에서는 적게 실행된 만큼 보정하기에 FPS50을 유지하고, 없는 곳에서는 그대로 로직이 적게 실행됨을 확인할 수 있다.


간단하게 구현했기에 더 정교한 로직도 가능할듯하다.

 

'CS > 프밍 잡지식' 카테고리의 다른 글

함수 호출 시 일어나는 일  (0) 2025.06.29
switch case 그리고 점프 테이블  (1) 2025.04.17
컴퓨터로 문자를 표현하는 법  (0) 2024.12.18