'How can I use pygame.Clock in a multiprocessing.Process call?

I'm an inexperienced python programmer, and for an interesting project I thought I'd try making a multiplayer LAN game (using pygame), with a server script and multiple client scripts connected through socket. I originally did this with threading, using two threads: one for the server, and one for the pygame section, since the program hangs due to socket.accept() if both are in one loop. However, this was incredibly laggy as soon as more than one client joined, so I decided to switch to using multiprocessing for performance. However, my pygame process crashes the script on startup due to TypeError: cannot pickle 'Clock' object exception. How can I fix this without changing the multiprocessing?

I've tried looking at the source code for pickle, multiprocessing, and pygame to try making my own class that handles the same functions as pygame.Clock, but I couldn't understand any of it, nor could I find anything online to help.

Relevant process definition:

class PygameProcess(mp.Process):
    
    def __init__(self, groupQueue, dataArray, name=None):
        super().__init__(name=name)

        self.running = True
        
        self.clock = pygame.time.Clock() # This is where the crash is
        
        self.groupQueue = groupQueue # Queue for accepting new sprites
        self.dataArray = dataArray # Shared memory array for sending the data to the clients' windows

        self.spriteGroup = pygame.sprite.Group()

    def run(self):
        self.screen = pygame.display.set_mode((400,400))
        pygame.display.set_caption("Multiplayer Test")
        self.screen.fill((255,255,255))

        pygame.display.flip()

        while self.running:

            while len(list(self.groupQueue.queue)) != 0:
                sprite = self.groupQueue.get()
                self.spriteGroup.add(sprite)
                self.groupQueue.task_done()

            self.clock.tick(60)

            dataStr = ""

            self.screen.fill((255,255,255))
            for index, sprite in enumerate(self.spriteGroup):
                sprite.update()
                sprite.draw(self.screen)
                dataStr += sprite.data
                if index != len(self.spriteGroup)-1:
                    dataStr += ",  "
            

            # Commented out Thread code, so I don't lose it

            #try:
            #    self.dataQueue.get_nowait()
            #except queue.Empty:
            #    pass
            #self.dataQueue.put(dataStr)
            self.dataArray.value = tuple(dataStr)

            pygame.display.update()

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False
    
        pygame.quit()

    def stop(self):
        self.running = False

Full code (apologies for lack of comments):

import pygame
import socket
import multiprocessing as mp
#import queue
import sys


# TODO: FIX "TypeError: can't pickle Clock objects"


'''CUSTOM PROCESSES'''

class ServerProcess(mp.Process):

    def __init__(self, groupQueue, dataArray, name=None):
        super().__init__(name=name)
        self.server = "192.168.104.48" # Fred = "192.168.1.254" # Bert = "192.168.1.7" # School = "192.168.104.48"
        self.port = 7777
        
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        
        self.connectedClients = [] 
        self.clientProcesss = []
        
        self.running = True

        self.groupQueue = groupQueue
        self.dataArray = dataArray



    def run(self):

        try:
            self.socket.bind((self.server, self.port))
            self.socket.settimeout(5)

        except socket.error as e:
            print(str(e))
        
        else:
            self.socket.listen(2)

            print("Server started, awaiting connection")

            while self.running:
                try:
                    connection, address = self.socket.accept()
                except socket.timeout:
                    pass
                else:
                    if not self.running:
                        connection.close()
                    else:
                        print(f"Connected to {address}")
    
                        self.connectedClients.append(ConnectedClient(connection, address))
    
                        self.clientProcesses.append(ClientProcess(self.connectedClients[-1], self.group, self.data, name=f"Client{len(self.connectedClients)}"))
                        self.clientProcesses[-1].start()
            
                        for clientProcess in self.clientProcesses:
                            if not clientProcess.is_alive():
                                index = self.clientProcesss.index(clientProcess)
                                self.connectedClients.pop(index)
                                self.clientProcesses.pop(index)

        finally:
            print("Server shutting down...")
            try:
                self.stop()
            except Exception:
                pass
            try:
                self.socket.close()
            except Exception:
                pass

    def stop(self):
        for process in self.clientProcesses:
            process.stop()

        self.running = False

