The Python Game Book

code games. learn Python.

User Tools

Site Tools


Sidebar

Github:

en:pygame:step006

Step 006 - Time-based Movement

Code Discussion

To avoid the hardware-dependence of frame-based movement you can force pygame to calculate time-based movement. Please note that we still speak of the illusion of movement, because all what pygame does is blitting blobs (surfaces) to different positions - but in a more reliable way :-) !

Time-based movement does not mean that your pygame program will run on crappy computers as smooth as on fast machines - pygame can not (yet) do that! Time-based movement just means that independent of the speed of the computer, in the source code example below the ball will always take the same amount of time to cross the screen. On fast machines, the movement will appear very smooth, while on slow machines the movement will appear bumpy.

Calculating the passed Time

The secret to time-based movement is to let pygame calculate how much time was passed since the last cycle of the mainloop (to be exact: since the last call of the clock.tick command). Pygame provide this information in milliseconds. In the source code example below I convert this value into seconds 1). This time value is multiplied to the speed vector:

    #calculate new center of ball (time-based)
    ballx += dx * seconds # float, since seconds passed since last frame is a decimal value
    bally += dy * seconds 

Note that the value of seconds will be something like 0.05 seconds per frame 2) so you need rather high dx and dy values to see any movement at all. In this example dx and dy speed values are set at 60 pixel per second.

Blitting on a clean Screen

In this pygame example, the whole background is blitted onto the screen each cycle (frame) of the mainloop. This will overwrite all old images of the ball surface. On top of the fresh background, the ball is blitted each frame. While this method of cleaning the whole screen allow you care-free programming, very big pygame windows (screen resolution) or slow computers will show a drop in the frame-rate. The next chapters will show you more time- and memory-efficient methods of cleaning only those parts of the screen that need cleaning. This is called dirty rects cleaning.

Source Code on Github

To run this example you need:

file in folder download
006_time_based_movement.py pygame Download the whole Archive with all files from Github:
https://github.com/horstjens/ThePythonGameBook/archives/master

View/Edit/Download the file directly in Github: https://github.com/horstjens/ThePythonGameBook/blob/master/pygame/006_time_based_movement.py

click reload in your browser if you see no code here:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
006_time_baed_movement.py
url: http://thepythongamebook.com/en:part2:pygame:step006
author: horst.jens@spielend-programmieren.at
licence: gpl, see http://www.gnu.org/licenses/gpl.html

works with python3.4 and pyhton2.7
 
bouncing ball. Movement is now time based.
Because at coding, you never know exactly how many milliseconds
will have been passed between two frames, this example use pygame's
clock function to calculate the passed time and move the ballsurface at
constantly the same speed. 
If you toggle the wild circle painting by pressing SPACE, the computer
has more to paint, framerate will drop, more time will pass between 
2 frames and movement of the ball surface will be choppy (less smooth).
However, the movent speed remain unchanged because of the time-based movement.
"""
#the next line is only needed for python2.x and not necessary for python3.x
from __future__ import print_function, division
import pygame
import random


def wildPainting():
    """draw random circles to give the cpu some work to do"""
    pygame.draw.circle(background, (random.randint(0,255),
                       random.randint(0,255), random.randint(0,255)),
                       (random.randint(0,screenrect.width),
                       random.randint(0,screenrect.height)),
                       random.randint(50,500))

#pygame.mixer.pre_init(44100, -16, 2, 2048) # setup mixer to avoid sound lag
pygame.init()
screen=pygame.display.set_mode((640,480)) # try out larger values and see what happens !
screenrect = screen.get_rect()
background = pygame.Surface(screen.get_size()) #create surface for background
background.fill((255,255,255))     #fill the background white (red,green,blue)
background = background.convert()  #convert surface for faster blitting
background2 = background.copy()    # clean background to restore for later 
ballsurface = pygame.Surface((50,50))     #create a new surface (black by default)
ballsurface.set_colorkey((0,0,0))         #make black the transparent color (red,green,blue)
#pygame.draw.circle(Surface, color, pos, radius, width=0)
pygame.draw.circle(ballsurface, (0,0,255), (25,25),25) # paint blue circle
ballsurface = ballsurface.convert_alpha()        # for faster blitting. because transparency, use convert_alpha()
ballrect = ballsurface.get_rect()
ballx, bally = 550,240             # start position for the ball surface (topleft corner)
dx,dy  = 60, 50                    # speed of ball surface in pixel per second !
                  
screen.blit(background, (0,0))     #blit the background on screen (overwriting all)
screen.blit(ballsurface, (ballx, bally))  #blit the ball surface on the screen (on top of background)
 
 
clock = pygame.time.Clock()        #create pygame clock object
mainloop = True
FPS = 60                           # desired max. framerate in frames per second. 
playtime = 0
paint_big_circles = False
cleanup = True
 
while mainloop:
    milliseconds = clock.tick(FPS)  # milliseconds passed since last frame
    seconds = milliseconds / 1000.0 # seconds passed since last frame (float)
    playtime += seconds
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            mainloop = False # pygame window closed by user
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                mainloop = False # user pressed ESC

            elif event.key == pygame.K_1: 
                FPS = 10
            elif event.key == pygame.K_2:
                FPS = 20
            elif event.key == pygame.K_3:
                FPS = 30
            elif event.key == pygame.K_4:
                FPS = 40
            elif event.key == pygame.K_5:
                FPS = 50
            elif event.key == pygame.K_6:
                FPS = 60
            elif event.key == pygame.K_7:
                FPS = 70
            elif event.key == pygame.K_8:
                FPS = 80
            elif event.key == pygame.K_9:
                FPS = 90
            elif event.key == pygame.K_0:
                FPS = 1000 # absurd high value
            elif event.key == pygame.K_x:
                paint_big_circles =  not paint_big_circles # toggle
            elif event.key == pygame.K_y:
                cleanup = not cleanup # toggle boolean value
            elif event.key == pygame.K_w: # restore old background
                background.blit(background2, (0,0)) # clean the screen

                
               
            
    pygame.display.set_caption("x: paint ({}) y: cleanup ({}) ,"
                               " w: white, 0-9: limit FPS to {}"
                               " (now: {:.2f})".format(
                    paint_big_circles, cleanup, FPS,clock.get_fps()))
    if cleanup:
        screen.blit(background, (0,0))     #draw background on screen (overwriting all)
    if paint_big_circles:
       wildPainting()
    #calculate new center of ball (time-based)
    ballx += dx * seconds # float, since seconds passed since last frame is a decimal value
    bally += dy * seconds 
    # bounce ball if out of screen
    if ballx < 0:
        ballx = 0
        dx *= -1 
    elif ballx + ballrect.width > screenrect.width:
        ballx = screenrect.width - ballrect.width
        dx *= -1
    if bally < 0:
        bally = 0
        dy *= -1
    elif bally + ballrect.height > screenrect.height:
        bally = screenrect.height - ballrect.height
        dy *= -1
    # paint the ball    
    screen.blit(ballsurface, (round(ballx,0), round(bally,0 )))
    pygame.display.flip()          # flip the screen 30 times a second
print("This 'game' was played for {:.2f} seconds".format(playtime))

1)
through dividing by 1000.0 - the .0 is important or python will return an integer value. At least in python version 2.x
2)
for a frame-rate of 20 frames per second
en/pygame/step006.txt · Last modified: 2020/05/15 22:40 by horst