게임 타이머 구현
Project/Win32API로 던그리드 모작

게임 타이머 구현

Woon2World :: Programming & Art Life

 

가변 프레임

이전의 게임 타이머 구현은 아래 글에서 볼 수 있다.

 

https://jartlife.tistory.com/52

 

게임 프레임 관리

게임의 동작 게임은 일정 시간 간격마다 사용자 입력을 처리하고 객체들을 업데이트한 뒤 객체들을 렌더링한다. 따라서 게임에서 타이머의 역할은 매우 중요하다. 게임의 거의 모든 기반이 타

jartlife.tistory.com

 

유저는 30프레임, 60프레임 등등 FPS(시간 당 프레임)를 조절할 수 있다.

 

가변 프레임 환경에선, 프레임 기반의 이벤트 처리 요청이 정확한 시간 조건을 보장하지 못한다.

 

현재 FPS가 20 FPS라면 60 프레임 뒤에 발생할 사건은 3초 뒤에 발생하는 것이 되고,

현재 FPS가 60 FPS라면 60 프레임 뒤에 발생할 사건은 1초 뒤에 발생하는 것이 된다.

 

따라서 타이머는 프레임 기반 처리 뿐만 아니라 실시간 기반 처리도 가능해야 한다.

 

몇 프레임 뒤에 사건이 발생하길 원하는 경우보다, 몇 초 뒤에 사건이 발생하길 원하는 경우가 훨씬 많기 때문이다.

 

 

 

 

또한, 기존 방식에선 update()가 무조건 렉 없이 성공할 것을 가정하는 반면,

 

가변 프레임 방식에선 FPS를 너무 높이 설정할 경우 프레임에 할당된 시간 안에 update()를 처리하지 못하는 경우도 발생한다.

 

따라서 이에 대한 추가적인 처리도 필요하다.

( 렉이 프레임에 할당된 시간의 2배를 넘는 경우, update() 도 생략하는 것으로 구현했다. )

 

 

 

 

기존의 timer 클래스를 수정하여, 내부적으로 worldtime_timer와 frame_timer를 갖도록 했다.

 

실시간 기반 처리는 worldtime_timer, 프레임 기반 처리는 frame_timer 가 관장한다.

 

사용자는 여전히 timer 클래스만 알고 있으면 되지만, 이용할 수 있는 기능이 늘었다.

 

 

 

 


개요

timer

: 게임 내 모든 시간 관련 동작을 담당하는 클래스입니다.

프로그램 내 유일하게 존재합니다.

 

  • on_timer
    시간 기반 처리
    : 일정 시간(프레임 당 시간)마다 이 함수를 호출하면 됩니다.
    게임을 업데이트, 렌더합니다.

  • ms_per_frame
    현재 프레임 당 시간
    • return type : const float
      : 현재 프레임 당 시간을 반환합니다.

  • setFPS
    FPS 설정
    • parameters : fps
      : 설정할 fps 값이 필요합니다.

  • curFPS
    현재 FPS
    • return type : const int
      : 현재 FPS 값을 반환합니다.

  • add_request
    실시간 기반 처리 요청
    • parameters : ms_time, func
      : ms_time 은 호출 당시 기준, 몇 밀리초 뒤에 처리될지를 결정합니다.
      func 은 ms_time 뒤 처리(실행)될 함수입니다.

      • func 에는 std::function<void()> 로 변환할 수 있는 함수가 들어가야 합니다.
        람다 표현식을 이용해 실제 호출할 함수를 감싸도록 하는 것을 권장합니다.
        예) [ &args... ]() { your_static_func( args... ); }
        [ this, &args... ]() { this->your_member_func( args... ); }

  • add_loop_request
    실시간 기반 반복적 처리 요청
    • parameters : ms_time, func, condition
      : add_request 와 유사하나, condition이 추가되었습니다.
      condition 은 처리의 계속 조건이 됩니다.

      • condition 에는 std::functioin<bool()> 로 변환할 수 있는 함수가 들어가야 합니다.
        람다 표현식을 이용해 실제 호출할 함수를 감싸도록 하는 것을 권장합니다.
        예) [ &args... ]() { return your_static_func( args... ); }
        [ this, &args... ]() { return this->your_member_func( args... ); }

 

 

 


코드

#ifndef _timer
#define _timer
 
#ifdef max
#undef max        // c 매크로 max를 제거하기 위함
#endif
 
#include <chrono>
#include <iostream>
#include "TMP.h"
#include <algorithm>
#include <queue>
#include <numeric>
#include <deque>
#include <functional>
#include <utility>
 
class timer
{
public:
    // after ms_time, func will be executed.
    // func sholde be compatible with std::function< void() >
    template < typename Func >
    static void add_request( const double ms_time, Func func ) noexcept
    {
        worldtime_timer::add_request( worldtime_timer::request{ ms_time, func } );
    }
 
