c++ - 游戏时间步长上的灯光师: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 );
}
目标:
- 我不希望渲染受帧速率限制。我应该在繁忙的循环中渲染
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
我的实际问题
- 我该如何
SDL_GetTicks()
替换std::chrono::high_resolution_clock::now()
?似乎无论我需要使用什么,count()
但我从 Howard Hinnant 本人那里读到了这句话:
如果你使用 count(),和/或你的计时码中有转换因子,那么你就太努力了。所以我想也许有更直观的方法。
- 如何将所有
float
s 替换为实际的 std::chrono_literal 时间值,除了我将浮点 deltaTime 作为模拟修改器传递给更新函数的末尾?
解决方案
下面我使用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中的所有内容几乎完全相同,除了一些double
s 更改为 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_point
Clock
double
从这里开始,代码几乎与Fix your Timestep相同,除了使用duration
或time_point
代替double
for 类型。
请注意,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::duration
和1 / 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";
}
推荐阅读
- javascript - Bootstrap-4 模态页脚中的可滚动文本区域
- javascript - 在输入类型 date 中限制日期
- linux - Zinc-7000 向 UART 返回未读字节的信息
- c# - 如何在树视图中显示所有目录?(C#)
- java - AutoFitTextureView 无法解析 AndroidStudio 中的符号
- angular - 关闭角度模态并在后退按钮单击时保持在同一页面上
- php - XML 解析错误 - XmlHttpObj 为空
- vue.js - Vue安装的jquery代码在DOM重新渲染上不起作用
- javascript - 溢出:在包含动态 ul 的 div 中滚动
- javascript - 节点以编程方式设置导入模块不可用的进程环境变量