Skip to main content

ConstantGameSpeed — wiki

Definitely read dewitter's article found in the module comments. It will help you decide if this clock-controller class is right for your program. It also gives a good explanation of the intent and mechanics of interpolation/prediction provided by this solution.

This clock provides a fixed timestep, i.e. a constant DT. It is a design choice that resolves nasty issues that arise out of variable timing. See the fix-your-timestep article in the comments for a discussion.

Because the wiki mangles parts of the code (e.g. less than, greater than, and other symbols) it is also hosted here for convenience.

#!/usr/bin/env python

# This file is part of GameClock.
#
# GameClock is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# GameClock is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with GameClock.  If not, see /www.gnu.org/licenses/>.

# Compatible: Python 2.7, Python 3.2

"""gameclock.py - Game clock for Gummworld2.

GameClock is a fixed time-step clock that keeps time in terms of game
time. It will attempt to keep game time close to real time, so if an
interval takes one second of game time then the user experiences one
second of real time. In the worst case where the CPU cannot keep up with
the game load, game time will subjectively take longer but still remain
accurate enough to keep game elements in synchronization.

GameClock manages time in the following ways:
        
    1.  Register special callback functions that will be run when they
        are due.
    2.  Schedule game logic updates at a constant speed, independent of
        frame rate.
    3.  Schedule frames at capped frames-per-second, or uncapped.
    4.  Invoke a pre-emptive pause callback when paused.
    5.  Schedule miscellaneous items at user-configurable intervals.
    6.  Optionally sleep between schedules to conserve CPU cycles.
    7.  Gracefully handle corner cases.

Note the Python Library docs mention that not all computer platforms'
time functions return time in fractions of a second. This module will not
work on such platforms.

USAGE

Callback:
    
    clock = GameClock(
        update_callback=update_world,
        frame_callback=draw_scene,
        pause_callback=pause_game)
    while 1:
        clock.tick()

Special callbacks can be directly set and cleared at any time:
    
    clock.update_callback = my_new_update
    clock.frame_callback = my_new_draw
    clock.pause_callback = my_new_pause
    
    clock.update_callback = None
    clock.frame_callback = None
    clock.pause_callback = None

Scheduling miscellanous callbacks:
    
    def every_second_of_every_day(dt):
        "..."
    clock.schedule_interval(every_second_of_every_day, 1.0)

Callbacks can be any kind of callable that accepts the callback signature.

The update_callback receives a single DT argument, which is the time-step
in seconds since the last update.

The frame_callback receives a single INTERPOLATION argument, which is the
fractional position in time of the frame within the current update time-
step. It is a float in the range 0.0 to 1.0.

The pause_callback receives no arguments.

User-defined interval callbacks accept at least a DT argument, which is
the scheduled item's interval, and optional user-defined arguments. See
GameClock.schedule_interval.

DEPRECATIONS

Old Style Game Loop

Use of the old style game loop is deprecated. Don't do this anymore:
    
    if clock.update_ready:
        update(clock.dt_update)
    if clock.frame_ready:
        draw(clock.interpolate)

The old style game loop will work on sufficiently fast hardware. Timekeeping
will break on slow hardware that cannot keep up with a heavy workload. This is
because the old style ignores the cost of the frame routine. By using callbacks
instead, cost is factored into the frame scheduling and overloading the CPU has
fewer negative side effects.

The update_ready and frame_ready public attributes have been removed.

CREDITS

The inspiration for this module came from Koen Witters's superb article
"deWiTTERS Game Loop", aka "Constant Game Speed independent of Variable FPS"
at http://www.koonsolo.com/news/dewitters-gameloop/.

The clock was changed to use a fixed time-step after many discussions with
DR0ID, and a few readings of
http://gafferongames.com/game-physics/fix-your-timestep/.

Thanks to Koen Witters, DR0ID, and Glenn Fiedler for sharing.

Pythonated by Gummbum. While the builtin demo requires pygame, the module
does not. The GameClock class is purely Python and should be compatible with
other Python-based multi-media and game development libraries.
"""

__version__ = '$Id: gameclock.py 428 2013-08-28 05:43:47Z stabbingfinger@gmail.com $'
__author__ = 'Gummbum, (c) 2011-2014'


import functools
import time


class _IntervalItem(object):
    """An interval item runs after an elapsed interval."""
    # __slots__ = ['func', 'interval', 'lasttime', 'life', 'args', 'id']
    id = 0
    def __init__(self, func, interval, curtime, life, args):
        self.func = func
        self.interval = float(interval)
        self.lasttime = curtime
        self.life = life
        self.args = args
        self.id = _IntervalItem.id
        _IntervalItem.id += 1


class GameClock(object):
    
    def __init__(self,
            max_ups=30,
            max_fps=0,
            use_wait=False,
            time_source=time.time,
            update_callback=None,
            frame_callback=None,
            paused_callback=None):
        
        # Configurables.
        self.get_ticks = time_source
        self.max_ups = max_ups
        self.max_fps = max_fps
        self.use_wait = use_wait
        self.update_callback = update_callback
        self.frame_callback = frame_callback
        self.paused_callback = paused_callback
        
        # Time keeping.
        TIME = self.get_ticks()
        self._real_time = TIME
        self._game_time = TIME
        self._last_update = TIME
        self._last_update_real = TIME
        self._next_update = TIME
        self._last_frame = TIME
        self._next_frame = TIME
        self._next_second = TIME
        self._update_ready = False
        self._frame_ready = False
        self._paused = 0
        
        # Schedules
        self._need_sort = False
        self._schedules = []
        self._unschedules = []
        
        # Metrics: update and frame progress counter in the current one-second
        # interval.
        self.num_updates = 0
        self.num_frames = 0
        # Metrics: duration in seconds of the previous update and frame.
        self.dt_update = 0.0
        self.dt_frame = 0.0
        # Metrics: how much real time a callback consumes
        self.cost_of_update = 0.0
        self.cost_of_frame = 0.0
        # Metrics: average updates and frames per second over the last five
        # seconds.
        self.ups = 0.0
        self.fps = 0.0
    
    @property
    def max_ups(self):
        return self._max_ups
    @max_ups.setter
    def max_ups(self, val):
        self._max_ups = val
        self._update_interval = 1.0 / val
    
    @property
    def max_fps(self):
        return self._max_fps
    @max_fps.setter
    def max_fps(self, val):
        self._max_fps = val
        self._frame_interval = 1.0 / val if val>0 else 0
    
    @property
    def game_time(self):
        """Virtual elapsed time in game milliseconds.
        """
        return self._game_time
    @property
    def paused(self):
        """The real time at which the clock was paused, or zero if the clock
        is not paused.
        """
        return self._paused
    
    @property
    def interpolate(self):
        interp = (self._real_time - self._last_update_real) / self._update_interval
        if interp < 0.0:
            return 0.0
        if interp > 1.0:
            return 1.0
        return interp
    
    def tick(self):
        # Now.
        real_time = self.get_ticks()
        self._real_time = real_time
        
        # Pre-emptive pause callback.
        if self._paused:
            if self.paused_callback:
                self.paused_callback()
            return
        
        # Check if update and frame are due.
        update_interval = self._update_interval
        game_time = self._game_time
        if real_time >= self._next_update:
            self.dt_update = update_interval  # fixed timestep: good
            self._last_update_real = real_time
            game_time += update_interval
            self._game_time = game_time
            self._last_update = game_time
            self._next_update = real_time + update_interval
            self.num_updates += 1
            if self.update_callback:
                self._update_ready = True
        if real_time - self._last_frame >= self._update_interval or (
                real_time + self.cost_of_frame < self._next_update and
                real_time >= self._next_frame):
            self.dt_frame = real_time - self._last_frame
            self._last_frame = real_time
            self._next_frame = real_time + self._frame_interval
            self.num_frames += 1
            if self.frame_callback:
                self._frame_ready = True

        # Check if a schedule is due, and when.
        sched_ready = False
        sched_due = 0
        if self._schedules:
            sched = self._schedules[0]
            sched_due = sched.lasttime + sched.interval
            if real_time >= sched_due:
                sched_ready = True
        
        # Run schedules if any are due.
        if self._update_ready or sched_ready:
            self._run_schedules()
        
        # Run the frame callback (moved inline to reduce function calls).
        if self.frame_callback and self._frame_ready:
            get_ticks = self.get_ticks
            t = get_ticks()
            self.frame_callback(self.interpolate)
            self.cost_of_frame = get_ticks() - t
            self._frame_ready = False
        
        # Flip metrics counters every second.
        if real_time >= self._next_second:
            self._flip(real_time)
        
        # Sleep to save CPU.
        if self.use_wait:
            upcoming_events = [
                self._next_frame,
                self._next_update,
                self._next_second,
            ]
            if sched_due != 0:
                upcoming_events.append(sched_due)
            next_due = functools.reduce(min, upcoming_events)
            t = self.get_ticks()
            time_to_sleep = next_due - t
            if time_to_sleep >= 0.002:
                time.sleep(time_to_sleep)
        
    def pause(self):
        """Pause the clock so that time does not elapse.
        
        While the clock is paused, no schedules will fire and tick() returns
        immediately without progressing internal counters. Game loops that
        completely rely on the clock will need to take over timekeeping and
        handling events; otherwise, the game will appear to deadlock. There are
        many ways to solve this scenario. For instance, another clock can be
        created and used temporarily, and the original swapped back in and
        resumed when needed.
        """
        self._paused = self.get_ticks()
    
    def resume(self):
        """Resume the clock from the point that it was paused."""
        real_time = self.get_ticks()
        paused = self._paused
        for item in self._schedules:
            dt = paused - item.lasttime
            item.lasttime = real_time - dt
        self._last_update_real = real_time - (paused - self._last_update_real)
        self._paused = 0
        self._real_time = real_time
    
    def schedule_interval(self, func, interval, life=0, args=[]):
        """Schedule an item to be called back each time an interval elapses.
        
        While the clock is paused time does not pass.
        
        Parameters:
            func -> The callback function.
            interval -> The time in seconds (float) between calls.
            life -> The number of times the callback will fire, after which the
                schedule will be removed. If the value 0 is specified, the event
                will persist until manually unscheduled.
            args -> A list that will be passed to the callback as an unpacked
                sequence, like so: item.func(*[item.interval]+item.args).
            
        """
        # self.unschedule(func)
        item = _IntervalItem(
            func, interval, self.get_ticks(), life, [interval]+list(args))
        self._schedules.append(item)
        self._need_sort = True
        return item.id
    
    def unschedule(self, func):
        """Unschedule managed functions. All matching items are removed."""
        sched = self._schedules
        for item in list(sched):
            if item.func == func:
                sched.remove(item)

    def unschedule_by_id(self, id):
        """Unschedule a single managed function by the unique ID that is
        returned by schedule_interval().
        """
        sched = self._schedules
        for item in list(sched):
            if item.id == id:
                sched.remove(item)                
    @staticmethod
    def _interval_item_sort_key(item):
        return item.lasttime + item.interval
    
    def _run_schedules(self):
        get_ticks = self.get_ticks
        
        # Run the update callback.
        if self.update_callback and self._update_ready:
            t = get_ticks()
            self.update_callback(self.dt_update)
            self.cost_of_update = get_ticks() - t
            self._update_ready = False
        
        # Run the interval callbacks.
        if self._need_sort:
            self._schedules.sort(key=self._interval_item_sort_key)
            self._need_sort = False
        real_time = self._real_time
        for sched in self._schedules:
            interval = sched.interval
            due = sched.lasttime + interval
            if real_time >= due:
                sched.func(*sched.args)
                sched.lasttime += interval
                need_sort = True
                if sched.life > 0:
                    if sched.life == 1:
                        self._unschedules.append(sched.id)
                        need_sort = False
                    else:
                        sched.life -= 1
                if need_sort:
                    self._need_sort = True
            else:
                break
        if self._unschedules:
            for id in self._unschedules:
                self.unschedule_by_id(id)
            del self._unschedules[:]
    
    def _flip(self, real_time):
        self.ups = self.num_updates
        self.fps = self.num_frames
        
        self.num_updates = 0
        self.num_frames = 0
        
        self._last_second = real_time
        self._next_second += 1.0

