Pygame and socket in multiplayer game: OverflowError

Viewed 739

I'm trying to create a simple multiplayer game in Python using pygame and socket modules. It just consists in two circles that are controlled with the W, A, S, D keys from two different computers.

At first I created a client with the recv() in the middle of the pygame loop. It worked well but the recv() blocked the loop, so the movement of the circles wasn't smooth and I had to increase the velocity to 6 (before it was set to 0.6) in order to have a normal speed. This is the code (I've summarised it a bit):

Client 1st version

#import modules

def main(sock):
    pygame.init()
    
    #Display screen and set initial me_x and me_y

    vel = 0.6
   
    while True:
        keys_pressed = pygame.key.get_pressed()

        #Change me_x and me_y (with A, D, W, S keys)

        #Make sure me_x and me_y don't get off the screen

        screen.fill(color)
        
        if other_x and other_y:
            pygame.draw.circle(screen, colorMe, (other_x, other_y), radi)

        pygame.draw.circle(screen, colorOther, (int(me_x), int(me_y)), radi)

        pygame.display.flip()

        sock.send(int(me_x).to_bytes(3, byteorder = 'big') + int(me_y).to_bytes(3, byteorder = 'big'))

        otherPos = sock.recv(BUFSIZ)

        other_x = int.from_bytes(otherPos[:3], byteorder = 'big')
        other_y = int.from_bytes(otherPos[3:], byteorder = 'big')
        print(other_x, other_y)

#CONNECT TO TCP SOCKET

other_x = None
other_y = None

main(client_socket)

Then, I tried to put the recv() in a thread to stop blocking the loop:

Client 2nd version

#import modules

def main(sock):
    pygame.init()
    
    #Display screen and set initial me_x and me_y

    vel = 0.6
   
    while True:
        for i in range(30):
            keys_pressed = pygame.key.get_pressed()

            #Change me_x and me_y (with A, D, W, S keys)

            #Make sure me_x and me_y don't get off the screen

            screen.fill(color)
            
            if other_x and other_y:
                pygame.draw.circle(screen, colorOther, (other_x, other_y), 15)

            pygame.draw.circle(screen, colorMe, (int(me_x), int(me_y)), 15)

            pygame.display.flip()

        msg = int(me_x).to_bytes(3, byteorder = 'big') + int(me_y).to_bytes(3, byteorder = 'big')
        sock.send(msg)

def recv_pos(sock):
    while True:
        other_pos = sock.recv(BUFSIZ)
        other_x = int.from_bytes(other_pos[:3], byteorder = 'big')
        other_y = int.from_bytes(other_pos[3:], byteorder = 'big')

        print(other_x, other_y)

#CONNECT TO TCP SOCKET

other_x = None
other_y = None

receive_thread = threading.Thread(target = recv_pos, args = (client_socket,))
receive_thread.daemon = True
receive_thread.start()

main(client_socket)

However, when I start 2 instances of the client 2 in two different computers, it gives me an OverflowError:

OverflowError: Python int too large to convert to C long

I added the for i in range(30): because I thought that the server was getting collapsed because there were too much messages being sent at the same time. The output is the same: after 3 seconds more or less, the program crashes giving an OverflowError.

I added a print() statement in both versions just after the recv() to see which values of x and y I was receiving. In the 1st version, all of them were in the width and height range. However, in the 2nd version, 1/5 of the received messages was a big number such as 124381473265. If this number is even bigger, it gives the OverflowError. I don't understand why this is happening: I'm encoding and decoding the messages in the same way in both versions, but one works and the other doesn't.

I'm not including the server code because I don't think it's necessary. It's just just transmits the messages between both clients without modifying them. The server is not the error, as in the 1st client version the messages are sent and received properly.

Any help will be apprecieted. If you need more information about any point just tell me.

Thanks in advance!

1 Answers

Put your socket code into a thread, or use select.select() for non-blocking socket-reads. Then post custom event-messages back to the main loop when a data-gram arrives from the server.

import pygame
import enum

class NetworkEvents( enum.IntEnum ):
    EVENT_HANGUP    = pygame.USEREVENT + 1
    EVENT_MESSAGE   = pygame.USEREVENT + 2

