Constant game speed independent of variable FPS in

2020-05-21 08:18发布

I've been reading Koen Witters detailed article about different game loop solutions but I'm having some problems implementing the last one with GLUT, which is the recommended one.

After reading a couple of articles, tutorials and code from other people on how to achieve a constant game speed, I think that what I currently have implemented (I'll post the code below) is what Koen Witters called Game Speed dependent on Variable FPS, the second on his article.

First, through my searching experience, there's a couple of people that probably have the knowledge to help out on this but don't know what GLUT is and I'm going to try and explain (feel free to correct me) the relevant functions for my problem of this OpenGL toolkit. Skip this section if you know what GLUT is and how to play with it.

GLUT Toolkit:

  • GLUT is an OpenGL toolkit and helps with common tasks in OpenGL.
  • The glutDisplayFunc(renderScene) takes a pointer to a renderScene() function callback, which will be responsible for rendering everything. The renderScene() function will only be called once after the callback registration.
  • The glutTimerFunc(TIMER_MILLISECONDS, processAnimationTimer, 0) takes the number of milliseconds to pass before calling the callback processAnimationTimer(). The last argument is just a value to pass to the timer callback. The processAnimationTimer() will not be called each TIMER_MILLISECONDS but just once.
  • The glutPostRedisplay() function requests GLUT to render a new frame so we need call this every time we change something in the scene.
  • The glutIdleFunc(renderScene) could be used to register a callback to renderScene() (this does not make glutDisplayFunc() irrelevant) but this function should be avoided because the idle callback is continuously called when events are not being received, increasing the CPU load.
  • The glutGet(GLUT_ELAPSED_TIME) function returns the number of milliseconds since glutInit was called (or first call to glutGet(GLUT_ELAPSED_TIME)). That's the timer we have with GLUT. I know there are better alternatives for high resolution timers, but let's keep with this one for now.

I think this is enough information on how GLUT renders frames so people that didn't know about it could also pitch in this question to try and help if they fell like it.

Current Implementation:

Now, I'm not sure I have correctly implemented the second solution proposed by Koen, Game Speed dependent on Variable FPS. The relevant code for that goes like this:

#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();
}

This implementation doesn't fell right. It works in the sense that helps the game speed to be constant dependent on the FPS. So that moving from point A to point B takes the same time no matter the high/low framerate. However, I believe I'm limiting the game framerate with this approach. [EDIT: Each frame will only be rendered when the time callback is called, that means the framerate will be roughly around TICKS_PER_SECOND frames per second. This doesn't feel right, you shouldn't limit your powerful hardware, it's wrong. It's my understanding though, that I still need to calculate the elapsedTime. Just because I'm telling GLUT to call the timer callback every TIMER_MILLISECONDS, it doesn't mean it will always do that on time.]

