如果您不熟悉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::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::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几乎完全相同,除了一些
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.
。设置
duration
和time_point
的本地类型别名以使用基于Clock
和double
的秒。从现在开始,该代码几乎与Fix your Timestep相同,除了使用
duration
或time_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::duration
和dt
的总和类型相同。这将是最精确的精度,它可以精确地表示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/