RealoFoxtrot
RealoFoxtrot

Reputation: 47

Python: how to bounce off the side of the screen

Okay, I am using inventwithpython to teach myself how to code. I am attempting to reuse the code in my own way to understand how it works, but this part is giving me trouble:

in Chapter 17 There is a set of animation code where different size boxes bounce off the side of the screen

for b in blocks:
    # move the block data structure
    if b['dir'] == DOWNLEFT:
        b['rect'].left -= MOVESPEED
        b['rect'].top += MOVESPEED
    if b['dir'] == DOWNRIGHT:
        b['rect'].left += MOVESPEED
        b['rect'].top += MOVESPEED
    if b['dir'] == UPLEFT:
        b['rect'].left -= MOVESPEED
        b['rect'].top -= MOVESPEED
    if b['dir'] == UPRIGHT:
        b['rect'].left += MOVESPEED
        b['rect'].top -= MOVESPEED

    # check if the block has move out of the window
    if b['rect'].top < 0:
        # block has moved past the top
        if b['dir'] == UPLEFT:
            b['dir'] = DOWNLEFT
        if b['dir'] == UPRIGHT:
            b['dir'] = DOWNRIGHT
    if b['rect'].bottom > WINDOWHEIGHT:
        # block has moved past the bottom
        if b['dir'] == DOWNLEFT:
            b['dir'] = UPLEFT
        if b['dir'] == DOWNRIGHT:
            b['dir'] = UPRIGHT
    if b['rect'].left < 0:
        # block has moved past the left side
        if b['dir'] == DOWNLEFT:
            b['dir'] = DOWNRIGHT
        if b['dir'] == UPLEFT:
            b['dir'] = UPRIGHT
    if b['rect'].right > WINDOWWIDTH:
        # block has moved past the right side
        if b['dir'] == DOWNRIGHT:
            b['dir'] = DOWNLEFT
        if b['dir'] == UPRIGHT:
            b['dir'] = UPLEFT

I want to create it so that my block moves left and right and bounces off each side of the screen.

however, when i attempt my own change to this code, all that happens is the block flies off the screen without bouncing. Have tried several variations, with all the exact same result. Currently it looks like this: Edit: updating for Full Code

import pygame, sys, time
from pygame.locals import *

# set up pygame
pygame.init()

# set up the window
WINDOWWIDTH = 480
WINDOWHEIGHT = 800
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT), 0, 32)
pygame.display.set_caption('Jumper')

#Directions
LEFT = 4
RIGHT = 6
UP = 8
DOWN = 2

MOVESPEED = 4

# set up the colors
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)


b1 = {'rect':pygame.Rect(240, 700, 20, 20), 'color':GREEN, 'dir':LEFT}
blocks = [b1]

# run the game loop
while True:
# check for the QUIT event
for event in pygame.event.get():
    if event.type == QUIT:
        pygame.quit()
        sys.exit()

# draw the black background onto the surface
windowSurface.fill(BLACK)

for b in blocks:
    # move the block data structure
    if b['dir'] == LEFT:
        b['rect'].left -= MOVESPEED
    if b['dir'] == RIGHT:
        b['rect'].left += MOVESPEED

        if b['rect'].left < 0:
             b['dir'] = RIGHT


        if b['rect'].right > WINDOWWIDTH:
                b['dir'] = LEFT

             # draw the block onto the surface
    pygame.draw.rect(windowSurface, b['color'], b['rect'])

# draw the window onto the screen
pygame.display.update()
time.sleep(0.02)

Upvotes: 4

Views: 3042

Answers (1)

Michael Geary
Michael Geary

Reputation: 28870

The problem in your code is simply the mixed-up indentation. If you fix the indentation it works correctly:

import pygame, sys, time
from pygame.locals import *

# set up pygame
pygame.init()

# set up the window
WINDOWWIDTH = 480
WINDOWHEIGHT = 800
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT), 0, 32)
pygame.display.set_caption('Jumper')

#Directions
LEFT = 4
RIGHT = 6
UP = 8
DOWN = 2

MOVESPEED = 4

# set up the colors
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)


b1 = {'rect':pygame.Rect(240, 700, 20, 20), 'color':GREEN, 'dir':LEFT}
blocks = [b1]

# run the game loop
while True:
    # check for the QUIT event
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()

    # draw the black background onto the surface
    windowSurface.fill(BLACK)

    for b in blocks:
        # move the block data structure
        if b['dir'] == LEFT:
            b['rect'].left -= MOVESPEED
        if b['dir'] == RIGHT:
            b['rect'].left += MOVESPEED

        if b['rect'].left < 0:
             b['dir'] = RIGHT
        if b['rect'].right > WINDOWWIDTH:
                b['dir'] = LEFT

        # draw the block onto the surface
        pygame.draw.rect(windowSurface, b['color'], b['rect'])

    # draw the window onto the screen
    pygame.display.update()

time.sleep(0.02)