class ClientProcess(mp.Process):

    def __init__(self, client, groupQueue, dataArray, name=None):
        super().__init__(name=name)
        self.client = client
        self.running = True
        self.group = groupQueue
        self.dataArray = dataArray

    def run(self):
        self.client.connection.send(str.encode("Connected"))
    
        request = ""
        while self.running:
            try:
                data = self.client.connection.recv(2048)
                request = data.decode("utf-8")
                
                print(f"Received request of '{request}' from {self.client.address}")

                if not data:
                    break

                elif request == "Data":
                    #dataStr = self.dataQueue.get()
                    #try:
                    #    self.dataQueue.put_nowait(dataStr)
                    #except queue.Full:
                    #    pass
                    #self.client.connection.sendall(str.encode(dataStr))
                    #self.dataQueue.task_done()
                    self.client.connection.sendall(str.encode("".join(self.dataArray)))

                elif request.split(", ")[0] == "Create Player":
                    try:
                        self.client.player = Player(tuple(int(x) for x in request.split(", ")[1][1:-1].split(",")), int(request.split(", ")[2]), int(request.split(", ")[3]), 10, 15)
                        self.groupQueue.put(self.client.player)
                        self.client.connection.sendall(str.encode("Creating your player"))
                    except ArrayError:
                        self.client.player = None
                        self.client.connection.sendall(str.encode("Player creation request denied, invalid parameters"))
                        print(f"{self.client.connection} attempted to make a player with invalid arguments")

                elif request == "Left":
                    self.client.connection.sendall(str.encode("Turning Left"))
                    if len(list(self.client.player.turnRequests.queue)) < 2 and self.client.player.xVel == 0:
                        self.client.player.turnRequests.put("Left")
                
                elif request == "Right":
                    self.client.connection.sendall(str.encode("Turning Right"))
                    if len(list(self.client.player.turnRequests.queue)) < 2 and self.client.player.xVel == 0:
                        self.client.player.turnRequests.put("Right")

                elif request == "Up":
                    self.client.connection.sendall(str.encode("Turning Up"))
                    if len(list(self.client.player.turnRequests.queue)) < 2 and self.client.player.yVel == 0:
                        self.client.player.turnRequests.put("Up")

                elif request == "Down":
                    self.client.connection.sendall(str.encode("Turning Down"))
                    if len(list(self.client.player.turnRequests.queue)) < 2 and self.client.player.yVel == 0:
                        self.client.player.turnRequests.put("Down")

                elif request == "Disconnect":
                    self.client.connection.sendall(str.encode("Disconnecting..."))
                    print(f"Disconnecting {self.client.address}")
                    break

                else:
                    print(f"Sending '{request}' to {self.client.address}")
                    self.client.connection.sendall(str.encode(request))
        
            except Exception as e:
                print("Error:", e)
                break
    
        print(f"Lost connection to {self.client.address}")

        self.client.player.kill()

        self.client.connection.close()

    def stop(self):
        self.running = False

class PygameProcess(mp.Process):
    
    def __init__(self, groupQueue, dataArray, name=None):
        super().__init__(name=name)

        self.running = True
        
        self.clock = pygame.time.Clock() # This is where the crash is
        
        self.groupQueue = groupQueue # Queue for accepting new sprites
        self.dataArray = dataArray # Shared memory array for sending the data to the clients' windows

        self.spriteGroup = pygame.sprite.Group()

    def run(self):
        self.screen = pygame.display.set_mode((400,400))
        pygame.display.set_caption("Multiplayer Test")
        self.screen.fill((255,255,255))

        pygame.display.flip()

        while self.running:

            while len(list(self.groupQueue.queue)) != 0:
                sprite = self.groupQueue.get()
                self.spriteGroup.add(sprite)
                self.groupQueue.task_done()

            self.clock.tick(60)

            dataStr = ""

            self.screen.fill((255,255,255))
            for index, sprite in enumerate(self.spriteGroup):
                sprite.update()
                sprite.draw(self.screen)
                dataStr += sprite.data
                if index != len(self.spriteGroup)-1:
                    dataStr += ",  "
            

            # Commented out Thread code, so I don't lose it

            #try:
            #    self.dataQueue.get_nowait()
            #except queue.Empty:
            #    pass
            #self.dataQueue.put(dataStr)
            self.dataArray.value = tuple(dataStr)

            pygame.display.update()

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False
    
        pygame.quit()

    def stop(self):
        self.running = False