    // every ms_time, func will be executed.
    // func sholde be compatible with std::function< void() >
    // Condition should be compatible with std::function< bool() >
    template < typename Func, typename Condition >
    static void add_loop_request( const double ms_time, Func func, Condition condition ) noexcept
    {
        add_request( ms_time, [ func, condition ]() {
            if ( condition() )
            {
                func();
                add_loop_request( ms_time, func, condition );
            } } );
    }
 
    static constexpr void setFPS( const int fps ) noexcept { frame_timer::setFPS( fps ); }
    static constexpr const float ms_per_frame() noexcept { return frame_timer::ms_per_frame(); }
    static constexpr const int curFPS() noexcept { return static_cast< int >1000.0f / ms_per_frame() ); }
 
    static void on_timer()
    {
        std::cout << "\n현재 FPS : " << curFPS() << std::endl;
 
        static float lag = 0;
        std::cout << "루틴 시작 당시 lag : " << lag << std::endl;
        auto elapsed = static_cast< float >( timefunc( go_routines, lag ) );
        lag = std::max( lag + elapsed - ms_per_frame(), 0.0f );
    }
 
    timer() = delete;
 
private:
    static void go_routines( const float current_lag )
    {
        if ( current_lag < ms_per_frame() * 2 )
        {
            worldtime_timer::on_timer();
            frame_timer::on_timer( current_lag );
        }
    }
 
    class frame_timer
    {
    public:
        static constexpr void setFPS( const int fps ) noexcept
        {
            _ms_per_frame = 1000.0f / fps;
        }
 
        static constexpr const float ms_per_frame() noexcept
        {
            return _ms_per_frame;
        }
 
        static void on_timer( const float current_lag )
        {
            update();
            if ( current_lag < ms_per_frame() )
            {
                render();
            }
        }
 
    private:
 
        static void update()
        {
            std::cout << "업데이트 루틴!\n";
            for ( int i = 0; i < 20000000++i )
            {
                i += 2;
            }
        }
 
        static void render()
        {
            std::cout << "렌더 루틴!\n";
            for ( int i = 0; i < 40000000++i )
            {
                i += 2;
            }
        }
 
        static float _ms_per_frame;
    };
 
    class worldtime_timer
    {
    public:
        class request
        {
        public:
            template < typename Func >
            explicit request( const double ms_time, Func func ) :
                _world_time_expected{ ms_time + world_time }, func{ func } {}
 
            request() = delete;
 
            request( const request& other ) : _world_time_expected{ other._world_time_expected },
                func{ other.func } {}
 
            request& operator=const request& other )
            {
                if ( this != &other )
                {
                    _world_time_expected = other._world_time_expected;
                    func = other.func;
                }
 
                return *this;
            }
 
            request( request&& other ) noexcept : _world_time_expected{ other._world_time_expected },
                func{ std::move( other.func ) } {}
 
            request& operator=( request&& other ) noexcept
            {
                if ( this != &other )
                {
                    _world_time_expected = other._world_time_expected;
                    func = std::move( other.func );
                }
 
                return *this;
            }
 
            const bool operator>const request& other ) const noexcept
            {
                return _world_time_expected > other._world_time_expected;
            }
 
            const double world_time_expected() const noexcept
            {
                return _world_time_expected;
            }
 
            void sub( const double value ) noexcept
            {
                _world_time_expected -= value;
            }
 
            void handle() const
            {
                func();
            }
 
        private:
            double _world_time_expected;
            std::function< void() > func;
        };
 
        static void on_timer()
        {
            update_world_time();
            prevent_world_time_overflow();
 
            while ( should_handle_a_request() )
            {
                handle_a_request();
            }
 
            std::cout << "worldtime : " << world_time << std::endl;
            std::cout << "남은 요청 수 : " << requests.size() << std::endl;
        }
 
        static void add_request( request&& req )
        {
            requests.push( req );
        }
 
    private:
        static void update_world_time()
        {
            using namespace std::chrono;
            static auto last_time = system_clock::now();
            auto cur_time = system_clock::now();
 
            world_time += duration_cast< nanoseconds >( cur_time - last_time ).count()
                / static_cast< double >( nanoseconds::period::den / milliseconds::period::den );
            last_time = cur_time;
        }
 
        static void prevent_world_time_overflow()
        {
            if ( world_time > world_time_limit() )
            {
                world_time -= world_time_limit();
                rearrange_requests( world_time_limit() );
            }
        }
 
        static void rearrange_requests( const double sub_val )
        {
            decltype( requests ) temp;
 
            while ( !requests.empty() )
            {
                auto req = requests.top();
                req.sub( sub_val );
 
                temp.push( std::move( req ) );
                requests.pop();
            }
 
            requests = std::move( temp );
        }
 
        static constexpr const double world_time_limit()
        {
            constexpr const double prevent_time = 100000;            // 100s
            return std::numeric_limits< double >::max() - prevent_time;
        }
 
        static const bool should_handle_a_request()
        {
            if ( requests.empty() )
            {
                return false;
            }
            return requests.top().world_time_expected() < world_time;
        }
 
        static void handle_a_request()
        {
            requests.top().handle();
            requests.pop();
        }
 
    private:
        static double world_time;
        static std::priority_queue< request, std::deque< request >std::greater< request > > requests;
    };
};
 
