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