Advertisement

high fps == bad animation?

Started by May 13, 2002 04:27 PM
26 comments, last by endo 22 years, 9 months ago
I am still very confused!!

I'd be very greatful if someone could explain what to put where in my GLUT program which looks basically like this at the moment


    /* balltest.cpp - glut program for testing the ball class to be used with the breakout game */#include <ctime>#include <GL/glut.h>#include <windows.h>#include "ball.h"#include "paddle.h"#include "polygon.h"#include "collisions.h"#include "block.h"#include "timer.h"typedef std::vector< GenericBlock* > BlockPtrVector;//globals will go into a game running class laterBall ball;Paddle paddle;TexturedBlock test( 7, 20 );Timer gameTimer;float framesPerSec; void calcFps( float& fps ){	static int frames = 0;	float currentTime = gameTimer.getTime( );	frames++;	if( frames > 100 )	{		fps = (double)frames/(currentTime/1000);		frames = 0;		gameTimer.reset( );		cout << "FPS : " << fps << endl;	}}void resize( int width, int height ){		if( width == 0 )	{		width = 1;	}	glViewport( 0, 0 , width, height );	glMatrixMode( GL_PROJECTION );	glLoadIdentity( );	glOrtho( -100, 100, -100, 100, 100, -100 );		//doesn't keep anything square	glMatrixMode( GL_MODELVIEW );	glLoadIdentity( );}void mouse( int button, int state, int x, int y ){	switch( button )	{	case GLUT_LEFT_BUTTON:		if( state == GLUT_DOWN )		{			ball.getMove( )->triple[ 0 ] = 0.0f;			ball.getMove( )->triple[ 1 ] = 2.0f;			ball.getMove( )->triple[ 2 ] = 0.0f;		}		break;	case GLUT_RIGHT_BUTTON:		if( state == GLUT_DOWN )		{			exit( 0 );		}		break;	default:		break;	}	glutPostRedisplay( );}void special( int key, int x, int y ){	switch( key )	{	case GLUT_KEY_LEFT:		paddle.movePaddle( 3.0f, LEFT );		glutPostRedisplay( );		break;			case GLUT_KEY_RIGHT:		paddle.movePaddle( 3.0f, RIGHT );		glutPostRedisplay( );		break;			case GLUT_KEY_UP:		switch( paddle.type )		{		case FLAT:			paddle.type = CONVEX;			break;					case CONVEX:			paddle.type = CONCAVE;			break;					case CONCAVE:			paddle.type = FLAT;			break;		default:			break;		}		paddle.buildPaddles( );		break;					case GLUT_KEY_DOWN:		switch( paddle.type )		{		case FLAT:			paddle.type = CONCAVE;			break;					case CONVEX:			paddle.type = FLAT;			break;					case CONCAVE:			paddle.type = CONVEX;			break;		default:			break;		}		paddle.buildPaddles( );		break;				default:		break;	}		glutPostRedisplay( );}void renderScene( ){	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );	glLoadIdentity( );	glColor3f( 1.0f, 0.0f, 0.0f );	//testing a block drawing	Vector3d loc;	test.setLocation( loc );	test.drawBlock( );	ball.drawBall( );	paddle.drawPaddle( );		glutSwapBuffers( );	calcFps( framesPerSec );}void init( ){	glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );	glEnable( GL_DEPTH_TEST );}//runs the game, updates the animation, checks for collisions etc.void idle( ){	//moving the paddle first	bool left = false, right = false;	GLfloat offset;	switch( paddle.type )	{	case FLAT:		offset = paddle.width/2;		break;	case CONCAVE:	case CONVEX:		offset = paddle.width*0.75;		break;	default:		break;	}	if( paddle.move > 0 )	{		if( paddle.currentLoc->triple[ 0 ] <= (-100 + offset) )		{			paddle.currentLoc->triple[ 0 ] = -100 + offset;		//clamps it to the edge			left = true;		}		else if( paddle.currentLoc->triple[ 0 ] >= (100 - offset) )		{			paddle.currentLoc->triple[ 0 ] = 100 - offset;			//clamps to edge			right = true;		}				if( !left || !right )		{			paddle.currentLoc->triple[ 0 ] = paddle.currentLoc->triple[ 0 ] + (paddle.move * paddle.direction);			paddle.move -= 0.2f;			//the lower this value the more momentum is present on paddle		}	}	if( ball.getActive( ) )		//if false ball is drawn but doesnt move	{		ball.moveBall( );		MyPolygon hit;		if( ballPaddleCollision( paddle, *ball.getLoc( ), ball.getRadius( ), hit ) )		{			Vector3d newVector;			Vector3d hitNormal = *hit.getNormal( );			Vector3d ballMovement = *ball.getMove( );			//reflected = incoming - 2*dotproduct(normal,incoming)*normal			newVector = ballMovement - (hitNormal * 2 * dotProduct( hitNormal, ballMovement ));			ball.setMove( newVector );		}	}	glutPostRedisplay( );}int main( int argc, char** argv ){	glutInit( &argc, argv );	glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB );	glutInitWindowSize( 350, 450 );	glutInitWindowPosition( 300, 300 );	glutCreateWindow( "balltest program" );	init( );	glutDisplayFunc( renderScene );	glutReshapeFunc( resize );	glutIdleFunc( idle );	glutMouseFunc( mouse );	glutSpecialFunc( special );	glutMainLoop( );	return 1;}    


