Multiplayer game

Saturday, I felt like learning how to use sockets. So I started to work on my first multiplayer game.

It’s nicknamed “flat” for now. It will get a real name when I know what the objective will be. Right now, you move around and invert the color of the gameboard wherever you go, watching others do the same (I planned on making this feature into a snake game).

flat-random-squares
The only thing significant about these random squares is that they were streamed from a separate process using python sockets.

After testing each new feature of python, I added code in large chunks, and managed to make the server multithreaded by Sunday. It now supports multiple connections.

ring-2clients-demo
Two clients connected to the same server.

The most important feature of this project is a single class I designed to handle data synchronization. The DataHandler class:

class DataHandler:
  def __init__(self,objects,name="unnDH"):
    self.objects = objects
    self.changes = {}
    self.name = name
        
  def __setitem__(self,key,value): #change something
    if not (self.changes.__contains__(key) and self.changes[key]==value):
      self.changes[key] = value
        
  def __getitem__(self,key): #shows unapplied changes
    return self.changes[key] if self.changes.__contains__(key) else self.objects[key]
  
  #removes all changes that don't contain a value that's actually new
  def cleanChanges(self): 
    for key in self.changes:
      if self.objects.__contains__(key) and self.objects[key] == self.changes[key]:
        self.changes.delitem(key)     

  #apply changes and empty changes dictionary
  def applyChanges(self):
    for key in self.changes:
      self.objects[key] = self.changes[key]
    self.changes = {}
        
  #encodes changes, also applies them locally
  def getUpdate(self):
    self.cleanChanges()
    result = str(self.changes).replace(" ","").encode()
    self.applyChanges()
    return result
    
  #encodes all data without emulating or applying changes
  def getRefresh(self):
    result = str(self.objects).replace(" ","").encode()
    return result
    
  def putUpdate(self,stream): #recieving data
    if type(stream)==DataHandler:
      updates = stream
    elif type(stream)==bytes:
      updates = eval(stream.decode())
    else:
      print(self.name + " putUpdate: unknown update type " + str(type(stream)) + " " + str(stream))
    for key in updates:
      #assume that if the held object is a DataHandler, its update must be a dataHandler update dictionary. 
      if type(self.objects[key])==DataHandler:
        #updates are recursive, to change only parts of parts 
        self.objects[key].putUpdate(updates[key]) 
      else:
        self.objects[key] = updates[key] #put regular update
    if len(self.changes) > 0:
      print(self.name + "flatData was given update while tracking changes. (bad things may happen now)")

 

This class allows the Server to properly handle game data during a game tick.

When the Server applies rules other mechanics to its data in a DataHandler, it sees the effects in real time – to the Server, the DataHandler is no different than a python dictionary object. Whenever a new client joins, the Server uses getRefresh() to send a complete copy of the data as it currently appears to all players, so that the new client is on the same page as everyone else.

However, when the game tick is over, the server uses getUpdate() to send each client a copy of only the data that has been changed in this game tick. Calling getUpdate() applies all of the changes to the core data and empties the change list, and the DataHandler is ready for the next tick to begin.

DataHandlers are good for complex data, because they encode and evaluate strings without complicated conversions specific to the type of data (any primitive can be sent without writing new code). They can even be nested, to recursively send only parts of parts, and so on. That’s my most important accomplishment so far.

However, none of these optimizations are currently applied to the board, which is hundreds of times larger than the player data updates. I will need to create a separate class of data handler for tracking array changes.

 

Next on my list of priorities:

  • Create an array data handler.
  • Round out the access methods of DataHandler to where it can be used recursively, tracking and emulating changes from as many ticks as there are levels of linked DataHandlers. Updates will be given to clients according to how long they have been unresponsive, unless they have been unresponsive for longer than changes are tracked.
  • Refactor Server to have a self-contained game controller that isn’t responsible for networking.
  • Run game controller outputs through a filter to remove data that’s irrelevant to the player (off-screen movement, etc.)
  • Scan for servers (right now the code only checks for servers running on this computer.)
  • Redesign DataHandlers to remove the hard-coded distinctions between server access and client access. Instead support the creation of viewer identities, through which data is accessed – automatically tracking which clients have received or sent which data, but without duplicating any data internally.

Leave a comment