Me and the gameloop are not friends

Started by
3 comments, last by TwoNybble 8 years, 10 months ago

Me and my gameloop are not friends...
Ok, on a more serious note I have a ton of trouble when it comes to the gameloop and would appreciate some help in this matter.

I have read the infamous http://gafferongames.com/game-physics/fix-your-timestep/ article and the equally http://www.koonsolo.com/news/dewitters-gameloop/ article, but for some reason I can't ever implement a fixed timestep gameloop correctly sad.png

I chose to go with the Gafferon Games approach as it seems to be the article that is referred to the most. I understand the article (what the article is saying), but there seems to be something wrong with my implementation all the time.

No matter what, I seem to always have a constant hiccup or stutter. It is like my app is trying to play catch up all the time? Maybe it comes down to the interpolation calculation being off? Can someone please tell me what I am doing wrong, at this point I'm not really sure what I should do.


I am trying to target 60 FPS just as a FYI, I have tried to target 30 FPS as well but that seems to make it worse (hiccups / stutter).

Also I am developing for Android using OpenGL ES 2.0, My testing hardware is a Kyocera Hydro Edge [ http://www.boostmobile.com/shop/phones/kyocera-hydro-edge/ ].

I have tested on other android phones too, but I still have the same issues

If you have question please ask, I would really like to solve this problem once and for all. Thanks smile.png



Update game logic method and render method


public void Update(float deltatime)
{
        //Update the position of the 1st scrolling background
	prevBack1X = back1X;
	back1X -= 120.0f * deltatime;
	if(back1X + space1.getWidth() <= 0.0f) //Warp the background to the start position because it has gone offscreen
	{
		back1X = 0.0f;
		prevBack1X = 0.0f;
	}

	//Update the position of the 2nd scrolling background
	prevBack2X = back2X;
	back2X -= 360.0f * deltatime;
	if(back2X + space2.getWidth() <= 0.0f) //Warp the background to the start position because it has gone offscreen
	{
		back2X = 0.0f;
		prevBack2X = 0.0f;
	}
}

public void Render(float alpha)
{
	//Interpolation (LERP) to get render position
	pos = prevBack1X + (back1X - prevBack1X) * alpha;
	pos2 = prevBack2X + (back2X - prevBack2X) * alpha;
	
	batch.Begin(projectionMatrix);
	
	//Draw the first background and the background that follows
	//to complete the scrolling background illusion 
	batch.Draw(background1, pos, 0.0f);
	batch.Draw(background1, pos + background1.getWidth(), 0.0f);
	
	//Draw the second background and the background that follows
	//to complete the scrolling background illusion 
	batch.Draw(background2, pos2, 0.0f);
	batch.Draw(background2, pos + background1.getWidth(), 0.0f);
	
	batch.End();
}


The Gameloop! And frame time calculation


private long nowTime = System.nanoTime();
private long lastTime = nowTime;
private float accum = 0.0f;
private float frameTime = 0.0f;
private float fixedDT = 1.0f / 60.0f; //Target 60 FPS for game update speed

public void onDrawFrame(GL10 gl)
{
    CalculateFrameTime(); //Calc the frametime in secs

    accum += frameTime;
    while(accum >= fixedDT)
    {
        Update(fixedDT);
        accum -= fixedDT;
    }
    alpha = accum / fixedDT;

    //System.out.println("FrameTime: " + frameTime + " | Accum: " + accum + " | Alpha: " + alpha);

    Render(alpha);
}

private void CalculateFrameTime()
{
    //Calc the frame time in seconds
    nowTime = System.nanoTime();
    frameTime = (nowTime - lastTime) / 1000000000.0f;
    lastTime = nowTime;
}

Sample of FrameTime (in secs), Accum value, and alpha (println statement above)


FrameTime: 0.015107584 | Accum: 0.001972055 | Alpha: 0.1183233
FrameTime: 0.013520525 | Accum: 0.01549258 | Alpha: 0.92955476
FrameTime: 0.016633602 | Accum: 0.015459513 | Alpha: 0.92757076
FrameTime: 0.018495347 | Accum: 6.215237E-4 | Alpha: 0.03729142
FrameTime: 0.016633604 | Accum: 5.884599E-4 | Alpha: 0.035307594
FrameTime: 0.019990845 | Accum: 0.003912637 | Alpha: 0.23475821
FrameTime: 0.014893942 | Accum: 0.002139911 | Alpha: 0.12839466
FrameTime: 0.013428963 | Accum: 0.015568874 | Alpha: 0.9341324
FrameTime: 0.016664123 | Accum: 0.015566329 | Alpha: 0.9339797
FrameTime: 0.018495345 | Accum: 7.2833896E-4 | Alpha: 0.043700334
FrameTime: 0.016603082 | Accum: 6.6475384E-4 | Alpha: 0.039885227
FrameTime: 0.020021364 | Accum: 0.0040194504 | Alpha: 0.24116701
FrameTime: 0.014863421 | Accum: 0.0022162031 | Alpha: 0.13297218
FrameTime: 0.014130932 | Accum: 0.016347136 | Alpha: 0.9808281
FrameTime: 0.015931634 | Accum: 0.015612101 | Alpha: 0.93672603
FrameTime: 0.018525865 | Accum: 8.046292E-4 | Alpha: 0.048277747
FrameTime: 0.016603084 | Accum: 7.410459E-4 | Alpha: 0.04446275
FrameTime: 0.021577904 | Accum: 0.0056522824 | Alpha: 0.33913693
FrameTime: 0.039188158 | Accum: 0.011507105 | Alpha: 0.6904263
FrameTime: 0.005829391 | Accum: 6.698277E-4 | Alpha: 0.040189657
FrameTime: 0.007019686 | Accum: 0.0076895137 | Alpha: 0.4613708
FrameTime: 0.007874256 | Accum: 0.015563769 | Alpha: 0.9338261
FrameTime: 0.016816728 | Accum: 0.015713831 | Alpha: 0.94282985
FrameTime: 0.02145582 | Accum: 0.0038363151 | Alpha: 0.2301789
FrameTime: 0.031435985 | Accum: 0.0019389652 | Alpha: 0.1163379
FrameTime: 0.001800702 | Accum: 0.0037396671 | Alpha: 0.22438002
FrameTime: 0.014985503 | Accum: 0.0020585023 | Alpha: 0.12351013
FrameTime: 0.017366093 | Accum: 0.0027579274 | Alpha: 0.16547564
FrameTime: 0.012574393 | Accum: 0.015332321 | Alpha: 0.9199392
FrameTime: 0.016572561 | Accum: 0.015238216 | Alpha: 0.91429293
FrameTime: 0.0179765 | Accum: 0.01654805 | Alpha: 0.99288297
FrameTime: 0.015321227 | Accum: 0.01520261 | Alpha: 0.9121565
FrameTime: 0.016694643 | Accum: 0.015230587 | Alpha: 0.91383517
FrameTime: 0.019136272 | Accum: 0.0010335259 | Alpha: 0.06201155
FrameTime: 0.016572561 | Accum: 9.394195E-4 | Alpha: 0.056365166
FrameTime: 0.017823897 | Accum: 0.0020966493 | Alpha: 0.12579896
FrameTime: 0.015595911 | Accum: 0.0010258928 | Alpha: 0.061553564
FrameTime: 0.014222493 | Accum: 0.015248386 | Alpha: 0.9149031
FrameTime: 0.016694643 | Accum: 0.015276363 | Alpha: 0.91658175
FrameTime: 0.016908286 | Accum: 0.015517982 | Alpha: 0.93107885
FrameTime: 0.016603084 | Accum: 0.0154543985 | Alpha: 0.92726386
FrameTime: 0.016694644 | Accum: 0.015482375 | Alpha: 0.9289425
FrameTime: 0.018983671 | Accum: 0.0011327118 | Alpha: 0.067962706
FrameTime: 0.014222493 | Accum: 0.015355205 | Alpha: 0.9213123
FrameTime: 0.016633604 | Accum: 0.0153221395 | Alpha: 0.91932833
FrameTime: 0.016938806 | Accum: 0.015594279 | Alpha: 0.9356567
FrameTime: 0.016603082 | Accum: 0.015530696 | Alpha: 0.93184173
FrameTime: 0.016664123 | Accum: 0.015528152 | Alpha: 0.9316891
FrameTime: 0.019044712 | Accum: 0.001239527 | Alpha: 0.07437161

Advertisement

The game loop looks fine to me, but there seems to be something wrong with the Update() method.


	if(back1X + space1.getWidth() <= 0.0f) //Warp the background to the start position because it has gone offscreen
	{
		back1X = 0.0f;
		prevBack1X = 0.0f;
	}

The backgrounds might be further scrolled than exactly one width, so you shouldn't just set the values to 0, but instead add the widths:


	if(back1X + space1.getWidth() <= 0.0f) //Warp the background to the start position because it has gone offscreen
	{
		back1X += space1.getWidth();
		prevBack1X += space1.getWidth();
	}

Same with the other background.

blah :)