'''CLASSES'''

class ConnectedClient():

    def __init__(self, connection, address):
        self.connection = connection
        self.address = address
        self.player = None

class Player(pygame.sprite.Sprite):

    def __init__(self, colour, x, y, width, height):
        super().__init__()
        self.colour = colour
        self.upFacingWidth = width
        self.upFacingHeight = height
        self.width = width
        self.height = height
        self.image = pygame.Surface([self.width,self.height])
        self.image.fill(self.colour)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.xVel = 0
        self.yVel = 0
        self.speed = 1
        self.turnRequests = mp.Queue(2)

        self.data = ""
    
    def update(self):
        self.rect.centerx += self.xVel
        self.rect.centery += self.yVel

        if self.rect.x % 5 == 0 and self.rect.y % 5 == 0:
            
            invalidTurn = True
            
            while invalidTurn and len(list(self.turnRequests.queue)) > 0:

                currentTurn = self.turnRequests.get()

                if currentTurn == "Left":
                    self.xVel = -1 * self.speed
                    self.yVel = 0
                    invalidTurn = False
                    self.height = self.upFacingWidth
                    self.width = self.upFacingHeight
                    self.image = pygame.Surface([self.width,self.height])
                    self.image.fill(self.colour)

                if currentTurn == "Right":
                    self.xVel = self.speed
                    self.yVel = 0
                    invalidTurn = False
                    self.height = self.upFacingWidth
                    self.width = self.upFacingHeight
                    self.image = pygame.Surface([self.width,self.height])
                    self.image.fill(self.colour)

                if currentTurn == "Up":
                    self.yVel = -1 * self.speed
                    self.xVel = 0
                    invalidTurn = False
                    self.height = self.upFacingHeight
                    self.width = self.upFacingWidth
                    self.image = pygame.Surface([self.width,self.height])
                    self.image.fill(self.colour)

                if currentTurn == "Down":
                    self.yVel = self.speed
                    self.xVel = 0
                    invalidTurn = False
                    self.height = self.upFacingHeight
                    self.width = self.upFacingWidth
                    self.image = pygame.Surface([self.width,self.height])
                    self.image.fill(self.colour)

        self.data = ""

        dataTuple = (self.colour, self.rect.x, self.rect.y, self.width, self.height)

        for index, item in enumerate(dataTuple):
            if isinstance(item, tuple):
                for secondaryIndex, secondaryItem in enumerate(item):
                    self.data += str(secondaryItem)
                    if secondaryIndex != len(item)-1:
                        self.data += ","
            else:
                self.data += str(item)
            if index != len(dataTuple)-1:
                self.data += ":"


        print(self.data)

    def draw(self, surface):
        surface.blit(self.image, (self.rect.x, self.rect.y))

'''SUBROUTINES'''



'''MAIN'''

if __name__ == "__main__":
    spriteGroupQueue = mp.Queue()
    currentDataArray = mp.Array("d", tuple(""))
    serverProcess = ServerProcess(spriteGroupQueue, currentDataArray, name = "Server")
    serverProcess.start()
    pygameProcess = PygameProcess(spriteGroupQueue, currentDataArray, name = "Pygame")
    pygameProcess.start()
    threadsAlive = True
    while threadsAlive:
        if not serverProcess.is_alive() and pygameProcess.is_alive():
            pygameProcess.stop()
            threadsAlive = False
        if not pygameProcess.is_alive() and serverProcess.is_alive():
            serverProcess.stop()
            threadsAlive = False
    sys.exit(0)


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source