Creating a 2D multiplayer game in Python
I have begun work on the creation of a simple 2D LAN-based multiplayer game with Python.
I decided to log my progress as I go in a series of articles, partly for my own future reference, and partly for anyone else who is taking on a similar
endeavour for the first time and is looking to learn from other’s mistakes. And I am learning a lot by doing, and logging everything as I go, so I can guarantee there will be some mistakes along the way!
Writing a computer game is a very interesting learning experience/challenge, as it combines a variety of different aspects of programming. If you’re learning a new programming language, it’s a great way to really get to grips with it once you’re past the basics – if you don’t mind making your learning curve steeper, that is!
In this particular example I’m planning on using `pygame` for game mechanics, physics and rendering, as well as the native Python `sockets` library for networking, and also a little from the `tkinter` library for the startup menu. Of the three, “pygame” is the only one I have had some real experience with in the past.
For the sake of simplicity I probably won’t explain the ins and outs of every third-party library used in this project, but I will try to give you plenty of references for further reading.
The first step in the development of any project has got to be the planning: in this case deciding what this game is about, how it plays, and the general structure of it.
This game will be a 2D platformer PvP (player-versus-player) game, in which 2-4 players will have to shoot at each other with the magical powers of so-called “Mushkas”. To simplify the task of game networking a little, I decided to stick with LAN multiplayer for now. I might add support for remote connections in a second phase.
For the multiplayer aspect of the game, I decided to opt for a client/server structure. A central server is running the physics simulation (the actual “game”), and each client connected to it sends in updates about the player’s actions, and gets updates about the current game state in return.
The start menu
So, let’s start from the beginning: an initial menu, which will allow players to name themselves, customize their character a little, and then host a new game (as the server) or connect to an existing one on the local network.
In this article I’ll be using tkinter
for the intial, bare-bones version of said menu.
# import the tkinter library, which is used to create the GUI
import tkinter as tk
# import ttk, which basically a nicer-looking "upgrade" of tk
from tkinter import ttk
# sys and json are used to save the player's details
import sys
import json
# Location of the player configuration file
PLAYER_CONFIG = 'config/player.json'
class MainWindow: # main window class
# a list of the characters the player can choose from
char_images = ['img/mushka1.gif', 'img/mushka5.gif']
# Defaults for player configuration:
cur_char = 0 # index of the default character
player = "[n00b]" # player's default nickname
First we create the MainWindow
class, and set some initial parameters. Then the __init__
method:
def __init__(self, root, *args, **kwargs):
# Load player configuration file, if present.
try:
playerf = open(PLAYER_CONFIG, 'r')
data = json.loads(playerf.read())
playerf.close()
self.player = data["name"]
self.cur_char = data["char"]
except FileNotFoundError:
# If the file does not exist, just pass.
# It will be created later
pass
self.root = root
root.wm_title("SHROOMz") # set the title of the window
# create a Frame object within the window to contain all other objects
mainframe = self.mainframe = ttk.Frame(root, *args, **kwargs)
# add the frame to the window
mainframe.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S))
mainframe.columnconfigure(0, weight=1)
mainframe.rowconfigure(0, weight=1)
# add a large label as a title
ttk.Label(self.mainframe,
text = ".:: A game of SHROOMz ::.",
font=("Lucida Grande", 16)).grid(
column=1, row=1, sticky = (tk.W, tk.E))
# load the image for the currently selected character.
# Note: only GIF files supported.
image = tk.PhotoImage(file=self.char_images[self.cur_char])
# Create a clickable button with the current character image.
# When the button is clicked, the switch_char method is called
self.picture = ttk.Button(mainframe, image=image, command=self.switch_char)
self.picture.photo = image
self.picture.grid(column=1, row=2, sticky=(tk.N, tk.S)) # place in the Frame
# create a StringVar to link to the player name input field,
# then create said field
self.playername = playername = tk.StringVar()
name_entry = ttk.Entry(mainframe, width=25, textvariable=playername)
name_entry.grid(column=1, row=3, sticky=(tk.S, tk.N))
# prefill the field with the player's nickname
# name_entry.delete(0, tk.END) to empty the field
name_entry.insert(0, self.player)
# Add three buttons, one to host a new game, one to connect to one, and one to exit
ttk.Button(mainframe, text="Host new LAN game",
command=self.host).grid(column=1, row=4, sticky=(tk.W, tk.E))
ttk.Button(mainframe, text="Connect to LAN game",
command=self.client).grid(column=1, row=5, sticky=(tk.W, tk.E))
ttk.Button(mainframe, text="Quit",
command=self.quit).grid(column=1, row=6, sticky=(tk.N, tk.S))
# add some padding to all the window's children
for child in mainframe.winfo_children():
child.grid_configure(padx=15, pady=10)
Nothing particularly special here, we just create a window and add elements to it: a title, a button to select the character, an input field for the nickname, buttons to join or host a game, and a “Quit” button. Each button references a method of this class, which will hande it’s behaviour.
Now to define the methods to the buttons:
def switch_char(self):
# when the button is clicked, select the next character and update the image
self.cur_char += 1
if self.cur_char > len(self.char_images) - 1:
self.cur_char = 0
# load image for the next character and replace the current image
image = tk.PhotoImage(file=self.char_images[self.cur_char])
self.picture.config(image=image)
self.picture.photo = image
The first method simply cycles through the possible character images, and updates the displayed image.
def host(self):
# close window an start a new server
# print(self.playername.get())
self.close_win()
self.save_player()
import python.server
self.exit()
def client(self):
# close window and start a new client
self.close_win()
self.save_player()
import python.client
self.exit()
def quit(self):
self.save_player()
self.exit()
The host
and client
methods are pretty similar: they close the current window, save the player’s configuration by calling self.save_player
, import the corresponding module and quit the program once the module has finished executing. Of course at the moment the external modules don’t do anything yet – that’s the next step! For now, they’re just empty files.
Last but not least, the “utility” methods:
def save_player(self):
# Save the current player configuration to
# the configuration file
playerf = open(PLAYER_CONFIG, 'w')
playerf.write(json.dumps({
"name": self.playername.get(),
"char": self.cur_char}
))
playerf.close()
def close_win(self):
# Hide the current window
self.root.withdraw()
def exit(self):
# Exit the program
self.close_win()
sys.exit()
A note on modules
You will have noticed the server
and client
methods import external modules – python.server
and python.client
respectively. These are in reality just plain .py
files stored in a subfolder of my main directory called python
. In order to access them like this, that folder must contain a file __init__.py
. This may or may not contain code, which will be executed whenever the python
module is called. In our case, it’s just a blank file – we’re not actually coing to include python
as a module, just other modules contained in it. The main reason for this is to have a “clean” directory structure, with just one file upfront (the “main menu” file, which I called SHROOMz.py
), and the rest “hidden away”.
And this is it! The basic menu is in place. Next, I will be getting in to the basics of server/client communication, in order to connect our players!
Further reading
Python’s tkinter
module: docs.python.org
Python’s ttk
module: docs.python.org
JSON basics: weblog.seanbone.ch
Python’s json
module: docs.python.org