Demo code showing a simple usage of the various aspects of GameClock class.

from gameclock import GameClock

"""
USAGE TIPS

When first trying this demo follow these steps. These tips assume the
stock (unmodified) settings are used.

    1.  Initially the game uses a Pygame clock loop, unthrottled. Use this
        mode to compare the maximum frame rate between this mode and the
        GameClock loop. Press the M key to toggle frame rate throttling.
    2.  Press the M key to throttle Pygame to 30 FPS. This is the typical
        method employed in Pygame to fix the rate of game ticks.
    3.  Press the L key to swith to GameClock loop. Note the UPS (updates
        per second) are 30, as with the Pygame clock. The frame rate should
        be much higher, and the motion of the balls should be smoother.
    4.  Press the Tab key to cycle GameClock to the next settings, which
        throttle the frame rate at 120 per second. Switch between Pygame and
        GameClock with the L key and compare the smoothness of the motion.
    5.  In GameClock mode with a CPU meter running press the W key to toggle
        Wait (GameClock uses time.wait()) and watch the effect on CPU usage.
    6.  Press the Tab key to watch how smoothness of motion is affected when
        the GameClock frame rate is throttled to 60 FPS, and again at 30.
        Note that at 30 FPS there is no apparent difference between
        GameClock and Pygame.
    7.  Press the Tab key again to view GameClock throttled to 6 UPS. Ball
        class implements two kinds of interpolation: motion, and screen edge
        collision. Use the P key to toggle screen edge prediction. Note
        that when Predict is on the balls behave well when colliding with
        screen edges. When Predict is off predict() assumes it will never
        change course, and update() snaps it back from the predicted
        position. The effect is visually jarring, and is visible even at
        higher frame rates.
    8.  Pressing K toggles whether collisions kill balls. It does not
        toggle collision detection. There is no appreciable difference here
        between Pygame and GameClock.
    9.  Pressing B creates 25 more balls.
    10. There are a couple gotchas with GameClock that have been called out
        in code comments. See update_gameclock().

ABOUT THE DEMO

This demo sends a ball careening about the window. It is probably not the
best usage for the GameClock class, but it provides a good basis for
demonstrating linear motion prediction, and salving an eyesore with some
judicious collision prediction.

You could certainly use the GameClock simply as a timer and FPS throttle,
but that only scratches the surface.

With an implementation like this demo you're deciding you want to update
some visual aspects of the game as often as possible, while time passes at a
slower, constant rate for the game mechanics. This is done by separating the
game mechanics routine from the display routine and calling them on
independent cycles. If the game mechanics are comparatively more costly in
computing power, there is potentially a lot of benefit in choosing to update
the mechanics at a much lower rate than updating frames for display.

Of course, in order to update the display meaningfully you need to modify
it. Otherwise you're seeing the same image posted repeatedly. But if the
display changes are waiting on game mechanics to post, you can only update
the display as fast as you can compute the entire world. This is where
prediction fits in: updating the display in advance of the game mechanics.

The design problem is what do you predict? First, it should make a positive
difference in the user experience. Second, the more you add to your display
computation the lower your frame rate.

There are two kinds of prediction Ball can use: motion and collision. Once
we start predicting the motion we notice that when the ball collides with
the screen edge the rebound jars the eye. This is because simple motion
prediction assumes there will be no course changes and overshoots the edge.
In most cases the prediction is right, but in edge collision it is wrong,
and the next update snaps it back from the predicted position.

If this were invisible it wouldn't be a problem. Rather it is quite
annoying. The problem can be solved by predicting collisions, making
update() and predict() adjust their calculations by the interpolation value
at the time the collision occurred. And we see the ill effect is resolved
when we turn on screen-edge collision prediction (enabling with the P key).

A notable distinction is there are two collision conditions that change the
ball's behavior: collision with the screen edges and collision with another
ball. The distinction is that predicting screen edge collision makes a
visible difference.

By contrast, when two balls collide it does not visually matter whether they
react immediately or there is a delay, even at a very low 6 updates-per-
second. Therefore, the potentially expensive ball-vs-ball collision
detection can be done less frequently. Of course, if you're shooting very
fast bullets it would matter, but that doesn't apply to our demo.

Ultimately the choice of what to predict and what to defer is a project
decision. Hopefully this explanation has illuminated the reasoning used in
designing the demo's prediction capabilities and has shown that if done
intelligently, such tricks can add virtual performance to your application.

THE INTERFACE

Input keys:
    L -> Loop; Toggle between Pygame and GameClock timed loops.
    Tab:[TicksPerSecond MaxFPS] -> Cycle through the GameClock settings.
    K -> Kill; Toggle whether collisions kill balls.
    M -> MaxFPS; Toggle Pygame clock's MaxFPS throttle.
    P -> Predict; Toggle whether the ball uses its GameClock predictive algorithm.
    W -> Wait; Toggle whether GameClock uses time.sleep().
    B -> Balls; Make 25 more balls.

The title bar displays the runtime settings and stats. If the stats are
chopped off you can increase the window width in main().

Stats:
    Runtime:[FPS=%d UPS=%d] -> The number of frames and updates that
        occurred during the previous second.
"""
import random
import pygame
from pygame.locals import (
    Color, QUIT, KEYDOWN, K_ESCAPE, K_TAB, K_1, K_b, K_k, K_l, K_m, K_p, K_w,
)
# GameClock control.
TICKS_PER_SECOND = 30.0
MAX_FRAME_SKIP = 5.0
# Ball control.
MAX_BALL_SPEED = 240.0  # pixels per second
INIT_BALLS = 100        # initial number of balls
## Note to tweakers: Try adjusting these GameClock settings before adjusting
## the fundamental ones above.
SETTINGS = (
    # TicksPerSecond   MaxFPS
    (TICKS_PER_SECOND, 0),                     # unlimited FPS
    (TICKS_PER_SECOND, MAX_BALL_SPEED / 2),    # max FPS is half ball speed
    (TICKS_PER_SECOND, TICKS_PER_SECOND * 2),  # max FPS is double TPS
    (TICKS_PER_SECOND, TICKS_PER_SECOND),      # max FPS is TPS
    (TICKS_PER_SECOND / 5, 0),                 # TPS is 6; unlimited FPS
)
# Use Pygame clock, or GameClock.
USE_PYGAME_CLOCK = True
PYGAME_FPS = 0
# Ball uses prediction? Enable this to see how combining interpolation and
# prediction can smooth frames between updates, and solve visual artifacts.
USE_PREDICTION = True
# Balls are killed when they collide.
DO_KILL = False
# Appearance.
BGCOLOR = Color(175,125,125)
## NO MORE CONFIGURABLES.
# Game objects.
elapsed = 0
game_ticks = 0
pygame_clock = None
clock = None
screen = None
screen_rect = None
eraser_image = None
sprite_group = None

