Breakout 3D Sample vPython App

Reverend Jim 0 Tallied Votes 481 Views Share

I wrote this 3D Breakout game as a sample project to learn both Python and the visual module, vPython. It is based on a similar game I had on my Amiga back in the 80s. I don't expect that my Python is either standard or as elegant as it could be so constructive feedback is always appreciated.

#########################################################################################
#                                                                                       #
#  Name:                                                                                #
#                                                                                       #
#    Breakout3D.py                                                                      #
#                                                                                       #
#  Description:                                                                         #
#                                                                                       #
#    A 3D version of Breakout (requires red/cyan glasses). Black out the bricks on 4    #
#    walls to win.                                                                      #
#                                                                                       #
#  Options:                                                                             #
#                                                                                       #
#    Change the following variables in the code to modify the game                      #
#                                                                                       #
#    NO_LOSE_MODE = False            #set True to make floor solid (self play mode)     #
#    SHOW_TRAIL   = True             #set True to make ball leave temporary trail       #
#    SHOW_COORDS  = False            #set True to display ball coordinates              #
#                                                                                       #
#  Future:                                                                              #
#                                                                                       #
#    Randomize start velocity on ball launch                                            #
#    Allow above options to be specified on the command line                            #
#    Change velocity vector depending on what part of the paddle is hit.                #
#    Add shadow as training aid                                                         #
#                                                                                       #
#  Audit:                                                                               #
#                                                                                       #
#    2011-09-05  added NO_LOSE_MODE, SHOW_TRAIL & SHOW_COORDS options                   #
#                change ball radius based on z coordinate to improve depth perception   #
#                ball is red if coming at you, green if going away                      #
#    2011-09-04  R J de Graff original code                                             #
#                                                                                       #
#########################################################################################

from visual import *

def SideBrick(bricks,y,z):      #check if a left or right side brick was hit

    halfy = bricks[0].height / 2
    halfz = bricks[0].width  / 2
    
    for brick in bricks:
        if brick.visible:
            if brick.y-halfy <= y <= brick.y+halfy and brick.z-halfz <= z <= brick.z+halfz:
                return True,brick
    return False,""

def TopBrick(bricks,x,z):       #check if a top brick was hit               

    halfx = bricks[0].width  / 2
    halfz = bricks[0].length / 2

    for brick in bricks:
        if brick.visible:
            if brick.x-halfx <= x <= brick.x+halfx and brick.z-halfz <= z <= brick.z+halfz:
                return True,brick
    return False,""

def BackBrick(bricks,x,y):      #check if a back brick was hit              

    halfx = bricks[0].length / 2
    halfy = bricks[0].height / 2

    for brick in bricks:
        if brick.visible:
            if brick.x-halfx <= x <= brick.x+halfx and brick.y-halfy <= y <= brick.y+halfy:
                return True,brick
    return False,""

def UpdateStatus(status,numballs,numbricks):

    status.text = "Balls=%d  Bricks=%d" % (numballs,numbricks)

def Reset(bricks):              #set all bricks visible                     

    for brick in bricks:
        brick.visible = True
    return len(bricks)

def EmptyBuffer(myscene):       #flush all buffered left clicks             

    while myscene.mouse.clicked > 0:
        temp = myscene.mouse.getclick()

NO_LOSE_MODE = False            #set True to make floor solid               
SHOW_TRAIL   = True             #set True to make ball leave temporary trail
SHOW_COORDS  = False            #set True to display ball coordinates       
    
brickLeft  = []                 #all bricks on the left wall                
brickRight = []                 #all bricks on the right wall               
brickTop   = []                 #all bricks on the top wall                 
brickBack  = []                 #all bricks on the back wall                

myscene = display(title="3D Breakout",width=1000,height=640,fullscreen=True)
myscene.select()
myscene.autoscale = True
myscene.userzoom  = True
myscene.userspin  = False
myscene.range     = 360
myscene.cursor.visible = False

myscene.stereo = 'redcyan'      #redblue yellowblue crosseyed passive active

