I’m sure that once in a while, you and your friends go online to play a multiplayer game. Have you ever wondered about the inside of that game and how everything works?
In this tutorial, you will learn about multiplayer game programming by creating a sample game. Along the way you will also learn about object-oriented programming.
For this tutorial, you will be using Python and the PyGame modules. If you are new to Python or PyGame, you should first look at this earlier tutorial on beginning game programming, which explains some of the basics of PyGame.
The first step is to make sure that you have PyGame installed. You can download a Mac installer for PyGame here. Make sure you download the Lion installer if you have Mac OSX 10.7 or up. Otherwise, download the Snow Leopard installer.
You can also download and install PyGame in these ways:
- With MacPorts using:
sudo port install python2.7 py27-game
- With Fink using:
sudo fink install python27 pygame-py27
- With Homebrew and pip using the command found here.
If you are running Windows, then you can find your installer here.
Note: If you had trouble in the last tutorial, make sure you have the 32-bit version of Python on your system. If you have a 64-bit system, then you need to run
python2.7-32 to run Python.
Lastly, download the resources for this project, which include some images and sounds that you’ll need for this game.
The Rules of the Game
The game you’re going to make in this tutorial is called “Boxes”. You may be familiar with playing this game on paper with some friends while you were in school!
In case you’re not familiar with the game, here are the rules:
- The board consists of a 7×7 grid of points (which makes a 6×6 grid of cubes if you were to connect the dots).
- On each player’s turn, the player fills in the horizontal or vertical line segment connecting two neighboring points.
- If filling in a line segment completes a box on the grid, the player becomes the owner of that square and gets a point. The player also gets to place another line segment on the same turn.
- The player with the most squares/points at the end of the game wins!
Although these rules are very simple, it’s a fun game to play, especially if you’re bored. But wouldn’t it be great if you could play this online?
Object-Oriented Programming: A Quick Introduction
Before we begin, let’s discuss something called Object-Oriented Programming which you’re going to use in this tutorial.
Object-oriented programming, also known as OOP, is a type of programming based on objects. Object are bundles of data and associated logic. For example, you might have a “dog” object that consists of some data (the dog’s name or favorite treat) and associated logic (for example, instructions on how to bark).
Objects are made from templates called classes that define what kinds of data the object can hold and what kinds of things the object can do. These are known as the object’s properties and methods, respectively.
Methods are functions that represent something you can ask the object to do. For example, the statement
car.drive() can be interpreted as telling the object in the “car” variable to “drive”. Properties are variables that belong to an object. Continuing the example, your car object might have a property called
gas, and the statement
car.gas = 100 would set the car’s
gas to 100.
These two statements manipulate a car object that already exists. Recall that the car’s class is the template that defines how to make a car object and what a car is by defining its properties and methods. Within the definitions of those methods, you will find the code that manipulates the car from the inside. For instance, instead of
car.gas = 100, you might find
self.gas=100, which is a car object telling itself –
self, get it? – to set its own gas to 100.
OOP is a large topic but the basics above are all you need to get started. Your code will describe the Boxes game as the interaction of various objects. Those objects all have properties and methods, which you will define in the object’s class. And when you write a piece of code, you should remember whether you’re writing the class code that defines what an object can do from the “inside” of the object, or code that manipulates an object from the “outside” of that object.
Setting Up a Basic Object-Oriented Game
There are a couple of ways to use an object-oriented framework for your game. Your Boxes game will take a simple approach in which there is one class for the client and one for the server. For now, let’s just create the main client class that will run when the user starts the game.
At the start of making every game, I like to make a folder for the game. When you unzipped the resources for this project, it should have created a folder or you called boxes. This is where you will put your source code for the game – right here alongside all the images.
Create a file in this directory called boxes.py using your favorite text editor (if you don’t have one, you can use TextEdit on the Mac, or Notepad in Windows). Then add this import of the file:
This imports the PyGame module for you to use. Before you go any further, you should test that at least this much is working. To do this, open Terminal and switch to your boxes directory using the cd command. Then enter python boxes.py. For example, here’s what it looks like on my machine:
cd /Users/jmeyer/Downloads/boxes python boxes.py
If you get no errors after running this, that means you have PyGame installed correctly, and you are good to go.
Note: If running the code above gives you an
ImportError saying there is “No module named pygame”, then you have not installed PyGame or else you have installed PyGame into a copy of Python different from the one you are running. For instance, if you used MacPorts to install Python 2.7 and PyGame with
port install python2.7 py27-game, then make sure to run the same Python by calling
python2.7 from the Terminal.
If running the code above gives you this specific error:
ImportError: /Library/Frameworks/SDL.framework/Versions/A/SDL: no appropriate 64-bit architecture (see "man python" for running in 32-bit mode)
That means you need to run Python in 32-bit mode, like this:
Next add the class definition, as well as one thing every class should have:
class BoxesGame(): def __init__(self): pass #put something here that will run when you init the class.
The first line of this code tells the compiler that you are creating a new class called BoxesGame. The second line defines a method called
__init__. The surrounding double underscores are a hint that this is a special method name. In fact, this name identifies the method as the class’s
__init__ method, the method that you run whenever you want to create or instantiate an object of the class.
Now you’ll fill in the body of the
init function to do some PyGame initialization. Add this to the code you wrote above, in place of the comment beginning with
#put something here...:
#1 pygame.init() width, height = 389, 489 #2 #initialize the screen self.screen = pygame.display.set_mode((width, height)) pygame.display.set_caption("Boxes") #3 #initialize pygame clock self.clock=pygame.time.Clock()
Make sure you indent it correctly, so that everything lines up to the left margin of where the “#put something here…” comment was. You can read more about the matter here: Python Indentation.
Let’s look at the code you just added, one chunk at a time:
- First you initialize PyGame and two variables that you’ll use to set up the screen,
- Then you initialize the screen using those two variables. You also set the title of the screen.
- Finally, you initialize the PyGame clock, which you’ll need for tracking time in the game.
Next let’s add the
update() loop, which runs every periodically to update the game, draw the graphics and receive user input. Do this by simply adding the following after the __init__ method (the left margin should be equal to the left margin of __init__):
def update(self): #sleep to make the game 60 fps self.clock.tick(60) #clear the screen self.screen.fill(0) for event in pygame.event.get(): #quit if the quit button was pressed if event.type == pygame.QUIT: exit() #update the screen pygame.display.flip()
This is a basic update loop that clears the screen and checks to see if the user wants to quit the game. You’ll be adding more to this later.
Running the Python file now won’t do anything yet, as all you’ve done is defined the class BoxesGame. You still need to create an object of this class and start the game!
Now that you have the update loop ready, let’s add the code that will run the main game class. After that, you’ll set up some of the basic graphics in the game, such as drawing the board.
Add this code to the end of the file to start the game (the left margin should be equal to the left margin of the file):
bg=BoxesGame() #__init__ is called right here while 1: bg.update()
This is the nice thing about object-oriented programming: The code that actually makes things happen is only three lines long!
At this point, the entire file should look like this:
import pygame class BoxesGame(): def __init__(self): pass #1 pygame.init() width, height = 389, 489 #2 #initialize the screen self.screen = pygame.display.set_mode((width, height)) pygame.display.set_caption("Boxes") #3 #initialize pygame clock self.clock=pygame.time.Clock() def update(self): #sleep to make the game 60 fps self.clock.tick(60) #clear the screen self.screen.fill(0) for event in pygame.event.get(): #quit if the quit button was pressed if event.type == pygame.QUIT: exit() #update the screen pygame.display.flip() bg=BoxesGame() #__init__ is called right here while 1: bg.update()
That’s it. Now wasn’t that easy? This is a good time to run the game:
As you can see, running the game results in a very impressive black screen! Yay!
You may not understand this now, but game writing is a strategic process. Think of it as being an architect. You have just built a strong base for your building. Large buildings must have very good bases and so you must think your plan through before you start.
Let’s add another method. If you don’t remember what this means, reread the section of the tutorial called, “Object Oriented Programming: A Quick Introduction.”
Drawing the Board and Lines on the Screen
In PyGame, the upper left of the window is coordinate (0, 0). So let’s define a coordinate system for the points in the Boxes grid that is similar, with (0,0) representing the upper left point and (6,6) representing the bottom right point:
Somehow, you need a way to represent the potential line segments in the game. Well, there are two different types of line segments: horizontal and vertical lines. Let’s imagine you make a list of all the potential horizontal and vertical line combinations. It would look something like this:
In programming terms, a list is also known as an array. And when you have a list of lists, like the horizontal and vertical line combinations here, that’s called a 2D array.
For example, to represent the horizontal line from (0, 0) to (1, 1), that would be row 0, column 0 in the “horizontal lines” list.
Note that the horizontal lines list has 6 rows and 7 columns, and the vertical lines list has 7 rows and 6 columns.
Add these two lines to __init__ to define these two arrays:
self.boardh = [[False for x in range(6)] for y in range(7)] self.boardv = [[False for x in range(7)] for y in range(6)]
A quick way to create an array is to do this:
[valuePerItem for x in y]. In this case, you fill an array with an array filled with
False stands for an empty space.
Now that you have the board representation, let’s get to the code of drawing the board.
First of all, create a new method called
initGraphics(). This method will be something you call from
__init__ but, to keep your code organized, you’re creating a separate method just for the purpose of loading the graphics. Add this right before the
def initGraphics(self): self.normallinev=pygame.image.load("normalline.png") self.normallineh=pygame.transform.rotate(pygame.image.load("normalline.png"), -90) self.bar_donev=pygame.image.load("bar_done.png") self.bar_doneh=pygame.transform.rotate(pygame.image.load("bar_done.png"), -90) self.hoverlinev=pygame.image.load("hoverline.png") self.hoverlineh=pygame.transform.rotate(pygame.image.load("hoverline.png"), -90)
As you can see, you have three main sprites: an normal (empty) line, a done (occupied) line and a hover line. You rotate each of these lines by 90 degrees to draw the horizontal versions of them. These files came with the resources you downloaded earlier and should be in the same directory as your Python file.
You have a method to load all of the graphics, but you have yet to call it. Try to guess where to add what!
Once you have an answer, click the Show button below to see if you’re right.
[spoiler title=”Solution”]Add this at the end of __init__:
#initialize the graphics self.initGraphics()
Next you should add the code that actually draws the board. To loop through every x and y in a grid, you must add a
for loop inside of a
for loop. (For all of you Inception fans, a for-loop-ception.) You need a loop that loops through the x- and y-values. Add this right after the
def drawBoard(self): for x in range(6): for y in range(7): if not self.boardh[y][x]: self.screen.blit(self.normallineh, [(x)*64+5, (y)*64]) else: self.screen.blit(self.bar_doneh, [(x)*64+5, (y)*64]) for x in range(7): for y in range(6): if not self.boardv[y][x]: self.screen.blit(self.normallinev, [(x)*64, (y)*64+5]) else: self.screen.blit(self.bar_donev, [(x)*64, (y)*64+5])
This code simply loops through the grid and checks whether or not that part on the grid has been clicked. The code does this for both the horizontal and vertical lines.
self.boardh[x][y] returns either true or false, depending on whether the appropriate line segment has been filled in yes.
Running the program now still won’t do anything. All you’ve done is defined what the game should do if it ever gets that method call.
Now let’s add the method call to the update function. Add this after you clear the screen with
#draw the board self.drawBoard()
And of course, as a good programmer, you remember to add a comment to explain the code.
Run your code now. When you do, you should see the grid drawn on the screen:
Every time I write map drawing code, I like to test it out, both because it’s fun and because it’s a good way to find bugs. Add this after you initialize the boards by defining
Run the code and as you can see, one horizontal line is lit up – the line from (5, 3) to (5, 4):
Pretty cool, huh? Delete the line of test code you just added.
Good job. You’ve finished drawing your map, which is one of the most difficult things to do in game programming.
Adding Other Types of Lines
Next you need to find the line to which the mouse is closest so that you can draw a hover line at that spot.
First, at the top of the file, add this line to import the math library, which you’ll need soon:
pygame.display.flip(), add this big chunk of code:
#1 mouse = pygame.mouse.get_pos() #2 xpos = int(math.ceil((mouse-32)/64.0)) ypos = int(math.ceil((mouse-32)/64.0)) #3 is_horizontal = abs(mouse - ypos*64) < abs(mouse - xpos*64) #4 ypos = ypos - 1 if mouse - ypos*64 < 0 and not is_horizontal else ypos xpos = xpos - 1 if mouse - xpos*64 < 0 and is_horizontal else xpos #5 board=self.boardh if is_horizontal else self.boardv isoutofbounds=False #6 try: if not board[ypos][xpos]: self.screen.blit(self.hoverlineh if is_horizontal else self.hoverlinev, [xpos*64+5 if is_horizontal else xpos*64, ypos*64 if is_horizontal else ypos*64+5]) except: isoutofbounds=True pass if not isoutofbounds: alreadyplaced=board[ypos][xpos] else: alreadyplaced=False
Wow! That's a lot of code. Let's go over the sections one-by-one:
- First you get the mouse position with PyGame's built-in function.
- Next you get the position of the mouse on the grid, using the fact that each square is 64x64 pixels.
- You check if the mouse is closer to the top and bottom or the left and right, in order to determine whether the user is hovering over a horizontal or vertical line.
- You get the new position on the grid based on the
- You initialize the variable
boardv, whichever is correct.
- Finally, you try drawing the hover line to the screen, taking into consideration whether it is horizontal or vertical and on the top, bottom, left or right. You also check if the line is out of bounds. If it is, or if the line has already been drawn, you don't draw the hover line.
Run the program and you get... drum roll, please... a map where a line lights up as your mouse moves over it!
If you're like me, you probably have your mouse whizzing across the board by now. Take some time to enjoy your results.
OK, now you have a grid that lights up when the player's mouse moves over a line. But this isn't a game where you just have to move your mouse around a bunch. You need to add the click-to-lay-down-line functionality.
To do this, you're going to use PyGame's built-in mouse function, which is simply
pygame.mouse.get_pressed(). The function returns either 1 or 0, depending on whether the mouse button is currently pressed down.
Before I tell you how to implement this in your game, try figuring it out yourself. Remember how you used
if statements before and how to create a piece on the board.
[spoiler title="Solution"]Add this directly after the last block of code defining hover behavior:
if pygame.mouse.get_pressed() and not alreadyplaced: if is_horizontal: self.boardh[ypos][xpos]=True else: self.boardv[ypos][xpos]=True
Run the program now and voilà! If you click, you place a line just where you were hovering. As you can see, the code you added checks if the mouse is pressed and if the line should be horizontal or vertical, and places the line accordingly.
One problem, though, is that if you click at the bottom of the screen (below where the boxes are drawn), the game crashes. Let's see why this is. When something crashes, usually it gives you an error report in the Terminal. In this case, the report looks like this:
Traceback (most recent call last): File "/Users/school/Desktop/Dropbox/boxes/WIPBoxes.py", line 103, in <module> bg.update() File "/Users/school/Desktop/Dropbox/boxes/WIPBoxes.py", line 69, in update self.boardh[ypos][xpos]=True IndexError: list index out of range
This error is saying that the array
boardh that you tried to access doesn't go as far as where you clicked. Remember that variable called
isoutofbounds? That will come in handy here. Simply change this:
if pygame.mouse.get_pressed() and not alreadyplaced: #-----------to------------- if pygame.mouse.get_pressed() and not alreadyplaced and not isoutofbounds:
Now if you try clicking outside of the board, the game doesn't crash. Good job – you have just demonstrated the word debugging!
Before you begin implementing the game logic on the server side, let's first add some finishing touches to the client side.
One thing that really bugs me are the spaces at the junctions of the lines. Fortunately, you can fix this quite easily using a 7x7 grid of square dots to fill in those spaces. Of course, you do need the image file, so let's load that right now and at the same time add all of the other images you will be using in this section.
Add this to the end of
self.separators=pygame.image.load("separators.png") self.redindicator=pygame.image.load("redindicator.png") self.greenindicator=pygame.image.load("greenindicator.png") self.greenplayer=pygame.image.load("greenplayer.png") self.blueplayer=pygame.image.load("blueplayer.png") self.winningscreen=pygame.image.load("youwin.png") self.gameover=pygame.image.load("gameover.png") self.score_panel=pygame.image.load("score_panel.png")
Now that you image is loaded, let's draw each of the 49 dots onto the screen. Add this to the end of
#draw separators for x in range(7): for y in range(7): self.screen.blit(self.separators, [x*64, y*64])
All right, enough code! It's time for a test run. Run the game, and you should get a better-looking grid.
Next, let's put a head-up display or HUD at the bottom of the screen. First, you need to create the
drawHUD() method. Add this code after
def drawHUD(self): #draw the background for the bottom: self.screen.blit(self.score_panel, [0, 389])
This code also draws the background of the score panel.
Let me go over the way PyGame handles fonts. There are three steps:
- First you define a font with a set size.
- Next you call
font.render("your text here")to create a surface for those letters in that font.
- Then you draw the surface just as you would an image.
Now that you know that, you can use this information to draw the next part of the HUD: the "Your Turn" indicator. Add this code at the bottom of
#create font myfont = pygame.font.SysFont(None, 32) #create text surface label = myfont.render("Your Turn:", 1, (255,255,255)) #draw surface self.screen.blit(label, (10, 400))
Also add this after the call to
This code creates the font, renders it in white and then draws it onto the screen. Before you try running the game, add this after the call to
Run the program and you should get some text that says "Your Turn" at the bottom of the screen. If you look closely, you can also see the nicely textured background.
This is great, but you still need to add the indicator after the "Your Turn" text to let the player know it's their round.
Before you do, though, you want the game to know whose turn it is. Make sure it knows by adding this to the end of
self.turn = True
Now for that indicator. Add this to the end of
self.screen.blit(self.greenindicator, (130, 395))
Run the game and you will see the green score indicator. You can check that off of your list of things to do.
Next let's add the text for each player's score. Initialize the variables for the two scores by tacking this onto the end of
self.me=0 self.otherplayer=0 self.didiwin=False
Here you also add another variable that you will use later in this step.
Remember how to add text? You're going to do the same type of thing you did before, but with differently sized fonts. Add this to the end of
#same thing here myfont64 = pygame.font.SysFont(None, 64) myfont20 = pygame.font.SysFont(None, 20) scoreme = myfont64.render(str(self.me), 1, (255,255,255)) scoreother = myfont64.render(str(self.otherplayer), 1, (255,255,255)) scoretextme = myfont20.render("You", 1, (255,255,255)) scoretextother = myfont20.render("Other Player", 1, (255,255,255)) self.screen.blit(scoretextme, (10, 425)) self.screen.blit(scoreme, (10, 435)) self.screen.blit(scoretextother, (280, 425)) self.screen.blit(scoreother, (340, 435))
Run the game to check out your work.
You are now officially done with the HUD. There are just a couple more things to do on the client side, so bear with me.
Next, let's add a very simple owner grid that contains values representing a player. These values will let you keep track of who owns which squares. You need this to color the squares properly, and to keep track of the score. Remember, the person who controls the most squares wins!
First initialize another array by adding this at the end of
self.owner = [[0 for x in range(6)] for y in range(6)]
Now draw the owner grid onto the screen using the same kind of 2d-array loop that you used to loop through the lines arrays. Add this to the bottom of the class:
def drawOwnermap(self): for x in range(6): for y in range(6): if self.owner[x][y]!=0: if self.owner[x][y]=="win": self.screen.blit(self.marker, (x*64+5, y*64+5)) if self.owner[x][y]=="lose": self.screen.blit(self.othermarker, (x*64+5, y*64+5))
This method checks if it needs to draw in a given square and if it does, it draws the correct color (each player will have his or her own color).
Right now this code won't work because you need the server to tell the client which color to draw, which you will do in the next part of the tutorial. For now, you just won't call this method.
You have one more thing to add to the user interface: winning and losing screens. Define this last method and add it to the bottom of the class:
def finished(self): self.screen.blit(self.gameover if not self.didiwin else self.winningscreen, (0,0)) while 1: for event in pygame.event.get(): if event.type == pygame.QUIT: exit() pygame.display.flip()
Of course, there is no way yet to trigger these screens in the game. That, too, you'll take care of in the next part of the tutorial, when you implement the server side of the game.
Remember that by adding all of these game elements now, you are making sure that the server will be able to manipulate the client however it wants. From here on out, you won't need to make many changes to the client other than a little glue between the client and the server.
But just to make sure it works, try calling the
finished() method in the last part of
__init__. You should get a game over screen that looks like the image to the right.
Where to Go from Here?
Here is the source code from the tutorial so far.
Congratulations! You have finished the client side of a very organized and good-looking game. This, of course, is not the end since you haven't implemented any game logic, but excellent job on the client side!
Now you should go look at Part 2 of this tutorial, which is all about the server-side - and you'll finally start making this game truly multiplayer!