Frameskipping on slower platforms

Previous topic - Next topic

Wampus

There have been some threads about how to keep game movement at a fixed speed, regardless of how many frames per second you app is running at. A recap:-

MrTAToad's Application Timer (updates movement based on timer) -http://www.glbasic.com/forum/index.php?topic=3512.0
Bigsofty's conversion of a BB routine to update movement at fixed rate - http://www.glbasic.com/forum/index.php?topic=5297.0
Simple timer thread - http://www.glbasic.com/forum/index.php?topic=5335.0

The above is great if an app was written with variable frame rates in mind. However, what if an app was written assuming it would be able to run at a full 60 frames per second? If it is used on platforms that are not fast enough to achieve that screen refresh rate then the app will appear to run more slowly. This is particularly bad for game applications where the movement should be the be same regardless of platform. A couple of solutions:-


  • Rewrite the way the game treats movement so that all of it can be calculated according to a timer
  • Use frameskipping to attempt to catch-up the game speed to the screen refresh rate

Choice 1 would be ideal. The problem is its very time consuming. If the game is large it would be a very difficult thing to convert all the movement to a timer based approach.

Choice 2 is less time consuming. Assuming that your game treats game mechanics (movement, play input, changes of status, etc.) and drawing the screen in separate routines, it might be possible to only call the screen drawing routines when there is sufficient time to do so. This will not create the smooth movement of choice 1 but it could mean the difference between a game that is simply broken or at least playable on slower platforms. Its a technique that is often used in emulators with acceptable results

I'm going to see if I can achieve the 2nd approach using GLBasic today. If anyone has done this before or has any ideas about how to do it in GLBasic please post a response in this thread. I'll post my code and the results at the end of the day, even if I fail.

MrTAToad

My routine should increase the return value on slower machines and reduce it on faster ones, so movement on all types of machines should be the same.

Wampus

#2
I will have to try your routine tomorrow MrTAToad to see if I can get a useful frameskipping thing going. My attempt failed completely but maybe your code does something that mine doesn't.

Nothing usable came out of my efforts today. After a couple of hours I had a routine on the PC that seemed to work but when I tried to test it on an iPod Touch it went very wrong. Many tweaks later and I've just above given up. My code is now a mess since I've made so many small changes to it to see if I could get some stability. For some reason GETTIMER() and GETTIMERALL() behave very erratically on my iPod Touch. I suspect its something to do with the multitasking stuff going on in the background. The only other method I have to get a value to reflect the screen refresh is to use PLATFORMINFO$("TIME"). That takes an entire second to update, which is far too slow for frameskipping to be any use.

I have also discovered that beer and coding don't go well together. Anyway, here is the code (that failed, so is probably useless to everyone):-

Code (glbasic) Select
// --------------------------------- //
// Project: FrameSkippingTest
// Start: Friday, November 26, 2010
// IDE Version: 8.174


SETCURRENTDIR("Media") // seperate media and binaries?




// Global Variables -------------------------------------------- //

SELECT PLATFORMINFO$("DEVICE")

CASE "DESKTOP"

GLOBAL SpriteNumber = 100 //  Number of sprites to display. (Set this to 3000 or so for a PC/Mac)
GLOBAL SpriteInc = 100 //  Increase or decrease sprite numbers by this amount. (Set this to 500 or so for a PC/Mac)

CASE "IPHONE"

GLOBAL SpriteNumber = 10
GLOBAL SpriteInc = 10

CASE "IPOD TOUCH"

GLOBAL SpriteNumber = 10
GLOBAL SpriteInc = 10

CASE "IPOD"

GLOBAL SpriteNumber = 10
GLOBAL SpriteInc = 10

DEFAULT

GLOBAL SpriteNumber = 100
GLOBAL SpriteInc = 100

ENDSELECT

GLOBAL gap = 7 //  Gap between sprites

GLOBAL ScreenX = 480 // x dimension of screen
GLOBAL ScreenY = 320 //  y dimension of screen
GLOBAL ScreenFPS = -1 //  limitfps settings. Should be -1 for testing or you won't get a true result

GLOBAL MidX //  Middle of screen in x terms
GLOBAL MidY //  Middle of screen in y terms

GLOBAL SpriteX //  x dimension of sprite
GLOBAL SpriteY //  y dimension of sprite