wallLeft   = -250               #x coordinate of the left wall              
wallRight  =  250               #x coordinate of the right wall             
wallTop    =  150               #y coordinate of the ceiling                
wallBottom = -150               #y coordinate of the floor                  
wallBack   = -300               #z coordinate of the back wall              
wallFront  =  200               #z coordinate of the front wall             

brickColor = (0.7,0.7,1)

for x in range(-200,201,100):   #draw the bricks on the top                 
    for z in range(-260,141,100):
        brickTop.append(box(pos=(x,wallTop,z),size=(95,0.1,95),color=brickColor))

for y in range(-120,121,60):    #draw the bricks on the left and right      
    for z in range(-260,141,100):
        brickLeft.append (box(pos=(wallLeft ,y,z),size=(0.1,55,95),color=brickColor))
        brickRight.append(box(pos=(wallRight,y,z),size=(0.1,55,95),color=brickColor))

for x in range(-200,201,100):   #draw the bricks on the back                
    for y in range(-120,121,60):
        brickBack.append(box(pos=(x,y,wallBack),size=(95,55,0.1),color=brickColor))

paddle = box(pos=(0,wallBottom,0),size=(90,.1,90),color=(1,1,1))

ball = sphere (radius = 5.0, make_trail=False)

if SHOW_TRAIL:
    ball.make_trail = True
    ball.trail_type = "curve"
    ball.interval   = 10
    ball.retain     = 100
    
ball.mass     = 1.0
ball.velocity = vector(0.15, 0.23, 0.27)
ball.visible  = False

dt = 1.0    #0.5 is half normal speed and 2.0 is double normal

numbricks = Reset(brickLeft+brickRight+brickTop+brickBack)
numballs  = 3

newball   = True
hitfound  = False
gameover  = False

coords    = label(text="",pos=(wallLeft+40,wallBottom+5,wallFront),height=20,color=(.5,.5,.5),box=False)
status    = label(text="",pos=(0,wallBottom+5,wallFront),height=20,box=False)

status.text = "Left Click to launch or ESC to exit"

while true:

    rate(400)

    #update paddle position based on mouse position

    px =  min(max(2*myscene.mouse.pos[0],wallLeft+20),wallRight-20)
    pz = -min(max(2*myscene.mouse.pos[1],wallBack),wallFront)-100
    paddle.pos = (px,wallBottom,pz)

    #If waiting to launch a new ball then idle until keypress. We have to poll rather than
    #pause or we won't be able to move the paddle while we wait.

    if gameover:
    
        if myscene.mouse.clicked > 0:
            numbricks = Reset(brickLeft+brickRight+brickTop+brickBack)
            numballs  = 3
            newball   = True
            gameover  = False
            UpdateStatus(status,numballs,numbricks)
            
    elif newball:

        if myscene.mouse.clicked > 0:
            newball = False
            paddle.color = (1,1,1)
            ball.pos = paddle.pos
            ball.velocity = vector(0.15, 0.23, 0.27)
            ball.visible = True     
            UpdateStatus(status,numballs,numbricks)
            
    else:

        #calculate new ball position and adjust size based on z position

        ball.pos = ball.pos + ball.velocity * dt
        ball.radius = 4 + (ball.z+400) / 100.0
        
        if ball.velocity.z > 0:
            ball.color = (1,0,0)
        else:
            ball.color = (0,1,0)
        
        if SHOW_COORDS:
            coords.text = "%4d %4d %4d" % (ball.x,ball.y,ball.z)

        #check for hit on a visible brick

        if ball.x <= wallLeft:
            ball.velocity.x = -ball.velocity.x
            hitfound,brick = SideBrick(brickLeft,ball.y,ball.z)

        if ball.x >= wallRight:
            ball.velocity.x = -ball.velocity.x
            hitfound,brick = SideBrick(brickRight,ball.y,ball.z)

        if ball.z <= wallBack:
            ball.velocity.z = -ball.velocity.z
            hitfound,brick = BackBrick(brickBack,ball.x,ball.y)

        if ball.z >= wallFront:
            ball.velocity.z = -ball.velocity.z

        if ball.y >= wallTop:
            ball.velocity.y = -ball.velocity.y
            hitfound,brick = TopBrick(brickTop,ball.x,ball.z)

        #if hit then clear brick

        if hitfound:
            brick.visible = False
            hitfound   = false
            numbricks -= 1
            UpdateStatus(status,numballs,numbricks)

        #check if paddle hit or miss

        if ball.y <= wallBottom and numballs > 0:      
            if NO_LOSE_MODE:
                ball.velocity.y = -ball.velocity.y
            else:
                if paddle.x-45 <= ball.x <= paddle.x+45 and paddle.z-45 <= ball.z <= paddle.z+45 and wallBottom - ball.y <= 1:
                    ball.velocity.y = -ball.velocity.y
                elif wallBottom - ball.y > 50:
                    ball.velocity.y = -ball.velocity.y
                    numballs -= 1
                    newball = (numballs > 0)
                    EmptyBuffer(myscene)
                    paddle.color = (1,1,0)
                    ball.visible = False
                    status.text = "Left Click to launch or ESC to exit"
 
        #check if out of bricks (win) or balls (lose)

        if not gameover and (numbricks == 0 or numballs == 0):
            gameover = True
            EmptyBuffer(myscene)
            if numbricks == 0:
                status.text = "Congratulations - You Win - Left Click for new game or ESC to exit"
            else:
                status.text = "Sorry - You've got no balls - Left Click for new game or ESC to exit"
