Following on from yesterday’s introduction to Pong, we’re sharing Boing!, the Python-based tribute to Pong created by Eben Upton exclusively for Code the Classics. Read on to get a detailed look at the code for Boing!
You can find the download link for the Boing! code in the Code the Classics book, available now in a variety of formats. Be sure to stick with today’s blog post until the end, for a special Code the Classics offer.
From Pong to Boing!
To show how a game like Pong can be coded, we’ve created Boing! using Pygame Zero, a beginner-friendly tool for making games in Python. It’s a good starting point for learning how games work – it takes place on a single screen without any scrolling, there are only three moving objects in the game (two bats and a ball), and the artificial intelligence for the computer player can be very simple – or even non-existent, if you’re happy for the game to be multiplayer only. In this case, we have both single-player and two-player modes.
The code can be divided into three parts. First, there’s the initial startup code. We import from other Python modules so we can use their code from ours. Then we check to make sure that the player has sufficiently up-to-date versions of Python and Pygame Zero. We set the
HEIGHT variables, which are used by Pygame Zero when creating the game window. We also create two small helper functions which are used by the code.
The next section is the largest. We create four classes:
Game. The first three classes inherit from Pygame Zero’s Actor class, which amongst other things keeps track of an object’s location in the game world, and takes care of loading and displaying sprites.
Ball define the behaviour of the corresponding objects in the game, while
Impact is used for an animation which is displayed briefly whenever the ball bounces off something. The
Game class’s job is to create and keep track of the key game objects, such as the two bats and the ball.
Further down, we find the
draw functions. Pygame Zero calls these each frame, and aims to maintain a frame rate of 60 frames per second. Gameplay logic, such as updating the position of an object or working out if a point has been scored, should go in
update, while in
draw we tell each of the Actor objects to draw itself, as well as displaying backgrounds, text, and suchlike.
draw functions make use of two global variables:
game. At any given moment, the game can be in one of three states: the main menu, playing the game, or the game-over screen. The
draw functions read the
state variable and run only the code relevant to the current state. So if
state is currently
State.MENU, for example,
update checks to see if the SPACE bar or the up/down arrows are pressed and updates the menu accordingly, and
draw displays the menu on the screen. The technical term for this kind of system is ‘finite state machine’.
The Game class’s job is to create and keep track of the key game objects
game variable references an instance of the
Game class as described above. The
__init__ (constructor) method of
Game optionally receives a parameter named
controls. When we create a new
Game object for the main menu, we don’t provide this parameter and so the game will therefore run in attract mode – in other words, while you’re on the main menu, you’ll see two computer-controlled players playing against each other in the background. When the player chooses to start a new game, we replace the existing
Game instance with a new one, initialising it with information about the controls to be used for each player – if the controls for the second player are not specified, this indicates that the player has chosen a single-player game, so the second will be computer-controlled.
Two types of movement
In Boing!, the
Ball classes inherit from Pygame Zero’s Actor class, which provides a number of ways to specify an object’s position. In this game, as well as games in later chapters, we’re setting positions using the
y attributes, which by default specify where the centre of the sprite will be on the screen. Of course, we can’t just set an object’s position at the start and be done with it – if we want it to move as the game progresses, we need to update its position each frame. In the case of a
Bat, movement is very simple. Each frame, we check to see if the relevant player (which could be a human or the computer) wants to move – if they do, we either subtract or add 4 from the bat’s Y coordinate, depending on whether they want to move up or down. We also ensure that the bat does not go off the top or bottom of the screen. So, not only are we only moving along a single axis, our Y coordinate will always be an integer (i.e. a whole number). For many games, this kind of simple movement is sufficient. Even in games where an object can move along both the X and Y axes, we can often think of the movement along each axis as being separate. For example, in the next chapter’s game, Cavern, the player might be pressing the right arrow key and therefore moving along the X axis at 4 pixels per frame, while also moving along the Y axis at 10 pixels per frame due to gravity. The movement along each axis is independent of the other.
Able to move at any angle, the ball needs to move at the same speed regardless of its direction
Ball, things get a bit more complicated. Not only can it move at any angle, it also needs to move at the same speed regardless of its direction. Imagine the ball moving at one pixel per frame to the right. Now imagine trying to make it move at a 45° angle from that by making it move one pixel right and one pixel up per frame. That’s a longer distance, so it would be moving faster overall. That’s not great, and that’s before we’ve even started to think about movement in any possible direction.
The solution is to make use of vector mathematics and trigonometry. In the context of a 2D game, a vector is simply a pair of numbers: X and Y. There are many ways in which vectors can be used, but most commonly they represent positions or directions.
You’ll notice that the
Ball class has a pair of attributes,
dy. Together these form a vector representing the direction in which the ball is heading. If
dy are 1 and 0.5, then each time the ball moves, it’ll move by one pixel on the X axis and a half a pixel on the Y axis. What does it mean to move half a pixel? When a sprite is drawn, Pygame Zero will round its position to the nearest pixel. So the end result is that our sprite will move down the screen by one pixel every other frame, and one pixel to the right every frame (Figure 1).
We still need to make sure that our object moves at a consistent speed regardless of its direction. What we need to do is ensure that our direction vector is always a ‘unit vector’ – a vector which represents a distance of one (in this case, one means one pixel, but in some games it will represent a different distance, such as one metre). Near the top of the code you’ll notice a function named
normalised. This takes a pair of numbers representing a vector, uses Python’s
math.hypot function to calculate the length of that vector, and then divides both the X and Y components of the vector by that length, resulting in a vector which points in the same direction but has a length of one (Figure 2).
Vector maths is a big field, and we’ve only scratched the surface here. You can find many tutorials online, and we also recommend checking out the Vector2 class in Pygame (the library on top of which Pygame Zero is built).
Update Raspbian to try Boing! and other Code the Classics games on your Raspberry Pi.
The full BOING! tutorial, including challenges, further explanations, and a link to the downloadable code can be found in Code the Classics, the latest book from Raspberry Pi Press.
We’re offering £1 off Code the Classics if you order it before midnight tomorrow from the Raspberry Pi Press online store. Visit the store now, or use the discount code PONG at checkout if you make a purchase before midnight tomorrow.
As always, Code the Classics is available as a free PDF from the Wireframe website, but we highly recommend purchasing the physical book, as it’s rather lovely to look at and would make a great gift for any gaming and/or coding enthusiast.