ps I really need to sort this out, it has been the bane of my life for the past few months so I ignored it and wrote other bits of the program

[edited by - endo on May 15, 2002 9:45:38 AM]

[edited by - endo on May 15, 2002 9:57:21 AM]
Well, here goes. Lots of people have different ways of organizing code, some of them are effective and some aren''t.

First, you need a framecount variable, either a global variable or a static member of your rendering class. Second you need a timer. You can probably use the ANSI standard C timer.h, although I''d recommend a high performance counter if you''re programming for Windows. Just search for High Performance Counter on the Internet, I''m getting my info from page 316 of Zen of Direct3D Game Programming.

Now then, I always try to organize my code so that the system-dependent code is as localized as possible. This means that my rendering loop looks roughly like this

GlutMainLoop()
{
...
RedrawWindow()
{
...
Draw3DGraphics(p_gRootGraphicsFrame);
CalcFrameRate();
DrawInterfaceGraphics();
...

As you can see I have the CalcFrameRate() procedure outside of the rendering code but within the redraw function. It needs to be inside the redraw function so that it gets called whenever the window if refreshed for whatever reason, but I have it outside the actual 3D drawing code so that the framerate counter will still work if I decide to link to an OpenGL, Direct3D, software mode or whatever version of Draw3DGraphics();

Now that you have the framerate (perhaps recalculated every certain number of seconds) what should you do with it? Well that depends on how advanced your animation is. If you''re just trying to make the program animate at a constant speed, you can calculate how far the framerate deviates from your desired rate ''normalFPS'' (say 30 fps) by dividing normalFPS by frameRate. Then when you''re calculating where to move your paddle or ball or whatever just multiply the object coordinates (for normalFPS) by (normalFPS/frameRate). That way if you''re getting 60 fps for some reason, the paddle would only move half as far per frame, and so forth.

The better way to do things is base your animation on object (sprite, whatever) velocity and the time that has elapsed since the previous frame. If you want to do it this way, I would make another global/static variable called elapsedTime and put CalcElapsedTime() just before the call to Draw3DGraphics() so that everything will end up in just the right place on the screen no matter how the framerate fluctuates (and it will, trust me ). So here is what that would look like:

GlutMainLoop
{
...
RedrawWindow
{
...

CalcElapsedTime();
Animate3DWorld(p_gRootGraphicsFrame,gElapsedTime);
// Animate does not render it just calculates WorldMatrices
// for the different frames
CalcFrameRate();
Draw3DGraphics(p_gRootGraphicsFrame);
DrawInterfaceGraphics();
...

I hope this answers your question about what to do with the framerate counter.


Advertisement
I like my method (a snippet of it based around the NeHe tutorial code so hopefully you'll understand it all w/out explanation):

	// in the winmain:// blah	float start;						// Stores logic based time// blah	start=TimerGetTime(); // grab the time right before the loop starts, for the speed control	// GAME LOOP	while(!done)			// Loop That Runs While done=FALSE	{		done = HandleWinMessages(msg); // get messages, check if recieved a "quit" message		// Draw The Scene.  Watch For ESC Key And Quit Messages From DrawGLScene()		if (active)								// Program Active?		{			if (keys[VK_ESCAPE])	// Was ESC Pressed?			{				done=TRUE;						// ESC Signalled A Quit			}			else		// Not Time To Quit, Update Screen			{				if (!paused)				{					while (TimerGetTime()-start>0)					{// Do game logic here						if (!DoGameLogic()) done = true;						start+=60.0f/1000.0f;					}				}				else if (!DoPauseLogic()) done = true;				// Draw screen			}		}		if (!active||paused) start=TimerGetTime()+1;	}   


Okay, an explanation: After each logic call, the "start" variable increments by the interval of time you want between logic updates. It continues calling the logic until the logic time has caught up with the actual time, and then the screen is drawn, then we repeat the loop.

This way, if the machine is too slow then it will have a lower framerate, but do more logic processes inbetween frames and thus appear to run at the same rate (just choppier, as will always be with low framerates), and if the machine is too fast it will actually skip over the logic's while-loop until the real time has caught up with the logical time, do another frame of logic, then continue to draw until the real time has caught up with the logical time again ... so the logic will not go too fast.

This seems to be the best of the ways ... the multiplier method isn't completely accurate (movement rate is based on how much stuff should have moved the *previous* frame, and it requires everything to be floats or doubles, even keyframe animation counters), and the "delay" method doesn't help computers that are too slow.

[edited by - Zaphos on May 16, 2002 8:46:31 AM]
The question is not "why a talking monkey," but rather, "why not a talking monkey." -Monkey Island 4
Is this easier to do in Win32 than in glut? Should I just port the whole project across now?
No it shouldn't be easier to do in win32 unless glut doesn't give you access to your main program loop? I've never used glut, but I assume it must. You just need to control your game with a loop like this:

    logicalTime = GetTime();while(!done){ // or whatever your main game loop is    while(GetTime()>logicalTime){         DoLogic();         logicalTime+=intervalSize; // if you wanted 60 logic updates per second and GetTime were in terms of milliseconds, for example, this would be 60.0/1000.0    }    DoFrameDrawing();}    


This should be do-able in any library / language.

Note: GetTime(), DoLogic(), and DoFrameDrawing() should be replaced with the actual functions or set of functions that do these, of course. They are just meant to indicate what type of stuff should go at that point in the program.

Luck!

[edited by - Zaphos on May 17, 2002 11:01:44 AM]
The question is not "why a talking monkey," but rather, "why not a talking monkey." -Monkey Island 4
It doesn''t give access to the main event loop, which I think has confused my issue a bit. It registers callback functions for things like mouse and keyboard interaction, repainting, etc. A very simple way to get OpenGL up and running quickly...

Maybe I should just start changing it all now
Advertisement
Don''t use time deltas for animation!!! I know it seems like a good idea. But it is very unstable. You''re basically doing Euler integration on a uncontrolled time step which is BAD. (just do a Google search on "Euler integrate unstable". Tying everything into the frame rate is equally bad.

Anyway, the method I have found that has served my studio well in the past 2 projects is simple. Update your game logic at 60hz and render as often as you can. Basically, you decouple the game/animation logic from the frame rate. You get the convenience of controlled fixed time steps (SO much better for debugging) without throttling your frame rate. Here''s some pseudo code:

static const unsigned long sTimeStep = 1000/60; // 60hz in msecs

unsigned long lastUpdate = 0;
unsigned long thisTime = 0;

while (gameIsRunning)
{
thisTime = getCurrentTime();

if ((thisTime - lastUpdate) >= sTimeStep)
updateGameLogic();

renderGame();
}

Trust me. This makes development a dream (You ever set a breakpoint in a time based game? Ugh.) And your game will play predictably on any machines.
That seems to be what I have been trying (or similar anyway!). How do I fit that into my program so far. This is a simplified version of my code so far, its a bit messy though where I've been trying to sort this out:

Pleeeeaaaassseeee help me sort this out, I really wanna make it look pretty :D


#include <ctime>
#include <GL/glut.h>
#include <windows.h>
#include "ball.h"
#include "paddle.h"
#include "polygon.h"
#include "collisions.h"
#include "block.h"
#include "timer.h"


//globals will go into a game running class later
Ball ball;
Timer gameTimer;
const float startTime = gameTimer.getTime( );
float framesPerSec;
static float frameStart;
static float elapsedTime;



void calcFps( float& fps )
{
static int frames = 0;
float currentTime = gameTimer.getTime( );

frames++;

if( frames > 100 )
{
fps = (double)frames/(currentTime/1000);
frames = 0;
gameTimer.reset( );
cout << "FPS : " << fps << endl;
}
}


void resize( int width, int height )
{
if( width == 0 )
{
width = 1;
}

glViewport( 0, 0 , width, height );

glMatrixMode( GL_PROJECTION );
glLoadIdentity( );

glOrtho( -100, 100, -100, 100, 100, -100 ); //doesn't keep anything square

glMatrixMode( GL_MODELVIEW );
glLoadIdentity( );
}


void mouse( int button, int state, int x, int y )
{
switch( button )
{
case GLUT_LEFT_BUTTON:
if( state == GLUT_DOWN )
{
ball.getMove( ).triple[ 0 ] = 0.0f;
ball.getMove( ).triple[ 1 ] = 2.0f;
ball.getMove( ).triple[ 2 ] = 0.0f;
}
break;

case GLUT_RIGHT_BUTTON:
if( state == GLUT_DOWN )
{
exit( 0 );
}
break;

default:
break;
}
glutPostRedisplay( );
}


void special( int key, int x, int y )
{
switch( key )
{
case GLUT_KEY_LEFT:
glutPostRedisplay( );
break;

case GLUT_KEY_RIGHT:
glutPostRedisplay( );
break;

case GLUT_KEY_UP:
break;


case GLUT_KEY_DOWN:
break;

default:
break;
}

glutPostRedisplay( );
}


void renderScene( )
{
frameStart = gameTimer.getTime( );

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glLoadIdentity( );
glColor3f( 1.0f, 0.0f, 0.0f );

ball.drawBall( );

// Sleep( 50 ); //changes framerate to ~20fps
glutSwapBuffers( );
calcFps( framesPerSec );

elapsedTime = gameTimer.getTime( ) - frameStart;
}


void init( )
{
glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
glEnable( GL_DEPTH_TEST );
}


//runs the game, updates the animation, checks for collisions etc.
void idle( )
{
//need to move the ball better according to time not framerate

ball.moveBall( );
renderScene( );

glutPostRedisplay( );
}


int main( int argc, char* argv[] )
{
glutInit( &argc, argv );
glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB );
glutInitWindowSize( 350, 450 );
glutInitWindowPosition( 300, 300 );

glutCreateWindow( argv[ 0 ] );

init( );
glutDisplayFunc( renderScene );
glutReshapeFunc( resize );
glutIdleFunc( idle );
glutMouseFunc( mouse );
glutSpecialFunc( special );
glutMainLoop( );

return 0;
}

[edited by - endo on May 17, 2002 2:42:09 PM]
AP: But your way skips *logic* steps when the game runs to slowly, which is *bad*? Ie, it *doesn't* seperate framerate from logic rate! My way seems much better, as it keeps the rate of game logic equal on all systems, while not using time deltas (or at least, what I think you're calling time deltas).


Endo:
As long as you can put this:
while(GetTime()>logicalTime){   DoLogic();   logicalTime+=intervalSize;}  


around your game logic, and logicalTime gets initialized to GetTime() at the beginning of your program, you should be fine.

I can't find your game logic in the code you posted, but then I didn't look very hard. You should be able to put that loop in by yourself, though

[edited by - Zaphos on May 17, 2002 3:40:27 PM]
The question is not "why a talking monkey," but rather, "why not a talking monkey." -Monkey Island 4
AP''s suggestion is fine as far as it goes, but it does have certain limitations:
1. the engine is locked into a 60 Hz update speed (actually, it could be *much* lower depending on the user''s vsync setup, overall system speed and numerous other factors). Allowing a graphics-poor system to clobber your simulation accuracy is probably a bad choice.
2. it assumes that the only way to use deltaTime is Euler''s method. This isn''t true. I''d recommend using quaternions for animation transformations and integrating over time using the Runge-Kutta method. That should give you smooth and accurate animation regardless of the framerate.

If these terms are foreign to you, you''re about to find out that rendering triangles, quads and textures is only the tip of the iceberg when it comes to writing a graphics engine

This topic is closed to new replies.

Advertisement