GLOBAL MoveX[] //  To hold x movement steps in
GLOBAL MoveY[] //  To hold y movement steps in
GLOBAL PColour[] //  To hold colours in

GLOBAL incr1 //  Counter
GLOBAL incr2 //  Counter
GLOBAL steps1 //  Counter
GLOBAL steps2 //  Counter
GLOBAL moves1 //  Counter
GLOBAL moves2 //  Counter
GLOBAL colcount //  Counter

GLOBAL time% // main actual timer
GLOBAL dtime% //  loop timer
GLOBAL FramesPerSecond //  simple FPS counter
GLOBAL frskip = 0 //  how many frames to skip
GLOBAL frskiprecord = 0 //  frameskip
GLOBAL frameaverage = 0 //  average framerate
GLOBAL frametotal = 0 //  total framerate miss
GLOBAL framecounter = 0 //  Counter
GLOBAL frames[] //  Array for last 5 framerates
DIM frames[10]

GLOBAL controldelay = 0 //  how long to wait until increase or decrease of sprites
GLOBAL mx, my, b1, b2 //  Mouse controls


// ------------------------------------------------------------- //




// Set up screen dimensions ------------------------------------ //

SETSCREEN ScreenX, ScreenY, 0
LIMITFPS ScreenFPS

LOADSPRITE "sprite.png", 1
GETSPRITESIZE 1, SpriteX, SpriteY

MidX = (ScreenX / 2)
MidY = (ScreenY / 2)

// ------------------------------------------------------------- //




// Initialise Movement ----------------------------------------- //

DIM MoveX[1441]
DIM MoveY[1441]
DIM PColour[769]

colcount = 0
incr1 = 255
steps1 = 127
moves1 = 127

FOR i = 0 TO 255

PColour[colcount] = RGB(incr1, steps1, moves1)

DEC incr1, 0.5
INC steps1, 0.5
INC colcount, 1

NEXT

incr1 = 127
steps1 = 255
moves1 = 127

FOR i = 0 TO 255

PColour[colcount] = RGB(incr1, steps1, moves1)

DEC steps1, 0.5
INC moves1, 0.5
INC colcount, 1

NEXT

incr1 = 127
steps1 = 127
moves1 = 255

FOR i = 0 TO 255

PColour[colcount] = RGB(incr1, steps1, moves1)

DEC moves1, 0.5
INC incr1, 0.5
INC colcount, 1

NEXT

incr1 = 0
incr2 = 0
steps1 = 0
steps2 = 0
moves1 = 0
moves2 = 0
colcount = 0

// ------------------------------------------------------------- //




// Main Loop --------------------------------------------------- //

time = GETTIMERALL()

WHILE KEY(01) = FALSE



Movement()
Controls()






IF frskip = 0

DrawScreen()

SELECT PLATFORMINFO$("DEVICE")

CASE "DESKTOP"

ALPHAMODE -1.0
FramesPerSecond=FPS()
PRINT "FPS: "+FramesPerSecond,0,0
PRINT "Sprites on screen: "+SpriteNumber,0,10
PRINT "DTIME: "+dtime, 0, 20
PRINT "Average DTIME: "+frameaverage,0,30
PRINT "Timer: "+time,0,40
PRINT "Frameskip: "+frskiprecord,0,50
PRINT "Gettimer: "+GETTIMER(),0,60
frskiprecord = 0

dtime = GETTIMERALL() - time

FrameSkip()

SHOWSCREEN

time = GETTIMERALL()

DEFAULT

ALPHAMODE -1.0
FramesPerSecond=FPS()
PRINT "FPS: "+FramesPerSecond,0,0
PRINT "Sprites on screen: "+SpriteNumber,0,10
PRINT "DTIME: "+dtime, 0, 20
PRINT "Highest DTIME: "+frameaverage,0,30
PRINT "Timer: "+time,0,40
PRINT "Frameskip: "+frskiprecord,0,50
PRINT "Gettimer: "+GETTIMER(),0,60
frskiprecord = 0

dtime = GETTIMERALL() - time

FrameSkipIDevice()

SHOWSCREEN

time = GETTIMERALL()

ENDSELECT


ELSE

DEC frskip

ENDIF






WEND

END

// ------------------------------------------------------------- //







