The Python Game Book

code games. learn Python.

User Tools

Site Tools


Sidebar

Github:

en:pygame:step005

Step 005 - Frame-based Movement

screenshot screenshot
simple version pretty version

Code Discussion

Frame-based Movement

In this code example, the ball surface seems to move around. Keep in mind that the ball surface is not moving at all, it is just blitted to another position each frame (each cycle of the main loop). This movement is a frame-based movement and will depend on the actual frame-rate that your computer manages to display. Thus, the same pygame program will run slower on old or busy computers than on fast machines.

Manipulate the size of the pygame window (screen) or the workload of your computer (by moving big applications around) and you should notice that the ball needs a different amount of time to cross the screen.

Vectors

The new variables dx and dy are variables to control the speed of the ball surface (in pixels per second). You could call (dx, dy) a speed vector. Because integer values would mean a minimum visible movement of one pixel per frame, dx and dy are decimal values (float). In the mainloop, the current movement is added to the current position (resulting in a float value). Because pygame is (yet) unable to blit at the position of a half-pixel, the position is rounded to integer values just for blitting.

#calculate new center of ball 
ballx += dx
#bally += dy # y movement deactivated

# paint the ball    
screen.blit(ball, (round(ballx,0), round(bally,0)))    

Line too long

The pygame.display.set_caption line is too long to fit comfortably in this wiki. Here is the code line in its natural length:

    pygame.display.set_caption("FPS: %.2f X: %.2f Y: %.2f dx: %.2f dy:%.2f" % (clock.get_fps(), ballx, bally, dx, dy))
In Python, you can split a code line into several lines if:

  • you split between opening <key>(</key> and closing <key>)</key> brackets
  • you have two strings enclosed in <key>“</key>-signs where the second string directly follows the first string. Python will interpred those two strings as one large string. See the source code example below.
  • a line ends with space and backslash <key> \</key> see this example:

result = 5 + 7 + \
+ 10
print result # result is 5+7+10=22

Bouncing off Walls

If the ball (surface) hits the edge of the screen, the speed vector component dx or dy are inverted by multiplying with -1 to make sure that the ball surface position ballx or bally stay inside of the screen.

    # bounce ball if out of screen
    if ballx < 0:
        ballx = 0
        dx *= -1 

Drawing on the Screen

You can draw on any pygame surface, even directly on the screen. While all changed surfaces need to be blitted again on the screen to make their changes visible, the screen surface is updated with the pygame.display.flip() command each frame (main loop cycle). Because the whole background is blitted on top of the screen (practically cleaning the screen) each frame, you are free to experiment with moving pictures.

Note that the random module has to be imported at the start of the program.

import random # after importing pygame

The next code-lines draw an glittering, pulsating circle. add this code inside the mainloop:

    # ----- pulsating circle -----------
    colour = (random.randint(0,255),random.randint(0,255),random.randint(0,255))
    if radius > 100 or radius < 5:
        dr *= -1
    radius += dr
    pygame.draw.circle(screen, colour , (100,100), radius, 2) # draw pulsating circle
    # -------- end of pulsating circle -------

Documentation

Source Code on Github

To run this example you need:

file in folder download comment
005_frame_based_movement.py pygame Download the whole Archive with all files from Github:
https://github.com/horstjens/ThePythonGameBook/archives/master
simple version
005_frame_based_movement_pretty.py pygame pretty version

Simple Version

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

If using Python 3, be sure to replace the division operators in the following line:

pygame.draw.circle(background, (0,0,200), (screenrect.width/2, screenrect.height/2), screenrect.width/3)

with the ”//“ operator to truncate the quotients and return integers, rather than floating point numbers. And remember that print is now a function.