def logger(*args):
    if logging:
        print ' '.join([str(a) for a in args])
logging = True

class Ball(pygame.sprite.Sprite):
    size = (40, 40)

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.surface.Surface(self.size)
        self.rect = self.image.get_rect()
        self._detail_block(self.image, Color('red'), self.rect)
        w, h = screen_rect.size
        self.x = float(random.randrange(self.size[0], w - self.size[0]))
        self.y = float(random.randrange(self.size[1], h - self.size[1]))
        self.rect.center = round(self.x), round(self.y)
        self.dx = random.choice([-1, 1])
        self.dy = random.choice([-1, 1])
        # Speed is pixels per second.
        self.speed = MAX_BALL_SPEED
        ## These prorate the speed step made in update() by remembering the
        ## interpolation value when a screen edge collision occurs. This
        ## removes all occurrence of twitchy rebounds.
        self.predictive_rebound_x = 0.0
        self.predictive_rebound_y = 0.0

    def _dim(self, color, frac):
        c = Color(*color)
        c.r = int(round(c.r * frac))
        c.g = int(round(c.g * frac))
        c.b = int(round(c.b * frac))
        return c

    def _detail_block(self, image, color, rect):
        image.fill(self._dim(color, 0.6))
        tl, tr = (0, 0), (rect.width - 1, 0)
        bl, br = (0, rect.height - 1), (rect.width - 1, rect.height - 1)
        pygame.draw.lines(image, color, False, (bl, tl, tr))
        pygame.draw.lines(image, self._dim(color, 0.3), False, (tr, br, bl))

    def update(self, *args):
        """Call once per tick to update state."""
        ## If prediction is enabled then predict() handles rebounds.
        use_prediction = list(args).pop(0)
        if not use_prediction:
            self._rebound(0.0)
        ## Speed step needs to be adjusted by the value of interpolation
        ## at the time the ball collided with an edge (predictive_rebound_*).
        self.x += self.dx * self.speed / TICKS_PER_SECOND * (1 - self.predictive_rebound_x)
        self.y += self.dy * self.speed / TICKS_PER_SECOND * (1 - self.predictive_rebound_y)
        self.predictive_rebound_x, self.predictive_rebound_y = 0.0, 0.0
        self.rect.center = round(self.x), round(self.y)

    def predict(self, interpolation, use_prediction):
        """Call as often as you like. Hitting the edge is predicted, and the
        ball's direction is changed appropriately."""
        ## If prediction is not enabled then update() handles rebounds.
        if use_prediction:
            self._rebound(interpolation)
        ## Interpolation needs to be adjusted by the value of interpolation
        ## at the time the ball collided with an edge (predictive_rebound_*).
        x = self.x + self.dx * self.speed / TICKS_PER_SECOND * (interpolation - self.predictive_rebound_x)
        y = self.y + self.dy * self.speed / TICKS_PER_SECOND * (interpolation - self.predictive_rebound_y)
        self.rect.center = round(x), round(y)

    def _rebound(self, interpolation):
        ## 1. Handle screen edge collisions.
        ## 2. Update the prediction_rebound_* adjusters.
        r = self.rect
        if r.left < screen_rect.left:
            r.left = screen_rect.left
            self.x = float(r.centerx)
            self.dx = -self.dx
            self.predictive_rebound_x = interpolation
        elif r.right >= screen_rect.right:
            r.right = screen_rect.right - 1
            self.x = float(r.centerx)
            self.dx = -self.dx
            self.predictive_rebound_x = interpolation
        if r.top < screen_rect.top:
            r.top = screen_rect.top
            self.y = float(r.centery)
            self.dy = -self.dy
            self.predictive_rebound_y = interpolation
        elif r.bottom >= screen_rect.bottom:
            r.bottom = screen_rect.bottom - 1
            self.y = float(r.centery)
            self.dy = -self.dy
            self.predictive_rebound_y = interpolation

