Raspberry Pi Projects (2014)
Part II. Software Projects
Chapter 5. Pingby Mike Cook
In This Chapter
• Describe movement to a computer
• Display a dynamic game
• Discover one way to detect collisions between on-screen objects
• Handle the physics of reflection
• Make single-player and two-player versions of the game
A version of Ping-Pong, or table tennis, was one of the early electronic games; it was first produced by Willy Higginbotham in 1958 and used an oscilloscope as a display. This was long before the advent of microcomputers. You could make a version of the game by using just logic gates, with no processors involved. I made one back in 1970 that generated its own TV signal. It was, by today’s standards, a weird hybrid of an analogue and digital circuit – with the path of the ball being driven by two analogue integrators, and those voltages triggering comparators against an analogue sawtooth TV time base. By 1975 all the logic had been combined into one chip, the AY-3-8500, making it a much easier circuit to build. It even had a choice of games which were simple variants on the same theme. This chip appeared in hundreds of products and was the first low-cost home TV console. I even built a console using this chip as well. A few years after that, home microcomputer systems came along, and table tennis was one of the first graphics games to be implemented.
Early Commercial Products
On the commercial side, the Magnavox Odyssey, designed by Ralph Baer, was the first game console to go on sale to the general public. This was first demonstrated in 1968, but was not commercially available until 1972. It was seen before its launch by Nolan Bushnell, who cofounded Atari, so he assigned the newly appointed engineer Allan Alcorn to implement the game as a training exercise. The results were so impressive that Bushnell decided to launch it as an video arcade game using the name Pong. This name sounded odd to U.K. ears, as it is a slang word for a very bad smell. Just a few days into the testing of the first prototype in a bar, the owner rang up to say that the game had stopped working. On investigation they found that the money had backed up and jammed the mechanism. So the first upgrade was to fit a bigger coin box – something Bushnell later said he was “happy to live with”. Inevitably Magnavox and Atari ended up in court, but out-of-court settlements were reached. Pong then went on to be released as a home TV console.
So the game has a honored place in the history of computing and serves as an interesting introduction into arcade type games on the Raspberry Pi. You might think that being a game from the early days of computing it will be simple, and it is, but it is not as simple as you might hope. The early games were written in Assembler language; with today’s computing power you can write a table tennis game in a high-level language like Python.
The Ping Game
Basically, what I am calling Ping is a copy of the bat-and-ball game which spawned a whole generation of computer games. What you are going to implement here are two games, a one-player game and a two-player game. Figure 5.1 shows the screen display of the two-player game, but they look very similar. As with all projects, it is best to start implementing features one at a time and building up the sophistication and complication as you go. However, first you need a bit of theory.
Figure 5-1: The screen display of the Ping game.
On-screen Movement
Movement on the computer screen is created by drawing a number of separate pictures in quick succession with one or more elements moving between each frame. In the case of a ball moving across the screen, there are two components to the movement: a change in the X coordinate and a change in the Y coordinate. Whenever you are dealing with changes there is a special word scientists use – delta, which is represented by the Greek letter delta (∆) and just means “change”. So to describe the path of an object moving in a straight line, all you need is two quantities – ∆X and ∆Y. On each successive frame ∆X and ∆Y are added to the X and Y coordinates to get the new position to plot the object. This defines the angle of movement as shown in Figure 5.2.
Figure 5-2: The movement angle defined by the two values ∆X and ∆Y.
If you want to define the exact angle Θ (angles are always called theta), you can apply the formula shown in Figure 5.2. However, for this project there is no need to work in terms of angles, basically because all reflections are from orthogonal surfaces. This means that you are considering only horizontal or vertical surfaces to reflect from. Take a look at Figure 5.3; here you see a block bouncing off, or being reflected from, a vertical surface. The angle it is reflected at is equal to the incoming angle or incident angle. But the point is that you don’t have to know the actual angle – in fact, you don’t care. All that you need to do is reverse the sign of ∆X – that is, make it negative. The same goes if the block is approaching the reflecting surface from the other direction. ∆X will be negative in that case, and all you need to do is to reverse the sign. If you make a negative number negative, you end up with a positive.
Figure 5-3: The reflection from a vertical surface by negating ∆X.
I think you can see that exactly the same applies for reflections off a horizontal surface – only this time it is ∆Y that is negated. So when the time comes to bounce off a surface all that you need to do is to invert the appropriate delta value.
Detecting Collisions
Now all you need to get some bouncy action is to work out when things collide. This is easy for humans to spot but can be a bit tricky for a computer. Every object you draw on the screen will be put there by specifying one or more coordinates. However, that describes only one point on the object. Take a rectangle, for example: You specify the X and Y coordinates of the top-left corner and the width and height, as well as the line thickness. When you draw a line, you specify the X and Y coordinates of the start of the line, and the coordinates of where you want it to finish, along with the line thickness. There are two ways of detecting if these overlap: The first is to look at what is already drawn on the screen to see if anything is in the place you are going to draw the next block. The second, and the one you shall use here, is to compute the extent of the objects and see if there is an overlap. Figure 5.4 shows this calculation for a rectangle and a line. Note the difference in how the line thickness is handled. For a line the thickness is built up by drawing in pixels either side of the required line, whereas for a rectangle the thickness is built up by drawing in pixels inside the rectangle. This is the way that Pygame handles the graphic coordinates; other systems such as Apple’s QuickTime take a different approach, with the defined line being in between the pixels and any line thickness being below and to the right of the line. There are many ways to implement graphics drawing routines.
Figure 5-4: Calculating limits for a collision.
You will see from Figure 5.4 that the limits set depend on the direction of approach the object has, so when going from left to right it is different than going from right to left.
The Test Bounce
Armed with this information, you can now get some coding done and set the framework for your game. Type in the code in Listing 5.1, and it will bounce a square around a window at high speed.
Listing 5.1 Bounce Test 1
#!/usr/bin/env python
"""
Bounce
A Raspberry Pi test
"""
import time # for delays
import os, pygame, sys
pygame.init() # initialise graphics interface
os.environ['SDL_VIDEO_WINDOW_POS'] = 'center'
pygame.display.set_caption("Bounce")
screenWidth = 400
screenHeight =400
screen = pygame.display.set_mode([screenWidth,
screenHeight],0,32)
background = pygame.Surface((screenWidth,screenHeight))
# define the colours to use for the user interface
cBackground =(255,255,255)
cBlock = (0,0,0)
background.fill(cBackground) # make background colour
dx = 5
dy = 10
def main():
X = screenWidth / 2
Y = screenHeight /2
screen.blit(background,[0,0])
while True :
checkForEvent()
#time.sleep(0.05)
drawScreen(X,Y)
X += dx
Y += dy
checkBounds(X,Y)
def checkBounds(px,py):
global dx,dy
if px > screenWidth-10 or px <0:
dx = -dx
if py > screenHeight-10 or py < 0:
dy = - dy
def drawScreen(px,py) : # draw to the screen
screen.blit(background,[0,0]) # set background colour
pygame.draw.rect(screen,cBlock, (px, py, 10 ,10), 0)
pygame.display.update()
def terminate(): # close down the program
print ("Closing down please wait")
pygame.quit() # close pygame
sys.exit()
def checkForEvent(): # see if you need to quit
event = pygame.event.poll()
if event.type == pygame.QUIT :
terminate()
if event.type == pygame.KEYDOWN and
event.key == pygame.K_ESCAPE :
terminate()
if __name__ == '__main__':
main()
You will see that the direction of the block is defined by the two variables dx and dy (for delta X and delta Y) and is fixed at five pixels in the X direction and ten in the Y direction per frame. The square is drawn in solid black on a white background.
The main function first of all defines the square in the center of the screen, and goes into an endless loop in which it checks the keyboard for a quit and draws the screen. Then it updates the square’s position and checks for any collision between the square and the sides of the window. Note as the sides of the window are not a drawn line, there is no need to account for the line thickness here.
In the main loop there is a sleep command, but this is commented out with a #, which means that it is ignored. Try removing the # and see how much slower the block will move around. Also experiment with changing the values of dx and dy and see how this changes both the speed and the angle of trajectory of the block. Finally, have a play with adjusting the screenWidth and screenHeight variables.
Improving the Ping Game
Well, Listing 5.1 is a good start, but a number of things are not quite right in it. First of all, the object bouncing about is a square, and although that is true to the original game, you can do much better these days. So let’s make the object a circle instead of a square. This will change the collision calculations because a circle is drawn by defining its center point. Also, if you rely on simply detecting when the position of the ball is greater than the collision limits you calculated, you will most times draw the ball actually overlapping the line. This will look like the ball has penetrated into the reflecting object. It would be much better if, when you detect a collision, the position of the ball is adjusted to sit on the colliding surface. This is shown in Figure 5.5; the ball’s position is shown in successive frames, and when the collision occurs the position of the ball would normally be behind the reflecting object. This does not look good, so it is hauled back to sit on the line. This has the effect of slowing down the ball at the point of collision, but you do not perceive this as you are expecting a collision to occur at the surface and the brain’s expectation overrules anything else.
Figure 5-5: Snapping the ball to the colliding object.
To make the bouncing a bit more interesting, a sound should be produced at the moment of impact, something that did not occur in the first incarnations of the game. Finally, the code makes the bounding rectangle (the rectangle containing the ball) adjustable from the cursor keys of the keyboard. All this is added to produce the code in Listing 5.2.
Listing 5.2 The Improved Bounce Code
#!/usr/bin/env python
"""
Bounce with sound
A Raspberry Pi test
"""
import time # for delays
import os, pygame, sys
pygame.init() # initialise graphics interface
pygame.mixer.quit()
pygame.mixer.init(frequency=22050, size=-16,
channels=2, buffer=512)
bounceSound = pygame.mixer.Sound("sounds/bounce.ogg")
os.environ['SDL_VIDEO_WINDOW_POS'] = 'center'
pygame.display.set_caption("Bounce2")
screenWidth = 400
screenHeight =400
screen = pygame.display.set_mode([screenWidth,
screenHeight],0,32)
background = pygame.Surface((screenWidth,screenHeight))
# define the colours to use for the user interface
cBackground =(255,255,255)
cBlock = (0,0,0)
background.fill(cBackground) # make background colour
box = [screenWidth-80,screenHeight-80]
delta = [5,10]
hw = screenWidth / 2
hh = screenHeight /2
position = [hw,hh] # position of the ball
limit = [0, 0, 0, 0] #wall limits
ballRad = 8 # size of the ball
def main():
global position
updateBox(0,0) # set up wall limits
screen.blit(background,[0,0])
while True :
checkForEvent()
time.sleep(0.05)
drawScreen(position)
position = moveBall(position)
def moveBall(p):
global delta
p[0] += delta[0]
p[1] += delta[1]
if p[0] <= limit[0] :
bounceSound.play()
delta[0] = -delta[0]
p[0] = limit[0]
if p[0] >= limit[1] :
bounceSound.play()
delta[0] = -delta[0]
p[0] = limit[1]
if p[1] <= limit[2] :
bounceSound.play()
delta[1] = - delta[1]
p[1] = limit[2]
if p[1] >= limit[3] :
bounceSound.play()
delta[1] = - delta[1]
p[1] = limit[3]
return p
def drawScreen(p) : # draw to the screen
screen.blit(background,[0,0]) # set background colour
pygame.draw.rect(screen,(255,0,0), (hw -
(box[0]/2),hh - (box[1]/2),box[0],box[1]), 2)
pygame.draw.circle(screen,cBlock, (p[0], p[1]),ballRad, 2)
pygame.display.update()
def updateBox(d,amount):
global box, limit
box[d] += amount
limit[0] = hw - (box[0]/2) +ballRad #leftLimit
limit[1] = hw + (box[0]/2) -ballRad #rightLimit
limit[2] = hh - (box[1]/2) + ballRad #topLimit
limit[3] = (hh + (box[1]/2))-ballRad #bottomLimit
def terminate(): # close down the program
print ("Closing down please wait")
pygame.quit() # close pygame
sys.exit()
def checkForEvent(): # see if you need to quit
event = pygame.event.poll()
if event.type == pygame.QUIT :
terminate()
if event.type == pygame.KEYDOWN :
if event.key == pygame.K_ESCAPE :
terminate()
if event.key == pygame.K_DOWN :
# expand / contract the box
updateBox(1,-2)
if event.key == pygame.K_UP :
updateBox(1,2)
if event.key == pygame.K_LEFT :
updateBox(0,-2)
if event.key == pygame.K_RIGHT :
updateBox(0,2)
if __name__ == '__main__':
main()
You can see here that not only have the functions’ names changed but the variables defining the movement and some other parameters also have changed from being separate named variables to being items in a list. This makes it easer to pass them into and out of functions. A function is restricted to returning only one item, but by packing lots of variables into a list you get to return many under the one name. Note that in this code checking for a collision involves only checking the position of the ball against predefined limits. This eliminates the need to do calculations such as adjusting for the ball radius every time you want to do a collision check. Precalculating these limits helps to speed up the overall program and gets the ball moving faster and smoother.
The sound is handled by loading in a bounce sound. Note here though that the initialisation of the sound does not use the default values but uses
pygame.mixer.init(frequency=22050, size=-16,
channels=2, buffer=512)
This ensures that the sound is produced as soon as possible after the software command is given. Without this there is an unacceptable delay, which is sometimes called latency. You need to create a sound file in the .ogg format with a sample rate of 22.05 KHz; call it bounce.ogg and place it inside a folder called sounds. This sounds folder should be placed in the same folder as the Python code. A short blip sound will do for now. If you like, you can look inside the Python games folder at some of the sound files that are there; you can copy and rename beep.ogg, for example, here. I use the application Audacity for creating sound files. It is free, and there are versions that run on Windows machines or Macs.
A Single-Player Game
The next step produces a usable single-player game, complete with scoring. The point of a single-player game is to see how many times you can return the ball from a perfect computer player. You get three balls, and the number of returns you make from those balls is your score. If it is the highest one of the session, it is transferred to the left number, and that is the one to beat next time. There are a few steps in going from the simple bounce around a box to the single player, but space in the book restricts me from going through all the intermediate stages.
The chief change is that you no longer need to look at just a single value to detect a collision because a bat also has a limited length; you need to see if the ball is sailing over the top or underneath it. The top and bottom collisions are still the same, though. There are a few shortcuts I have taken to simplify the detection of a collision with a ball as opposed to a rectangle. Figure 5.6 shows the dimensions of the bat and ball. For the detection of a collision, you can consider just the bounding box of the circular ball and not worry about the actual geometry of the circle.
Figure 5-6: Measurements for the bat and ball.
Now when you consider bat-and-ball collisions you must look at both the X and Y elements of the coordinates of the bat and ball. Figure 5.7 summarises this by showing both sets of conditions that have to be met. This is complicated by the fact that whereas the X coordinate of the bat is fixed, the Y coordinate is going to change with input from the player. Note here that unlike conventional coordinates, Pygame has the Y-axis numbers increase as you move down the screen.
Figure 5-7: The collision geometry of the bat and ball.
The overall structure of the game also needs to be defined. Previously there was just a ball bouncing around the screen. Now you have to have more code to define the various stages of the game. Each game consists of three balls, and a rally is the time one ball spends in play. The score advances each time you hit the ball, and a ball is lost when it collides against the far-right bounding box. After each ball is lost, the automatic left-hand player serves a new ball at a randomly chosen speed and direction. This structure needs to be imposed on the simple game mechanics. Let’s see how all this comes together in Listing 5.3.
Listing 5.3 The Single-Player Ping Game
#!/usr/bin/env python
"""
Ping - Tennis game one player
with score
For the Raspberry Pi
"""
import time # for delays
import os, pygame, sys
import random
pygame.init() # initialise graphics interface
pygame.mixer.quit()
pygame.mixer.init(frequency=22050, size=-16,
channels=2, buffer=512)
bounceSound = pygame.mixer.Sound("sounds/bounce.ogg")
os.environ['SDL_VIDEO_WINDOW_POS'] = 'center'
pygame.display.set_caption("Ping 1 player")
screenWidth = 500
screenHeight =300
screen = pygame.display.set_mode([screenWidth,
screenHeight],0,32)
background = pygame.Surface((screenWidth,screenHeight))
textSize = 36
scoreSurface = pygame.Surface((textSize,textSize))
font = pygame.font.Font(None, textSize)
pygame.event.set_allowed(None)
pygame.event.set_allowed([pygame.KEYDOWN,pygame.QUIT])
# define the colours to use for the user interface
cBackground =(255,255,255)
cBall = (0,0,0)
background.fill(cBackground) # make background colour
cText = (0,0,0)
box = [screenWidth-10,screenHeight-10]
deltaChoice = [ [15,1], [14,1], [13,1], [12,1],
[11,1], [10,1], [15,2], [14,2], [13,2], [12,2], [11,2], [10,2] ]
maxDelta = 11
delta = deltaChoice[random.randint(0,maxDelta)]
hw = screenWidth / 2
hh = screenHeight /2
ballPosition = [-hw,hh] # position of the ball off-screen
batMargin = 30 # how far in from the wall is the bat
batHeight = 24
batThick = 6
batInc = 20 # bat / key movement
batX = [batMargin, screenWidth - batMargin]
batY = [hh, hh] # initial bat position
limit = [0, 0, 0, 0, 0, 0] #wall limits & bat limits
ballRad = 8 # size of the ball
rally = True
pause = True
score = 0
best = 0 # high score
balls = 3 # number of balls in a turn
ballsLeft = balls
def main():
global ballPosition, rally, balls, pause, score, best
updateBox(0,0) # set up wall limits
updateScore()
screen.blit(background,[0,0])
while True :
ballsLeft = balls
if score > best:
best = score
score = 0
updateScore()
while ballsLeft > 0:
ballPosition = waitForServe(ballPosition)
while rally :
checkForEvent()
time.sleep(0.05)
drawScreen(ballPosition)
ballPosition = moveBall(ballPosition)
ballsLeft -= 1
print "press space for",balls,"more balls"
pause = True
while pause :
checkForEvent()
def waitForServe(p) :
global batY, rally, delta
computerBatDelta = 2
serveTime = time.time() + 2.0 #automatically serve again
while time.time() < serveTime :
checkForEvent()
drawScreen(p)
batY[0] += computerBatDelta
# move bat up and down when waiting
if batY[0] > limit[3] or batY[0] < limit[2]:
computerBatDelta = -computerBatDelta
p[0] = batX[0]
p[1] = batY[0]
delta = deltaChoice[random.randint(0,maxDelta)]
rally = True
return p
def moveBall(p):
global delta, batY, rally, score, batThick
p[0] += delta[0]
p[1] += delta[1]
# now test to any interaction
if p[1] <= limit[2] : # test top
bounceSound.play()
delta[1] = - delta[1]
p[1] = limit[2]
elif p[1] >= limit[3] : # test bottom
bounceSound.play()
delta[1] = - delta[1]
p[1] = limit[3]
elif p[0] <= limit[0] : # test missed ball player 1
p[0] = limit[0]
rally = False
print " missed ball"
elif p[0] >= limit[1] : # test missed ball player 2
p[0] = limit[1]
rally = False
print " missed ball"
# now test left bat limit
elif p[0] <= limit[4] and p[1] >= batY[0] - ballRad
and p[1] <= batY[0] + ballRad + batHeight:
bounceSound.play()
p[0] = limit[4]
delta[0] = random.randint(5,15)
if random.randint(1,4) > 2 :
# random change in y direction
delta[1] = 16 - delta[0]
else :
delta[1] = -(16 - delta[0])
# Test right bat collision
elif p[0] >= limit[5] and p[1] >= batY[1] - ballRad
and p[1] <= batY[1] + ballRad + batHeight:
bounceSound.play()
delta[0] = - delta[0]
p[0] = limit[5]
score+= 1
updateScore()
batY[0] = p[1] - ballRad # make auto opponent follow bat
#batY[1] = p[1]- ballRad # temporary test for auto player
return p
def updateScore():
global score, best, scoreRect, scoreSurface
scoreSurface = font.render(str(best)+" : "+str(score),
True, cText, cBackground)
scoreRect = scoreSurface.get_rect()
scoreRect.centerx = hw
scoreRect.centery = 24
def drawScreen(p) : # draw to the screen
global rally
screen.blit(background,[0,0]) # set background colour
pygame.draw.rect(screen,(255,0,0), (hw - (box[0]/2),
hh - (box[1]/2),box[0],box[1]), 4)
pygame.draw.line(screen,(0,255,0), (batX[0], batY[0]),
(batX[0], batY[0]+batHeight),batThick)
pygame.draw.line(screen,(0,255,0), (batX[1], batY[1]),
(batX[1], batY[1]+batHeight),batThick)
screen.blit(scoreSurface, scoreRect)
if rally :
pygame.draw.circle(screen,cBall, (p[0], p[1]),
ballRad, 2)
pygame.display.update()
def updateBox(d,amount):
global box, limit
box[d] += amount
limit[0] = hw - (box[0]/2) +ballRad #leftLimit
limit[1] = hw + (box[0]/2) -ballRad #rightLimit
limit[2] = hh - (box[1]/2) + ballRad #topLimit
limit[3] = (hh + (box[1]/2))-ballRad #bottomLimit
limit[4] = batX[0] + ballRad + batThick/2
#x Limit ball approaching from the right
limit[5] = batX[1] - ballRad - batThick/2
#x Limit ball approaching from the left
def terminate(): # close down the program
print ("Closing down please wait")
pygame.quit() # close pygame
sys.exit()
def checkForEvent(): # see if you need to quit
global batY, rally, pause
event = pygame.event.poll()
if event.type == pygame.QUIT :
terminate()
if event.type == pygame.KEYDOWN :
if event.key == pygame.K_ESCAPE :
terminate()
if event.key == pygame.K_DOWN :
# expand / contract the box
updateBox(1,-2)
if event.key == pygame.K_UP :
updateBox(1,2)
if event.key == pygame.K_LEFT :
updateBox(0,-2)
if event.key == pygame.K_RIGHT :
updateBox(0,2)
if event.key == pygame.K_s :
rally = True
if event.key == pygame.K_SPACE :
pause = False
if event.key == pygame.K_PAGEDOWN :
if batY[1] < screenHeight - batInc :
batY[1] += batInc
if event.key == pygame.K_PAGEUP :
if batY[1] > batInc :
batY[1] -= batInc
if __name__ == '__main__':
main()
There are a lot more changes this time, but hopefully this code is recognisable in structure from Listing 5.2. The various phases of the program are defined by the Boolean variables rally and pause, which can be changed from the keyboard, in the checkForEvent() function, along with the Y position of the bat. I chose to use the Page Up and Page Down keys for the movement of the bat as they are on the far right of my keyboard.
The updateBox() function has two more limits added to it, that of the ball approaching the bat from the left or right. The drawScreen() function now draws the two bats and only draws the ball if the rally variable is true. It also draws a surface bitmap containing the score. updateScore()is a new function that, as its name implies, changes the surface containing the score and positions the score rectangle. This works just like you saw in Chapter 4, “Here’s the News”, with the teleprompter.
The moveBall() function has grown some. The list of if statements is now replaced by a string of elif clauses based on the original if statement. This means that only one of the sections of code will be executed in each pass. This is because the positional condition for a ball to be off the right side of the screen would also trigger the right ball bat collision, so you must carefully test for collisions in the correct order to avoid a misidentification of what is colliding with what. The function takes the first case that is true from the following list:
• A collision with the top of the box
• A collision with the bottom of the box
• A collision with the left edge of the box (missed ball, computer player)
• A collision with the right edge of the box (missed ball, human player)
• A collision with the left bat
• A collision with the right bat
In the event of any of those conditions being met, the code will take the appropriate action. In the case of the top or bottom walls, the ball’s direction will change appropriately as you have seen before. If the active player hits the ball, the score will be incremented along with an elastic collision. If the computer’s bat hits it, a new value of the two delta variables will be chosen from the list defined at the start of the program. All collisions also cause a bounce sound to be played. Basically, this function controls the action on the screen. The last line keeps the computer’s bat in line with the ball. You can also make the player’s bat follow the ball for testing, but then the game element disappears altogether.
The waitForServe() function is used to restart the rally when the ball has been missed. Here the computer’s bat moves up and down the screen for two seconds before being served with a new random set of delta values. In the two-player game described in the next section of this chapter, this will be expanded.
That leaves us with main() as the only function you have not looked at. As usual, this function orchestrates the whole program. After a bit of initialisation it enters a while True endless loop, which initialises the number of balls and score for a game, before entering a while loop that basically counts down the number of balls in a game. Finally, the third while loop controls the rally and keeps the screen action going until a ball is missed. When it is moveBall() sets the rally variable to False, and that loop terminates. When all the balls have been played the final whileloop in this function just checks for any events, one of which could be the spacebar, which sets the pause variable to False and allows another game to be played.
A Two-Player Game
It doesn’t take much to turn this into a two-player game, but there is a subtle change in what the object of the game is, and that has a few ramifications in the code. In the one-player game the point was to simply return the ball to the perfect opponent. So there was no need to do anything about altering the flight of the ball when the player hit it back. In a two-player game, however, you not only have to return the ball, but you also have the opportunity of changing the flight of the ball to make it more difficult for your opponent to return. In a real game of Ping-Pong, this is done by adding top spin to the ball. In your Ping game you can simulate the same sort of effect by having more delta movement in the Y direction, the further from the center of the bat the collision occurs. This involves a further calculation once the collision has been detected. The other thing that needs changing is the method of serving. The serve goes to the player who has just lost the point, and there needs to be a bit of an element of surprise for the opposing player. Therefore, the serve can be played early by pressing a key, but if the player waits too long, the serve will happen automatically. All these changes can be seen in Listing 5.4, the two-player game.
Listing 5.4 The Two-Player Game of Ping
#!/usr/bin/env python
"""
Ping - Tennis game two player
with score
For the Raspberry Pi
"""
import time # for delays
import os, pygame, sys
import random
pygame.init() # initialise graphics interface
pygame.mixer.quit()
pygame.mixer.init(frequency=22050, size=-16,
channels=2, buffer=512)
bounceSound = pygame.mixer.Sound("sounds/bounce.ogg")
outSound = pygame.mixer.Sound("sounds/out.ogg")
p0hitSound = pygame.mixer.Sound("sounds/hit0.ogg")
p1hitSound = pygame.mixer.Sound("sounds/hit1.ogg")
os.environ['SDL_VIDEO_WINDOW_POS'] = 'center'
pygame.display.set_caption("Ping 2 players")
screenWidth = 500
screenHeight =300
screen = pygame.display.set_mode([screenWidth,
screenHeight],0,32)
background = pygame.Surface((screenWidth,screenHeight))
textSize = 36
scoreSurface = pygame.Surface((textSize,textSize))
font = pygame.font.Font(None, textSize)
pygame.event.set_allowed(None)
pygame.event.set_allowed([pygame.KEYDOWN,pygame.QUIT])
# define the colours to use for the user interface
cBackground =(255,255,255)
cBall = (0,0,0)
background.fill(cBackground) # make background colour
cText = (0,0,0)
box = [screenWidth-10,screenHeight-10]
deltaChoice = [ [15,1], [14,1], [13,1], [12,1], [11,1],
[10,1], [15,2], [14,2], [13,2], [12,2], [11,2], [10,2] ]
maxDelta = 11
delta = deltaChoice[random.randint(0,maxDelta)]
hw = screenWidth / 2
hh = screenHeight /2
ballPosition = [-hw,hh] # position of the ball off-screen
batMargin = 30 # how far in from the wall is the bat
batHeight = 24
batThick = 6
batInc = 20 # bat / key movement
batX = [batMargin, screenWidth - batMargin]
batY = [hh, hh] # initial bat position
limit = [0, 0, 0, 0, 0, 0] #wall limits & bat limits
ballRad = 8 # size of the ball
rally = True
pause = True
server = 0 # player to serve
serve =[False,False]
score = [0,0] # players score
balls = 5 # number of balls in a turn
ballsLeft = balls
batMiddle = (ballRad - (batHeight + ballRad))/2
def main():
global ballPosition, rally, balls, pause, score, server
updateBox(0,0) # set up wall limits
updateScore()
screen.blit(background,[0,0])
while True :
ballsLeft = balls
score = [0,0]
updateScore()
while ballsLeft > 0:
ballPosition = waitForServe(ballPosition,server)
while rally :
checkForEvent()
time.sleep(0.05)
drawScreen(ballPosition)
ballPosition = moveBall(ballPosition)
ballsLeft -= 1
print "press space for",balls,"more balls"
pause = True
while pause :
checkForEvent()
def waitForServe(p,player) :
global batY, rally, delta, serve
computerBatDelta = 2
serve[player] = False
serveTime = time.time() + 4.0 #automatically serve again
while time.time() < serveTime and serve[player] == False:
checkForEvent()
drawScreen(p)
batY[player] += computerBatDelta
# move bat up and down when waiting
if batY[player] > limit[3] or batY[player] <
limit[2]:
computerBatDelta = -computerBatDelta
p[0] = batX[player]
p[1] = batY[player]
delta = deltaChoice[random.randint(0,maxDelta)]
if player == 1 :
delta[0] = -delta[0]
p1hitSound.play()
else:
p0hitSound.play()
rally = True
return p
def moveBall(p):
global delta, batY, rally, score, batThick, server
p[0] += delta[0]
p[1] += delta[1]
# now test to any interaction
if p[1] <= limit[2] : # test top
bounceSound.play()
delta[1] = - delta[1]
p[1] = limit[2]
elif p[1] >= limit[3] : # test bottom
bounceSound.play()
delta[1] = - delta[1]
p[1] = limit[3]
elif p[0] <= limit[0] : # test missed ball left player
outSound.play()
rally = False
score[1] += 1
server = 0
p[0] = hw
updateScore()
elif p[0] >= limit[1] : # test missed ball right player
outSound.play()
rally = False
score[0] += 1
server = 1
p[0] = hw
updateScore()
# Test left bat collision
elif p[0] < limit[4] and p[1] >= batY[0] - ballRad
and p[1] <= batY[0] + ballRad + batHeight:
batBounce(p[1],batY[0],0)
p[0] = limit[4]
# Test right bat collision
elif p[0] >= limit[5] and p[1] >= batY[1] - ballRad
and p[1] <= batY[1] + ballRad + batHeight:
batBounce(p[1],batY[1],1)
p[0] = limit[5]
return p
def batBounce(ball, bat, player) :
global delta
point = bat - ball
delta[1] = int(-14.0 * ((point * 0.05) + 0.6))
delta[0] = 16 - abs(delta[1])
if player == 1 :
delta[0] = -delta[0]
p1hitSound.play()
else:
p0hitSound.play()
def updateScore():
global scoreRect, scoreSurface
scoreSurface = font.render(str(score[0])+" : "
+str(score[1]), True, cText, cBackground)
scoreRect = scoreSurface.get_rect()
scoreRect.centerx = hw
scoreRect.centery = 24
drawScreen(ballPosition)
def drawScreen(p) : # draw to the screen
global rally
screen.blit(background,[0,0]) # set background colour
pygame.draw.rect(screen,(255,0,0), (hw - (box[0]/2),
hh - (box[1]/2),box[0],box[1]), 4)
pygame.draw.line(screen,(0,255,0), (batX[0], batY[0]),
(batX[0], batY[0]+batHeight),batThick)
pygame.draw.line(screen,(0,255,0), (batX[1], batY[1]),
(batX[1], batY[1]+batHeight),batThick)
screen.blit(scoreSurface, scoreRect)
if rally :
pygame.draw.circle(screen,cBall, (p[0], p[1]),
ballRad, 2)
pygame.display.update()
def updateBox(d,amount):
global box, limit
box[d] += amount
limit[0] = hw - (box[0]/2) + ballRad #leftLimit
limit[1] = hw + (box[0]/2) -ballRad #rightLimit
limit[2] = hh - (box[1]/2) + ballRad #topLimit
limit[3] = (hh + (box[1]/2))-ballRad #bottomLimit
limit[4] = batX[0] + ballRad + batThick/2
#x Limit ball approaching from the right
limit[5] = batX[1] - ballRad - batThick/2
#x Limit ball approaching from the left
def terminate(): # close down the program
print ("Closing down please wait")
pygame.quit() # close pygame
sys.exit()
def checkForEvent(): # see if you need to quit
global batY, rally, pause, serve
event = pygame.event.poll()
if event.type == pygame.QUIT :
terminate()
if event.type == pygame.KEYDOWN :
if event.key == pygame.K_ESCAPE :
terminate()
if event.key == pygame.K_DOWN :
# expand / contract the box
updateBox(1,-2)
if event.key == pygame.K_UP :
updateBox(1,2)
if event.key == pygame.K_LEFT :
updateBox(0,-2)
if event.key == pygame.K_RIGHT :
updateBox(0,2)
if event.key == pygame.K_s :
rally = True
if event.key == pygame.K_SPACE :
pause = False
if event.key == pygame.K_q :
serve[0] = True
if event.key == pygame.K_HOME :
serve[1] = True
if event.key == pygame.K_PAGEDOWN :
if batY[1] < screenHeight - batInc :
batY[1] += batInc
if event.key == pygame.K_PAGEUP :
if batY[1] > batInc :
batY[1] -= batInc
if event.key == pygame.K_z :
if batY[0] < screenHeight - batInc :
batY[0] += batInc
if event.key == pygame.K_a :
if batY[0] > batInc :
batY[0] -= batInc
if __name__ == '__main__':
main()
Here more keyboard keys come into play. The Home key has been added for the right player’s serve key, along with the bat movement keys of Page Up and Page Down. The A, Q and Z keys perform the same functions for the left player.
The updateBox() and drawScreen() functions are unchanged, but the updateScore() function has been altered to accommodate the score of both players. The moveBall() function has been enhanced to include top and bottom limits for the left player’s bat, and for both players a new function batBounce() is called when a collision is detected. More on this shortly.
The waitForServe() function now includes code to allow either player to serve the ball, and there is a longer time before an automatic serve, along with a separate serve sound for each player. There are also a few changes to global variables, adding one to indicate who is serving.
So back to the batBounce() function. This performs two functions – the first to determine the ball’s return velocity vector and the second to play a sound dependent on the player striking the ball. As the code is written this is the same as the serve sound, but it could be changed.
The major new feature in this function is in determining how far along the bat the collision occurred. This basically is a floating-point number between -1 and +1, with zero being returned if it is plumb center. After you have gotten this fraction it needs to be multiplied by the number that corresponds to the maximum Y velocity you want if the ball just grazes the top or bottom of the bat. In the code I have used a value of 14. Then to keep the overall speed of the ball constant, the X velocity is the maximum velocity you want it to be if there is a dead center hit minus any Y component of the speed. This is all done in the two lines
delta[1] = int(-14.0 * ((point * 0.05) + 0.6))
delta[0] = 16 - abs(delta[1])
Note how the floating-point calculation is converted to an integer before assigning it to the delta global variable. This keeps the delta values as integers, as operations on integers are much faster to perform for the computer.
The final bit of fun is the sound effects. What I did was scour the Internet looking for tennis sounds and found some interesting examples of famous tennis players’ grunts as they hit the ball. Using Audacity, I clipped out the short hit/grunt noise and saved it as an .ogg format file. I found a line judge’s “out” call, along with some real bounce sounds. These sounds enhance the playing of the game tremendously, especially because there is also some crowd noise. However, for that retro 70s sound, you can’t beat simple tonal bleeps; the choice is yours.
Over to You
Well, that is the end of my bit, but it is not the end of this project. There are many changes, enhancements and improvements you can make. This applies to both the single- and two-player version of the game. The simplest involves changing the colours: You might want green grass, for example, or filled coloured balls or bats. You could even draw a picture with a painting package or photo-editing package to act as the background, or you might want to include a net or white lines on the court. You could have different sounds for a serve and a return, which is easy because they are called up in different places in the code. You can upgrade the sounds in the single-player game to match what you did in the two-player game.
You might want to change things like the number of balls in a game or even the scoring to make it more like a real game of tennis, with a player winning a game only when he or she is two points ahead. You can even use the real tennis terms of advantage and deuce in the score box.
I have deliberately left in the code that alters the bounding box from the cursor keys. How about a variation of the game that allows the serving player to alter the size of the court as the serve takes place? Or how about a court that automatically shrinks as the rally gets longer? Basically, the whole game is one of anticipating where the ball will end up, and by changing the court size only slightly, you can make it more difficult to judge as you don’t have any previous experience to go on.
It is only a small step from this game to making a “knock the bricks out of a wall” game with all the variants that can bring. It’s within your grasp to do this now if you understand the basics of what was done here.