Foreword
Humans, parrots, donkeys, flies and pigs have that thing called a "heartbeat". X times per seconds it pumps around our blood. Games do the same more or less, with the only difference that an insane high heartbeat will make you drop dead, while games thrive on it. You should try to get the cyclus done at least 30 times per second.
This article
is meant for beginners, explaining the GameLoop and more specifically the role
of timers in a game. At the end, a small example of the Engine22 eGameLoop
component is given.
Get ready for the Launch!
When
programming something, there are generally three ways to execute something:
·
Single
shot
·
Event
driven
·
Looped
Batch
files or scripts are usually good examples of a single-shot program. You start
the program, a list of instructions is executed one-by-one (eventually pausing
to query the operator to do X yes or no), and then it shuts down. Copying or
converting files, downloading data, starting an advanced calculation in the
background, unpacking files, printing a document, et cetera.
Event
Driven programs are your usual Desktop Application. It starts, initializes
stuff, and then waits for user-input. Press key, click a button, swipe the
screen, drag & drop items, and so on. A Form-based application made in Delphi
or .NET are good examples. Components like a button or listview generate all
kinds of events when being pressed, entered, changed, moved-over, and so on. On a deeper
layer, the OS (Windows, Linux, …) is registering input and sends your
application “messages”, which are then translated to events you can chose to use.
By the
way, in industrial types of programs, events can also be “interrupts”.
Increment counter if sensorX generates a pulse, engage safety procedure when door-sensor
loses signal, et cetera. Just saying input doesn’t have to be a mouse-click or
keyboard button. A mouse-click or button-press by the way generates an
interrupt in the OS as well, causing a message being sent to your application.
C’mon baby Do the Loop
And then
we have the “Looped” kind of program. Grandpa telling the same story over and
over and over again, and again. Until you knock his head with a wheelchair maybe.
You write code, and then tell to repeat this code until X happens. Where X
could be the end-of-your-program, a STOP signal, or whenever this (sub)task is
considered completed/aborted. Note it executes the same code again and again,
but that doesn’t mean the exact same thing has to happen. A typical scenario is
that we check input parameters each “cycle” which may alter the routing (if
button1Pressed then … else … ) or behaviour of other code parts.
Industrial
applications, often running on PLC’s or Micro-Controllers, are usually doing “the
loop” approach. Each cycle, they check input(sensors / touchscreen), run
regulators, and send output (relays, pumps, valves, servo’s). It’s the
heartbeat that keeps the program going. And if there is no heartbeat (stuck in
a loop, error raised, hardware issue), the “Watchdog” will time-out and reset
the chip.
And
games aren’t much different. Note that applications can combine both events and
(multiple!) loops, but again the “Game Loop” is what makes your game drawing,
moving, sounding and doing stuff continuously.
You probably
know how to make an ordinary loop:
while not ( flagSTOP ) do begin
runTask( …. );
end;
There is
one little-big problem with this code though; no matter how simple “runTask( … )”
is, your CPU will go 100% and everything else freezes. It’s trying to execute “runTask”
as much as possible, no pausing, no breathing-space for any other application.
This
piece of code has two problems. We have no speed-control (the heartbeat pacemaker
going mad). And since it’s executed in the main-thread, this means it will block
other main-thread tasks in the same program, such as refreshing the screen or
handling button-clicks. As for the speed-control, one seemingly simple solution
would be using a Timer component. Delphi, .NET, Qt, Java, they all have Timer
components that can run code each X milliseconds. The “idle” time between two
cycles is then available for other stuff.
Real-time TV
Problem
solved then? Hmmm, not quite. At least, it depends on the accuracy we want. See,
your default Timer isn’t very accurate. Why? Because the Windows OS (don’t know
for Linux) itself doesn’t have a very accurate timing. Why? Because Windows OS
isn’t designed as a “Real-time” operating system? Why? Because Windows doesn’t
need to be. Why? Because Windows typically isn’t used on time-critical
hardware, such as a vehicle ECU or a packaging machine controller. Why? Ah, drop dead.
Though
we all love a good performance, 99,9% of the Windows applications isn’t
time-critical. People won’t die if we miss a frame, and we don’t control a
packaging machine that screws up if its pneumatic valve is powered 10
milliseconds late. Machinery often requires a real-time system to guarantee
the same predictable results over and over again (24/7!). Which is why you
shouldn’t use a Windows computer for that. Why? I told you why, because the
Windows OS isn’t designed for real-time applications. It does not guarantee
that your program will execute the loop again over 872 microseconds. On a
typical Windows computer, we have dozens of other programs and background tools
running at the same time, which can all claim the CPU or memory, so we simply
can’t give any guarantees.
Now a
game isn’t time-critical in the sense that you will get hurt if the framerate
drops from 60 to 30 in all of a sudden. Although… I’ve seen kids on Youtube
that went ape after getting shot in a Death-match game due a computer hick-up… Nevertheless,
we want a smooth experience. If your television refresh rate fluctuates between
15 to 100 FPS all the time, you’ll be puking after five minutes. Like in
machinery, we want to run our game at a certain pace, and guard that interval.
Say we use
a Delpti TTimer and put the interval on 16,66 milliseconds. That means the
timer-event is triggered 1000/16,66 = 60 times per second (thus FPS = 60). In
theory. We’ll use this event to execute our game-loop. In this loop we could:
Rush-Hour
Now
since we have only 16,66 milliseconds between 2 cycles, that is quite a lot to
do in a short time!! Yep, true, but you’ll be amazed what a computer can do. Don’t
forget about half of the work is done by another processor-set, the video-card,
which is on steroids. The picture above implies everything happens in sequence, but it's common to have some multi-threading going on to execute these sub-tasks in parallel. While the video-card is drawing, you could update audio for example. You’ll also learn that a lot of programs out
there are slow as shit because bad programming. If your photo-realistic game
can poop 40 frames per second, there is no reason why some silly editing
program takes 5 seconds to do a single simple task.
A good
engine is a powerful beast, doing thousands, no MILLIONS!, of calculations every
second. The key to get this fast is not really doing formula’s in a very
optimized way, but avoiding stuff that doesn’t have to be calculated (every
cycle). Don’t do extensive searches if you could also do that just once during
the loading-phase. Use smart algorithms to to reduce searching lengths. Don’t calculate advanced physics for an object miles away.
Don’t animate a 30 legged centipede that is somewhere behind you. Anyway, optimizing
is another story.
60
Frames per Second is a nice strive, though I’m already happy if the game just
keeps above the 30 FPS line. Above thirty, the human eye thinks animations are
smooth, but lower framerates will appear choppy. How many frames you can reach,
depends on what you’re doing (how complex & how many entities), and how fast
the computer is obviously.
If we’re
trying to do more than the computer can handle, it means the framerate will
drop and our timer will be executed repeating without any delays between the
cycles. This can be temporarily, for example when entering a heavy scene, or if
the computer is doing a Virus-Scan on the background (slowdowns aren’t always
our fault!). Doctors making rush-hour on the ER, more bloody patients coming in
than we can handle. If it’s structural, meaning we never reach our desired
framerate and the CPU hitting 100% all the time, we should consider lowering
the target framerate (allow less patients), doing less code/optimize (train our
doctors), or switch to a more powerful platform (a bigger hospital).
Take a break
At the
other hand it may happen we can easily perform our tasks in the given
timeframe. If the timer runs at 58,8 FPS, our timeframe is 1000/58,8 = 17
milliseconds. If doing our GameLoop only takes 10 milliseconds, we have 7 more “free
time” milliseconds. Which is great, because this allows us to do other stuff, it
gives some room to other background applications, and otherwise at least the
CPU isn’t running 100%, making a lot of noise all the time.
Here is
the tricky part though. After you finished the Game-Loop, you should check how
much time that took, and how much take you can rest before taking the next
step. A Delphi TTimer does that, but not very accurately, because non-real-time
Windows isn’t generating timer-pulses very precisely on the background. That
also causes our beloved “sleep( )” function to be unreliable.
Calling “sleep(1)” may actually put your thread in bed for 10 milliseconds or
so. So, how to keep a steady framerate then?
Engine22 “eMainloop” class
There
are several High-Precision timer components for Delphi, and I’m sure the same
thing is available for .NET or any other language. Engine22 also provides the “eMainloop”
class (E22_Util.Mainloop), which is sort of a timer. You set it up by giving a
desired target-framerate -which is basically a maximum speed-, and a Callback
function. This callback function is called every time the eMainLoop object
triggers. So, typically you execute your game (or whatever it is you’re making)
stuff there.
Var
Looper : eMainloop;
Procedure TForm1.initialize();
begin
self.looper := eMainloop.create( self.handle
);
self.looper. setTargetSpeed( 60 );
self.looper.setCallback( self. gameMainLoop
);
self.looper.enabled := true;
end;
procedure TForm1.gameMainLoop(
const deltaTime : eFloat );
begin
checkInput();
updatePhysics();
updateAI();
updateSound();
renderGraphics();
end; // gameMainLoop
So “gameMainLoop”
gets called, 60 times per second (hopefully) in this example. The elapsed time
between 2 frames, in theory 1000/60 = 16,66 ms is given as an argument you can
use. How to? Check the “DeltaTime” part at the bottom of this article:
How it
solves the timing issue? By not using the Windows OS timer messages, but using
the Windows vWMTick_ASAP signal instead. This one is given mega-fucking-fast. The
application is allowed to process messages every time we receive a tick, and we
measure the elapsed time using the Windows QueryPerformanceCounter() function. This
function returns an ever incrementing tick-counter, which can be converted to
milliseconds by dividing it with the clock frequency, which you can get via QueryPerformanceFrequency(
ptrFrequency ). If the elapsed time exceeds our targetframerate, we call the given
callback, “gameMainLoop” in this case.
Enough for today. For people with some experience, this whole story probably sounds all too obvious. But for a newcomer, it's probably good to understand what's going on. After all, game-code doesn't quite look like a common (Event driven) Desktop program. And since the looping/timer is such an essential thing, you'd better not rely on the standard Timer and dig a bit deeper in order to get control.
No comments:
Post a Comment