Your socket-reading code should handle partial-packets, hang-ups and delays too. I think your error above is caused by trying to unpack a partial (or somehow junk) packet. Commonly people use the pickle module to encapsulate this data, but to begin with, I would do pre-testing with simple string data. It's easier to debug. Then once the transmission code is all bedded-down and tested, change to binary - if necessary.

I like to use select() in my socket code no matter what. It gives fine-grained control over what's happening with the socket.

Here's a simple threaded socket-conversation handler. It really only supports socket-hangup and incoming message. But it could be easily extended to handle further message types.

import threading
import pygame
import random
import enum
import socket
import select
import time

class ConversationHandlerThread( threading.Thread ):
    """ A thread that handles a conversation with a single remote server.
        Accepts commands of 'close', 'red', 'green' or 'blue', and posts messages
        to the main PyGame thread for processing """
    def __init__( self, server_address, server_port ):
        threading.Thread.__init__(self)
        self.server_address = server_address
        self.server_port    = server_port
        self.server_socket  = None
        self.data_buffer    = ''
        self.daemon         = True # exit with parent
        self.done           = False

    def stop( self ):
        self.done = True

    def connect( self ):
        self.server_socket = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
        while True:
            try:
                self.server_socket.connect( ( self.server_address, self.server_port ) )
                print( "Connected to %s:%d" % ( self.server_address, self.server_port ) )
                break;
            except:
                print( "Failed to connect %s:%d" % ( self.server_address, self.server_port ) )
                time.sleep( 12 )
                print( "Retrying..." )

    def run( self ):
        """ Connects to Server, then Loops until the server hangs-up """
        self.connect()

        # Now we're connected, start reading commands
        read_events_on   = [ self.server_socket ]
        while ( not self.done ):
            # Wait for incoming data, or errors, or 0.3 seconds
            (read_list, write_list, except_list) = select.select( read_events_on, [], [], 0.5 )

            if ( len( read_list ) > 0 ):
                # New data arrived, read it
                incoming = self.server_socket.recv( 8192 )
                if ( len(incoming) == 0):
                    # Socket has closed
                    new_event = pygame.event.Event( NetworkEvents.EVENT_HANGUP, { "address" : self.server_address } )
                    pygame.event.post( new_event )
                    self.server_socket.close()
                    self.done = True
                else:
                    # Data has arrived
                    try:
                        new_str = incoming.decode('utf-8')
                        self.data_buffer += new_str
                    except:
                        pass # don't understand buffer

                    # Parse incoming message (trivial parser, not high quality) 
                    # commands are '\n' separated
                    if (self.data_buffer.find('\n') != -1 ):
                        for line in self.data_buffer.split('\n'):
                            line = line.strip()
                            # client disconnect command
                            if ( line == 'close' ):
                                new_event = pygame.event.Event( NetworkEvents.EVENT_HANGUP, { "address" : self.server_address } )
                                pygame.event.post( new_event )
                                self.server_socket.close()
                                self.done = True

                            # only make events for valid commands
                            elif ( line in ( 'red', 'green', 'blue' ) ):
                                new_event = pygame.event.Event( NetworkEvents.EVENT_MESSAGE, { "address" : self.server_address, "message" : line  } )
                                pygame.event.post( new_event )
                        self.data_buffer = ''  # all used-up

The beauty of putting all this complexity into a thread, is that once it's started, you can forget about it.

# Start the network-handler thread
thread1 = ConversationHandlerThread( '127.0.0.1', 5555 )
thread1.start()

In your main loop, process the events like any others:

for event in pygame.event.get():
    if ( event.type == pygame.QUIT ):
        done = True

    elif ( event.type == NetworkEvents.EVENT_HANGUP ):
        print(" CLIENT DISCONNECTED %s " % ( str(event.address) ) )

    elif ( event.type == NetworkEvents.EVENT_MESSAGE ):
        print(" CLIENT MESSAGE FROM %s - %s " % ( str(event.address), event.message ) )
        if ( event.message == 'red' ):
            new_sprite = AlienSprite( RED )
            SPRITES.add( new_sprite )
Related