You never actually mentioned it, but it looks to me like you are using OpenGL ES with Android. Your "game loop" is actually the onDrawFrame callback, which the system will try to call each time the screen is refreshed. If your frames are longer than this refresh rate (usually 16.67 milliseconds or 60 FPS), they will start to queue up on the GPU. This will either result in ignored frames and stuttering like what you are experiencing.

From your data, it seems that this may be what is happening. Notice that your frames are frequently longer than 0.0167 seconds, and that this error accumulates several times over 0.03 seconds. When the onDrawFrames callback gets that far behind, you're going to see some stuttering as the system tries to play catch up. I suspect that you may have an inefficient implementation behind your "batch" object, or simply are not using a very powerful testing device (if you're using an emulator, try with a physical device to make sure this is even an issue in reality).

If you are sure that you are running as efficiently as possible on capable hardware, you may want to look into separating your update and rendering logic into their own threads. In this scheme, you could be updating the game state at the same time as you are rendering a previous frame. This can be much more efficient on Android devices -- especially those with multiple cores.

You never actually mentioned it, but it looks to me like you are using OpenGL ES with Android


Totally missed that, I updated my original post with the information on my testing hardware
You are correct, I'm developing for Android using OpenGL ES 2.0. Using physical hardware, its a

Kyocera Hydro Edge (http://www.boostmobile.com/shop/phones/kyocera-hydro-edge/)

Your "game loop" is actually the onDrawFrame callback, which the system will try to call each time the screen is refreshed. If your frames are longer than this refresh rate (usually 16.67 milliseconds or 60 FPS), they will start to queue up on the GPU. This will either result in ignored frames and stuttering like what you are experiencing.

From your data, it seems that this may be what is happening. Notice that your frames are frequently longer than 0.0167 seconds, and that this error accumulates several times over 0.03 seconds. When the onDrawFrames callback gets that far behind, you're going to see some stuttering as the system tries to play catch up. I suspect that you may have an inefficient implementation behind your "batch" object, or simply are not using a very powerful testing device (if you're using an emulator, try with a physical device to make sure this is even an issue in reality).

