├── intro_ball.gif ├── README.md └── main.py /intro_ball.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexElvers/pygame-with-asyncio/HEAD/intro_ball.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pygame with asyncio 2 | This is a simple example how to connect pygame with asyncio. 3 | 4 | The example uses a queue for handling pygame events. It runs a loop (`pygame_event_loop`) that is requesting pygame events with blocking (`pygame.event.wait()`). Using `pygame.event.get()` would produce high CPU load because it is not blocking. This event loop is started with `run_in_executor` that uses a thread pool (you could set the size of the thread pool to 1). A direct call from the main thread would not work because asyncio expects that functions use `await` for blocking operations (or `yield from` in older Python versions). The asyncio queue is not thread safe, so you have to use `run_coroutine_threadsafe` for putting events on the queue. 5 | 6 | Handling events now works by using `await event_queue.get()` in a coroutine. 7 | 8 | ```python 9 | def pygame_event_loop(loop, event_queue): 10 | while True: 11 | event = pygame.event.wait() 12 | asyncio.run_coroutine_threadsafe(event_queue.put(event), loop=loop) 13 | ``` 14 | 15 | The image of the ball is copied from the pygame documentation. 16 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import time 5 | 6 | import pygame 7 | 8 | 9 | FPS = 100 10 | width, height = 700, 400 11 | 12 | 13 | class Ball: # using a Sprite would be better 14 | def __init__(self): 15 | self.ball = pygame.image.load("intro_ball.gif") 16 | self.rect = self.ball.get_rect() 17 | self.speed = [2, 2] 18 | 19 | def move(self): 20 | self.rect = self.rect.move(self.speed) 21 | if self.rect.left < 0 or self.rect.right > width: 22 | self.speed[0] = -self.speed[0] 23 | if self.rect.top < 0 or self.rect.bottom > height: 24 | self.speed[1] = -self.speed[1] 25 | 26 | def draw(self, screen): 27 | screen.blit(self.ball, self.rect) 28 | 29 | 30 | def pygame_event_loop(loop, event_queue): 31 | while True: 32 | event = pygame.event.wait() 33 | asyncio.run_coroutine_threadsafe(event_queue.put(event), loop=loop) 34 | 35 | 36 | async def animation(screen, ball): 37 | black = 0, 0, 0 38 | 39 | current_time = 0 40 | while True: 41 | last_time, current_time = current_time, time.time() 42 | # call usually takes a bit longer than ideal for framerate, so subtract from next wait 43 | # sleeptime = 1/FPS - delayed = 1/FPS - (now-last-1/FPS) 44 | # also limit max delay to avoid issues with asyncio.sleep() returning immediately for negative values 45 | await asyncio.sleep(min(1 / FPS - (current_time - last_time - 1 / FPS), 1 / FPS)) # tick 46 | ball.move() 47 | screen.fill(black) 48 | ball.draw(screen) 49 | pygame.display.flip() 50 | 51 | 52 | 53 | async def handle_events(event_queue, ball): 54 | while True: 55 | event = await event_queue.get() 56 | if event.type == pygame.QUIT: 57 | break 58 | elif event.type == pygame.KEYDOWN: 59 | if event.key == pygame.K_SPACE: 60 | if ball.speed == [0, 0]: 61 | ball.speed = [2, 2] 62 | else: 63 | ball.speed = [0, 0] 64 | else: 65 | print("event", event) 66 | asyncio.get_event_loop().stop() 67 | 68 | 69 | def main(): 70 | loop = asyncio.get_event_loop() 71 | event_queue = asyncio.Queue() 72 | 73 | pygame.init() 74 | 75 | pygame.display.set_caption("pygame+asyncio") 76 | screen = pygame.display.set_mode((width, height)) 77 | 78 | ball = Ball() 79 | 80 | pygame_task = loop.run_in_executor(None, pygame_event_loop, loop, event_queue) 81 | animation_task = asyncio.ensure_future(animation(screen, ball)) 82 | event_task = asyncio.ensure_future(handle_events(event_queue, ball)) 83 | try: 84 | loop.run_forever() 85 | except KeyboardInterrupt: 86 | pass 87 | finally: 88 | pygame_task.cancel() 89 | animation_task.cancel() 90 | event_task.cancel() 91 | 92 | pygame.quit() 93 | 94 | 95 | if __name__ == "__main__": 96 | main() 97 | --------------------------------------------------------------------------------