I'm not sure how can I fix this and to be completely honest, I have no idea what is the game loop in GLUT, you know, the while( game_is_running ) loop in Koen's article. [EDIT: It's my understanding that GLUT is event-driven and that game loop starts when I call glutMainLoop() (which never returns), yes?]

I thought I could register an idle callback with glutIdleFunc() and use that as replacement of glutTimerFunc(), only rendering when necessary (instead of all the time as usual) but when I tested this with an empty callback (like void gameLoop() {}) and it was basically doing nothing, only a black screen, the CPU spiked to 25% and remained there until I killed the game and it went back to normal. So I don't think that's the path to follow.

Using glutTimerFunc() is definitely not a good approach to perform all movements/animations based on that, as I'm limiting my game to a constant FPS, not cool. Or maybe I'm using it wrong and my implementation is not right?

How exactly can I have a constant game speed with variable FPS? More exactly, how do I correctly implement Koen's Constant Game Speed with Maximum FPS solution (the fourth one on his article) with GLUT? Maybe this is not possible at all with GLUT? If not, what are my alternatives? What is the best approach to this problem (constant game speed) with GLUT?

[EDIT] Another Approach:

I've been experimenting and here's what I was able to achieve now. Instead of calculating the elapsed time on a timed function (which limits my game's framerate) I'm now doing it in renderScene(). Whenever changes to the scene happen I call glutPostRedisplay() (ie: camera moving, some object animation, etc...) which will make a call to renderScene(). I can use the elapsed time in this function to move my camera for instance.

My code has now turned into this:

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();
}

Conclusion, it's working, or so it seems. If I don't move the camera, the CPU usage is low, nothing is being rendered (for testing purposes I only have a grid extending for 4000.0f, while zFar is set to 1000.0f). When I start moving the camera the scene starts redrawing itself. If I keep pressing the move keys, the CPU usage will increase; this is normal behavior. It drops back when I stop moving.

Unless I'm missing something, it seems like a good approach for now. I did find this interesting article on iDevGames and this implementation is probably affected by the problem described on that article. What's your thoughts on that?

Please note that I'm just doing this for fun, I have no intentions of creating some game to distribute or something like that, not in the near future at least. If I did, I would probably go with something else besides GLUT. But since I'm using GLUT, and other than the problem described on iDevGames, do you think this latest implementation is sufficient for GLUT? The only real issue I can think of right now is that I'll need to keep calling glutPostRedisplay() every time the scene changes something and keep calling it until there's nothing new to redraw. A little complexity added to the code for a better cause, I think.

What do you think?

3条回答
戒情不戒烟
2楼-- · 2020-05-21 09:06

You could use glutIdleFunc, which is called continuously whenever possible--similar to the while(game_is_running) loop. That is, whatever logic you would otherwise put into that while loop, you could put into the callback for glutIdleFunc. You can avoid using glutTimerFunc by keeping track of the ticks on your own, as in the article you linked (using glutGet(GLUT_ELAPSED_TIME)).

查看更多
干净又极端
3楼-- · 2020-05-21 09:10

Have, as an example, a mouse-driven rotation matrix that updates at a fixed frame-rate, independently of the rendering frame-rate. In my program, space-bar toggles benchmarking mode, and determines the Boolean fxFPS.

Let go of the mouse button while dragging, and you can 'throw' an object transformed by this matrix.

If fxFPS is true then the rendering frame-rate is throttled to the animation frame-rate; otherwise identical frames are drawn repeatedly for benchmarking, even though not enough milliseconds will have passed to trigger any animation.

If you're thinking about slowing down AND speeding up frames, you have to think carefully about whether you mean rendering or animation frames in each case. In this example, render throttling for simple animations is combined with animation acceleration, for any cases when frames might be dropped in a potentially slow animation.

To accelerate the animation, rotations are performed repeatedly in a loop. Such a loop is not too slow compared with the option of doing trig with an adaptive rotation angle; just be careful what you put inside any loop that actually takes longer to execute, the lower the FPS. This loop takes far less than an extra frame to complete, for each frame-drop that it accounts for, so it's reasonably safe.

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);
        }
    }
}

And then later...

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
}

Enjoy throwing things around an axis; I find that most people do. Notice that the fps affects nothing whatsoever, in the interface or the rendering. I've minimised the use of divisions, so comparisons should be nice and accurate, and any inaccuracy in the clock does not accumulate unnecessarily.

Syncing of multiplayer games is another 18 conversations, I would judge.

查看更多
Emotional °昔
4楼-- · 2020-05-21 09:11

glut is designed to be the game loop. When you call glutMainLoop(), it executes a 'for loop' with no termination condition except the exit() signal. You can implement your program kind of like you're doing now, but you need some minor changes. First, if you want to know what the FPS is, you should put that tracking into the renderScene() function, not in your update function. Naturally, your update function is being called as fast as specified by the timer and you're treating elapsedTime as a measure of time between frames. In general, that will be true because you're calling glutPostRedisplay rather slowly and glut won't try to update the screen if it doesn't need to (there's no need to redraw if the scene hasn't changed). However, there are other times that renderScene will be called. For example, if you drag something across the window. If you did that, you'd see a higher FPS (if you were properly tracking the FPS in the render function).

查看更多
登录 后发表回答