在 GLUT 中与可变 FPS 无关的恒定游戏速度?

Posted

技术标签:

【中文标题】在 GLUT 中与可变 FPS 无关的恒定游戏速度?【英文标题】:Constant game speed independent of variable FPS in OpenGL with GLUT? 【发布时间】:2011-03-18 19:50:57 【问题描述】:

我一直在阅读 Koen Witters detailed article 关于不同游戏循环解决方案的信息,但我在使用 GLUT 实现最后一个解决方案时遇到了一些问题,这是推荐的解决方案。

在阅读了几篇关于如何实现恒定游戏速度的文章、教程和其他人的代码后,我认为我目前实现的(我将在下面发布代码)就是 Koen Witters 所说的 游戏速度取决于可变 FPS,他的文章中的第二个。

首先,根据我的搜索经验,有几个人可能有知识可以帮助解决这个问题,但不知道 GLUT 是什么,我将尝试解释(请随时纠正我)我这个OpenGL工具包的问题的相关功能。如果您知道 GLUT 是什么以及如何使用它,请跳过此部分。

GLUT 工具包:

GLUT 是一个 OpenGL 工具包,可帮助完成 OpenGL 中的常见任务。 glutDisplayFunc(renderScene) 采用指向renderScene() 函数回调的指针,该回调将负责渲染所有内容。 renderScene()函数在回调注册后只会被调用一次。 glutTimerFunc(TIMER_MILLISECONDS, processAnimationTimer, 0) 在调用回调 processAnimationTimer() 之前需要经过的毫秒数。最后一个参数只是传递给计时器回调的值。 processAnimationTimer() 不会被每个 TIMER_MILLISECONDS 调用一次。 glutPostRedisplay() 函数请求 GLUT 渲染一个新帧,因此我们需要在每次更改场景中的某些内容时调用它。 glutIdleFunc(renderScene) 可用于注册对renderScene() 的回调(这不会使glutDisplayFunc() 无关紧要)但应避免使用此函数,因为在未接收到事件时会连续调用空闲回调,从而增加CPU 负载。 glutGet(GLUT_ELAPSED_TIME) 函数返回自调用glutInit(或首次调用glutGet(GLUT_ELAPSED_TIME))以来的毫秒数。这就是我们使用 GLUT 的计时器。我知道高分辨率计时器有更好的替代方案,但我们暂时先使用这个。

我认为这是关于 GLUT 如何渲染帧的足够信息,因此不了解它的人也可以提出这个问题,以便在他们喜欢它时尝试提供帮助。

当前实施:

现在,我不确定我是否正确实施了 Koen 提出的第二个解决方案,游戏速度取决于可变 FPS。相关代码如下:

#define TICKS_PER_SECOND 30
#define MOVEMENT_SPEED 2.0f

const int TIMER_MILLISECONDS = 1000 / TICKS_PER_SECOND;

int previousTime;
int currentTime;
int elapsedTime;

void renderScene(void) 
    (...)

    // Setup the camera position and looking point
    SceneCamera.LookAt();

    // Do all drawing below...

    (...)


void processAnimationTimer(int value) 
    // setups the timer to be called again
    glutTimerFunc(TIMER_MILLISECONDS, processAnimationTimer, 0);

    // Get the time when the previous frame was rendered
    previousTime = currentTime;

    // Get the current time (in milliseconds) and calculate the elapsed time
    currentTime = glutGet(GLUT_ELAPSED_TIME);
    elapsedTime = currentTime - previousTime;

    /* Multiply the camera direction vector by constant speed then by the
       elapsed time (in seconds) and then move the camera */
    SceneCamera.Move(cameraDirection * MOVEMENT_SPEED * (elapsedTime / 1000.0f));

    // Requests to render a new frame (this will call my renderScene() once)
    glutPostRedisplay();


void main(int argc, char **argv) 
    glutInit(&argc, argv);

    (...)

    glutDisplayFunc(renderScene);

    (...)

    // Setup the timer to be called one first time
    glutTimerFunc(TIMER_MILLISECONDS, processAnimationTimer, 0);
    // Read the current time since glutInit was called
    currentTime = glutGet(GLUT_ELAPSED_TIME);

    glutMainLoop();

