| name | pygame |
| description | [Applies to: **/*.py] Definitive guide for building modern, performant, and maintainable Pygame applications using `pygame-gui` for UI, focusing on structure, responsive layout, theme-driven styling, and performance best practices. |
| source | cursor_mdc |
Pygame Best Practices
This guide outlines the essential patterns for building robust and visually appealing Pygame applications, leveraging pygame-gui for all UI interactions. Adhere to these principles for maintainable, performant, and scalable game development.
1. Core Application Structure
Always structure your Pygame application with a clear main loop, a single UIManager, and a fixed timestep. Separate game logic from UI logic.
❌ BAD: Scattered UI element creation, no UIManager, variable framerate.
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Bad Example")
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.MOUSEBUTTONDOWN:
print("Mouse clicked!")
screen.fill((0, 0, 0))
pygame.draw.rect(screen, (255, 0, 0), (100, 100, 50, 50))
pygame.display.flip()
✅ GOOD: Dedicated UIManager, fixed timestep, clear event processing.
import pygame
import pygame_gui
from pygame_gui.core.interfaces import IUIManagerInterface
class Game:
def __init__(self, screen_size: tuple[int, int]):
pygame.init()
self.screen = pygame.display.set_mode(screen_size)
pygame.display.set_caption("Good Example")
self.manager: IUIManagerInterface = pygame_gui.UIManager(screen_size, 'data/themes/default.json')
self.clock = pygame.time.Clock()
self.is_running = True
self.fps = 60
pygame_gui.elements.UIButton(
relative_rect=pygame.Rect((350, 275), (100, 50)),
text='Click Me',
manager=self.manager
)
def run(self):
while self.is_running:
time_delta = self.clock.tick(self.fps) / 1000.0
self._handle_events()
self._update(time_delta)
self._draw()
pygame.quit()
def _handle_events(self):
for event pygame.event.get():
event. == pygame.QUIT:
.is_running =
event. == pygame_gui.UI_BUTTON_PRESSED:
()
.manager.process_events(event)
():
.manager.update(time_delta)
():
.screen.fill((, , ))
pygame.draw.rect(.screen, (, , ), (, , , ))
.manager.draw_ui(.screen)
pygame.display.flip()
__name__ == :
game = Game((, ))
game.run()
2. UI Management with pygame-gui
pygame-gui is the definitive choice for polished, interactive UIs.
2.1. Initialize UIManager Once
Create a single UIManager instance early in your application lifecycle.
self.manager: IUIManagerInterface = pygame_gui.UIManager(screen_size, 'data/themes/default.json')
2.2. Responsive Layouts with relative_rect and Anchors
Always use relative_rect for UI element positioning. Leverage anchors for responsive designs that adapt to window resizing and container movement. Avoid hardcoding absolute pixel positions.
❌ BAD: Hardcoded absolute positions, no responsiveness.
pygame_gui.elements.UIButton(
relative_rect=pygame.Rect(50, 50, 100, 30),
text='Button', manager=self.manager
)
✅ GOOD: Relative positioning and anchors for dynamic layouts.
pygame_gui.elements.UIButton(
relative_rect=pygame.Rect(0, 0, 100, 30),
text='Bottom Right',
manager=self.manager,
anchors={'right': 'right', 'bottom': 'bottom',
'left': 'right', 'top': 'bottom'}
).set_relative_position((-110, -40))
pygame_gui.elements.UIButton(
relative_rect=pygame.Rect(0, 0, 150, 50),
text='Centered',
manager=self.manager,
anchors={'center': 'center'}
)
2.3. Theme-Driven Styling with JSON
Externalize all UI styling (colors, fonts, sizes) into JSON theme files. This enables rapid visual iteration without code changes.
❌ BAD: Hardcoded colors and fonts throughout the code.
pygame_gui.elements.UIButton(
...
text='Red Button',
manager=self.manager,
object_id='#red_button'
)
✅ GOOD: Load a JSON theme file and use object_id for specific styling.
{
"button": {
"colours": {
"normal_bg": "#25292e",
"hovered_bg": "#35393e",
"pressed_bg": "#15191e",
"normal_text": "#FFFFFF"
},
"font": {
"name": "fira_code",
"size": "20",
"bold": "0",
"italic": "0"
}
},
"#red_button": {
"colours": {
"normal_bg": "#FF0000"
}
self.manager: IUIManagerInterface = pygame_gui.UIManager(screen_size, 'data/themes/default.json')
pygame_gui.elements.UIButton(
relative_rect=pygame.Rect((10, 10), (100, 30)),
text='Themed Button',
manager=self.manager,
object_id='#red_button'
)
3. Performance Considerations
3.1. Fixed Timestep
Always use pygame.time.Clock().tick(FPS) to cap your frame rate and calculate time_delta. This ensures consistent game speed across different hardware and is crucial for pygame-gui updates.
❌ BAD: No frame rate cap, variable time_delta.
while is_running:
for event in pygame.event.get():
pygame.display.update()
✅ GOOD: Consistent frame rate and time_delta.
self.clock = pygame.time.Clock()
self.fps = 60
while self.is_running:
time_delta = self.clock.tick(self.fps) / 1000.0
self.manager.update(time_delta)
pygame.display.update()
3.2. Avoid "Threaded Flip" Anti-pattern
Never call pygame.display.flip() or pygame.display.update() from a separate thread. All drawing operations must occur on the main thread to prevent crashes and undefined behavior.
4. Component Architecture
Encapsulate complex UI elements or game screens into their own classes. Each screen/component should manage its own pygame-gui elements and event handling.
from pygame_gui.core.interfaces import IUIManagerInterface
class MainMenuScreen:
def __init__(self, manager: IUIManagerInterface, screen_rect: pygame.Rect):
self.manager = manager
self.container = pygame_gui.elements.UIPanel(
relative_rect=screen_rect,
manager=manager,
anchors={'left': 'left', 'right': 'right', 'top': 'top', 'bottom': 'bottom'}
)
self.start_button = pygame_gui.elements.UIButton(
relative_rect=pygame.Rect(0, 0, 200, 50),
text='Start Game',
manager=self.manager,
container=self.container,
anchors={'center': 'center'}
)
self.exit_button = pygame_gui.elements.UIButton(
relative_rect=pygame.Rect(0, 60, 200, 50),
text='Exit',
manager=self.manager,
container=self.container,
anchors={'centerx': 'centerx', 'top_target': self.start_button}
)
def process_event() -> :
event. == pygame_gui.UI_BUTTON_PRESSED:
event.ui_element == .start_button:
()
event.ui_element == .exit_button:
()
():
.container.hide()
():
.container.show()
5. Type Hints
Always use type hints for clarity, maintainability, and to leverage static analysis tools. This is especially important for pygame and pygame-gui objects.
import pygame
from pygame_gui.core.interfaces import IUIManagerInterface
from typing import Tuple
def create_button(manager: IUIManagerInterface, rect: pygame.Rect, text: str) -> pygame_gui.elements.UIButton:
"""Creates a standard UI button."""
return pygame_gui.elements.UIButton(relative_rect=rect, text=text, manager=manager)
def get_screen_size(screen: pygame.Surface) -> Tuple[int, int]:
"""Returns the width and height of the screen."""
return screen.get_width(), screen.get_height()
6. pygame-gui vs pygame-menu
pygame-gui: Use for complex, interactive, in-game UIs (inventories, settings panels, dialogues). Its robust layout, theming, and event system are ideal for rich interfaces.
pygame-menu: Use for simple, quick-to-implement menus like start screens, pause overlays, or basic option screens where pygame-gui might be overkill. It offers a simpler API for common menu patterns.
For most projects, pygame-gui is the recommended default due to its flexibility and modern feature set.
7. Accessibility
Design your UI with accessibility in mind. Use clear, high-contrast themes, ensure interactive elements are sufficiently large, and provide keyboard navigation where appropriate. pygame-gui supports keyboard navigation and theme-driven text scaling, which are foundational for accessibility.
8. PEP 8 Adherence
Follow PEP 8 style guidelines rigorously. Consistent code is readable code. Use linters (e.g., Flake8, Black) to enforce this automatically.