101arrowz
101arrowz

Reputation: 1905

How can I create two separate pygame displays? If I can't, how could I create two instances of pygame?

I want to create two separate pygame displays. I know that it's impossible to do that with a single instance of pygame. I was wondering how/if I could implement any of the following solutions:

  1. Create a separate module to run the function that uses the separate display. Will this use a separate instance of pygame from my main code and therefore be able to run?
  2. Run the script from within itself with some special parameters using a subshell, parse sys.argv, and then run the function in the second instance. How would I ensure cross-platform compatibility?
  3. Something else?

I don't care about how inefficient, ugly, etc. the program is as a result. I simply want to make this work.

Importantly, the main code must communicate with the relevant code (i.e. change some variables belonging to extraDisplayManager below) but I plan to use pickle to save to a file and communicate that way. I'm already multi-threading the relevant part though, so a lack of synchronicity doesn't matter.

My code is fairly long and complicated, so I won't post it here, but the gist of the relevant part is:

def mainCode(*someargs):
    d = pygame.display.set_mode(dimensions)
    if relevantArg:
        extraDisplayManager = RelevantClass(*someotherargs)
        threading.Thread(target=extraDisplayManager.relevantFunction,
                         daemon=True).start()


...

class RelevantClass:
    def relevantFunction(self, *someotherargs):
        self.d = pygame.display.set_mode(dimensions)
        while True:
            updateDisplay(someargs) # This is part of my main code, but it
            # is standalone, so I could copy it to a new module

I'd appreciate if you could answer some of my questions or show me some relevant documentation.

Upvotes: 1

Views: 259

Answers (1)

sloth
sloth

Reputation: 101072

If you really (really) need two displays, you can use python's multiprocessing module to spawn a process and use a Queue to pass data between the two processes.

Here's an example I hacked together:

import pygame
import pygame.freetype
import random
import multiprocessing as mp

# a simple class that renders a button
# if you press it, it calls a callback function
class Button(pygame.sprite.Sprite):
    def __init__(self, callback, *grps):
        super().__init__(*grps)
        self.image = pygame.Surface((200, 200))
        self.image.set_colorkey((1,2,3))
        self.image.fill((1,2,3))
        pygame.draw.circle(self.image, pygame.Color('red'), (100, 100), 50)
        self.rect = self.image.get_rect(center=(300, 240))
        self.callback = callback

    def update(self, events, dt):
        for e in events:
            if e.type == pygame.MOUSEBUTTONDOWN:
                if (pygame.Vector2(e.pos) - pygame.Vector2(self.rect.center)).length() <= 50:
                    pygame.draw.circle(self.image, pygame.Color('darkred'), (100, 100), 50)
                    self.callback()
            if e.type == pygame.MOUSEBUTTONUP:
                    pygame.draw.circle(self.image, pygame.Color('red'), (100, 100), 50)

# a simple class that display a text for 1 second anywhere
class Message(pygame.sprite.Sprite):
    def __init__(self, screen_rect, text, font, *grps):
        super().__init__(*grps)
        self.image = pygame.Surface((300, 100))
        self.image.set_colorkey((1,2,3))
        self.image.fill((1,2,3))
        self.rect = self.image.get_rect(center=(random.randint(0, screen_rect.width), 
                                                random.randint(0, screen_rect.height)))
        font.render_to(self.image, (5, 5), text)
        self.timeout = 1000
        self.rect.clamp_ip(screen_rect)

    def update(self, events, dt):
        if self.timeout > 0:
            self.timeout = max(self.timeout - dt, 0)
        else:
            self.kill()

# Since we start multiple processes, let's create a mainloop function
# that can be used by all processes. We pass a logic_init_func-function
# that can do some initialisation and returns a callback function itself.
# That callback function is called before all the events are handled.
def mainloop(logic_init_func, q):
    import pygame
    import pygame.freetype
    pygame.init()
    screen = pygame.display.set_mode((600, 480))
    screen_rect = screen.get_rect()
    clock = pygame.time.Clock()
    dt = 0
    sprites_grp = pygame.sprite.Group()

    callback = logic_init_func(screen, sprites_grp, q)

    while True:
        events = pygame.event.get()
        callback(events)

        for e in events:
            if e.type == pygame.QUIT:
                return

        sprites_grp.update(events, dt)
        screen.fill((80, 80, 80))
        sprites_grp.draw(screen)
        pygame.display.flip()
        dt = clock.tick(60)

# The main game function is returned by this function.
# We need a reference to the slave process so we can terminate it when
# we want to exit the game.
def game(slave):
    def game_func(screen, sprites_grp, q):

        # This initializes the game.
        # A bunch of words, and one of it is randomly choosen to be
        # put into the queue once the button is pressed
        words = ('Ouch!', 'Hey!', 'NOT AGAIN!', 'that hurts...', 'STOP IT')

        def trigger():
            q.put_nowait(random.choice(words))

        Button(trigger, sprites_grp)

        def callback(events):
            # in the mainloop, we check for the QUIT event
            # and kill the slave process if we want to exit
            for e in events:
                if e.type == pygame.QUIT:
                    slave.terminate()
                    slave.join()

        return callback

    return game_func

def second_display(screen, sprites_grp, q):

    # we create font before the mainloop
    font = pygame.freetype.SysFont(None, 48)

    def callback(events):
        try:
            # if there's a message in the queue, we display it
            word = q.get_nowait()
            Message(screen.get_rect(), word, font, sprites_grp)
        except:
            pass

    return callback

def main():
    # we use the spawn method to create the other process
    # so it will use the same method on each OS.
    # Otherwise, fork will be used on Linux instead of spawn
    mp.set_start_method('spawn')
    q = mp.Queue()

    slave = mp.Process(target=mainloop, args=(second_display, q))
    slave.start()

    mainloop(game(slave), q)


if __name__ == '__main__':
    main()

enter image description here

Of course this is only a simple example; you probably want more error handling, stop nesting functions like crazy, etc etc. Also, there are also other ways to do IPC in python.

Last, but not least, think about if you really need two pygame displays, because multiprocessing adds a ton of complexity. I answered some pygame questions on SO already and almost always the OP was asking an XY question when asking about threading/multiprocessing with pygame.

Upvotes: 1

Related Questions