这个实现不正确。它的工作原理是帮助游戏速度保持恒定,取决于 FPS。因此,无论高/低帧速率如何,从 A 点移动到 B 点都需要相同的时间。但是,我相信我用这种方法限制了游戏帧率。 [编辑:只有在调用时间回调时才会渲染每一帧,这意味着帧率大约是每秒TICKS_PER_SECOND 帧。这感觉不对,你不应该限制你强大的硬件,这是错误的。不过我的理解是,我仍然需要计算elapsedTime。仅仅因为我告诉 GLUT 每隔TIMER_MILLISECONDS 调用一次计时器回调,并不意味着它总是会准时这样做。]

我不知道如何解决这个问题,老实说,我不知道 GLUT 中的游戏循环是什么,你知道,Koen 文章中的 while( game_is_running ) 循环。 [编辑:我的理解是 GLUT 是事件驱动的,当我调用 glutMainLoop()(永远不会返回)时,游戏循环就开始了,是吗?]

我想我可以用glutIdleFunc() 注册一个空闲回调并将其用作glutTimerFunc() 的替换,只在必要时渲染(而不是像往常一样一直渲染)但是当我用空回调测试它时(比如@ 987654345@),它基本上什么也没做,只是黑屏,CPU 飙升到 25% 并一直保持在那里,直到我杀死游戏并恢复正常。所以我不认为这是要走的路。

使用glutTimerFunc() 绝对不是执行基于此的所有动作/动画的好方法,因为我将我的游戏限制为恒定的 FPS,而不是酷。还是我用错了,我的实现不对?

究竟如何才能在可变 FPS 的情况下保持恒定的游戏速度?更准确地说,我如何使用 GLUT 正确实现 Koen 的 Constant Game Speed with Maximum FPS 解决方案(他的文章中的第四个)?也许这对 GLUT 来说根本不可能?如果没有,我的替代方案是什么? 使用 GLUT 解决这个问题(恒定的游戏速度)的最佳方法是什么?

[编辑]另一种方法:

我一直在尝试,这就是我现在能够实现的目标。我现在在renderScene() 中进行计算,而不是在定时函数上计算经过的时间(这限制了我的游戏的帧速率)。每当场景发生变化时,我都会调用glutPostRedisplay()(即:相机移动、一些对象动画等),这将调用renderScene()。例如,我可以使用此函数中的经过时间来移动我的相机。

我的代码现在变成了这样:

int previousTime;
int currentTime;
int elapsedTime;

void renderScene(void) 
    (...)

    // Setup the camera position and looking point
    SceneCamera.LookAt();

    // Do all drawing below...

    (...)


void renderScene(void) 
    (...)

    // Get the time when the previous frame was rendered
    previousTime = currentTime;

    // Get the current time (in milliseconds) and calculate the elapsed time
    currentTime = glutGet(GLUT_ELAPSED_TIME);
    elapsedTime = currentTime - previousTime;

    /* Multiply the camera direction vector by constant speed then by the
       elapsed time (in seconds) and then move the camera */
    SceneCamera.Move(cameraDirection * MOVEMENT_SPEED * (elapsedTime / 1000.0f));

    // Setup the camera position and looking point
    SceneCamera.LookAt();

    // All drawing code goes inside this function
    drawCompleteScene();

    glutSwapBuffers();

    /* Redraw the frame ONLY if the user is moving the camera
       (similar code will be needed to redraw the frame for other events) */
    if(!IsTupleEmpty(cameraDirection)) 
        glutPostRedisplay();
    


void main(int argc, char **argv) 
    glutInit(&argc, argv);

    (...)

    glutDisplayFunc(renderScene);

    (...)

    currentTime = glutGet(GLUT_ELAPSED_TIME);

    glutMainLoop();

结论,它正在工作,或者看起来是这样。如果我不移动相机,CPU 使用率很低,什么都不会被渲染(出于测试目的,我只有一个网格扩展到 4000.0f,而 zFar 设置为 1000.0f)。当我开始移动相机时,场景开始重绘。如果我一直按移动键,CPU使用率会增加;这是正常行为。当我停止移动时它会回落。