TrustyTony 888 pyMod Team Colleague Featured Poster

You have long experience in programming, and it shows. Code optimizes checking brick hits from only one wall, however I feel that using attribute containing all bricks would simplify much the program. You could even calculate crossing point mathematically, direction is changed only at walls and pad, which can be thought as conditional wall. Then one scan of bricks would be enough. Could it not happen that ball hits border of 4 bricks?

Reverend Jim 4,780 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

I feel that using attribute containing all bricks would simplify much the program

I see what you mean. I'll code this up and see where it goes.

Could it not happen that ball hits border of 4 bricks?

Because there is a space between the bricks it is impossible to hit more than one at a time. As for the corners, it is possible for the ball to hit exactly at the junction of two or more walls. That is why I test each wall separately instead of using if-elif-elif etc. Or am I missing your point?

Thanks for the feedback and suggestions. I do have the experience programming but I am still relatively new to the idioms of Python.

TrustyTony 888 pyMod Team Colleague Featured Poster

Comments on coding style. Those flag variables are less than ideal considering the normal Python way and your naming does not confirm Python PEP8 convention. Also you use little too much white space, and you are not using multiline string for multiline comment, but instead box comment which is not so much encouraged in the style guide. Nowadays computers are so fast that does not really so much difference how you check the tiles. For me the program was not so easy to check in action as I do not own 3D glasses. My graphics card is also old and I had to switch to windowed mode to see the message texts of the program. It would be nice to have only one collision detect and return bricks hitted or empty list instead of current tuple (I have done quite similar style as more Pythonic style, but it somehow looks not ideal in this situation). pythonic pseudocode for main action could be:

for brick in bricks_hit(ball_position):
   reflect(movement_vector)
   remove(brick)
Reverend Jim 4,780 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

Excellent suggestions and your sample code is much clearer. As I said, I am still learning the Python idioms. Ultimately I code to make it easy for me to read but I am flexible so I will try to accommodate the accepted norms. Thanks for the input.

TrustyTony 888 pyMod Team Colleague Featured Poster
for brick in bricks_hit(ball_position):
   reflect(movement_vector, reflect_direction[brick])
   remove(brick)

Would maybe need dict of reflect directions supplied to reflection (like (-1, 0, 0) for doing movement_vector.x = -movement_vector.x) If we would have class holding the variables we maybe would not have them as parameters, but access through instance variables.

About separation between bricks, my logic says that one of these is true:

  1. ball can hit both bricks
  2. ball can go through between bricks
Reverend Jim 4,780 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

