::NPTEAM:: Network Programer Team

검색 :
RSS 구독 : 글 / 댓글 / 트랙백 / 글+트랙백

Windows 시간 관련 프로그래밍 정보

Edited by Dual(dual5651@hotmail.com)

이번글은 블로깅을 하면서 발견한 시간관련 글들을
종합하여 편집 함으로써 이부분에 대해 궁금해 하시는
분에게 도움이 되고자 해서 편집 & 작성하여 보았습니다.


1. Tick 구하기 - 1

타이머 문제로 골몰하다 멀티미디어 라이브러리의 바이너리를
뒤적거리기로 했다. 한참동안 winmm.lib 의 네이티브 코드를
뒤지다가 재미난 것을 한가지 발견. 일단 해당 부분을 뜯어
간단히 인라인 어셈으로 옮겨 보았다.

inline unsigned __int64 getMisteryCount()
{
  __asm  
  {  
    mov    edx,dword ptr ds:[7FFE000Ch]
    mov    eax,dword ptr ds:[7FFE0008h]
  }
}

아무런 문서도 없이 바이너리를 뒤지다 얻어낸 정보인지라
저 주소가 무엇을 의미하는지는 나도 모른다. 구글에도
없는것을 보니 완전한 un-documented feature 같다.

어쨌든 저 주소에는 1/10000 초 단위의 시간이 저장되어 있다.
물론 아래와 같이 10000 을 나누어 milli-second 단위로 변환해서
사용하는것도 가능하다.

inline unsigned int getElapsedMilliSecond()
{
  return static_cast< unsigned int >
  ( getMisteryCount()/10000 );
}

mov 를 두번 호출할 뿐인지라 속도는 경이적이다.
빠르지만 최악의 정밀도를 보여주는 GetTickCount()
보다도 4배정도 빠르다. 아무일도 하지 않고 루프만 돈
noproc() 에 거의 근접한다.



getMisteryCount() 와 getElapsedMilliSecond() 의
속도차이의 이유는 64-bit div 연산에 있다. 32-bit 프로세서에서
__int64 나누기를 하려면 수십개의 인스트럭션이 필요하다.
테스트 결과 getMisteryCount() 는 Win95/98/Me 등에선 동작하지
않는다. XP , 2000, 2003 등 NT 커널 기반 OS 에서만 돌아가는 듯
하다. 어쨌건 퍼포먼스 카운트용으로 이보다 더 좋은 타이머는
없을 듯 하다.

2. Tick 구하기 - 2

대부분 Speed Hack은 kernel.dll에 있는 GetTickCount()나
winmm.dll에 있는 timeGetTime() 함수를 조작해서 만들어 진다.
그러나 그러한 역할을 하는 걸 Win32API를 쓰지 않고 아래와 같은
ASM을 사용한다면 해결을 할 수 있지 않을까 싶다.

_asm
{
RDTSC
mov DWORD PTR iCurrentTick, eax
mov DWORD PTR iCurrentTick+ 4, edx
}

RDTSC 어셈은 8x86 CPU에 들어 있는 ASM이다.
CPU의 클럭값을 eax와 edx에 return하는 어셈이라
2개의 레지스터에서 메모리로 옮기면 GetTickCount()
역할을 할 수 있다.

3. Win32 환경에서의 타이머 종류

Ansi C의 time(), _ftime()

Ansi 표준 C 라이브러리의 함수들로,
time()은 최소 1초 단위의 값을 돌려준다.
메인 루프 제어용으로는 부적합하나 게임 저장
날짜/시간의 기록 등에는 유용할 것임.
_ftime()은 밀리초(1000분의 1초) 단위의 값을 돌려준다.

Win32 API의 timeGetTime() 함수

흔히 멀티미디어 타이머라고 하는 것으로,
윈도우즈가 시작된 이후 흐른 시스템 시간을 돌려준다.
단위는 밀리초. 기본 정밀도는 Win9x의 경우 1 밀리초,
NT 패밀리의 경우 5 밀리초 이상이라고 함.
이정도면 _ftime()과 함께 게임에서 써먹을 수 있을 만한 함수.

타이머의 해상도는 timeGetDevCaps()와
timeBeginPeriod()로 조정할 수 있다.

Win32 API의 WM_TIMER 메시지

일정한 시간 주기로 발생하는 메시지.
주기의 최소 단위는 역시 밀리초 단위.
메시지 펌프에서 잡거나 SetTimer()를 이용해서
콜백 함수를 호출하게 만들 수 있음.
그러나 메시지의 우선 순위가 낮기 때문에 정확하지 못하다.

Win32 API의 GetTickCount() 함수

시스템 틱 카운트를 돌려주며, 단위는 밀리초.
Win9x의 경우 기본 정밀도는 약 55 밀리초,
NT 급은 10에서 16 정도... timeGetTime()보다 못함...

고해상도 타이머

하드웨어가 고해상도 타이머를 지원하는 경우라면
가장 좋은 선택으로, 매우 정밀한 시간을 얻을 수 있다.
얻을 수 있는 시간의 해상도는 하드웨어마다 다르나,
어쨌든 적어도 위에 나온 것들보다는 정밀하다고 함.
관련 함수(자세한 사항은 MSDN 참고)

