首页 > 解决方案 > 游戏时间步长上的灯光师:std::chrono implementation

问题描述

如果您不熟悉 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 );
}

目标:

我目前的尝试(半固定):

#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();
}

这个实现的问题

我的实际问题

如果你使用 count(),和/或你的计时码中有转换因子,那么你就太努力了。所以我想也许有更直观的方法。

标签: c++c++11game-enginechronogame-loop

解决方案


下面我使用Fix your Timestep实现了几个不同版本的“最终触摸” <chrono>。我希望这个例子能转化为你想要的代码。

主要的挑战是弄清楚在Fix your Timestepdouble中每个代表什么单位。一旦完成,转换为相当机械。<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的 chronoseconds转换为 a double,因此它可以参与物理计算。

std::literals并且1s是 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中的所有内容几乎完全相同,除了一些doubles 更改为 type duration<double>(如果它们代表持续时间),而另一些则更改为time_point<Clock, duration<double>>(如果它们代表时间点)。

dt单位为duration<double>(以双精度为基础的秒数),我假设Fix your Timestep中的 0.01是 o 型,所需的值为 1./60。在 C++111.0s/60.中可以更改为seconds{1}/60..

和 的本地类型别名设置为使用duration和-基于秒。time_pointClockdouble

从这里开始,代码几乎与Fix your Timestep相同,除了使用durationtime_point代替doublefor 类型。

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

  • 如何用 std::chrono::high_resolution_clock::now() 替换 SDL_GetTicks()?似乎无论我需要使用什么 count()

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

  • 我如何用实际的 std::chrono_literal 时间值替换所有浮点数,除了我将浮点 deltaTime 作为模拟修改器传递给更新函数的末尾?

如上所述,您不需要将浮点数传递delaTime给更新函数,除非该函数签名超出您的控制范围。如果是这样的话,那么:

m_gameController->Update(deltaTime/1s);

版本 2

现在让我们更进一步:我们真的需要对 duration 和 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,由 a 表示long long,单位为1 / 60秒。

  • duration现在有一个奇怪的类型,我们甚至不必知道细节。Clock::duration它与 a和的总和类型相同dt。这将是可以精确表示 aClock::duration1 / 60秒的最粗精度。谁在乎它是什么。重要的是,基于时间的算术不会有截断误差,如果Clock::duration是基于积分的,甚至不会有任何舍入误差。(谁说在计算机上不能准确表示1 / 3?!)

  • 0.25s限制改为转换为250ms(在milliseconds{250}C++11 中)。

  • 的计算alpha应积极转换为基于双精度的单位,以避免与基于整数的除法相关的截断。

更多关于Clock

  • 如果steady_clock您不需要映射t到物理中的日历时间,和/或您不在乎是否会t慢慢偏离确切的物理时间,请使用。没有时钟是完美的,并且steady_clock永远不会调整到正确的时间(例如通过 NTP 服务)。

  • 如果system_clock您需要映射t到日历时间,或者如果您想t与 UTC 保持同步,请使用。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";
}

推荐阅读