Gaffer的游戏时间步长:std :: chrono实现 [英] Gaffer on games timestep: std::chrono implementation

查看:79
本文介绍了Gaffer的游戏时间步长:std :: chrono实现的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如果您不熟悉Gaffer on Game文章固定时间步长,可以在这里找到:
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();
双累加器= 0.0;

前一州;
状态电流;

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

累加器== frameTime;

而(累加器> = dt)
{
previousState = currentState;
Integrated(currentState,t,dt);
t + = dt;
累加器-= dt;
}

const double alpha =累加器/ dt;

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

render(state);
}

目标:




  • 我不想渲染受帧速率限制。我应该在忙循环中渲染

  • 我想要一个完全固定的时间步,在其中我用 float 增量时间调用更新函数

  • 不睡觉



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

  #include< algorithm> 
#include< chrono>
#include< SDL.h>

名称空间{
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();
而(m_mainWindow-> IsOpen())
{

float newTicks = SDL_GetTicks();
float frameTime = newTicks-previousTicks;
previousTicks = newTicks;

//帧中的32毫秒将使其为0.5,16毫秒将为1.0
float totalDeltaTime = frameTime / s_desiredFrameTime;

//不要在
以下执行任何操作,而(frameDiff< frame_period {1})
{
frameCurrent = std :: chrono :: high_resolution_clock ::现在();
frameDiff = frameCurrent-framePrev;
}

使用hr_duration = std :: chrono :: high_resolution_clock :: duration;
framePrev = std :: chrono :: time_point_cast< hr_duration>(framePrev + frame_period {1});
frameDiff = frameCurrent-framePrev;

//时间步长
int i = 0;
而(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



我的实际问题




  • 如何用 std :: chrono :: high_resolution_clock :: now() SDL_GetTicks() c $ c>?看来无论我需要使用什么 count(),但我都从霍华德·辛南特(Howard Hinnant)自己身上读过这句话:




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





  • 如何替换所有具有实际std :: chrono_literal时间值的 float s,除了我得到float deltaTime传递到更新函数中作为模拟修改器的末尾以外?


解决方案

下面,我实现了使用< chrono> 固定您的时间步长。我希望该示例将转换为您所需的代码。



主要挑战是弄清楚每个 double 表示固定时间步长。完成此操作后,对< chrono> 的转换就相当机械化了。



前沿问题



为了方便地更改时钟,请从 Clock 类型开始,例如:

 ,使用Clock = std :: chrono :: steady_clock; 

稍后我将证明一个人甚至可以拥有 Clock 可以根据 SDL_GetTicks()来实现。



如果您可以控制签名,则集成函数,对于时间参数,我建议使用基于双秒的单位:

 无效
集成(状态和状态,
std :: chrono :: time_point< Clock,std :: chrono :: duration< double>> ;,
std :: chrono: :duration< double> dt);

这将允许您传递想要的任何内容(只要 time_point 基于 Clock ),而不必担心显式转换为正确的单位。再加上物理计算通常是在浮点数中完成的,因此这也适用于此。例如,如果 State 仅持有加速度和速度:

  struct状态
{
双重加速= 1; // m / s ^ 2
double velocity = 0; // m / s
};

integrate 应该计算出新的速度:

  void 
积分(State& state,
std :: chrono :: time_point< Clock ,std :: chrono :: duration< double>,
std :: chrono :: duration< double< double dt)
{
使用命名空间std :: literals;
state.velocity + = state.acceleration * dt / 1s;
};

表达式 dt / 1s 只是将基于 double 的时间转换为 double 参加物理计算。



std :: literals 1s 是C ++ 14。如果您坚持使用C ++ 11,则可以用 seconds {1} 替换它们。



版本1

 使用命名空间std :: literals; 
auto constexpr dt = 1.0s / 60。
使用duration = std :: chrono :: duration< double> ;;
使用time_point = std :: chrono :: time_point<时钟,持续时间> ;;

time_point t {};

time_point currentTime = Clock :: now();
持续时间累加器= 0s;

州先前的州;
State currentState;

而(!quit)
{
time_point newTime = Clock :: now();
auto frameTime = newTime-currentTime;
if(frameTime> 0.25s)
frameTime = 0.25s;
currentTime = newTime;

累加器== frameTime;

而(累加器> = dt)
{
previousState = currentState;
tegral(currentState,t,dt);
t + = dt;
累加器-= dt;
}

const double alpha =累加器/ dt;

State state = currentState * alpha + previousState *(1-alpha);
render(state);
}

此版本使修正您的时间步,除了某些 double 更改为 duration< double> (如果它们代表持续时间),其他则更改为 time_point< Clock,duration< double>> (如果它们代表时间点)。



dt 的单位为 duration< double> (基于双秒的秒数),我假设>确定您的时间步长中的0.01 是o型,期望值为1./60。在C ++ 11中, 1.0s / 60。可以更改为 seconds {1} / 60。

设置了持续时间 time_point 的本地类型别名使用基于时钟和基于 double 的秒。



从此以后,代码几乎与固定时间步长相同,除了使用持续时间 time_point 代替 double 类型。



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



  • 如何用std :: chrono :: high_resolution_clock替换SDL_GetTicks(): :现在()?似乎不管我需要使用count()


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



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


如上所述,您不需要传递将 delaTime 浮动到更新函数,除非该函数签名不受您的控制。如果是这种情况,则:

  m_gameController-> Update(deltaTime / 1s); 

版本2



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



不是。使用基于整数的时间单位可以执行以下操作:

 使用命名空间std :: literals; 
auto constexpr dt = std :: chrono :: duration< long long,std :: ratio< 1,60> {1};
使用duration = decltype(Clock :: duration {} + dt);
使用time_point = std :: chrono :: time_point<时钟,持续时间> ;;

time_point t {};

time_point currentTime = Clock :: now();
持续时间累加器= 0s;

州先前的州;
State currentState;

而(!quit)
{
time_point newTime = Clock :: now();
auto frameTime = newTime-currentTime;
if(frameTime> 250ms)
frameTime = 250ms;
currentTime = newTime;

累加器== frameTime;

而(累加器> = dt)
{
previousState = currentState;
tegral(currentState,t,dt);
t + = dt;
累加器-= 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 秒。


  • 持续时间现在具有一个奇怪的类型,我们甚至不必知道其详细信息。它与 Clock :: duration dt 的总和相同。这将是最粗略的精度,它可以精确地表示 Clock :: duration 1 / 60 。谁在乎它是什么。重要的是,如果 Clock :: duration 是基于整数的,则基于时间的算法将没有截断错误,甚至没有舍入错误。 (谁说不能完全代表计算机上的 1 / 3 ?!)


  • 0.25s 的限制被转换为 250ms 毫秒{250}


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




有关<$ c $的更多信息c>时钟




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


  • 如果需要将 t 映射到日历时间,或者如果需要<$ c,请使用 system_clock $ c> t 与UTC保持同步。游戏进行时,这需要对时钟进行一些小的(可能是毫秒级或更小的调整)。


  • 如果您不在乎得到 steady_clock 还是 system_clock <,请使用 high_resolution_clock / code>,并希望每次将代码移植到新平台或编译器时都会得到惊讶。 :-)


  • 最后,如果愿意,您甚至可以坚持使用 SDL_GetTicks()自己的时钟像这样:




例如:

  struct Clock 
{
using duration = std :: chrono :: milliseconds;
使用rep = duration :: rep;
使用period = duration :: period;
使用time_point = std :: chrono :: time_point< Clock> ;;
静态constexpr bool is_steady = true;

静态
time_point
now()noexcept
{
return time_point {duration {SDL_GetTicks()}};
}
};

切换:




  • 使用Clock = std :: chrono :: steady_clock;

  • 使用Clock = std: :chrono :: system_clock;

  • 使用Clock = std :: chrono :: high_resolution_clock;

  • struct Clock {...}; //基于SDL_GetTicks的



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



附录



我完整的状态完整代码:

 结构状态
{
双重加速= 1; // m / s ^ 2
double velocity = 0; // m / s
};

无效
集成(State& state,
std :: chrono :: time_point< Clock,std :: chrono :: duration< double>> ;,
std :: chrono :: duration< double> dt)
{
使用命名空间std :: literals;
state.velocity + = state.acceleration * dt / 1s;
};

状态运算符+(状态x,状态y)
{
return {x.acceleration + y.acceleration,x.velocity + y.velocity};
}

状态运算符*(状态x,双y)
{
return {x.acceleration * y,x.velocity * y};
}

void render(State state)
{
使用命名空间std :: chrono;
静态自动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<<速度=
<< state.velocity<< m / s\n;
}


If you're not familiar with the Gaffer on Games article "Fix your Timestep", you can find it here: https://gafferongames.com/post/fix_your_timestep/

I'm building a game engine, and in an effort to get more comfortable with std::chrono I've been trying to implement a fixed time step using std::chrono for.. a couple of days now and I can't seem to wrap my head around it. Here is the pseudo-code I'm working towards:

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

Goals:

  • I don't want rendering to be frame-rate bound. I should render in the busy loop
  • I want a fully fixed time-step where I call my update function with a float delta time
  • No sleeps

My current attempt (semi-fixed):

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

Problems with this implementation

  • Rendering, handling input, etc is tied to the frame rate
  • I'm using SDL_GetTicks() instead of std::chrono

My actual question

  • How can I replace SDL_GetTicks() with std::chrono::high_resolution_clock::now()? It seems like no matter what I need to use count() but I read from Howard Hinnant himself this quote:

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.

  • 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?

解决方案

Below I implement a couple of different versions of "final touch" from Fix your Timestep using <chrono>. My hope is that this example will translate to your desired code.

The main challenge is figuring out what unit each double represents in Fix your Timestep. Once that is done, the transformation to <chrono> is fairly mechanical.

Front matter

So that we can easily change out the clock, start with a Clock type, for example:

using Clock = std::chrono::steady_clock;

Later I'll show that one can even have Clock be implemented in terms of SDL_GetTicks() if desired.

If you have control over the signature if the integrate function, I recommend double-based seconds units for the time parameters:

void
integrate(State& state,
          std::chrono::time_point<Clock, std::chrono::duration<double>>,
          std::chrono::duration<double> dt);

This will allow you to pass anything you want in (as long the time_point is based on Clock), and not have to worry about explicit casting to the correct units. Plus physics computations are often done in floating point, so this lends itself to that as well. For example if State simply holds an acceleration and a velocity:

struct State
{
    double acceleration = 1;  // m/s^2
    double velocity = 0;  // m/s
};

and integrate is supposed to compute the new velocity:

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;
};

The expression dt/1s simply converts the double-based chrono seconds into a double so it can participate in the physics computation.

std::literals and 1s are C++14. If you are stuck at C++11, you can replace these with seconds{1}.

Version 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);
}

This version keeps everything almost exactly the same from Fix your Timestep, except some of the doubles get changed to type duration<double> (if they represent time durations), and others get changed to time_point<Clock, duration<double>> (if they represent points in time).

dt has units of duration<double> (double-based seconds), and I presume that the 0.01 from Fix your Timestep is a type-o, and the desired value is 1./60. In C++11 1.0s/60. can be changed to seconds{1}/60..

local type-aliases for duration and time_point are set up to use Clock and double-based seconds.

And from here on out, the code is nearly identical to Fix your Timestep, except for using duration or time_point in place of double for types.

Note that alpha is not a unit of time, but a dimension-less double coefficient.

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

As above. There is no use of SDL_GetTicks() nor .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?

As above, and you don't need to pass a float delaTime to the update function unless that function signature is out of your control. And if that is the case, then:

m_gameController->Update(deltaTime/1s);

Version 2

Now let's go a little further: Do we really need to use floating point for the duration and time_point units?

Nope. Here's how you can do the same thing with integral-based time units:

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

There is really very little that has changed from Version 1:

  • dt now has the value 1, represented by a long long, and has units of 1/60 of a second.

  • duration now has a strange type that we don't even have to know the details about. It is the same type as the resultant sum of a Clock::duration and dt. This will be the coarsest precision that can exactly represent both a Clock::duration and 1/60 of a second. Who cares what it is. What is important is that the time-based arithmetic will have no truncation error, or not even any round-off error if Clock::duration is integral-based. (Who said that one can not exactly represent 1/3 on a computer?!)

  • The 0.25s limit gets transformed instead into 250ms (milliseconds{250} in C++11).

  • The computation of alpha should aggressively convert to double-based units to avoid the truncation associated with integral-based division.

More about Clock

  • Use steady_clock if you don't need to map t to a calendrical time in your physics, and/or you don't care if t slowly drifts away from the exact physical time. No clock is perfect, and steady_clock never gets adjusted to the correct time (such as by an NTP service).

  • Use system_clock if you need to map t to a calendrical time, or if you want t to stay in sync with UTC. This will require a few small (probably millisecond-level or smaller) adjustments to Clock as the game plays.

  • Use high_resolution_clock if you don't care whether you get steady_clock or system_clock and want to be surprised which you get each time you port your code to a new platform or compiler. :-)

  • Finally, you can even stick with SDL_GetTicks() if you want by writing your own Clock like this:

E.g.:

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

Switching between:

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

requires zero changes to the event loop, the physics engine, or the render engine. Just recompile. Conversion constants get updated automatically. So you can easily experiment with which Clock is best for your application.

Appendix

My full State code for completeness:

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";
}

这篇关于Gaffer的游戏时间步长:std :: chrono实现的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