Hey guys,
just wanted to share another piece of code that provides a pretty stable method for timer based movement. It combines two different approaches: Scaling things using the time delta between two frames, and executing a fixed set of discrete steps for certain other operations.
The basic "library" (which mainly consists of an init and an update function) is very small and simple and doesn't really do much:
remstart
How to use this tiny library:
*call game_init() once after starting your program
*call game_update() somewhere in your main loop (preferably
at the beginning, before updating anything else in the game)
*You can use game_reset() if you wish to reset the library's
timer back to 0 (e.g. after loading a new level)
How to enable timerbased movement:
*De/Incremental Changes to all continuous values (floats and
doubles) over time should be multiplied with game.timeFactor
*Noncontinuous values (or those that can't simply be scaled with
the timeFactor for other reasons) should be multiplied with
game.fullUpdates (which is an int and usually 0 or 1, it only
gets higher if the games runs with less FPS than defined in
GAME__FULL_UPDATES_PER_SECOND)
*For changes that can't even be scaled with game.fullUpdates,
you may use a for loop ranging from 1 to game.fullUpdates
*In theory you could just use game.fullUpdates for *everything*
and leave game.timeFactor out completely, however, this would
mean that FPS rates above GAME__FULL_UPDATES_PER_SECOND would
not update any more often than that exact value each second.
Hence the advantage of timerbased movement would only be
given for slow computers, fast ones would not gain any benefit
Note:
*The constants below can be changed, however:
*GAME__MS_PER_FULL_UPDATE * GAME__FULL_UPDATES_PER_SECOND should
always equal 1000!
*GAME__FULL_UPDATES_PER_SECOND defines how many non-continuous
update steps are executed each secod
*GAME__MAX_TIME_DIF defines an upper boundary for the virtual
time passing between two frames. This effectively means that
if the game runs slower than this value allows (i.e. the game
loop requires more ms than this value), it will not be "timer
based" anymore but slow down. This is meant to prevent extreme
"jumping" of objects, which might allow player characters to
clip through walls etc. You can change this value, but I'd
recommend to not use anything above 100.
*you can access the game.runtime variable to avoid calling the
timer() function regularly. Note that it always starts at 0
though.
remend
rem Only change these constants if you know what you're doing (and
rem if you've read the paragraphs above)
#constant GAME__MS_PER_FULL_UPDATE = 20
#constant GAME__FULL_UPDATES_PER_SECOND = 50
#constant GAME__MAX_TIME_DIF = 100
type game_Type
systimer as integer
starttime as integer
runtime as integer
actualRuntime as integer
timedif as integer `limited by GAME__MAX_TIME_DIF
actualTimeDif as integer
timeFactor as float
fullUpdates as integer `for high frame rates usually 0, but 50 times per second 1; for FPS lower than 30 this number will occasionally be higher than one for apply multiple updates at once
nextFullUpdate as integer
fullUpdateSum as integer
endtype
function game_init()
global game as game_Type
game.starttime = timer()
game.runtime = 0
game.nextFullUpdate = 0
game.actualRuntime = 0
game.fullUpdateSum = 0
endfunction
function game_reset()
game_init()
endfunction
function game_update()
game.systimer = timer()
tprev = game.actualRuntime
game.actualRuntime = game.systimer - game.starttime
game.timedif = game.actualRuntime - tprev
game.actualTimeDif = game.timedif
if game.timedif > GAME__MAX_TIME_DIF then game.timedif = GAME__MAX_TIME_DIF
inc game.runtime, game.timedif
game.timeFactor = game.timedif/(1.0*GAME__MS_PER_FULL_UPDATE)
game.fullUpdates = 0
if game.runtime >= game.nextFullUpdate
dif = game.runtime - game.nextFullUpdate
updates = 1 + dif/GAME__MS_PER_FULL_UPDATE
if updates > 10
game.fullUpdates = 10
game.nextFullUpdate = game.runtime + GAME__MS_PER_FULL_UPDATE
else
inc game.nextFullUpdate, updates*GAME__MS_PER_FULL_UPDATE
game.fullUpdates = updates
endif
endif
inc game.fullUpdateSum, game.fullUpdates
endfunction
The usage is explained in the comments in the beginning. To summarize it very shortly:
-Call game_init() in the beginning of your program and game_update() in the main loop.
-When dealing with floats, you can just multiply any changes (e.g. movement speed of a character) with game.timeFactor
-When dealing with integers or more complex operations that need to be executed a fixed amount of times each frame, you can use game.fullUpdates, which is an integer and is usually either 0 or 1, but can also get higher in case the game's FPS rate falls below 50 (the exact configuration can be changed using the three constants in the code above).
Note that the GAME__MAX_TIME_DIF constant defines an upper boundary for the time delta between two frames. If the game gets slower than that, it will actually slow down instead of keeping everything timer based. This is meant to prevent "jumping" of values (and objects), which could for instance cause the player character to clip through walls.
I wrote a small example demonstrating how to use it exactly.
Rem Project: timer based movement
Rem Created: Wednesday, February 26, 2014
Rem ***** Main Source File *****
sync on
sync rate 0
sync
game_init()
size = 250
make matrix 1, size,size, 50,50
position camera size/2, 5.0, -10.0
autocam off
objects = 300
for i = 1 to objects
make object cube i, 1.0*(2+rnd(100)*0.02)
position object i, rnd(size), 0, rnd(size)
rotate object i, rnd(359), rnd(359), rnd(359)
next
limitedsyncrate = 0
hitpoints = 500
rem One Update immediately before the main loop can't hurt to avoid time jumps due to the loading process
game_update()
do
rem Calling game_update() should happen early in the main loop
game_update()
rem Movement
ud = keystate(17)-keystate(31)
rl = keystate(32)-keystate(30)
if ud or rl
if ud
move camera 1.0*ud*game.timeFactor
remstart
Alternatively you could do this:
move camera ud*game.fullUpdates
which is "integer-friendly" (as game.fullUpdates is not a float, as opposed to game.timeFactor)
or even this:
for i = 1 to game.fullUpdates
move camera ud
next
which in this case does the same as the line above, but might be required in some
more complicated places where you can't just scale a value up but have to run a
certain operation X times
remend
endif
if rl
rotate camera 0, ay#+90, 0
move camera 1.0*rl*game.timeFactor
rotate camera ax#, ay#, 0
endif
endif
rem Mouse - Note: mousemovex()/y() don't need additional timing, they behave equally for all fps rates
ax# = camera angle x() + 1.0*mousemovey()
if ax# > 85 then ax# = 85 else if ax# < -85 then ax# = -85
ay# = camera angle y() + 1.0*mousemovex()
rotate camera ax#, ay#, 0
rem Damage Player when leaving the map
cx# = camera position x() : cz# = camera position z()
if cx# < 0 or cx# > size or cz# < 0 or cz# > size
remstart
Hitpoints is an integer, hence decrementing it by something*game.timeFactor
would not work properly (as is would mostly be rounded down to 0 and not
change the hitpoints). Hence we use game.fullUpdates which sums up to exactly
50 each second.
remend
dec hitpoints, game.fullUpdates
showhitmessage = 1
else
showhitmessage = 0
endif
rem Update Cubes
for i = 1 to objects
rem Make sure each cube gets its own behaviour but stays consistent over time
randomize i
amp# = 0.3 + rnd(rnd(150))*0.04
spd# = 0.3*(0.1 + rnd(100)*0.009)
off# = rnd(359)
yoff# = rnd(100)*0.05 - 2.5
rem Use absolute timer value (game.runtime) for explicit placement
position object i, object position x(i), yoff# + amp#*sin(spd#*game.runtime + off#), object position z(i)
next
rem T for sync rate
if keystate(20)
if tpressed = 0
tpressed = 1
if limitedsyncrate
limitedsyncrate = 0
sync rate 0
else
limitedsyncrate = 1
sync rate 30
endif
endif
else
tpressed = 0
endif
rem Output and Stuff
set cursor 0,0
print "FPS: ", screen fps()
print "Press T to change FPS rate"
print "WASD to move, Mouse to look"
print
if showhitmessage then print "You're leaving the allowed zone. Not good!"
rem Show Hitpoints
if hitpoints > 0
x = screen width()/2
center text x, 20, "Hitpoints:"
box x-hitpoints/2, 45, x+hitpoints/2, 60
else
center text x, 20, "You're Dead"
endif
sync
loop
remstart
How to use this tiny library:
*call game_init() once after starting your program
*call game_update() somewhere in your main loop (preferably
at the beginning, before updating anything else in the game)
*You can use game_reset() if you wish to reset the library's
timer back to 0 (e.g. after loading a new level)
How to enable timerbased movement:
*De/Incremental Changes to all continuous values (floats and
doubles) over time should be multiplied with game.timeFactor
*Noncontinuous values (or those that can't simply be scaled with
the timeFactor for other reasons) should be multiplied with
game.fullUpdates (which is an int and usually 0 or 1, it only
gets higher if the games runs with less FPS than defined in
GAME__FULL_UPDATES_PER_SECOND)
*For changes that can't even be scaled with game.fullUpdates,
you may use a for loop ranging from 1 to game.fullUpdates
*In theory you could just use game.fullUpdates for *everything*
and leave game.timeFactor out completely, however, this would
mean that FPS rates above GAME__FULL_UPDATES_PER_SECOND would
not update any more often than that exact value each second.
Hence the advantage of timerbased movement would only be
given for slow computers, fast ones would not gain any benefit
Note:
*The constants below can be changed, however:
*GAME__MS_PER_FULL_UPDATE * GAME__FULL_UPDATES_PER_SECOND should
always equal 1000!
*GAME__FULL_UPDATES_PER_SECOND defines how many non-continuous
update steps are executed each secod
*GAME__MAX_TIME_DIF defines an upper boundary for the virtual
time passing between two frames. This effectively means that
if the game runs slower than this value allows (i.e. the game
loop requires more ms than this value), it will not be "timer
based" anymore but slow down. This is meant to prevent extreme
"jumping" of objects, which might allow player characters to
clip through walls etc. You can change this value, but I'd
recommend to not use anything above 100.
*you can access the game.runtime variable to avoid calling the
timer() function regularly. Note that it always starts at 0
though.
remend
rem Only change these constants if you know what you're doing (and
rem if you've read the paragraphs above)
#constant GAME__MS_PER_FULL_UPDATE = 20
#constant GAME__FULL_UPDATES_PER_SECOND = 50
#constant GAME__MAX_TIME_DIF = 100
type game_Type
systimer as integer
starttime as integer
runtime as integer
actualRuntime as integer
timedif as integer `limited by GAME__MAX_TIME_DIF
actualTimeDif as integer
timeFactor as float
fullUpdates as integer `for high frame rates usually 0, but 50 times per second 1; for FPS lower than 30 this number will occasionally be higher than one for apply multiple updates at once
nextFullUpdate as integer
fullUpdateSum as integer
endtype
function game_init()
global game as game_Type
game.starttime = timer()
game.runtime = 0
game.nextFullUpdate = 0
game.actualRuntime = 0
game.fullUpdateSum = 0
endfunction
function game_reset()
game_init()
endfunction
function game_update()
game.systimer = timer()
tprev = game.actualRuntime
game.actualRuntime = game.systimer - game.starttime
game.timedif = game.actualRuntime - tprev
game.actualTimeDif = game.timedif
if game.timedif > GAME__MAX_TIME_DIF then game.timedif = GAME__MAX_TIME_DIF
inc game.runtime, game.timedif
game.timeFactor = game.timedif/(1.0*GAME__MS_PER_FULL_UPDATE)
game.fullUpdates = 0
if game.runtime >= game.nextFullUpdate
dif = game.runtime - game.nextFullUpdate
updates = 1 + dif/GAME__MS_PER_FULL_UPDATE
if updates > 10
game.fullUpdates = 10
game.nextFullUpdate = game.runtime + GAME__MS_PER_FULL_UPDATE
else
inc game.nextFullUpdate, updates*GAME__MS_PER_FULL_UPDATE
game.fullUpdates = updates
endif
endif
inc game.fullUpdateSum, game.fullUpdates
endfunction
Feel free to use the code in any of your projects. In case you don't like the "namespace" (everything starting with 'game'), simply use search & replace on the word "game" in the code above and replace it by "timing" or whatever you want, that should do the job.