Tick Tock, A Better Clock

Things have been ominously quiet here on the blog the past two weeks! I've been busy with work lately (shocking, I know), but I set aside some time the past few evenings to work on Rubygame.

Yes, working on Rubygame. And if that isn't enough to shock you, what if I told you there'd be a release coming in the next few weeks, featuring some nice improvements to the Clock class?

It started out as Rebirth work, actually. I had been working on a new clock class, with numerous improvements: less stupid framerate calculation, compatibility with multithreaded ruby apps, and returning ClockTicked events when you called #tick, instead of raw milliseconds. Most of this was actually done months and months ago, as part of the "long lost Rubygame 3.0". Just as I dug up EventHandler and polished it up, I've been doing the same with the improved Clock.

Since I wanted the new Clock to be safe for multithreaded ruby apps, I couldn't just use the SDL_Delay C function any more, since that stops all ruby threads, not just the current. (This was pointed out to me a long long time ago, actually.) So instead of SDL_Delay, I figured I'd use Ruby's sleep method. Although it only returns integers, sleep will accept and function properly for floating point numbers, even small ones like 0.1. In those cases, sleep will return 0, which isn't very useful, but you can get the actual time difference quite simply:

def float_sleep( t )
  start = Time.now
  sleep t
  return Time.now - start

And in fact, this method can be quite accurate on certain systems, like mine:

>> float_sleep( 0.01 )
=> 0.010077

Not bad, eh? Unfortunately, that accuracy is not universal; it varies with CPU and operating system. Values on some systems can be several milliseconds off. For example, sleeping for 0.01s might actually sleep for 0.013, or 0.016, or even 0.02.

Being a few milliseconds off probably wouldn't be a problem for certain types of apps, but in a game the delays themselves are measured in milliseconds, and happen many times per second, every second. (An inaccuracy of 4ms per frame can mean the difference between 60 frames per second and 50.) Even worse, it's not a constant amount that you can compensate for, it's different each time you sleep, and that means the game framerate would be fluctuating all the time.

SDL_Delay has the same problem, actually, which is why Clock.delay uses a brief spinlock (empty loop constantly checking the time) at the end to get more accuracy. But, trying the same approach in ruby code actually makes the accuracy worse, perhaps due to some overhead with loops, or time checking, or both.

So, ruby sleep was not an option, but I still wanted to find a way to make Clock.delay work with multithreaded apps. Fortunately, Julian Raschke, author of the excellent Gosu game library, suggested an approach: use a loop to break up the SDL_Delay into smaller pieces, and call rb_thread_schedule between each chunk to let ruby keep running the other threads.

Hats off to Julian, because it has worked like a charm! Starting in Rubygame 2.5, Clock.wait and Clock.delay will take an additional optional argument to specify the number of milliseconds between each call to rb_thread_schedule (so smaller values run the other threads more often). It can also be false, the default value, to make them behave as they do in 2.4 and earlier (pausing all threads).

There will also be some other improvements to Clock:

  • A better algorithm for calculating framerate. (The current way is very stupid.)
  • More options for balancing CPU usage and accuracy, including the option to use Clock.wait instead of Clock.delay for framerate limiting, and an automatic calibration method to find the ideal granularity for the current system.
  • An option to have #tick return a ClockTicked event instance, which will have methods for retrieving the tick time as either milliseconds or seconds. There will also be a new event trigger, TickTrigger, to go along with the ClockTicked event.

As mentioned, the new version should be released within the next few weeks, and will be backwards-compatible with previous versions.


Have something interesting to say about this post? Email your thoughtful comments to comments@rubygame.org.