除非我遗漏了什么,否则这似乎是一个不错的方法。我确实在 iDevGames 上找到了this interesting article,这个实现可能受到那篇文章中描述的问题的影响。您对此有何看法?

请注意,我这样做只是为了好玩,我无意创建一些游戏来分发或类似的东西,至少在不久的将来不会。如果我这样做了,我可能会选择除了 GLUT 之外的其他东西。但是由于我使用的是 GLUT,除了 iDevGames 上描述的问题之外,你认为这个最新的实现对 GLUT 来说足够了吗?我现在唯一能想到的真正问题是,每次场景发生变化时我都需要继续调用glutPostRedisplay(),并一直调用它,直到没有新的东西可以重绘。我认为,为了更好的原因,为代码添加了一点复杂性。

你怎么看?

【问题讨论】:

可能应该迁移到 gamedev。 我认为这个问题“通常以某种方式与编程或软件开发有关”,但我同意游戏开发可能更适合这类问题。但是我相信那个网站的访问者仍然没有这个网站那么多。我也将其发布在那里,因为我无法将问题从一个站点转移到另一个站点。 【参考方案1】:

glut 旨在成为游戏循环。当您调用 glutMainLoop() 时,它会执行一个“for 循环”,除了 exit() 信号之外没有终止条件。你可以像现在这样实现你的程序,但是你需要做一些小的改动。首先,如果您想知道 FPS 是什么,您应该将该跟踪放入 renderScene() 函数中,而不是在您的更新函数中。自然,您的更新函数被调用的速度与计时器指定的一样快,并且您将 elapsedTime 视为帧之间时间的度量。通常,这是正确的,因为您调用 glutPostRedisplay 的速度相当慢,并且如果不需要,glut 不会尝试更新屏幕(如果场景没有更改,则无需重绘)。但是,还有其他时候会调用 renderScene。例如,如果您将某些东西拖过窗口。如果这样做,您会看到更高的 FPS(如果您在渲染函数中正确跟踪 FPS)。

【讨论】:

你是对的,但知道/打印 FPS 有点无关紧要。你说“你可以像现在这样实现你的程序,但你需要做一些小的改动。首先,(...)”。第二个呢?我还需要改变什么? 对不起,我不清楚。如果您的计时器运行速度快于最大帧速率,那么对 glutPostRedisplay() 的多次调用将聚集成对 renderScene() 的一次调用。我认为默认情况下,过剩会同步到刷新率:可能是 60 赫兹。因此,如果您的计时器运行得比这更快,您可以对世界做出比我们看到的更多的改变。 当然,如果你让你的定时器运行得足够快,你还不如使用 glutIdleFunc()。 因为它是空的,它会刺激你的处理器。您必须有一台 4 核机器,因此它占用了整个核心。如果你想让它玩得更好一点,你可以让代码产生,就像这里的答案之一一样:***.com/questions/4608232/… 问题是,你打算多久/多久改变一次场景状态。如果它只是响应用户输入,那么你根本不需要计时器。 其实可以,只是响应用户输入。但我只处于初始阶段,我可能需要更改场景以响应用户输入以外的事件,例如 NPC。但是,我目前正在尝试一些东西。我最好编辑上面的问题来解释我在做什么...... 我将此标记为已接受,因为它为我当前的实现提供了很多见解。但如果有人感兴趣,我实际上正在使用我的第二种方法。目前,它似乎正在发挥作用。【参考方案2】:

您可以使用glutIdleFunc,它会尽可能连续调用——类似于while(game_is_running) 循环。也就是说,无论您将在while 循环中放入什么逻辑,您都可以放入glutIdleFunc 的回调中。您可以通过自己跟踪刻度来避免使用glutTimerFunc,如您链接的文章中(使用glutGet(GLUT_ELAPSED_TIME))。

【讨论】:

我可能没有明确表示glutIdleFunc() 不是一个选项,但当我说 CPU 在调用空回调时负载飙升至 25% 时,我认为它是(有点)隐含的。难道没有别的办法了吗?或者使用 GLUT,如果我想要恒定的游戏速度,我注定要修复帧率?【参考方案3】:

