Using Multiprocessing With Pygame?
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?"