#!/usr/bin/env python
"""
005_bouncing_ball_frame_based.py
bouncing ball and pulsating circle
url: http://thepythongamebook.com/en:part2:pygame:step005
author: horst.jens@spielend-programmieren.at
licence: gpl, see http://www.gnu.org/licenses/gpl.html

works with python3.4 and python2.7

bouncing ball. each frame the complete screen is filled with the background,
making this example simple to code but possible slow on larger resolutions.
Each frame, a random-coloured circle is drawn with randomized radius directly on the screen.
Try to manipulate the display.set_mode values to change the resolution."""
#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
pygame.init()
screen=pygame.display.set_mode((640,480),) # try out larger values and see what happens !
screenrect = screen.get_rect()
# ------ constants ------------
clock = pygame.time.Clock()
mainloop = True
FPS = 30 # desired framerate in frames per second. 
playtime = 0
radius = 50 # for pulsating circle
dr = 1  # change of radius in pixel per frame
# ------- background ---------
background = pygame.Surface(screen.get_size())
background.fill((255,155,155))     #fill the background white (red,green,blue)
background = background.convert()
screen.blit(background, (0,0))     #draw background on screen (overwriting all)
# -------- bouncing ball surface ---------
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, (100,175,81), (25,25),25) # paint blue circle
ballsurface = ballsurface.convert_alpha()       # if you use tranparent colors you need convert_alpha()
ballrect = ballsurface.get_rect() # the rectangle of the ball surface, for collision detection
ballx, bally = 550, 240           # start position of the ball (x,y)
dx = 10                 # x speed vector of the ball in pixel per frame            
dy = 0                 # y speed vector of the ball in pixel per frame
# ----------- bouncing ball (drawing) ------
x1 = 50
y1 = 200
dx1 = 7
dy1 = 0
radius1 = 40
# --------- static big blue ball -----------
pygame.draw.circle(background, (0,0,200), (screenrect.width//2, screenrect.height//2), screenrect.width//3)
# --------- mainloop ----------
while mainloop:
    # do all this each frame
    milliseconds = clock.tick(FPS) # do not go faster than this framerate
    playtime += milliseconds / 1000.0
    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
    pygame.display.set_caption("FPS: %.2f X: %.2f Y: %.2f dx: %.2f dy:"
                               " %.2f" % (clock.get_fps(), ballx, bally, dx, dy))
    # ----- clean screen ----------
    screen.blit(background, (0,0))     #draw background on screen (overwriting all)
    # ------- bouncing ball (drawing) ---------
    x1 += dx1
    if x1 + radius1 >= screenrect.width:
        x1 = screenrect.width - radius1
        dx1 *= -1
    elif x1 - radius1 <= 0:
        x1 =  radius1
        dx1 *= -1
    pygame.draw.circle(screen, (255,255,0), (x1,y1), radius1)
    # -------- bouncing ball surface ----------
    ballx += dx
    bally += dy 
    if ballx < 0: # bounce ball if out of screen
        ballx = 0
        dx *= -1 
    elif ballx + ballrect.width > screenrect.width:
        ballx = screenrect.width - ballrect.width
        dx *= -1
    screen.blit(ballsurface, (round(ballx,0), round(bally,0)))    
    # ----- pulsating circle -----------
    colour = (random.randint(0,255),random.randint(0,255),random.randint(0,255))
    if radius >100 or radius < 5:
        dr *= -1
    radius += dr
    pygame.draw.circle(screen, colour , (100,100), radius, 2) # draw pulsating circle
    # --------- flip screen ------------------
    pygame.display.flip()          # flip the screen FPS times a second
print("This 'game' was played for {:.2f} seconds.".format(playtime))

Pretty Version

(by yipyip)

Note that this version move more objects than the simple version. View/Edit/Download the file directly in Github:

#!/usr/bin/env python

"""
Name   : blit_pulse2.py
URL    : http://thepythongamebook.com/en:part2:pygame:step003
Author : yipyip
Licence: gpl, see http://www.gnu.org/licenses/gpl.html

works with pyhton3.4 and python2.7
"""

####

import pygame as pyg
import random as rand

####

def random_rgb():
    
   return rand.randint(0, 255), rand.randint(0,255), rand.randint(0, 255)

####

class PygView(object):

  
    def __init__(self, width=800, height=600, fps=50):
        """Initializing background surface for static drawing
           and screen surface for dynamic drawing 
        """
        pyg.init()
        pyg.display.set_caption("Press ESC to quit")
        
        self.width = width
        self.height = height
        
        self.screen = pyg.display.set_mode((self.width, self.height), pyg.DOUBLEBUF)
        self.background = pyg.Surface(self.screen.get_size()).convert()  
        # white blackground
        self.background.fill((255, 255, 255))

        self.act_surface = self.screen
        self.act_rgb = 255, 0, 0

        
    def draw_static(self):

        self.act_surface = self.background


    def draw_dynamic(self):

        self.act_surface = self.screen


    def set_color(self, rgb):

        self.act_rgb = rgb

        
    def circle(self, x, y, radius, width):
        """Allocate surface for blitting and draw circle
        """
        rad2 = 2 * radius
        surface = pyg.Surface((rad2, rad2))
        pyg.draw.circle(surface, self.act_rgb, (radius, radius), radius, width)
        surface.set_colorkey((0, 0, 0))
        self.act_surface.blit(surface.convert_alpha(), (x, y))


    def run(self, draw_dynamic):
        """The mainloop
        """
        running = True
        while running:
            for event in pyg.event.get():
                if event.type == pyg.QUIT:
                    running = False 
                elif event.type == pyg.KEYDOWN:
                    if event.key == pyg.K_ESCAPE:
                        running = False

            draw_dynamic()
            pyg.display.flip()
            self.screen.blit(self.background, (0, 0))
            
        pyg.quit()

####

class Ball(object):
    """A circle object with no hardcoded dependency on pygame
       (and other libs too, obviously...)
    """
    def __init__(self, x, y, radius, speed_x=1, speed_pulse=0, color=(0,0,255), width=0):

        self.x = x
        self.y = y
        self.radius = radius
        self.act_radius = radius
        self.speed_x = speed_x
        self.speed_pulse = speed_pulse
        self.color = color
        self.width = width
        self.shrinking = True


    @property
    def max_x(self):

        return self.x + self.radius * 2
    
        
    def rel_move(self, dx, dy):

        self.x += dx
        self.y += dy


    def pulse(self):
        """Shrink or expand ball
        """
        if not self.speed_pulse:
            return

        # balls are shrinking first 
        if self.shrinking:
            if self.act_radius > self.width:
                self.act_radius -= self.speed_pulse
                self.act_radius = max(self.act_radius, self.width)
            else:
                self.shrinking = False
        else:
            if self.act_radius < self.radius:
                self.act_radius += self.speed_pulse
            else:
                self.shrinking = True
        
        
    def draw(self, view):
        """ Draw on a device with an appropriate interface
        """
        if self.speed_pulse:
            color = random_rgb()
        else:
            color = self.color 
        view.set_color(color)
        view.circle(self.x, self.y, self.act_radius, self.width)
     
####

def action(balls, width, view):
    """ Return a function for the pygame mainloop
    """
    # balls move to the right first 
    right_moving = [True] * len(balls)

    def animate_balls():
        """ Draw moving balls
        """
        for i, ball in enumerate(balls):
            if right_moving[i]:
                if ball.max_x < width:
                    ball.rel_move(ball.speed_x, 0)
                else:
                    right_moving[i] = False
            else:
                if ball.x > 0:
                    ball.rel_move(-ball.speed_x, 0)
                else:
                    right_moving[i] = True
            
            ball.pulse() 
            ball.draw(view)

    return animate_balls    
        
####

def main(width):
    """Simple example with stationary and moving balls
    """   
    view = PygView(width)
    
    view.draw_static()
    # args:  x, y, radius, speed_x, speed_pulse, color, border_width
    # border_width <= radius !
    ball01 = Ball(50, 60, 50, 0, 0, (255, 255, 0))
    ball01.draw(view)
    ball02 = Ball(250, 150, 190, 0, 0, (66, 1, 166))
    ball02.draw(view)

    view.draw_dynamic()
    ball1 = Ball(15, 130, 100, 1, 0, (255, 0, 0))
    ball2 = Ball(25, 200, 80, 2, 0, (0, 255, 155))
    ball3 = Ball(20, 220, 110, 1, 1, (100, 55, 155))
    ball4 = Ball(20, 400, 70, 3, 0, (250, 100, 255))
    ball5 = Ball(90, 390, 70, 0, 1, (250, 100, 255), 1)

    loopfunc = action((ball1, ball2, ball4, ball5), width, view)
    view.run(loopfunc)

####
    
if __name__ == '__main__':

    main(900)
    

Comment this Ptage

en/pygame/step005.txt · Last modified: 2020/05/15 22:39 by horst