Skip to main content
rule

pygame — Cursor rule: game structure and pygame-gui UI

A Cursor rule that sets the skeleton of a Pygame project: one UIManager, a fixed timestep, anchor-based layout and JSON themes.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/pygame.mdc
Open on GitHub

What pygame does

The .cursor/rules/pygame.mdc file attaches to Cursor and fires on Python files matching **/*.py. Inside is a set of patterns: a game class with a main loop, separate methods for events, update and draw, and a single UIManager from pygame-gui created at startup.

For the interface the rule asks for relative_rect and anchors instead of hardcoded pixel coordinates, so buttons survive a window resize. Styling moves into a JSON theme file, and individual elements get an object_id such as #red_button. Frame rate is capped with pygame.time.Clock().tick(FPS), and the resulting time_delta goes into manager.update — skip that and UI animations drift on displays with different refresh rates.

Every section comes as a pair: a bad example with UI logic scattered through the loop, then a working one. If you already use a different UI layer, say pygame_menu, the examples need rewriting — the rule is written around pygame-gui.

What it helps with

  • set up a game skeleton with separate event, update and draw methods
  • move button colours and fonts into a JSON theme instead of editing code
  • build a UI that does not fall apart when the window is resized
  • tie game speed to a fixed FPS rather than the machine's power
  • stop creating UI elements inside the main loop

Example prompts for the agent

  • Scaffold a Pygame game following the pygame rule: one UIManager, fixed timestep
  • Rewrite this button with relative_rect and anchors as the pygame rule requires
  • Move the button colours and font into a JSON theme and wire it up via object_id

How to install pygame

Manually: put the files in place

.cursor/rules/pygame.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about pygame

How do I install the pygame rule in Cursor?

Create .cursor/rules/pygame.mdc at the root of your project and paste the rule content into it. Cursor picks it up for files matching **/*.py.

What does the pygame rule do?

It sets the app structure: a main loop with a fixed FPS, a single UIManager from pygame-gui, anchor-based layout and styles in a JSON theme. Each point ships with a bad and a good example.

Do I need pygame-gui for this rule?

Yes, every UI example is built on pygame-gui. If you use another UI layer, the examples will not fit as-is.

Source text

pygame.mdc · CC0-1.0
---
description: 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.
globs: **/*.py
---
# 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.

```python
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
        # UI logic directly in main loop
        if event.type == pygame.MOUSEBUTTONDOWN:
            print("Mouse clicked!")

    screen.fill((0, 0, 0))
    # Draw game elements
    pygame.draw.rect(screen, (255, 0, 0), (100, 100, 50, 50))
    pygame.display.flip()
```

✅ GOOD: Dedicated `UIManager`, fixed timestep, clear event processing.

```python
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

        # Example UI button
        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 in pygame.event.get():
            if event.type == pygame.QUIT:
                self.is_running = False
            if event.type == pygame_gui.UI_BUTTON_PRESSED:
                print("Button pressed!")
            self.manager.process_events(event)

    def _update(self, time_delta: float):
        self.manager.update(time_delta)
        # Update game logic here

    def _draw(self):
        self.screen.fill((0, 0, 0))
        # Draw game elements
        pygame.draw.rect(self.screen, (255, 0, 0), (100, 100, 50, 50))
        self.manager.draw_ui(self.screen)
        pygame.display.flip()

if __name__ == '__main__':
    game = Game((800, 600))
    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.

```python
# In your Game class or main setup
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.

```python
# Button position will be fixed regardless of window size or parent container
pygame_gui.elements.UIButton(
    relative_rect=pygame.Rect(50, 50, 100, 30), # Absolute position
    text='Button', manager=self.manager
)
```

✅ GOOD: Relative positioning and anchors for dynamic layouts.

```python
# Button anchored to the bottom-right of its container, with a 10px offset
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'} # Stretch to maintain size
).set_relative_position((-110, -40)) # Offset from bottom-right corner

# Button centered in its container
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.

```python
# In your code
pygame_gui.elements.UIButton(
    ...
    text='Red Button',
    manager=self.manager,
    object_id='#red_button' # Still needs object_id for theming, but no theme loaded
)
# No theme loaded, so default styles apply or you'd set them programmatically.
```

✅ GOOD: Load a JSON theme file and use `object_id` for specific styling.

```json
// data/themes/default.json
{
    "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"
        }
    }
}
```

```python
# In your Game class or main setup
self.manager: IUIManagerInterface = pygame_gui.UIManager(screen_size, 'data/themes/default.json')

# In your UI creation
pygame_gui.elements.UIButton(
    relative_rect=pygame.Rect((10, 10), (100, 30)),
    text='Themed Button',
    manager=self.manager,
    object_id='#red_button' # Applies styles from the theme file
)
```

## 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`.

```python
# ...
while is_running:
    # No clock.tick(), game runs as fast as possible
    # time_delta would need to be calculated differently or omitted, leading to issues
    for event in pygame.event.get():
        # ...
    # ...
    pygame.display.update()
```

✅ GOOD: Consistent frame rate and `time_delta`.

```python
# ...
self.clock = pygame.time.Clock()
self.fps = 60
# ...
while self.is_running:
    time_delta = self.clock.tick(self.fps) / 1000.0 # Essential for consistent updates
    self.manager.update(time_delta) # UIManager relies on 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.

```python
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), # Offset from start_button
            text='Exit',
            manager=self.manager,
            container=self.container,
            anchors={'centerx': 'centerx', 'top_target': self.start_button} # Anchor to another element
        )

    def process_event(self, event: pygame.event.Event) -> bool:
        if event.type == pygame_gui.UI_BUTTON_PRESSED:
            if event.ui_element == self.start_button:
                print("Start Game Pressed!")
                return True
            if event.ui_element == self.exit_button:
                print("Exit Game Pressed!")
                return True
        return False

    def hide(self):
        self.container.hide()

    def show(self):
        self.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.

```python
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.

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

Everything for Game dev and 3D: Unity, Unreal, Godot, Blender →

From the same set69

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor