Intro
The following tutorial demonstrates one possible solution to approximate 'consistent' movement and spawning rate in a game, regardless of computers physical frame rate.
To clarify - Physical frame rate refers to how fast a computer can process & draw the objects/characters on screen. Back in the 8/16 bit era of computing, this was largely unnecessary as games were designed to run on one standard hardware configuration. Meaning there was virtually no diversity between users home systems. So developers could assume all systems would run have the same CPU and graphics speed. So how it runs on one machine, gives a 99.9% indication of how it will run
across all machines of that type.
Today however we don't have this luxury. Why ? - Because the computers people use today, are built from a virtually endless combination of components I.e. CPU, VIDEO, SOUND RAM and MotherBoard etc etc. This diversity gives each system vastly different performance. While this is great for users power & flexibility, let alone their bank balance. It does mean that program authors can no longer rely upon the "if it works on my machine" it'll work on all machines philosophy.
Traditionally movement and redrawing in 2D games was all interconnected. So each update programmers would move the characters and draw them to the screen at their new positions. This process is commonly referred to a refresh or frame. How fast this occurs, is therefore directly linked to how fast the computer can move and refresh the screen.
For example, lets say we have a program that draws 100 characters all moving left to right across the screen. If our program moves each character 1 pixel (dot) to the right each refresh. Then how fast the characters appear to move is now totally dependant upon how fast the computer can execute the program.
To get a clearer picture, Lets compare how long it would take a characters to move all the way across the screen, on two different computers. Assuming the screen is 800 pixels wide.
* Machine A - (slow system - older than 5 years) Sub 1000 Mhz
Lets say this machine can execute the program at a solid refresh rate
of 30 times per second. This means that each character moves
30 pixels to the left in any one second time period. So to complete
it's journey, it's going to take (800/30) = 26.6 seconds from start
to Finnish.
* Machine B - (fast system - less than 1 year old) faster than 3000 Mhz
Lets say this machine can execute the program at a solid refresh rate
of 1000 times per second. This means that each character moves 1000
pixels to left in one second. So to complete it's journey it's going
to take (800/1000) = 0.8 seconds from start to Finnish.
Clearly there's a big discrepancy here. People running the program on system A would see the characters slowly moving across the screen, while people running it on system B, would see the character flash across the screen in less than a second.
You might have noticed this yourself when playing some older DOS games on a new PC. Or perhaps while playing retro games under emulation. It's the exact same issue.
How Do We Solve This ?
There's a few approaches, the simplest is to cap the performance of your application. This will stop it from running too fast on more modern computers. To do this, we simply check how long it took our program to update the game characters and draw the screen. If the computer is faster than we need it to be, then it'll be able to refresh everything in less time than we expect! When this occurs, we force the computer to wait the excess time. Thus slowing our game down to our desired rate. This has the added benefit of giving back any extra processing time back to any other applications that might be running in the background.
If that sounds too complicated, then you're in luck. In PlayBasic you can just use the SETFPS feature, it does the exact the same thing.
I.e.
; Tell PB to limit this program to a 30 frames per second or less
SetFps 30
; declare the XPOS variable
Dim Xpos as integer
; Start of the main loop
Do
; Clear The Screen
Cls rgb(0,0,0)
; Move our example object from left to right
Xpos=Xpos+1
Circle Xpos,300,30,true
; Display how Fast it's running
Print Fps()
; Refresh the display
Sync
; loop back to the do-statement to keep the program running
loop
While this will work, it only addresses stopping programs from running too fast. Which is only halve of the problem. What happens if we run our program on a machine that is unable to maintain the required refresh rate ? The program slows down. The slower the computer, the slower the program. For games this can be unacceptable.
You can either choose to ignore those users (ie. why support obsolete technology) , or look into some methods that approximate movement based on some type system constant. Like the system TIMER()
The Raw Concept
The previous program above follows a linear path. Where it handles the movement / drawing each complete refresh cycle. That is to say, it steps the character(s), draws the character(s) and repeats that process over and over. So the user sees every step of the characters movement. The problem with this, as we've just discovered, is that this approach ties our programs performance to the performance of the computer it's been executed on. Which is not always good.
One alternative (and the method used in the included example) is that we can scale our movements according to the amount of time that has elapsed since the object was created.
About Timer()
When we talk about game timing in PlayBasic, we're not simply referring to getting the current time and date. Those just aren't accurate enough for our needs. This is because our game well be refreshing anywhere between say 30 to 100 frames per second. So we need something that can measure time at least at that speed or faster preferably. Introducing Timer()
The Timer() function returns the current time in what's called milliseconds. In case you're not familiar, there's 1000 milliseconds in a second. Or another way of thinking of it is that each millisecond represents a 1000th of a second. Either way, it's very fast!
Since the Timer() returns values that represent 1000th of seconds, it's not bound to the speed to the computer. So 10, 50, 100, 500 milliseconds takes the same amount of time regardless of how the fast the computer is. So it's perfect for our game timing needs.
Basic Implementation
To do this, we need to keep track of the creation time of each spawned character in our game. Then each refresh, we subtract the current time (in milliseconds) from it's creation time (in milliseconds). This gives us the total number of milliseconds that object has been alive for.
E.g
TimePast#= Timer()- ThisCharctersCreationTime
Now depending upon your personal preference here, and personally I like keep my objects speeds in pixels per frame, rather than pixels per millisecond. This means that once I've established the TIMEPAST, I then divide this by the Number of milliseconds that each frame should take. This gives me the number of Frames past since the object was created, or perhaps changed direction.
E.g
FamesPast#=TimePast#/MillisecondsPerFrame#
So calculating the objects position at any given time is just a matter of multiplying the characters movement speed by the FramesPast# value. This gives us how far the character has moved since it was created. So assuming the object is just moving along the X axis, that calculation might look like this.
E.g
DistanceMovedAlongXaxis#=CharacterSpeed#*FramesPast#
All that remains now is to calculate it's Current position. Which is just a matter of adding the Distance the character has moved since it was created, to it's starting position.
E.g
CurrentXposition# = CharactersStartingXposition# + DistanceMovedAlongXaxis#
And there we have it, the great mystery of the Timer Based Movement is solved. or is it ?
Calculating Milliseconds Per Frame You'll have notice in the above there's one missing ingredient from our calculations, whats thise mysterious MillisecondsPerFrame# value ?
MillisecondsPerFrame# is a the number of the milliseconds one complete refresh should take, when our game is running at our 'perfect' frame rate. To calculate it, we divide the total number of milliseconds in one second (1000) by whatever our ideal frame rate should be. In the included example is 50 frames per second.
E.G
GamesIdealFramesPerSecond# = 50
MillisecondsPerFrame# = 1000.0 / GamesIdealFramesPerSecond#
This gives us the number of milliseconds one complete frame should take. This can then use it to calculate the how many frames have past, or for example it can be used to calculate and control when new characters should be spawned.
Spawning New Characters ?
One of the most commonly overlooked topics when talking about timer based movement, is not only keeping our characters moving at a nice relative rate, but making sure we spawning new characters evenly also.
To get an idea of the problem, lets revisit the previous example. Lets imagine were want to move our characters on screen from left to right again. Each time we update the refresh the game, we add a new random character to the left hand side of the screen. So we theoretically have a constant stream of characters being spawned and moving across the screen.
The the main loop (in psuedo code) might look something like this.
; Start of main loop
do
; clear the screen
Cls
; draws/moves our list of characters across the screen
DrawCharacters
; Randomly Spawn a new character every loop
SpawnNewCharacter
; refresh the screen so the user can see the changes
Sync
; loop back to the DO statement to keep the program running
loop
Now assuming the characters movements are based on the computers timer, the characters should each appear to move at relatively the same speed regardless of the computer the loop is running on. But what about SpawnNewCharacter ?, should this be executed every time the main loop is processed ?
No, it shouldn't. Why ?, because the speed the of computer dictates how frequently the Main loop is being executed here. The slower the machine, the less frequently this loop will be executed. The faster the machine, the more frequently the loop is executed. In other words slower machines spawn less new characters than faster ones. Therefore Making the your games behave differently across different speed systems. Which is Not ideal!
How do we get over come this? Just as we did previously with movement, we just have make sure our character spawning mechanisms work independently of the speed of the host computer. So rather than adding a new character each time the computer processes the main loop. We only add a new character after a certain number of milliseconds has transpired.
So if we wished to spawn new characters at our games ideal frame rate, we need monitor the spawn time relative to our ideal frame rate. The method I normally use is to simply hold the Time of when the next frame is expected to start. Then each time the main loop executes, we're comparing the current system timer() with our expected time.
; Our ideal frame rate of our game
IdealFramesPerSecondRate=50
; the number of milliseconds one complete frame will take
MillisecondsPerFrame= 1000.0/IdealFramesPerSecondRate
; Init the Expected time in milliseconds of when the current frame has ended
StartOfNextFrame=Timer()+MillisecondsPerFrame
; Start of main loop
Do
; clear the screen
Cls 0
; draws/moves our list of characters across the screen
DrawCharacters
; Only Spawn a new character if enough time has passed
if Timer()=>StartOfNextFrame
; randomly spawn a new character
SpawnNewCharacter
; Adjust our time variable forward to the time that the
; next frame will start.
StartOfNextFrame=Timer()+MillisecondsPerFrame
endif
; refresh the screen so the user can see the changes
Sync
; loop back to the DO statement to keep the program running
loop
This will work, but there's a problem. It's only limiting the number of character spawns on computers that can process the main loop faster than our ideal frame rate. But on slower machines we'll still miss character spawns. Which the will change the behavior the game on those systems.
The solution is simple, rather than just assuming only one frame has passed and spawning one character, we'll actually calculate the number of frames (to meet our ideal frame rate) that have past since the last refresh. On fast machines this will generally only be one frame, but on slower machines, or when the game is being bogged down by rendering too much graphics perhaps. Two, three or more frames might have passed. So in order to cope with this, we need back track our character spawning and generate any characters that would have been otherwise missed.
What this effective does is ensures that we get a fairly consistent spawning behavior across the broad spectrum of possible computer speeds. It's not necessarily perfect, but it's highly unlikely the player will notice any inaccuracies. So it's more than good enough to make the illusion appear seamless !