def update_pygame():
    """Update function for use with Pygame clock."""
    global elapsed
    sprite_group.update(False)
    handle_collisions()
    elapsed += pygame_clock.get_time()
    if elapsed >= 1000:
        set_caption_pygame()
        elapsed -= 1000

def display_pygame():
    """Display function for use with Pygame clock."""
    sprite_group.clear(screen, eraser_image)
    sprite_group.draw(screen)
    pygame.display.update()

def update_gameclock(dt):
    """Update function for use with GameClock."""
    global game_ticks
    ## GOTCHA: Both Ball.update() and Ball.predict() modify sprite
    ## position, so the update and display routines must each perform
    ## erasure. This results in redundant erasures whenever an update and
    ## frame are ready in the same pass. This happens almost every game tick
    ## at high frame rates, often enough that an avoidance optimization
    ## would gain a few FPS.
    sprite_group.clear(screen, eraser_image)
    sprite_group.update(USE_PREDICTION)
    handle_collisions()

def display_gameclock(interpolation):
    """Display function for use with GameClock."""
    ## GOTCHA: See the comment in update_gameclock().
    sprite_group.clear(screen, eraser_image)
    for ball in sprite_group:
        ball.predict(interpolation, USE_PREDICTION)
    sprite_group.draw(screen)
    pygame.display.update()