If you are sure that you are running as efficiently as possible on capable hardware, you may want to look into separating your update and rendering logic into their own threads. In this scheme, you could be updating the game state at the same time as you are rendering a previous frame. This can be much more efficient on Android devices -- especially those with multiple cores.


I was thinking about this over the last few days actually

And did try to separate the logic and rendering like you suggested:


class LogicTask implements Runnable()
{
	public void run()
	{
		while(true)
		{
			CalculateFrameTime(); //Calc the frametime in secs

			accum += frameTime;
			while(accum >= fixedDT)
			{
				Update(fixedDT);
				accum -= fixedDT;
			}
			
                        alpha = accum / fixedDT;
		}
	}
}


public void onSurfaceCreated (GL10 gl, EGLConfig config)
{
	/* Other create code*/
	
	nowTime = System.nanotime();
	lastTime = nowTime;
	new Thread(new LogicTask()).start();
	
	/* Other create code*/
}

public void onDrawFrame(GL10 gl)
{
    Render(alpha);
}

But it was still the same sad.png I even tried setting the render mode to RENDERMODE_WHEN_DIRTY and then calling requestRender() right after the alpha = accum / fixeDT
But it was the same stuttering, hiccups, etc (now that I think about it I'm not even sure requestRender calls would be any different)


In regards to my batch object, I started to think that might be the problem too. My batch basically works like the following:
Add the sprite's data to an intermediate container
When we have to change textures or the intermediate container is full, I call my flush method to set everything up and make the draw call

My flush method:


public void FlushBatch()	
{
	//If there is nothing to draw back out
	if(vboIndex == 0)
		return;
	
	//Activate the texture to use and bind it
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, batchTextureID);
	
	//Bind the VBO Buffer
	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	
	//Bind the shader program and enable the attributes / upload the uniforms
	shader.UseProgram();
	shader.UseUniform("projectionMatrix", batchProjectionMatrix.data);
	shader.UseUniform("textureSampler", 0);
	shader.EnableAttribute("vertexPos", shader.AttributesTotalByteSize(), 0);
	shader.EnableAttribute("color", shader.AttributesTotalByteSize(), shader.Attribute("vertexPos").byteSize);
	shader.EnableAttribute("texCoords", shader.AttributesTotalByteSize(), shader.Attribute("vertexPos").byteSize + shader.Attribute("color").byteSize);

	//Take the data from the intermediate container (Array of floats) and place it all in the FloatBuffer
	vboBuffer.put(vertexDataContainer, 0, vboIndex);
	vboBuffer.position(0);
	
	//Feed our data to the VBO buffer on the gpu using what is inside the FloatBuffer. Make the draw call
	glBufferSubData(GL_ARRAY_BUFFER, 0, vboIndex * BYTES_PER_FLOAT, vboBuffer);
	glDrawArrays(GL_TRIANGLES, 0, vboIndex / 10);
	vboIndex = 0;
}

