游戏时间步长:std::chrono 实施
Gaffer on games timestep: std::chrono implementation
如果您不熟悉 Gaffer on Games 文章 "Fix your Timestep",您可以在这里找到它:
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
增量时间调用我的更新函数
- 不睡觉
我目前的尝试(半固定):
#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
我的实际问题
- 如何用
std::chrono::high_resolution_clock::now()
替换SDL_GetTicks()
?似乎无论我需要使用什么 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
s 替换为实际的 std::chrono_literal 时间值,但我将 float deltaTime 作为模拟修改器传递到更新函数的结尾除外?
下面我使用 <chrono>
从 Fix your Timestep 实现了几个不同版本的“final touch”。我希望这个例子能转化为您想要的代码。
主要的挑战是弄清楚每个 double
在 Fix your Timestep 中代表什么单位。一旦完成,到 <chrono>
的转换就相当机械了。
前题
为了方便我们更换时钟,从Clock
类型开始,例如:
using Clock = std::chrono::steady_clock;
稍后我将展示,如果需要,甚至可以根据 SDL_GetTicks()
实现 Clock
。
如果您可以控制 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
的计时码表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 是类型 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?
同上,您不需要将浮点数 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
的计算应积极转换为基于双精度的单位,以避免与基于整数的除法相关的截断。
更多关于Clock
如果您不需要将 t
映射到物理中的日历时间,请使用 steady_clock
,and/or 您不关心 t
慢慢地偏离了准确的物理时间。没有完美的时钟,steady_clock
永远不会调整到正确的时间(例如通过 NTP 服务)。
如果您需要将 t
映射到日历时间,或者如果您希望 t
与 UTC 保持同步,请使用 system_clock
。这将需要在游戏进行时对 Clock
进行一些小的(可能是毫秒级或更小的)调整。
如果您不关心得到的是steady_clock
还是system_clock
并且想对每次移植代码时得到的结果感到惊讶,请使用high_resolution_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";
}
如果您不熟悉 Gaffer on Games 文章 "Fix your Timestep",您可以在这里找到它: 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
增量时间调用我的更新函数 - 不睡觉
我目前的尝试(半固定):
#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
我的实际问题
- 如何用
std::chrono::high_resolution_clock::now()
替换SDL_GetTicks()
?似乎无论我需要使用什么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
s 替换为实际的 std::chrono_literal 时间值,但我将 float deltaTime 作为模拟修改器传递到更新函数的结尾除外?
下面我使用 <chrono>
从 Fix your Timestep 实现了几个不同版本的“final touch”。我希望这个例子能转化为您想要的代码。
主要的挑战是弄清楚每个 double
在 Fix your Timestep 中代表什么单位。一旦完成,到 <chrono>
的转换就相当机械了。
前题
为了方便我们更换时钟,从Clock
类型开始,例如:
using Clock = std::chrono::steady_clock;
稍后我将展示,如果需要,甚至可以根据 SDL_GetTicks()
实现 Clock
。
如果您可以控制 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
的计时码表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 是类型 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?
同上,您不需要将浮点数 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
的计算应积极转换为基于双精度的单位,以避免与基于整数的除法相关的截断。
更多关于Clock
如果您不需要将
t
映射到物理中的日历时间,请使用steady_clock
,and/or 您不关心t
慢慢地偏离了准确的物理时间。没有完美的时钟,steady_clock
永远不会调整到正确的时间(例如通过 NTP 服务)。如果您需要将
t
映射到日历时间,或者如果您希望t
与 UTC 保持同步,请使用system_clock
。这将需要在游戏进行时对Clock
进行一些小的(可能是毫秒级或更小的)调整。如果您不关心得到的是
steady_clock
还是system_clock
并且想对每次移植代码时得到的结果感到惊讶,请使用high_resolution_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";
}