def handle_collisions():
    """Handle collisions for both Pygame clock and GameClock."""
    for sprite in sprite_group:
        for other in pygame.sprite.spritecollide(sprite, sprite_group, False):
            if sprite is not other and DO_KILL:
                sprite.kill()
                other.kill()

def set_caption_pygame():
    """Set window caption for both Pygame clock and GameClock."""
    pygame.display.set_caption(
        'Loop=Pygame Kill=%s MaxFPS=%d Runtime:[FPS=%d Balls=%d]' % (
        DO_KILL, PYGAME_FPS, pygame_clock.get_fps(), len(sprite_group)))

def set_caption_gameclock(dt):
    pygame.display.set_caption(
        ' '.join(('Loop=GameClock Tab:[TPS=%d MaxFPS=%d] Predict=%s Wait=%s Kill=%s',
                 'Runtime:[FPS=%d UPS=%d Balls=%d]')) % (
        clock.max_ups, clock.max_fps, USE_PREDICTION, clock.use_wait,
        DO_KILL, clock.fps, clock.ups, len(sprite_group)))

def main():
    global clock, pygame_clock, screen, screen_rect, sprite_group, eraser_image
    global USE_PYGAME_CLOCK, DO_KILL, USE_PREDICTION, PYGAME_FPS
    screen = pygame.display.set_mode((800, 600))
    screen.fill(BGCOLOR)
    screen_rect = screen.get_rect()
    eraser_image = screen.copy()
    which_settings = 0
    pygame_clock = pygame.time.Clock()
    clock = GameClock(*SETTINGS[which_settings], update_callback=update_gameclock, frame_callback=display_gameclock)
    clock.schedule_interval(set_caption_gameclock, 1.0)
    clock.use_wait = False
    sprite_group = pygame.sprite.Group([Ball() for i in range(INIT_BALLS)])
    #
    game_is_running = True
    while game_is_running:
        if USE_PYGAME_CLOCK:
            pygame_clock.tick(PYGAME_FPS)
            update_pygame()
            display_pygame()
        else:
            clock.tick()
        #
        for e in pygame.event.get():
            if e.type == QUIT: quit()
            elif e.type == KEYDOWN:
                if e.key == K_ESCAPE: quit()
                elif e.key == K_TAB:
                    which_settings += 1
                    if which_settings >= len(SETTINGS):
                        which_settings = 0
                    (clock.max_ups, clock.max_fps) = SETTINGS[which_settings]
                elif e.key == K_1:
                    sprite_group.add(Ball())
                elif e.key == K_b:
                    sprite_group.add([Ball() for i in range(25)])
                elif e.key == K_k:
                    DO_KILL = not DO_KILL
                elif e.key == K_l:
                    USE_PYGAME_CLOCK = not USE_PYGAME_CLOCK
                elif e.key == K_m:
                    if PYGAME_FPS == 0:
                        PYGAME_FPS = 30
                    else:
                        PYGAME_FPS = 0
                elif e.key == K_p:
                    USE_PREDICTION = not USE_PREDICTION
                elif e.key == K_w:
                    clock.use_wait = not clock.use_wait

pygame.init()
main()