The Game Loop
Okay, now we're going to go from our pretty picture here, our Rothko, to a living world. And that world is provided by an illusion. The illusion our brain creates out of seeing many frames in quick succession. We're going to establish a game loop from scratch. We'll discuss time and time passing and we'll use input to move our character as well as adjusting the frame-rate. Let's bring this thing to life.
A Start Loop
In index.js:
let ANIMATION = {};
const start = (fps) => {
cancelAnimationFrame(ANIMATION.id);
loop();
}
const loop = () => {
ANIMATION.id = requestAnimationFrame(loop);
}
start()
As you can see, in the browser it's a simple as hooking into the browser's own loop with this poorly named, requestAnimationFrame
function. That runs at most once every 60 seconds. So we have 17 milliseconds to do everything we need to do in the universe before the browser says it's time to draw it again. So we can make a doOneFrame function to hold all of that and keep the loop function sparse.
const doOneFrame = () => {
// whole game exists here.
}
const loop = () => {
doOneFrame()
ANIMATION.id = requestAnimationFrame(loop);
}
Looping at a specified FPS
So we want a function that we can call with a given FPS, and that function will set up all the variables to track the FPS and run a loop on requestAnimationFrame
at the end.
Their all important but fpsInterval
is what gives us the number we need to know, how much time, should elapse before another frame should render?
let ANIMATION = {}
const start = (fps) => {
cancelAnimationFrame(ANIMATION.id);
ANIMATION.fps = fps || 60
ANIMATION.fpsInterval = 1000 / ANIMATION.fps;
ANIMATION.then = Date.now();
ANIMATION.startTime = ANIMATION.then;
ANIMATION.frameCount = 0;
ANIMATION.id = requestAnimationFrame(loop);
}
Date.now()
gives us a milisecond precision integer timestamp that we can use to compare to old values saved previously. We set this.animation.now
and then start filling everything else in from there. .sinceStart
So the recursive loop is similar, we only run a frame when enough time has passed for the user set FPS.
const loop = () => {
ANIMATION.now = Date.now();
ANIMATION.elapsed = ANIMATION.now - ANIMATION.then;
ANIMATION.sinceStart = ANIMATION.now - ANIMATION.startTime;
ANIMATION.currentFPS = (Math.round(1000 / (ANIMATION.sinceStart / ++ANIMATION.frameCount) * 100) / 100).toFixed(2);
if (ANIMATION.elapsed > ANIMATION.fpsInterval) {
doOneFrame() // whole game, right here.
ANIMATION.then = ANIMATION.now - (ANIMATION.elapsed % ANIMATION.fpsInterval); // After everything.
}
ANIMATION.id = requestAnimationFrame(() => loop());
}
const BOX = {
x: CANVAS.width/2,
y: CANVAS.height/2
}
const doOneFrame = () => {
BOX.x += .1
BOX.y += .1
box()
}
In Sum
We learned that loops can have data too and that data can help us determined the FPS and other metrics we can use to control our gameplay later. For now we've got a box that drifts. Let's add some Input controls so we can move him around.
Aside: Why not a while loop or setTimeout?
So, in C game programming there's a long tradition of while loops, from the very beginning. We have none of those. Instead we have a recursive function on a callback. And one must ask, is this really the most efficient way to do this?
The answer is unfortunately yes. While loops tend to employ this same feature, of making sure enough time has actually elapsed for the next frame to be called. Similarly there's a lot of old JS game tutorials with setTimeout as the recursive loop. Well, turns out, even if we did either of those, we wold still have to call requestAnimationFrame
with our recursive loop to get the true best performance from the browser. Remember, it's the browser.