例如,有一个鼠标驱动的旋转矩阵,它以固定的帧速率更新,与渲染帧速率无关。在我的程序中,空格键切换基准测试模式,并确定布尔 fxFPS。

在拖动时松开鼠标按钮,您可以“扔”一个由该矩阵转换的对象。

如果 fxFPS 为真,则渲染帧速率被限制为动画帧速率;否则会重复绘制相同的帧以进行基准测试,即使没有足够的毫秒时间来触发任何动画。

如果您正在考虑减慢和加快帧的速度,则必须仔细考虑在每种情况下您是指渲染帧还是动画帧。在此示例中,简单动画的渲染限制与动画加速相结合,适用于任何可能在可能很慢的动画中丢帧的情况。

为了加速动画,循环重复执行旋转。与使用自适应旋转角度进行触发相比,这样的循环不会太慢;只是要小心你放入任何实际上需要更长执行时间的循环中的内容,FPS 越低。对于它所考虑的每个丢帧,此循环所花费的时间远少于额外的帧,因此它相当安全。

int xSt, ySt, xCr, yCr, msM = 0, msOld = 0;
bool dragging = false, spin = false, moving = false;
glm::mat4 mouseRot(1.0f), continRot(1.0f);
float twoOvHght; // Set in reshape()
glm::mat4 mouseRotate(bool slow) 
    glm::vec3 axis(twoOvHght * (yCr - ySt), twoOvHght * (xCr - xSt), 0); // Perpendicular to mouse motion
    float len = glm::length(axis);
    if (slow)  // Slow rotation; divide angle by mouse-delay in milliseconds; it is multiplied by frame delay to speed it up later
        int msP = msM - msOld;
        len /= (msP != 0 ? msP : 1);
    
    if (len != 0) axis = glm::normalize(axis); else axis = glm::vec3(0.0f, 0.0f, 1.0f);
    return rotate(axis, cosf(len), sinf(len));

void mouseMotion(int x, int y) 
    moving = (xCr != x) | (yCr != y);
    if (dragging & moving) 
        xSt = xCr; xCr = x; ySt = yCr; yCr = y; msOld = msM; msM = glutGet(GLUT_ELAPSED_TIME);
        mouseRot = mouseRotate(false) * mouseRot;
    

void mouseButton(int button, int state, int x, int y) 
    if (button == 0) 
        if (state == 0) 
            dragging = true; moving = false; spin = false;
            xCr = x; yCr = y; msM = glutGet(GLUT_ELAPSED_TIME);
            glutPostRedisplay();
         else 
            dragging = false; spin = moving;
            if (spin) continRot = mouseRotate(true);
        
    

后来……

bool fxFPS = false;
int T = 0, ms = 0;
const int fDel = 20;
void display() 
    ms = glutGet(GLUT_ELAPSED_TIME);
    if (T <= ms)  T = ms + fDel;
        for (int lp = 0; lp < fDel; lp++) 
            orient = rotY * orient; orientCu = rotX * rotY * orientCu; // Auto-rotate two orientation quaternions
            if (spin) mouseRot = continRot * mouseRot; // Track rotation from thowing action by mouse
        
        orient1 = glm::mat4_cast(orient); orient2 = glm::mat4_cast(orientCu);
    
    // Top secret animation code that will make me rich goes here
    glutSwapBuffers();
    if (spin | dragging)  if (fxFPS) while (glutGet(GLUT_ELAPSED_TIME) < T); glutPostRedisplay();  // Fast, repeated updates of the screen

享受围绕轴扔东西的乐趣;我发现大多数人都是这样。请注意,fps 对界面或渲染没有任何影响。我已经尽量减少了除法的使用,所以比较应该是好的和准确的,时钟中的任何不准确都不会不必要地累积。

多人游戏的同步是另外 18 个对话,我会判断。

【讨论】:

以上是关于在 GLUT 中与可变 FPS 无关的恒定游戏速度?的主要内容,如果未能解决你的问题,请参考以下文章

限制 Sprite 套件帧率

试图理解 Java 中的高级游戏循环

游戏循环帧独立

使用 OpenGL 和 GLUT 创建 FPS 风格的运动系统

创建游戏循环(睡眠时间帮助)

Glut:如何为均匀运动生成恒定的时间间隔?