// ------------------------------------------------------------- //
// ---  MOVEMENT  ---
// ------------------------------------------------------------- //
FUNCTION Movement:

LOCAL woah1, woah2, woah3, woah4

woah1 = incr1
woah2 = incr2
woah3 = steps1
woah4 = steps2

FOR i = 0 TO 1440

MoveX[i] = MidX-(SpriteX/2) + (SIN(woah1+woah3) * (MidX-(SpriteX/2)))
MoveY[i] = MidY-(SpriteY/2) + (COS(woah2+woah4) * (MidY-(SpriteY/2)))

INC woah1, 1
INC woah2, 0.5
INC woah3, 0.2
INC woah4, 0.25

NEXT

INC incr1, 0.5
IF incr1 >= 1440 THEN DEC incr1, 1440

INC incr2, 0.4
IF incr2 >= 1440 THEN DEC incr2, 1440

INC steps1, 0.25
IF steps1 >= 1440 THEN DEC steps1, 1440

INC steps2, 0.25
IF steps2 >= 1440 THEN DEC steps2, 1440

INC colcount, 2
IF colcount >= 768 THEN DEC colcount, 768


ENDFUNCTION // MOVEMENT









// ------------------------------------------------------------- //
// ---  DRAWSCREEN  ---
// ------------------------------------------------------------- //
FUNCTION DrawScreen:

LOCAL PosX, PosY, PosXMax, PosYMax, SprX, SprY, SprXMax, SprYMax, cols, FramesPerSecond

STARTPOLY 1, 1
ALPHAMODE -0.5

moves1 = 0
moves2 = 0
cols = colcount

FOR i = 0 TO SpriteNumber

PosX = MoveX[moves1]
PosY = MoveY[moves2]
PosXMax = PosX + SpriteX
PosYMax = PosY + SpriteY

SprX = 0
SprY = 0
SprXMax = SpriteX
SprYMax = SpriteY

INC moves1, gap
IF moves1 >= 1440 THEN DEC moves1, 1440

INC moves2, gap
IF moves2 >= 1440 THEN DEC moves2, 1440

POLYVECTOR PosX, PosY, SprX, SprY, PColour[cols]
POLYVECTOR PosX, PosYMax, SprX, SprYMax, PColour[cols]
POLYVECTOR PosXMax, PosYMax, SprXMax, SprYMax, PColour[cols]

POLYVECTOR PosXMax, PosYMax, SprXMax, SprYMax, PColour[cols]
POLYVECTOR PosXMax, PosY, SprXMax, SprY, PColour[cols]
POLYVECTOR PosX, PosY, SprX, SprY, PColour[cols]

INC cols, 16
IF cols >= 768 THEN DEC cols, 768
NEXT

ENDPOLY


ENDFUNCTION // DRAWSCREEN





// ------------------------------------------------------------- //
// ---  FRAMESKIP  ---
// ------------------------------------------------------------- //
FUNCTION FrameSkip:

IF frskip = 0

frames[framecounter] = dtime
INC framecounter
IF framecounter = 10 THEN framecounter = 0

frametotal = frames[0]+frames[1]+frames[2]+frames[3]+frames[4]+frames[5]+frames[6]+frames[7]+frames[8]+frames[9]

IF framecounter = 9 THEN frameaverage = frametotal/10

SELECT dtime

CASE < 31 //  is the framerate up to 60 FPS?

frskip = 0

CASE < 48 //  is the framerate up to 30 FPS?

frskip = 1
frskiprecord = 1

CASE < 65 //  is the framerate up to 20 FPS?

frskip = 2
frskiprecord = 2

CASE < 82 //  is the framerate up to 15 FPS?

frskip = 3
frskiprecord = 3

CASE < 99 //  is the framerate up to 12 FPS?

frskip = 4
frskiprecord = 4

CASE < 116 //  is the framerate up to 10 FPS?

frskip = 5
frskiprecord = 5

CASE >= 116 //  Whoops! framerate is below 10 FPS

frskip = 0

// Unless this is very temporary then a disaster, basically.
// The target system is far too slow to be playing the
// game at any decent speed, or even worse: -
// Calculations during the game mechanics might be
// causing severe slow down so should turn off frameskipping
// just to display something on the screen in that case

frskiprecord = 256

DEFAULT

ENDSELECT


ENDIF



ENDFUNCTION // FRAMESKIP