Python uses indentation to tell what is nested inside of what. In C# and other languages that use curly braces or other block delimiters, you can get away with wrong indentation as long as you put the curly braces in the right places. But in Python, the indentation is critical. (And in all those other languages, proper indentation is still critical for code readability even if the compiler doesn't care about it.)

Compare your version of the code with the code above and you'll see where the indentation needed to be fixed.

Now on to a bigger problem. That sample code from the Inventing With Python site is... to put it nicely, not so good.

Pretend for a moment that you don't know anything about programming, and just take a look through their original code that you started with. Doesn't it seem like there's a huge amount of repetition there?

And very tricky repetition at that. Look at all the different ways it manages to use DOWNLEFT, DOWNRIGHT, UPLEFT, UPRIGHT, and how the code has to be so careful to get it right every single time.

Any time you are tempted to write this kind of tricky code, stop, sit back, and ask yourself, "Isn't there a simpler way to do this?"

In fact, you were definitely on the right track to strip out a bunch of their complicated code and make something simpler for your experiments.

But just for fun, let's go back to their code and see how we could simplify it but still do everything it does.

Start with those four names, DOWNLEFT etc. Why does each one have its own name? Aren't they really all basically the same thing? They are all diagonals, just diagonals in different directions.

And as the names suggest, a diagonal is really a combination of two directions, vertical and horizontal.

Now, how do we specify a position on the screen/window? We don't give a name to every possible position, we give it a coordinate with x and y values. The leftmost, topmost pixel is x=0, y=0 and we go from there, adding one to go right or down, subtracting one to go left or up.

So instead of making up names for the four diagonal directions, let's use the actual numbers! We'll represent DOWNRIGHT as x=1, y=1, UPLEFT as x=-1, y=-1, etc.

Now a wonderful thing happens. Consider the code that checks for a bounce off the left edge (if b['rect'].left < 0). Instead of having to make special cases to change DOWNLEFT to DOWNRIGHT or UPLEFT into UPRIGHT, we can ignore the y direction and simply change x=-1 to x=1.

The same goes for bouncing off the right edge, except we change x=1 to x=-1. In fact you could handle both of those cases by multiplying x by -1.

So we can do exactly that: If we bump into either the left or right edge, we multiply the x direction by -1. And the same thing for the top or bottom edge and the y direction.

Here is a revision of the original code that does this and makes a few other simplifications:

import pygame, sys, time
from pygame.locals import *

class Block( object ):
    def __init__( self, rect, color, dir ):
        self.rect = rect
        self.color = color
        self.dir = dir
    def move( self ):
        # reverse direction if the block will move out of the window
        if self.rect.left < SPEED or self.rect.right > WIN_WIDTH - SPEED:
            self.dir.x *= -1
        if self.rect.top < SPEED or self.rect.bottom > WIN_HEIGHT - SPEED:
            self.dir.y *= -1
        # move the block
        self.rect.left += self.dir.x * SPEED
        self.rect.top += self.dir.y * SPEED
    def draw( self ):
        pygame.draw.rect( windowSurface, self.color, self.rect )

class Direction( object ):
    def __init__( self, x, y ):
        self.x = x
        self.y = y

# set up pygame
pygame.init()

# set up the window
WIN_WIDTH = 400
WIN_HEIGHT = 400
windowSurface = pygame.display.set_mode( ( WIN_WIDTH, WIN_HEIGHT ), 0, 32 )
pygame.display.set_caption( 'Animation' )

# set up the movement speed
SPEED = 4

# set up the colors
BLACK = ( 0, 0, 0 )
RED = ( 255, 0, 0 )
GREEN = ( 0, 255, 0 )
BLUE = ( 0, 0, 255 )

# set up the block objects
blocks = [
    Block( pygame.Rect( 300, 80, 50, 100 ), RED, Direction( -1, 1 ) ),
    Block( pygame.Rect( 200, 200, 20, 20 ), GREEN, Direction( -1, -1 ) ),
    Block( pygame.Rect( 100, 150, 60, 60 ), BLUE, Direction( 1, -1 ) ),
]

# run the game loop
while True:
    # check for the QUIT event
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()

    # draw the black background onto the surface
    windowSurface.fill( BLACK )

    # move and draw each block
    for block in blocks:
        block.move()
        block.draw()

    # draw the window onto the screen
    pygame.display.update()
    time.sleep( 0.02 )

I also created classes for the blocks and directions instead of doing it all with inline code. Note how the main "move and draw" loop is now just three lines of code instead of the huge complicated mess from the original code.

Classes should be familiar to you from your C# background. The notation is a bit different in Python - for example, __init__() is the constructor - but the concepts are pretty similar. Let me know if anything in the code is unclear.

There's also a minor bug fix: The original code lets the block go past the edge of the screen before reversing it, so I adjusted the edge tests to reverse before it goes past the edge. That's why it says, for example, if self.rect.left < SPEED instead of if self.rect.left < 0.

Upvotes: 2

Related Questions