I had a thought or two on that. I thought I might add an invisible brick behind each wall. These bricks would be the same size as the walls. They would also start as enabled=False. The Remove function would only remove bricks that are enabled. That way the ball would reflect even if it hits a space between two smaller bricks. By the same token, I should be able to add the paddle as a brick and have the reflection done by the same logic. That would only leave the case where the ball misses the paddle and falls through the floor. I could add the reflection vector as a property to each brick.

This sort of stuff reminds me why I got into programming all those years (almost 40) ago. It's fun and there is always something new to learn.

TrustyTony 888 pyMod Team Colleague Featured Poster

I had a thought or two on that. I thought I might add an invisible brick behind each wall. These bricks would be the same size as the walls. They would also start as enabled=False. The Remove function would only remove bricks that are enabled. That way the ball would reflect even if it hits a space between two smaller bricks. By the same token, I should be able to add the paddle as a brick and have the reflection done by the same logic. That would only leave the case where the ball misses the paddle and falls through the floor. I could add the reflection vector as a property to each brick.

This sort of stuff reminds me why I got into programming all those years (almost 40) ago. It's fun and there is always something new to learn.

I think it would be nice to have down counter of hits so if the brick is indestructible, it has count say trillion trillions, otherwise it would have one or few. You could have one "lose life tile" under pad level which has counter 1 and is reinitialized to one after miss and one life is taken out. It would be nice to be hit last moment by the end of the paddle, however, and have radical change of direction.

How are you dealing with issue of movement per tick not matching the distance from the surface before hit, or maybe the effect of that is negligible (with high speed you could however tunnel through the brick, wouldn't you)

You probably have very limited amount of hit angles now as you have not different change of direction from the place of pad hit. You could make surface normal depend on the distance from the paddle centre towards the side of the hit. It is add surface parallel vector length relative to distance from middle of the paddle.

For reflection from obtuse angles, the reflection would not be same direction from the end of the brick as top, basically you would like to get surface normal from any surface of the block hit.

Then later you could create bricks at different levels and having different number of hits required (durability), blocking indestructible tiles etc.

TrustyTony 888 pyMod Team Colleague Featured Poster

This engine could be "slightly" better for your game: http://www.panda3d.org/

Reverend Jim commented: This is a cumulative upvote for all the help on this thread. +4
Reverend Jim 4,780 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

I haven't had a look at panda3d yet but I definitely will. Thanks.

I've been looking at ways to generalize the code so that I don't require separate functions for each wall condition. It seems that the more general I make the code the more unreadable and complex it is. Given a choice between readability and, complexity (or cleverness), I generally go for readability. For example, the code to check if a "left wall" brick has been hit is trivial and clear. The code to check if a generic brick has been hit is not. The first step is to compare the perpendicular distance. If within a given tolerance I must then check if the remaining two coordinates of the ball fall with the plane of the brick. This involves a level of indirection because I must now index the position vectors with indices that are determined by the orientation of the brick. For example, a "left" brick has the plane defined by y and z, a "top" brick by x and z and a "back" brick by x and y.

TrustyTony 888 pyMod Team Colleague Featured Poster

Sometimes it is OK not to be very general and repeat yourself even it is not so nice. If you would be planning to go to more realistic version generality can pay off. Reflection planes are planes in the coordinates, and there could be way to chose that hitting plane based on current movement direction. In one 2d breakout example the bounce was implemented object oriented way and the way was based on angle of movement, up being the 0 degree direction.

You noticed the second paragraph in PEP8, didn't you ;) Good rules, aren't they?

TrustyTony 888 pyMod Team Colleague Featured Poster

This engine could be "slightly" better for your game: http://www.panda3d.org/

Here actually full list from python pages, maybe you did not check it out yet:
http://wiki.python.org/moin/PythonGames

Reverend Jim 4,780 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

Aaarrrggghhh. So many choices, so little bandwidth. I see the panda3d download is about 84 meg. I'm on wireless at the cottage from May through mid September (retirement is AWESOME) where I try to stay under 100 meg a day up/down so I'll have to look at this (and the others) when I get home. With so many choices it will be hard to make an informed choice. But it's still nice to know what the choices are. Thanks.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.