我正在尝试使用 SFML 和 imgui-SFML 制作一个项目。我是 imgui 新手,不确定问题到底出在哪里。 根据我收集的信息,imgui::render 必须与 imgui::update 在同一时间步长中发生,这在 SFML 和正确处理 Delta 时间的上下文中是有限的且次优的。
这是我的问题的一些最小示例:
#include <SFML/Graphics.hpp>
#include "ImGui.h"
#include "ImGui-SFML.h"
void update(sf::Time dt, sf::RenderWindow& window);
void processEvents(sf::RenderWindow& window);
void render(sf::RenderWindow& window);
int main()
{
sf::RenderWindow window;
window.create(sf::VideoMode(800, 600), "window");
ImGui::SFML::Init(window);
sf::Clock clock;
sf::Time timeSinceLastUpdate = sf::Time::Zero;
sf::Time mTimePerFrame = sf::seconds(1.f / 60.f);
while (window.isOpen())
{
sf::Time elapsedTime = clock.restart();
timeSinceLastUpdate += elapsedTime;
while (timeSinceLastUpdate > mTimePerFrame)
{
timeSinceLastUpdate -= mTimePerFrame;
processEvents(window);
update(mTimePerFrame, window);
}
render(window);
}
return 0;
}
void update(sf::Time dt, sf::RenderWindow& window)
{
ImGui::SFML::Update(window, dt);
ImGui::Begin("Hello!", NULL);
ImGui::Button("click me", ImVec2(32, 32));
ImGui::End();
}
void processEvents(sf::RenderWindow& window)
{
sf::Event event;
while (window.pollEvent(event))
{
ImGui::SFML::ProcessEvent(event);
}
}
void render(sf::RenderWindow& window)
{
window.clear();
ImGui::SFML::Render();
window.display();
}
如果我删除第二个 while 循环( while (timeSinceLastUpdate > mTimePerFrame) ),一切都会正常工作,但这使我的游戏帧速率依赖,这显然是我想避免的。
关于新帧的错误是由this断言触发的,因为
update
,因此在第一个ImGui::SFML::Update
调用(最终调用上面链接的ImGui::NewFrame
)之前不会调用render
(调用ImGui::EndFrame
) 。即使您有一个以固定时间步长调用 update
的机制,外层循环实际上没有任何帧限制机制,所以会发生什么(或者更确切地说,如果不是断言失败的话会发生)是,外循环的大部分迭代,都不会进入内循环(timeSinceLastUpdate
尚未累积),但render
被调用。
解决此问题的一种简单方法是确保仅当当前外循环迭代上有游戏状态更新(即内循环至少有一次迭代)时才调用
render
。我们可以通过将内部循环从 while
转换为 if-do-while
并将 render(window);
移动到 if
内来实现此目的。这不仅解决了 ImGui::NewFrame
问题(因为现在 render
执行时它已经被调用),而且还避免了不必要的渲染/绘制未更新的游戏状态。
但是,现在游戏(外部)循环只是不断迭代,并不必要地保持其线程忙碌,同时每隔几微秒(如果不是更频繁)检查一次时间,然后
timeSinceLastUpdate
最终超过您的固定时间阈值。相反,我们可以让线程休眠大约所需的时间,然后才能进入 if
,这可以例如完成在 else
分支:
while (window.isOpen())
{
sf::Time elapsedTime = clock.restart();
timeSinceLastUpdate += elapsedTime;
if (timeSinceLastUpdate > mTimePerFrame)
{
do
{
timeSinceLastUpdate -= mTimePerFrame;
processEvents(window);
update(mTimePerFrame, window);
} while (timeSinceLastUpdate > mTimePerFrame);
render(window);
}
else
{
sf::sleep(mTimePerFrame - timeSinceLastUpdate /* - clock.getElapsedTime() which is negligible here */);
}
}
请注意,在 SFML 中,帧限制通常不是由
sf::sleep
完成,而是由 sf::Window::setVerticalSyncEnabled
(在 GPU 驱动程序级别实现的 VSync)或 sf::Window::setFramerateLimit
(使用 sf::sleep
和 sf::Window
自己的内部时钟)完成,后者与我们所做的特别相似。但是这两个函数都阻塞在 sf::Window::display
内部(我们称之为内部 render
)并且不精确(尤其是后者),因此,如果我们在这里使用它们,有时我们仍然可能在外循环中过早醒来,并且花一点时间“攻击”if
,为了防止这种情况,我们仍然需要在sf::sleep
分支内else
。
此外,我们还必须在
ImGui::EndFrame
的末尾调用 update
,这样,如果执行内部循环的多次迭代,我们最终不会在中间没有 ImGui::StartFrame
的情况下连续调用 ImGui::EndFrame
(这会触发 另一个)断言):
void update(sf::Time dt, sf::RenderWindow& window)
{
// ...
ImGui::EndFrame();
}
请注意,现在我们经常连续调用
ImGui::EndFrame
两次(内循环最后一次迭代中的 update
和后续的 render
),但这不是问题,因为由于 this check 随后的 ImGui::EndFrame
在新帧开始之前,调用无效。