// ------------------------------------------------------------- //
// ---  FRAMESKIPIDEVICE  ---
// ------------------------------------------------------------- //
FUNCTION FrameSkipIDevice:

LOCAL gettime

gettime = GETTIMER()

IF frskip = 0

frames[framecounter] = gettime
INC framecounter
IF framecounter = 10 THEN framecounter = 0

frametotal = 0

FOR i = 0 TO 9

IF frames[i] > frametotal THEN frametotal = frames[i]

NEXT

frameaverage = frametotal

SELECT frameaverage

CASE < 31 //  is the framerate up to 60 FPS?

frskip = 0

CASE < 48 //  is the framerate up to 30 FPS?

frskip = 1
frskiprecord = 1

CASE < 65 //  is the framerate up to 20 FPS?

frskip = 2
frskiprecord = 2

CASE < 82 //  is the framerate up to 15 FPS?

frskip = 3
frskiprecord = 3

CASE < 99 //  is the framerate up to 12 FPS?

frskip = 4
frskiprecord = 4

CASE < 116 //  is the framerate up to 10 FPS?

frskip = 5
frskiprecord = 5

CASE >= 116 //  Whoops! framerate is below 10 FPS

frskip = 0

// Unless this is very temporary then a disaster, basically.
// The target system is far too slow to be playing the
// game at any decent speed, or even worse: -
// Calculations during the game mechanics might be
// causing severe slow down so should turn off frameskipping
// just to display something on the screen in that case

frskiprecord = 256

DEFAULT

ENDSELECT

ENDIF



ENDFUNCTION // FRAMESKIPIDEVICE






FUNCTION FPS:
   STATIC OldTimeReport$,FPSDat,TimeReport$,FPSd
   OldTimeReport$=TimeReport$; FPSDat=FPSDat+1;
   TimeReport$=PLATFORMINFO$("TIME")
   IF OldTimeReport$<>TimeReport$; FPSd=FPSDat ; FPSDat=0; ENDIF
   RETURN FPSd



ENDFUNCTION





// ------------------------------------------------------------- //
// ---  CONTROLS  ---
// ------------------------------------------------------------- //
FUNCTION Controls:

IF controldelay > 0 THEN DEC controldelay

IF PLATFORMINFO$("DEVICE") <> "DESKTOP"

MOUSESTATE mx, my, b1, b2

IF controldelay > 0 THEN DEC controldelay

IF b1 AND mx < MidX AND controldelay = 0

IF SpriteNumber > SpriteInc

DEC SpriteNumber, SpriteInc

controldelay = 10

ENDIF

ELSEIF b1 AND mx >= MidX AND controldelay = 0

INC SpriteNumber, SpriteInc

controldelay = 10

ENDIF

ELSE

IF KEY(203) AND controldelay = 0

IF SpriteNumber > SpriteInc

DEC SpriteNumber, SpriteInc

controldelay = 10

ENDIF

ELSEIF KEY(205) AND controldelay = 0

INC SpriteNumber, SpriteInc

controldelay = 10

ENDIF

ENDIF

ENDFUNCTION // CONTROLS

Leginus

I did a very simple frame skipping when I first started "BumbleBomber".  I didnt use it in the end but it doubled frame rate.
Code (glbasic) Select

Global FrameSkip

While WHILE KEY(01) = FALSE
   FrameSkip=FrameSkip+0.1
   If FrameSkip>0.1
         Showscreen
         FrameSkip=0
   endif
Wend


Obviously you can change your frameskip value to whatever.
This doesnt take into account different speeds of pc's, but you if it is for ios you can have an "Optimise for 3g" button in your options to use this routine just for 3g

Wampus

Leginus having such an option is a good thing. A simple switch from 60FPS to 30FPS could mean even 1st generation iDevices are able to work with games designed for 3GS upwards.

What I wanted was some way to automatically detect whether frameskipping was necessary and adjust it on the fly. Sometimes even with 3GS devices and upwards there is a need for it. For example, when I'm taking a call on Skype I might open a game app, which will run slower because I'm making heavy use of multitasking.

The method I was previously using on PowFish, which I will now have to go back to, was to detect if the refresh rate is below ~55 when the app starts. If it was then the quality was adjusted downwards so that the app runs at 30FPS instead of 60FPS. This could be changed manually if necessary.