float timer::frame_timer::_ms_per_frame = 1000.0f / 30;
double timer::worldtime_timer::world_time = 0.0;
std::priority_queue< timer::worldtime_timer::request,
    std::deque< timer::worldtime_timer::request >,
    std::greater< timer::worldtime_timer::request > > timer::worldtime_timer::requests;
 
#endif
cs

 

 

 

 


예시

#include <iostream>
#include "randomvalue.h"
#include "timer.h"
#include <windows.h>
#include <random>
#include <chrono>
 
using namespace std;
using namespace std::chrono;
 
int main()
{
    auto last = system_clock::now();
 
    timer::setFPS( 60 );
 
    for ( int i = 0; i < 1000++i )
    {
        auto val = random_value( 100100000 );
        timer::add_request( val, [ val ]() { std::cout << val << " ms 요청 처리!\n"; } );
    }
 
    for ( ;; )
    {
        auto cur = system_clock::now();
        
        auto elapsed = duration_cast< milliseconds >( cur - last ).count();
 
        if ( elapsed > timer::ms_per_frame() )
        {
            timer::on_timer();
            last = cur;
        }
 
        Sleep( 5 );
    }
}
cs

 

 

만일 해당 코드를 실행시켜 보고 싶다면

 

 

namespace _rv
{
    std::random_device rd;
    std::default_random_engine dre( rd() );
}
 
const auto random_value( const std::uniform_int_distribution<>& uid ) PURE
{
    return uid( _rv::dre );
}
 
const auto random_value( const int lower_bound, const int upper_bound ) PURE
{
    return random_value( std::uniform_int_distribution<>{lower_bound, upper_bound} );
}
cs

 

 

template < typename _Timet = std::chrono::milliseconds, typename Func, typename ... Args >
auto timefunc( Func func, Args... args )
{
    using namespace std::chrono;
    using _Countt = std::chrono::nanoseconds;
 
    auto tp = system_clock::now();
    func( args... );
 
    return duration_cast<_Countt>( system_clock::now() - tp ).count()
        / static_cast<long double>( _Countt::period::den / _Timet::period::den );
}
cs

 

 

이 코드들도 필요할 것이다.

randomvalue.h 에 있는 random_value 와

TMP.h 에 있는 timefunc 의 구현이다.

 

 

요청들은 처리되어야 할 타이밍에 잘 처리되고 있다.

cout 출력은 경우에따라 0.5 ms 정도 걸리기도 하고 10ms 정도 걸리기도 해서, world_time 이 밀려 보이는 것은 이 때문이다. 실제 게임에선 체크용 cout 출력을 하지 않을 것이므로 문제 없다.

 

world_time 이 오버플로우가 일어나더라도 요청은 원하는 시간에, 원했던 순서대로 처리 된다.

world_time_limit() 의 반환값 을 수정하여 오버플로우 상황을 테스트해 볼 수 있다.

 

 

 

 


decltype 을 난사하고 싶은 욕구가 있었는데, decltype 이 과연 코드 가독성에 미치는 영향이 어느 정도일까 고민이 됐다.

돌아보니 이미 중첩 클래스 같은 걸 쓰고 있는 시점에서 이 고민은 의미가 없었다 ㅋㅋㅋㅋㅋㅋㅋ

 

이번 코드는 중간 과정 없이 거의 한 번에 짜서 정말 버그가 많을 거라 생각했는데, 놀랍게도 하나의 버그도 없었다.

 

버그가 있어도 곤란하지만 없어도 곤란하다는 심정을 알 것 같다. 좀 더 유심히 봐야겠다.

 

마음같아서는 람다 표현식 없이 멤버 함수를 바로 전달하도록 하고 싶지만 STL에 담으려면 동일한 자료형이어야 하고,, 그래서 타협을 본게 람다 표현식. ㅠㅠ

 

오버플로우 처리가 잘 되는지 확인 하기 위해 worldtime_limit을 500, 1000 정도로 바꿔봤는데 처리에는 문제가 없었다.

 

언제 그림 그리고 소리 나게 하냐....

 

 

 

 

'Project > Win32API로 던그리드 모작' 카테고리의 다른 글

게임 클래스와 씬 클래스 추가  (0) 2021.11.06
타이머 간략화  (0) 2021.11.06
git repository 생성  (0) 2021.09.17
현재 적용할 메모리 풀  (0) 2021.09.15
메모리 풀, 오브젝트 풀 개념  (0) 2021.08.25