둘 다 windows.h만 포함시키면 됨...

BOOL QueryPerformanceCounter(
LARGE_INTEGER *lpPerformanceCount  // pointer to counter value
);

BOOL QueryPerformanceFrequency(
LARGE_INTEGER *lpFrequency  // address of current frequency
);

고해상도 타이머의 해상도 및 지원 여부 알아내기

QueryPerformanceFrequency(&ticksPerSecond)의 반환값이 0이
아니면 고해상도 타이머를 지원하는 것이다.
이 때 tickPerSecond에는 초 당 틱 수가 설정된다.

예:

LARGE_INTEGER ticksPerSecond;
if (!QueryPerformanceFrequency(&ticksPerSecond))
{
  // 지원하지 않음
  return false;
}

현재 시간 얻기

QueryPerformanceCounter(&ticks)를 호출하면 ticks에
성능 카운터 수(음.. 컴퓨터가 켜진 후 지나간 틱 수)가 설정된다.
이를 위에서 얻은 ticksPerSecond로 나누면 초
단위의 시간을 얻을 수 있다.

예:

LARGE_INTEGER ticks;
QueryPerformanceCounter(&ticks);

float seconds = ((float)ticks.QuadPart
            / (float)ticksPerSecond.QuadPart;


지나간 시간 얻기

게임에서 실제로 필요한 것은 이전 프레임으로부터 흐른 시간인데,
그냥 현재 시간에서 이전 시간을 빼면 됨...

예: float GetElapsedSeconds(unsigned long elapsedFrames = 1)
{
static LARGE_INTEGER s_lastTime = m_startTime;
LARGE_INTEGER currentTime;

QueryPerformanceCounter(&currentTime);

float seconds = ((float)currentTime.QuadPart -
      (float)s_lastTime.QuadPart) / (float)m_ticksPerSecond.QuadPart;

// reset the timer
s_lastTime = currentTime;

return seconds;
} // end GetElapsedSeconds()

QueryPerformanceCounter의 문제점

일부 메인보드/칩셋에서 시간이 건너뛴다는 보고가 있음.

RDTSC

인텔 펜티엄 계열 CPU에서 제공하는 어셈블리 명령이다.
펜티엄은 내부적으로 TSC(Time Stamp Counter)라는
64비트 카운터를 유지하는데 이 카운터의 값은 클럭
사이클마다 증가한다.
RDTSC 명령은 내부 TSC 카운터의 값을 EDX와 EAX 레지스터에
복사하는 명령이다.
이 명령은 6~11 클럭을 소요한다. 고해상도 타이머가
이 명령을 이용해 구현되었다고 한다.

시간에 기반한 게임 갱신


고정 시간 간격

한 프레임에 걸리는 시간을 고정시키는 것.
간단히 말하면, 한 프레임에 걸리는 표준 목표
시간을 설정하고, 한 프레임에 걸린 시간이 그보다
작으면 남는 부분만큼 아무 일도 하지 않고 기다리는 것이다.

의사코드:

게임 메인 루프
{
      현재시각을 얻는다.

      한 프레임 처리;

      흐른시간 = 현재시각 - 아까 얻은 현재시각

      만일 ( 흐른시간 < 목표시간)
      {
        (목표시간 - 흐른시간)만큼 쉰다. //예: Sleep(...);
      }
}

장점: 시간 관리가 직관적이고 간편하다(시간 자체를 중심으로 바라볼 때).
또한 멀티미디어와의 시간 동기화가 편하다
(애니메이션과 배경음악/대사를 일치시키는 등).

단점: 한 프레임 처리에 걸린 시간이 목표시간보다 더 긴 경우 게임의 속도가
일정하지 않게 된다. 또한 최소 사양 이하의 컴퓨터라면 게임 전체가 느리게
돌아가게 된다.


가변 시간 간격

게임의 기능 및 객체들이 이전 프레임 이후 흐른 시간
(이하 프레임 시간)에 기반해서 갱신되게 하는 것.
예를 들어 어떤 객체가 1초당 10미터를 수평 이동
한다고 하면, 객체의 x 좌표는: x = x + 10 * (프레임 시간)
이렇게 하면 프레임 시간에 상관없이(즉 CPU 속도나 기타
다른 요인과 독립적으로) 객체는 항상 초 당 10 미터의
속력을 가지게 된다.

장점: 객체의 입장으로 바라볼 때 직관적.
게임을 실세계의 단위(미터, 초 등)에 기반해서 모델링할 때 크게 유용할 수 있다.

단점: 모든 객체들의 갱신 방식을 시간에 기반한 함수로 생각할 수 있어야 한다.
3D라면 당연하지만 전통적인 2D 게임이라면 사고의 전환이 필요함...

출처 : http://dual.inxzone.net/backup/blog/index.php?pl=184&ct1=1
2006/10/03 20:42 2006/10/03 20:42

맨 위로