Friday, October 18, 2013

Timing on Different Systems with Three.js

Let's consider the effect of timing on games.   Typically, all moving game objects and players/characters are updated every cycle through the game code.  This means that if you instruct the game to "move a character/object by 1 unit", it will indeed move by 1 unit on every pass through your game program.  
 Take our sun light object for example.  On every frame of animation we want its height to move smoothly, so we type:
sunHeight = sunHeight + 1;
Just like we expect, the sun will rise smoothly, and it will work on any computing device or smartphone.  That's all good, until you consider that not everyone's device is running at the same rate.  A powerful desktop computer might loop through this statement 60 times a second, while a smartphone or old computer might only get around to it 20 times a second.  That means that the person playing your game on their powerful desktop computer will see and feel a different game experience than the person playing on a smartphone or older computer system. 

How do we fix this problem?  We turn to time-based animation.  Essentially we measure how fast the user's system is looping through our game code per second, and then we multiply that time delay by the game objects' speed.  Here's how the above example will look now:
sunHeight = sunHeight + 1 * frameTime;
In the above snippet, the 'sunHeight' variable is still being raised at 1 unit every animation frame, just like before.  But now we have multiplied this amount by a new variable called 'frameTime'.  'frameTime' holds the amount of time it took for the user's system to complete 1 animation loop.  And this 'frameTime' value is continually refreshed on each loop through our game code (as we all know, computers can have performance hiccups now and then).  As with racing times, and your cholesterol, lower numbers are better for 'frameTime' - and higher numbers are worse.  A good computing system will quickly lap around your program, giving a lower frameTime.  A smaller or older device will take longer to do those laps around your code and will result in a higher number for frameTime.  But not to worry if you are one of the users of the less powerful systems!  The above equation is the great equalizer when it comes to game performance. 

To see how, let's imagine running through the above code with a fast computer. The 'frameTime' result will be a lower number.  Let's say it clocks out at 16 milliseconds, which is around 0.016 seconds. So  after the multiplication is performed, 'sunHeight' in this instance gets a small number added to it every frame and smoothly rises on our screen.  Now run the same game on an old computer or phone, and their 'frameTime' result may be as high as 50 milliseconds, or 0.05 seconds.  So after the multiplication is performed, 'sunHeight' in this instance gets a larger number added to it every animation frame.   This little 'frameTime' equation gives the slower computers a boost, so that they catch up to the faster computers.   If you put the fast and the slow systems side-by-side, the game's objects will move at the same rate!

The only downside to all this is that the slower computers will look a little more choppy and less-smooth than the faster computers, because the slower devices have to cover more ground each animation frame to catch up.  But the important thing is that all players of our games, regardless of their system specs, will see and feel the same game timing and movement that we wanted them to experience!   Just think if we could apply this method to Olympic racing athletes; all countries would be winners - actually they would all tie for the Gold medal!  :-)

The next order of business is somehow timing the end-user's system each animation loop so that we can apply the right amount of boost.  Three.js to the rescue!  The folks on the Three.js team have provided us with the perfect tool for this very purpose:  the 'THREE.Clock()' object. 

Here's how we declare a new clock variable in our game code: 
var clock = new THREE.Clock();

This kind of Three.js declaration should look familiar by now, right?  We name a variable 'clock' and fill it by typing '= new' and a call to the 'THREE.Clock()' object.  Now the variable 'clock' has all the accuracy and functionality of a real-life digital stopwatch that can measure how fast something is running.  To use it, we have to declare one more variable - the 'frameTime' variable that we saw earlier.  That's easily done by typing:
var frameTime = 0;
We initially set 'frameTime' equal to 0.  Then, as the game program runs, it will be refreshed every animation frame, so it will correctly hold the results of the system's performance.

One last thing to do is sample the 'clock' object every frame, so we get an accurate timing measurement.  In Three.js, this is accomplished by typing:
frameTime = clock.getDelta();
'frameTime' is now filled using the equals ( = ) sign and a call to one of our 'clock' object's helper functions, called getDelta.  So we type 'clock.getDelta();'  Delta is a mathematical term for how much something has changed - in this case how much the time has changed (or how much time has gone by).  A smaller Delta means a little time has passed, and a bigger Delta means a larger amount of time has passed.  

One more thing to mention is that when applying this equation to our old sunHeight code, it moved too slowly, compared with how it used to move before our changes.  So I had to tweak the initial speed of the sunHeight variable to something bigger.  It used to be 1, as in:
sunHeight = sunHeight + 1 * frameTime;
But now it is: 
sunHeight = sunHeight + 60 * frameTime;

I know that's a big jump in the speed amount, but it actually works out to look the same as our old example.  That's because 'frameTime' on most decent computers comes back with really small numbers, making the overall sunHeight movement very small.  Using the old number '1', the Sun light crawled up too slowly.  However with a simple change to '60', and after the new frameTime calculation, it moves very much like we intended it to do, which is pretty fast up and down in the sky.  If you wanted a realistic simulation of the Sun rising, you could try '0.05' or something.  It will rise VERY slowly.  You would have to wait all day to see the results!

I've included our new clock and frameTime code in the tempGame.js file.  Copy and Paste the following code and then save it as 'tempGame.js' , overwriting the old one:   

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
var clock = new THREE.Clock();

var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize, false); 

var cubeGeometry = new THREE.CubeGeometry(20,20,20);
var cubeMaterial = new THREE.MeshLambertMaterial({ color: 'rgb(0,255,0)' });
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
scene.add(cube);

var sphereGeometry = new THREE.SphereGeometry(5);
var sphereMaterial = new THREE.MeshBasicMaterial({ color: 'rgb(255,255,0)' });
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);

var light = new THREE.PointLight( 'rgb(255,255,255)', 1, 0 );
scene.add(light);

var sunRiseFlag = true;
var sunHeight = 0;
var frameTime = 0;

camera.position.y = 40;
camera.position.z = 160;

cube.rotation.x = 0.4;
cube.rotation.y = 0.6;

animate();

function animate(){
   
   requestAnimationFrame(animate);

   frameTime = clock.getDelta();

   if(sunRiseFlag == true){
      sunHeight = sunHeight + 60 * frameTime;
   }

   if(sunRiseFlag == false){
      sunHeight = sunHeight - 60 * frameTime;
   }

   if(sunHeight > 150){
      sunHeight = 150;
      sunRiseFlag = false;
   }

   if(sunHeight < 0){
      sunHeight = 0;
      sunRiseFlag = true;
   }

   light.position.set(50,sunHeight,50);
   sphere.position = light.position;

   renderer.render( scene, camera );

}


function onWindowResize(){
   camera.aspect = window.innerWidth / window.innerHeight;
   camera.updateProjectionMatrix();
   renderer.setSize( window.innerWidth, window.innerHeight );   
}


Once you have saved it, go back to your example01.html file and open with your Chrome browser.  Now the yellow Sun light will move up and down smoothly on your device.  And no matter what system you are running this code on, the Sun will reach high noon and sunset at exactly the same time for you.  If you are running this on an old device or a less-powerful smartphone, it might appear a little choppy.  But the important thing to note is that our future games will animate with the same timing, and the gameplay will be consistent and positive for everyone!

Till next time...