c++ - Gaffer游戏时间步长:std::chrono实现

标签 c++ c++11 game-engine chrono game-loop

如果您不熟悉Gaffer on Games文章“固定时间步长”,可以在这里找到:
https://gafferongames.com/post/fix_your_timestep/

我正在构建一个游戏引擎,并且为了使自己对std::chrono更加满意,我一直在尝试使用std::chrono实现固定的时间步,现在已经..几天了,我无法似乎缠住了我的头。这是我正在努力的伪代码:

double t = 0.0;
double dt = 0.01;

double currentTime = hires_time_in_seconds();
double accumulator = 0.0;

State previous;
State current;

while ( !quit )
{
    double newTime = time();
    double frameTime = newTime - currentTime;
    if ( frameTime > 0.25 )
        frameTime = 0.25;
    currentTime = newTime;

    accumulator += frameTime;

    while ( accumulator >= dt )
    {
        previousState = currentState;
        integrate( currentState, t, dt );
        t += dt;
        accumulator -= dt;
    }

    const double alpha = accumulator / dt;

    State state = currentState * alpha + 
        previousState * ( 1.0 - alpha );

    render( state );
}

目标:
  • 我不希望渲染受帧速率限制。我应该在忙循环中渲染
  • 我想要一个完全固定的时间步,在其中我用float delta时间
  • 调用更新函数
  • 不睡觉

  • 我目前的尝试(半固定):
    #include <algorithm>
    #include <chrono>
    #include <SDL.h>
    
    namespace {
        using frame_period = std::chrono::duration<long long, std::ratio<1, 60>>;
        const float s_desiredFrameRate = 60.0f;
        const float s_msPerSecond = 1000;
        const float s_desiredFrameTime = s_msPerSecond / s_desiredFrameRate;
        const int s_maxUpdateSteps = 6;
        const float s_maxDeltaTime = 1.0f;
    }
    
    
    auto framePrev = std::chrono::high_resolution_clock::now();
    auto frameCurrent = framePrev;
    
    auto frameDiff = frameCurrent - framePrev;
    float previousTicks = SDL_GetTicks();
    while (m_mainWindow->IsOpen())
    {
    
        float newTicks = SDL_GetTicks();
        float frameTime = newTicks - previousTicks;
        previousTicks = newTicks;
    
        // 32 ms in a frame would cause this to be .5, 16ms would be 1.0
        float totalDeltaTime = frameTime / s_desiredFrameTime;
    
        // Don't execute anything below
        while (frameDiff < frame_period{ 1 })
        {
            frameCurrent = std::chrono::high_resolution_clock::now();
            frameDiff = frameCurrent - framePrev;
        }
    
        using hr_duration = std::chrono::high_resolution_clock::duration;
        framePrev = std::chrono::time_point_cast<hr_duration>(framePrev + frame_period{ 1 });
        frameDiff = frameCurrent - framePrev;
    
        // Time step
        int i = 0;
        while (totalDeltaTime > 0.0f && i < s_maxUpdateSteps)
        {
            float deltaTime = std::min(totalDeltaTime, s_maxDeltaTime);
            m_gameController->Update(deltaTime);
            totalDeltaTime -= deltaTime;
            i++;
        }
    
        // ProcessCallbackQueue();
        // ProcessSDLEvents();
        // m_renderEngine->Render();
    }
    

    此实现存在问题
  • 渲染,处理输入等与帧速率
  • 相关
  • 我正在使用SDL_GetTicks()代替std::chrono

  • 我的实际问题
  • 如何将SDL_GetTicks()替换为std::chrono::high_resolution_clock::now()?似乎无论我需要使用count()是什么,但我都从霍华德·辛南特(Howard Hinnant)自己身上读过这句话:

  • If you use count(), and/or you have conversion factors in your chrono code, then you're trying too hard. So I thought maybe there was a more intuitive way.


  • 如何将所有float替换为实际的std::chrono_literal时间值,除了获得float deltaTime传递到更新函数中作为模拟修改器的末尾之外?
  • 最佳答案

    下面,我使用<chrono>Fix your Timestep实现了几个“最终触摸”版本。我希望该示例将转换为所需的代码。

    主要的挑战是弄清楚Fix your Timestep中每个double代表什么单位。一旦完成,到<chrono>的转换就相当机械了。

    前事

    这样我们就可以轻松更改时钟,以Clock类型开始,例如:

    using Clock = std::chrono::steady_clock;
    

    稍后我将展示如果需要,甚至可以根据Clock实现SDL_GetTicks()

    如果您可以控制integrate函数的签名,那么我建议为时间参数使用基于双秒的秒单位:
    void
    integrate(State& state,
              std::chrono::time_point<Clock, std::chrono::duration<double>>,
              std::chrono::duration<double> dt);
    

    这将允许您传递所需的任何内容(只要time_point基于Clock),而不必担心显式转换为正确的单位。加上物理计算通常是在浮点数中完成的,因此这也适用于此。例如,如果State仅包含加速度和速度:
    struct State
    {
        double acceleration = 1;  // m/s^2
        double velocity = 0;  // m/s
    };
    

    并且integrate应该计算新的速度:
    void
    integrate(State& state,
              std::chrono::time_point<Clock, std::chrono::duration<double>>,
              std::chrono::duration<double> dt)
    {
        using namespace std::literals;
        state.velocity += state.acceleration * dt/1s;
    };
    

    表达式dt/1s只是将基于double的chrono seconds转换为double,以便它可以参与物理计算。
    std::literals1s是C++ 14。如果您坚持使用C++ 11,则可以将它们替换为seconds{1}

    版本1
    using namespace std::literals;
    auto constexpr dt = 1.0s/60.;
    using duration = std::chrono::duration<double>;
    using time_point = std::chrono::time_point<Clock, duration>;
    
    time_point t{};
    
    time_point currentTime = Clock::now();
    duration accumulator = 0s;
    
    State previousState;
    State currentState;
    
    while (!quit)
    {
        time_point newTime = Clock::now();
        auto frameTime = newTime - currentTime;
        if (frameTime > 0.25s)
            frameTime = 0.25s;
        currentTime = newTime;
    
        accumulator += frameTime;
    
        while (accumulator >= dt)
        {
            previousState = currentState;
            integrate(currentState, t, dt);
            t += dt;
            accumulator -= dt;
        }
    
        const double alpha = accumulator / dt;
    
        State state = currentState * alpha + previousState * (1 - alpha);
        render(state);
    }
    

    此版本使所有内容与Fix your Timestep几乎完全相同,除了一些double更改为duration<double>类型(如果它们表示持续时间),而其他一些更改为time_point<Clock, duration<double>>(如果它们表示时间点)。
    dt的单位为duration<double>(基于双秒的秒数),我假设Fix your Timestep中的0.01是type-o,并且所需的值为1./60。在C++ 11中,1.0s/60.可以更改为seconds{1}/60.

    设置durationtime_point的本地类型别名以使用基于Clockdouble的秒。

    从现在开始,该代码几乎与Fix your Timestep相同,除了使用durationtime_point代替double作为类型。

    注意alpha不是时间单位,而是无量纲的double系数。

    • How can I replace SDL_GetTicks() with std::chrono::high_resolution_clock::now()? It seems like no matter what I need to use count()


    如上。没有使用SDL_GetTicks().count()

    • How can I replace all the floats with actual std::chrono_literal time values except for the end where I get the float deltaTime to pass into the update function as a modifier for the simulation?


    如上所述,除非该函数签名不受您的控制,否则您无需将float delaTime传递给更新函数。如果是这种情况,则:
    m_gameController->Update(deltaTime/1s);
    

    版本2

    现在让我们更进一步:我们是否真的需要对持续时间和time_point单位使用浮点数?

    没有。使用基于积分的时间单位可以执行以下操作:
    using namespace std::literals;
    auto constexpr dt = std::chrono::duration<long long, std::ratio<1, 60>>{1};
    using duration = decltype(Clock::duration{} + dt);
    using time_point = std::chrono::time_point<Clock, duration>;
    
    time_point t{};
    
    time_point currentTime = Clock::now();
    duration accumulator = 0s;
    
    State previousState;
    State currentState;
    
    while (!quit)
    {
        time_point newTime = Clock::now();
        auto frameTime = newTime - currentTime;
        if (frameTime > 250ms)
            frameTime = 250ms;
        currentTime = newTime;
    
        accumulator += frameTime;
    
        while (accumulator >= dt)
        {
            previousState = currentState;
            integrate(currentState, t, dt);
            t += dt;
            accumulator -= dt;
        }
    
        const double alpha = std::chrono::duration<double>{accumulator} / dt;
    
        State state = currentState * alpha + previousState * (1 - alpha);
        render(state);
    }
    

    与版本1相比,实际上几乎没有什么变化。
  • dt现在具有值1,由long long表示,并且具有1/60秒的单位。
  • duration现在具有一个奇怪的类型,我们甚至不必了解其详细信息。它与Clock::durationdt的总和类型相同。这将是最精确的精度,它可以精确地表示Clock::duration和1/60秒。谁在乎它是什么。重要的是,如果Clock::duration是基于整数的,则基于时间的算法将没有截断错误,甚至没有舍入错误。 (谁说不能完全代表计算机上的1/3 ?!)
  • 0.25s限制被转换为250ms(在C++ 11中为milliseconds{250})。
  • alpha的计算应积极地转换为基于 double 的单位,以避免与基于整数的除法相关的截断。

  • 有关Clock的更多信息
  • 如果您不需要将steady_clock映射到物理学中的日历时间,并且/或者您不关心t是否缓慢偏离实际的物理时间,请使用t。没有时钟是完美的,并且steady_clock永远都不会调整为正确的时间(例如NTP服务)。
  • 如果需要将system_clock映射到日历时间,或者希望t与UTC保持同步,请使用t。游戏进行时,这需要对Clock进行一些小的(可能是毫秒级或更小)调整。
  • 如果您不在乎是high_resolution_clock还是steady_clock,并且每次将代码移植到新平台或编译器时都会感到惊讶,请使用system_clock。 :-)
  • 最后,如果愿意,您甚至可以坚持使用SDL_GetTicks(),方法是编写自己的Clock,如下所示:

  • 例如。:
    struct Clock
    {
        using duration = std::chrono::milliseconds;
        using rep = duration::rep;
        using period = duration::period;
        using time_point = std::chrono::time_point<Clock>;
        static constexpr bool is_steady = true;
    
        static
        time_point
        now() noexcept
        {
            return time_point{duration{SDL_GetTicks()}};
        }
    };
    

    切换:
  • using Clock = std::chrono::steady_clock;
  • using Clock = std::chrono::system_clock;
  • using Clock = std::chrono::high_resolution_clock;
  • struct Clock {...}; // SDL_GetTicks based

  • 要求对事件循环,物理引擎或渲染引擎进行更改。只是重新编译。转换常数会自动更新。因此,您可以轻松地尝试哪种Clock最适合您的应用程序。

    附录

    我的完整State代码是完整的:
    struct State
    {
        double acceleration = 1;  // m/s^2
        double velocity = 0;  // m/s
    };
    
    void
    integrate(State& state,
              std::chrono::time_point<Clock, std::chrono::duration<double>>,
              std::chrono::duration<double> dt)
    {
        using namespace std::literals;
        state.velocity += state.acceleration * dt/1s;
    };
    
    State operator+(State x, State y)
    {
        return {x.acceleration + y.acceleration, x.velocity + y.velocity};
    }
    
    State operator*(State x, double y)
    {
        return {x.acceleration * y, x.velocity * y};
    }
    
    void render(State state)
    {
        using namespace std::chrono;
        static auto t = time_point_cast<seconds>(steady_clock::now());
        static int frame_count = 0;
        static int frame_rate = 0;
        auto pt = t;
        t = time_point_cast<seconds>(steady_clock::now());
        ++frame_count;
        if (t != pt)
        {
            frame_rate = frame_count;
            frame_count = 0;
        }
        std::cout << "Frame rate is " << frame_rate << " frames per second.  Velocity = "
                  << state.velocity << " m/s\n";
    }
    

    关于c++ - Gaffer游戏时间步长:std::chrono实现,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59441699/

    相关文章:

    c++ - 如何向该程序添加循环?

    c++ - 在 VS2015 C++ 中打印我的用户名

    c++ - 搭建C++游戏引擎增量开发环境

    java - 平稳移动物体,性能差

    Java 3D 文档

    c++ - 在重载的全局新运算符中使用静态对象导致核心转储运行时错误

    c++ - 返回类型 '?:'(三元条件运算符)

    c++ - 由于 if 语句 c++11 而避免重复代码的不便

    c++ - 自动提供getter和setter函数的基类C++

    c++ - 修改const shared_ptr&传递的数据可以吗?