Skip to content Skip to sidebar Skip to footer

Using Multiprocessing With Pygame?

I'm trying to separate my input loop from my game logic in my simple snake game that I've made with pygame, but, I'm really struggling to figure out why nothing is happening when I

Solution 1:

Generally in GUI applications it's common to want to separate the GUI from the logic. There are benefits to doing this as it means your GUI remains responsive even if your logic is busy. However, in order to run things concurrently there are many drawbacks, including overheads. It's also important to know that python is not 'thread safe', so you can break things (see race conditions) if you're not careful.

Simplified example with no concurrency

Your example is quite complex so lets start with a simple example: A simple pygame setup with a moving dot

import pygame
import numpy as np

# Initialise parameters#######################
size = np.array([800, 600])
position = size / 2
direction = np.array([0, 1])  # [x, y] vector
speed = 2
running = True

pygame.init()
window = pygame.display.set_mode(size)
pygame.display.update()

# Game loopwhile running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = Falseelif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_w:
                direction = np.array([0, -1])
            elif event.key == pygame.K_a:
                direction = np.array([-1, 0])
            elif event.key == pygame.K_s:
                direction = np.array([0, 1])
            elif event.key == pygame.K_d:
                direction = np.array([1, 0])

    position += direction * speed

    if position[0] < 0or position[0] > size[0] or position[1] < 0or position[1] > size[1]:
        running = False

    pygame.time.wait(10)  # Limit the speed of the loop

    window.fill((0, 0, 0))
    pygame.draw.circle(window, (0, 0, 255), position, 10)
    pygame.display.update()

pygame.quit()
quit()

We're going to split off the game logic from the gui

Mutliprocessing and other options:

So multiprocessing in python allows you to utilise multiple cores at the same time, through multiple interpreters. While this sounds good, as far as I/O goes: it comes with higher overheads and doesn't help at all (it will likely hurt your performance). Threading and asyncio both run on a single core i.e. they aren't 'parrallel' computing. But what they allow is to complete code while waiting for other code to finish. In other words you can input commands while your logic is running happily elsewhere.

TLDR: as a general rule:

  • CPU Bound (100% of the core) program: use multiprocessing,
  • I/O bound program: use threading or asyncio

Threaded version

import pygame
import numpy as np
import threading
import time

classLogic:
    # This will run in another threaddef__init__(self, size, speed=2):
        # Private fields -> Only to be edited locally
        self._size = size
        self._direction = np.array([0, 1])  # [x, y] vector, underscored because we want this to be private
        self._speed = speed

        # Threaded fields -> Those accessible from other threads
        self.position = np.array(size) / 2
        self.input_list = []  # A list of commands to queue up for execution# A lock ensures that nothing else can edit the variable while we're changing it
        self.lock = threading.Lock()

    def_loop(self):
        time.sleep(0.5)  # Wait a bit to let things load# We're just going to kill this thread with the main one so it's fine to just loop foreverwhileTrue:
            # Check for commands
            time.sleep(0.01)  # Limit the logic loop running to every 10msiflen(self.input_list) > 0:

                with self.lock:  # The lock is released when we're done# If there is a command we pop it off the list
                    key = self.input_list.pop(0).key

                if key == pygame.K_w:
                    self._direction = np.array([0, -1])
                elif key == pygame.K_a:
                    self._direction = np.array([-1, 0])
                elif key == pygame.K_s:
                    self._direction = np.array([0, 1])
                elif key == pygame.K_d:
                    self._direction = np.array([1, 0])

            with self.lock:  # Again we call the lock because we're editing
                self.position += self._direction * self._speed

            if self.position[0] < 0 \
                    or self.position[0] > self._size[0] \
                    or self.position[1] < 0 \
                    or self.position[1] > self._size[1]:
                break# Stop updatingdefstart_loop(self):
        # We spawn a new thread using our _loop method, the loop has no additional arguments,# We call daemon=True so that the thread dies when main dies
        threading.Thread(target=self._loop,
                         args=(),
                         daemon=True).start()


classGame:
    # This will run in the main thread and read data from the Logicdef__init__(self, size, speed=2):
        self.size = size
        pygame.init()
        self.window = pygame.display.set_mode(size)
        self.logic = Logic(np.array(size), speed)
        self.running = Truedefstart(self):
        pygame.display.update()
        self.logic.start_loop()

        # any calls made to the other thread should be read onlywhile self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = Falseelif event.type == pygame.KEYDOWN:
                    # Here we call the lock because we're updating the input listwith self.logic.lock:
                        self.logic.input_list.append(event)

            # Another lock call to access the positionwith self.logic.lock:
                self.window.fill((0, 0, 0))
                pygame.draw.circle(self.window, (0, 0, 255), self.logic.position, 10)
                pygame.display.update()

        pygame.time.wait(10)
        pygame.quit()
        quit()


if __name__ == '__main__':
    game = Game([800, 600])
    game.start()

So what was achieved?

Something light like this doesn't really need any performance upgrades. What this does allow though, is that the pygame GUI will remain reactive, even if the logic behind it hangs. To see this in action we can put the logic loop to sleep and see that we can still move the GUI around, click stuff, input commands etc. change:

# Change this under _loop(self) [line 21]
time.sleep(0.01)

# to this
time.sleep(2)

# if we tried this in the original loop the program becomes glitchy

Post a Comment for "Using Multiprocessing With Pygame?"