Maybe it is because I am not ping-ponging VBOs, which is causing the GPU to stall? Maybe I should orphan my VBOs when they are full?


What are your thoughts on this? Anyone's thoughts? Does any thing look out of place?

And did try to separate the logic and rendering like you suggested

I don't know if this is just a simplification of how you really split off the logic thread or not, but judging from what I see, this is likely a really bad way of doing it. If you are running the thread, and drawing the objects directly from the state being kept on the logic thread, you are going to run into a lot of problems. Variables need to be synchronized (or locked) before they can be accessed from other threads to avoid race conditions and read/write trouble.

Unfortunately, this means that if you want to continue to try to separate the logic and rendering threads, you will need to create a much better, safer way of passing data between the two threads. Generally this is done with a secure blocking queue or buffer with which you pass information about objects to be drawn. This can be fairly complex to do correctly. Bad implementations can actually end up being worse than single thread execution, always waiting on the other thread to complete and slowing the whole thing down.

A good place to start researching a proper solution is the Replica Island blog and source code.

You can ignore all that if you are already implementing something like that.

In regards to my batch object, I started to think that might be the problem too.

Just from looking at this snippet, I'd say that the batch likely is not your problem, especially since it looks like you are only putting 8 triangles in it... Although, I would suggest simply skipping the intermediate container and building the FloatBuffer directly. If you are completely replacing the Buffer Object each time, I would recommend using glBufferData instead of the “Sub” variant. Also, try drawing directly with the FloatBuffer instead of the Buffer Object, just to eliminate the possibility of them being slow on your particular device.

Lastly, consider keeping your backgrounds as static geometry in a static buffer and translating each in the vertex shader. Rebuilding the VBO each time with new coordinates can be very slow, although with only 8 triangles, I doubt this is where you are experiencing your slowdown.

Worst case scenario, try on a few different devices if you can to make sure it is not simply an inefficient implementation of some OpenGL ES functionality.

This topic is closed to new replies.

Advertisement