Let’s make our Pong clone playable.
Make sure you’ve read Part 1 before reading this. Below is the full source of Part 2, but I’ll only be explaining the new additions.
Please read my disclaimer if you haven’t already. Now, onto the code:
import os
import random
import pyglet
from pyglet.window import key
class Sprite(object):
def __init__(self, x, y):
if self.__class__.__name__ == 'Sprite':
raise NotImplementedError('Don\'t instantiate sprite base class')
self.x, self.y = x, y
self.dx, self.dy = 0, 0
self.image = images[self.__class__.__name__]
self.w, self.h = self.image.width, self.image.height
def draw(self):
self.image.blit(self.x, self.y)
def update(self):
self.x, self.y = self.x + self.dx, self.y + self.dy
self.draw()
class Background(Sprite):
pass
class Paddle(Sprite):
def start_moving(self, dy):
self.dy = dy
def stop_moving(self):
self.dy = 0
def ai(self, ball):
if ball.y > self.y + self.h:
self.start_moving(speed)
elif ball.y < self.y + self.h:
self.start_moving(-speed)
def update(self):
self.y = self.y + self.dy
if self.y + self.h >= window.height:
self.y = window.height - self.h
if self.y <= 0:
self.y = 0
self.draw()
class Ball(Sprite):
def __init__(self, x, y):
Sprite.__init__(self, x, y)
self.dx = random.randint(2, 6)
self.dy = random.randint(2, 6)
def update(self):
self.x, self.y = self.x + self.dx, self.y + self.dy
if collide(self, player) or collide(self, enemy):
self.dx = -self.dx + random.randint(0, 4)
self.dy = -self.dy + random.randint(0, 4)
if self.dx > 6:
self.dx = 6
if self.dx < -6:
self.dx = -6
if self.dy > 6:
self.dx > 6
if self.y + self.h >= window.height or self.y <= 0:
self.dy = -self.dy
self.draw()
def load_images():
images = {}
for filename in os.listdir('images'):
key = filename.split('.')[0]
value = pyglet.image.load(os.sep.join(['images', filename]))
images[key] = value
return images
def collide(a, b):
if a.y + a.h < b.y:
return False
if a.y > b.y + b.h:
return False
if a.x + a.w < b.x:
return False
if a.x > b.x + b.w:
return False
return True
def update(dt):
background.update()
ball.update()
player.update()
enemy.ai(ball)
enemy.update()
images = load_images()
background = Background(0, 0)
player = Paddle(100, 200)
enemy = Paddle(530, 200)
ball = Ball(120, 214)
speed = 4
move_dirs = {key.UP: speed, key.DOWN: -speed}
window = pyglet.window.Window()
@window.event
def on_key_press(symbol, modifiers):
if symbol in move_dirs.keys():
player.start_moving(move_dirs[symbol])
@window.event
def on_key_release(symbol, modifiers):
if symbol in move_dirs.keys() and player.dy == move_dirs[symbol]:
player.stop_moving()
pyglet.clock.schedule(update)
pyglet.app.run()
You can download a commented version here or a zip of the whole project so far here.
Won’t you take a walk with me through the new code in Part 2?
self.dx, self.dy = 0, 0
To make our Sprites move, we are adding velocity variables to our base Sprite’s init method. dx represents the sprite’s speed along the x axis, and dy represents the sprite’s speed along the y axis. Below is the code that makes the sprite move:
self.x, self.y = self.x + self.dx, self.y + self.dy
This line is added to the base Sprite’s update method before draw is called. The Sprite’s position on each axis increased by its dx and dy values. When draw is called, the Sprite will appear in its new position.
def start_moving(self, dy):
self.dy = dy
def stop_moving(self):
self.dy = 0
These two movement methods are being added to the Paddle class to enable movement along the y axis. Paddles only need to move up and down, so we don’t have to worry about dx.
def ai(self, ball):
if ball.y > self.y + self.h:
self.start_moving(speed)
elif ball.y < self.y + self.h:
self.start_moving(-speed)
Once we add this AI method to the Paddle class, we will eventually call it on update to make our opponent come alive. It’s actually a pretty horrible AI, and I am surprised it works as well as it does. It just follows the ball. If you’re wondering about the speed variable, we’ll get to that later.
def update(self):
self.y = self.y + self.dy
if self.y + self.h >= window.height:
self.y = window.height - self.h
if self.y <= 0:
self.y = 0
self.draw()
Above is Paddle’s new update method. After the Paddle is moved along the y axis, we need to make sure it hasn’t gone out of the screen. This is done by testing it’s y position against the window’s height and 0 for the top and bottom edges, respectively. You’ll notice that when checking for the top bound, we have to add the Paddle’s height to it’s y position. This is because the y position is at the bottom of the Sprite, and we want to make sure that the top doesn’t go out of bounds either. Finally, we draw the Sprite just like in the base class.
def __init__(self, x, y):
Sprite.__init__(self, x, y)
self.dx = random.randint(2, 6)
self.dy = random.randint(2, 6)
Above we are overriding Ball’s init method to make each new ball float toward the enemy Paddle at a random velocity.
def update(self):
self.x, self.y = self.x + self.dx, self.y + self.dy
if collide(self, player) or collide(self, enemy):
self.dx = -self.dx + random.randint(0, 4)
self.dy = -self.dy + random.randint(0, 4)
if self.dx > 6:
self.dx = 6
if self.dx < -6:
self.dx = -6
if self.dy > 6:
self.dx > 6
if self.y + self.h >= window.height or self.y <= 0:
self.dy = -self.dy
self.draw()
Ball’s update needs to change a lot. After moving according to its dx and dy, we check if the Ball is colliding with either Paddle. If a collision is happening, the Ball’s dx and dy are reversed with some random speed added. This creates the reflection or bouncing off effect that happens when the Ball hits a Paddle. We are also bouncing the ball if it passes a y bound.
def collide(a, b):
if a.y + a.h < b.y:
return False
if a.y > b.y + b.h:
return False
if a.x + a.w < b.x:
return False
if a.x > b.x + b.w:
return False
return True
This is a basic rectangle collision function that I use in all of my simple sprite-based games. Pass in two Sprites, and it returns whether or not their rectangles are overlapping (colliding).
def update(dt):
background.update()
ball.update()
player.update()
enemy.ai(ball)
enemy.update()
Our main update function is the same except for the line where we call the enemy Paddle’s ai method.
speed = 4
move_dirs = {key.UP: speed, key.DOWN: -speed}
In the main part of our script, we are defining the speed at which our Paddles will move. With that value, we create a dictionary that maps user input (keyboard keys) to speed values. The dict is a shortcut I like to use to save code later on. You’ll notice that from pyglet.window, we imported key at the top of the script. Below, we use this dictionary to move our Paddles.
@window.event
def on_key_press(symbol, modifiers):
if symbol in move_dirs.keys():
player.start_moving(move_dirs[symbol])
@window.event
def on_key_release(symbol, modifiers):
if symbol in move_dirs.keys() and player.dy == move_dirs[symbol]:
player.stop_moving()
on_key_press and on_key_release are two pyglet.window.Window events. I am decorating their declarations with @window.event, which tells Pyglet to register these events with our instance of pyglet.window.Window (we called it “window”). You can find more about setting event handlers in Pyglet here, and there is a listing of Window’s available events on its page in the Pyglet documentation.
In case you don’t have it already, you can get a copy of Part 2 of this project here.
Run the game, and you can use the UP and DOWN arrows to control your Paddle in a fierce battle against the enemy Paddle. You may notice that collision detection is not perfect and that the game doesn’t keep track of score. Also there are no win/lose conditions or respawning of Balls.
Stay tuned for Part 3.