46  Mastering Player Input: Keyboard, Mouse, and Gamepad

In our previous lessons, we explored the foundations of game development with Pyxel, including loading sprites and creating movement. Today, we’re diving into the critical topic of player input - the bridge between your players and the virtual worlds you create. We’ll explore how to capture and respond to keyboard presses, mouse movements, and even gamepad controls!

46.1 Why Input Matters: The Player’s Connection

Input is how players communicate their intentions to your game. Well-designed input systems create a feeling of responsiveness and control that’s essential for an enjoyable gaming experience. Think about it - even the most beautiful game with the most engaging story will fail if the controls feel clunky or unresponsive.

Let’s explore the three main types of input available in Pyxel:

46.2 Keyboard Input: The Classic Control Scheme

The keyboard is the most common input device for PC games, offering many keys for different actions. Pyxel provides two primary functions for detecting key presses:

46.2.1 btn() vs btnp(): Understanding the Difference

Pyxel offers two main functions for keyboard input:

  1. pyxel.btn(key): Returns True as long as the specified key is being held down
    • Perfect for continuous actions like movement
    • Example: Moving a character while an arrow key is held
  2. pyxel.btnp(key): Returns True only on the first frame when a key is pressed
    • Perfect for one-time actions like jumping, shooting, or menu selection
    • Example: Firing a weapon when the spacebar is pressed

Let’s see both in action:

import pyxel

class KeyboardDemo:
    def __init__(self):
        pyxel.init(160, 120, title="Keyboard Input Demo")
        self.x = 80  # X position
        self.y = 60  # Y position
        self.color = 7  # White
        pyxel.run(self.update, self.draw)
    
    def update(self):
        # Continuous movement with btn()
        if pyxel.btn(pyxel.KEY_LEFT):
            self.x = max(self.x - 2, 0)
        
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.x = min(self.x + 2, 160)
        
        if pyxel.btn(pyxel.KEY_UP):
            self.y = max(self.y - 2, 0)
        
        if pyxel.btn(pyxel.KEY_DOWN):
            self.y = min(self.y + 2, 120)
        
        # One-time actions with btnp()
        if pyxel.btnp(pyxel.KEY_SPACE):
            # Change color when spacebar is pressed
            self.color = (self.color + 1) % 16
        
        # Quit the game
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
    
    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue
        
        # Draw a square that moves with arrow keys
        pyxel.rect(self.x - 4, self.y - 4, 8, 8, self.color)
        
        # Display instructions
        pyxel.text(5, 5, "Use arrow keys to move", 7)
        pyxel.text(5, 15, "Press SPACE to change color", 7)
        pyxel.text(5, 25, "Press Q to quit", 7)
        
        # Show status
        pyxel.text(5, 100, f"Position: ({self.x}, {self.y})", 7)
        pyxel.text(5, 110, f"Color: {self.color}", 7)

KeyboardDemo()

When you run this code, you’ll be able to move a square around the screen with the arrow keys (using btn() for continuous movement) and change its color with the space bar (using btnp() for a one-time action).

46.2.2 Key Constants: The Magic Words

Pyxel provides constants for all the keys you might want to use:

  • Direction keys: pyxel.KEY_UP, pyxel.KEY_DOWN, pyxel.KEY_LEFT, pyxel.KEY_RIGHT
  • Letter keys: pyxel.KEY_A through pyxel.KEY_Z
  • Number keys: pyxel.KEY_0 through pyxel.KEY_9
  • Special keys: pyxel.KEY_SPACE, pyxel.KEY_RETURN, pyxel.KEY_ESCAPE, etc.

You can find the full list in the Pyxel documentation.

46.2.3 Advanced Keyboard Techniques

46.2.3.1 Detecting Multiple Keys

Pyxel can handle multiple key presses simultaneously. This allows for diagonal movement and combined actions:

# Diagonal movement
if pyxel.btn(pyxel.KEY_UP) and pyxel.btn(pyxel.KEY_RIGHT):
    # Move diagonally up and right
    self.y = max(self.y - 1, 0)
    self.x = min(self.x + 1, 160)

46.2.3.2 Input Buffering

For games requiring precise timing, you might want to implement input buffering - accepting input slightly before an action is possible:

# Simple input buffer for a jump
if pyxel.btnp(pyxel.KEY_SPACE):
    self.jump_buffer = 10  # Allow jump within 10 frames

# Later in the update
if self.jump_buffer > 0:
    if self.on_ground:  # If character is on ground
        self.do_jump()  # Execute the jump
        self.jump_buffer = 0  # Reset buffer
    else:
        self.jump_buffer -= 1  # Decrease buffer timer

46.3 Mouse Input: Point and Click Adventures

Mouse input provides an intuitive way for players to interact with your game, especially for menus, strategy games, or point-and-click adventures.

46.3.1 Enabling Mouse Input

Before using the mouse, you need to enable it:

pyxel.mouse(True)  # Enable mouse

46.3.2 Reading Mouse Position and Clicks

Pyxel makes it easy to get the mouse position and detect clicks:

# Get mouse position
mouse_x = pyxel.mouse_x
mouse_y = pyxel.mouse_y

# Detect mouse clicks
left_click = pyxel.btn(pyxel.MOUSE_BUTTON_LEFT)
right_click = pyxel.btn(pyxel.MOUSE_BUTTON_RIGHT)
middle_click = pyxel.btn(pyxel.MOUSE_BUTTON_MIDDLE)

# For single clicks (not held down)
left_click_once = pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT)

Let’s create a simple drawing application to demonstrate mouse input:

import pyxel

class MouseDemo:
    def __init__(self):
        pyxel.init(160, 120, title="Mouse Input Demo")
        pyxel.mouse(True)  # Enable mouse cursor

        self.canvas_color = 1  # Dark blue
        self.drawing_color = 7  # White
        self.drawing = False

        pyxel.run(self.update, self.draw)

    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

        # Start drawing when left mouse button is pressed
        if pyxel.btn(pyxel.MOUSE_BUTTON_LEFT):
            self.drawing = True
            # Draw a pixel at the mouse position
            pyxel.pset(pyxel.mouse_x, pyxel.mouse_y, self.drawing_color)
        else:
            self.drawing = False

        # Change drawing color with right mouse button
        if pyxel.btnp(pyxel.MOUSE_BUTTON_RIGHT):
            self.drawing_color = (self.drawing_color + 1) % 16

    def draw(self):
        # Canvas has already been modified in update with pyxel.pset

        # Draw UI
        pyxel.rectb(0, 0, 160, 120, 5)  # Border

        # Display instructions
        pyxel.text(5, 5, "Left click to draw", 7)
        pyxel.text(5, 15, "Right click to change color", 7)
        pyxel.text(5, 25, "Press Q to quit", 7)

        # Show current color
        pyxel.rect(130, 5, 15, 15, self.drawing_color)
        pyxel.rectb(130, 5, 15, 15, 7)

        # Show mouse coordinates
        pyxel.text(5, 105, f"Mouse: ({pyxel.mouse_x}, {pyxel.mouse_y})", 7)

MouseDemo()

This creates a simple drawing application where you can draw with the left mouse button and change colors with the right mouse button.

46.3.3 Button Detection: Clicking on UI Elements

Let’s create a simple button class to demonstrate how to detect when the mouse is over a UI element:

import pyxel

class Button:
    def __init__(self, x, y, width, height, text, color):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.text = text
        self.color = color
        self.hover = False

    def update(self):
        # Check if mouse is over the button
        mouse_over = (self.x <= pyxel.mouse_x <= self.x + self.width and
                      self.y <= pyxel.mouse_y <= self.y + self.height)
        self.hover = mouse_over

        # Return True if clicked
        return mouse_over and pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT)

    def draw(self):
        # Draw button with different color when hovering
        if self.hover:
            button_color = self.color + 1
        else:
            button_color = self.color
        pyxel.rect(self.x, self.y, self.width, self.height, button_color)
        pyxel.rectb(self.x, self.y, self.width, self.height, 7)

        # Center text
        text_x = self.x + (self.width - len(self.text) * 4) // 2
        text_y = self.y + (self.height - 5) // 2
        pyxel.text(text_x, text_y, self.text, 0)

class UIDemo:
    def __init__(self):
        pyxel.init(160, 120, title="Button UI Demo")
        pyxel.mouse(True)  # Enable mouse cursor

        # Create some buttons
        self.buttons = [
            Button(30, 30, 40, 15, "Red", 8),
            Button(30, 50, 40, 15, "Green", 11),
            Button(30, 70, 40, 15, "Blue", 12)
        ]

        self.background_color = 1

        pyxel.run(self.update, self.draw)

    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

        # Update buttons and check for clicks
        if self.buttons[0].update():  # Red button
            self.background_color = 8

        if self.buttons[1].update():  # Green button
            self.background_color = 11

        if self.buttons[2].update():  # Blue button
            self.background_color = 12

    def draw(self):
        pyxel.cls(self.background_color)

        # Draw buttons
        for button in self.buttons:
            button.draw()

        # Display instructions
        pyxel.text(5, 5, "Click a button to change background", 7)
        pyxel.text(5, 105, "Press Q to quit", 7)

UIDemo()

This demonstrates a more complex use of mouse input for UI interaction, with buttons that respond to hovering and clicking.

46.4 Gamepad Input: The Console Experience

For a truly authentic retro gaming experience, Pyxel supports gamepads, including classic SNES controllers through adapters. The input functions work just like keyboard input!

46.4.1 Reading Gamepad Buttons

Pyxel uses the same btn() and btnp() functions for gamepad input, just with different constants:

# Check if A button is pressed on gamepad 1
if pyxel.btn(pyxel.GAMEPAD1_BUTTON_A):
    player_jump()

# Check if Direction Pad Right is held on gamepad 1
if pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_RIGHT):
    move_player_right()

# For single presses (not held down)
if pyxel.btnp(pyxel.GAMEPAD1_BUTTON_START):
    pause_game()

46.4.2 Gamepad Constants

Pyxel provides constants for standard gamepad buttons:

  • D-Pad: pyxel.GAMEPAD1_BUTTON_DPAD_UP, pyxel.GAMEPAD1_BUTTON_DPAD_DOWN, etc.
  • Action buttons: pyxel.GAMEPAD1_BUTTON_A, pyxel.GAMEPAD1_BUTTON_B, etc.
  • Shoulder buttons: pyxel.GAMEPAD1_BUTTON_SHOULDER_L, pyxel.GAMEPAD1_BUTTON_SHOULDER_R
  • Menu buttons: pyxel.GAMEPAD1_BUTTON_START, pyxel.GAMEPAD1_BUTTON_SELECT

Pyxel supports up to 8 controllers by changing the number in the constant (e.g., GAMEPAD2_BUTTON_A for the second controller).

46.4.3 Two-Player Example

Let’s create a simple two-player movement demo using both keyboard and gamepad inputs:

import pyxel

class TwoPlayerDemo:
    def __init__(self):
        pyxel.init(160, 120, title="Two-Player Demo")

        # Player 1 (keyboard)
        self.p1_x = 40
        self.p1_y = 60
        self.p1_color = 8  # Red

        # Player 2 (gamepad)
        self.p2_x = 120
        self.p2_y = 60
        self.p2_color = 12  # Blue

        pyxel.run(self.update, self.draw)

    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

        # Update Player 1 (keyboard)
        #
        # Note that `min` and `max` compare a series of numbers. Their use
        # here prevents the player from moving beyond the screen edge
        if pyxel.btn(pyxel.KEY_LEFT):
            self.p1_x = max(self.p1_x - 2, 0)

        if pyxel.btn(pyxel.KEY_RIGHT):
            self.p1_x = min(self.p1_x + 2, 160)

        if pyxel.btn(pyxel.KEY_UP):
            self.p1_y = max(self.p1_y - 2, 0)

        if pyxel.btn(pyxel.KEY_DOWN):
            self.p1_y = min(self.p1_y + 2, 120)

        # Update Player 2 (gamepad)
        if pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_LEFT):
            self.p2_x = max(self.p2_x - 2, 0)

        if pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_RIGHT):
            self.p2_x = min(self.p2_x + 2, 160)

        if pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_UP):
            self.p2_y = max(self.p2_y - 2, 0)

        if pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_DOWN):
            self.p2_y = min(self.p2_y + 2, 120)

    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue

        # Draw Player 1
        pyxel.rect(self.p1_x - 4, self.p1_y - 4, 8, 8, self.p1_color)
        pyxel.text(self.p1_x - 2, self.p1_y - 2, "1", 7)

        # Draw Player 2
        pyxel.rect(self.p2_x - 4, self.p2_y - 4, 8, 8, self.p2_color)
        pyxel.text(self.p2_x - 2, self.p2_y - 2, "2", 7)

        # Display instructions
        pyxel.text(5, 5, "Player 1: Arrow Keys", 8)
        pyxel.text(85, 5, "Player 2: Gamepad", 12)
        pyxel.text(5, 105, "Press Q to quit", 7)

TwoPlayerDemo()

This example allows two players to control separate characters - one using the keyboard and the other using a gamepad.

This example allows the player to use either keyboard or gamepad interchangeably, providing flexibility in control options.

46.5 Practice Time: Your Input Control Quest

Now it’s your turn to create a Pyxel application using different types of input. Try these challenges:

  1. Create a program that displays different shapes based on which key is pressed (e.g., ‘C’ for circle, ‘R’ for rectangle)

  2. Make a simple menu system that can be navigated with either keyboard arrow keys or mouse clicks

  3. Create a drawing application that uses different colors based on which mouse button is pressed

Here’s a starting point for your quest:

import pyxel

class MyInputDemo:
    def __init__(self):
        pyxel.init(160, 120, title="My Input Demo")
        pyxel.mouse(True)  # Enable mouse

        # Initialize your variables here

        pyxel.run(self.update, self.draw)

    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

        # Handle various input methods
        # Your code here

    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue

        # Draw your visuals based on input
        # Your code here

        # Display instructions
        pyxel.text(5, 5, "Your instructions here", 7)

# Create and start your demo
MyInputDemo()

46.6 Common Bugs to Watch Out For

As you experiment with different input methods in Pyxel, watch out for these common issues:

  1. Forgetting to Enable Mouse: If your mouse input isn’t working, make sure you’ve called pyxel.mouse(True) to enable the mouse cursor.

  2. Using btnp() for Movement: Using btnp() for movement will result in jerky, step-by-step motion. Use btn() for continuous actions like movement.

  3. Input Conflicts: When allowing multiple input methods, be careful about conflicting controls. For example, if the up arrow controls a character but also navigates a menu, you might need to track game states.

  4. Missing Input Frames: Pyxel runs at a fixed frame rate. If your game logic is complex, you might miss some input frames. Consider using input buffers for critical actions.

  5. Boundary Checking: Always include boundary checks when moving objects based on input to prevent them from moving off-screen.

  6. Gamepad Connectivity: If gamepad input isn’t working, make sure your controller is properly connected and recognized by your operating system before starting Pyxel.

  7. Key Repeat Rates: Operating system key repeat settings may affect how btn() behaves for held keys. This is usually not a problem but something to be aware of.

46.7 Conclusion and Resources for Further Exploration

You’ve now learned how to capture and respond to keyboard, mouse, and gamepad input in your Pyxel games. These skills form the foundation of player interaction, allowing you to create responsive and engaging game experiences.

To further enhance your input handling skills, check out these resources:

  1. Pyxel GitHub Documentation - Official documentation for all Pyxel functions, including detailed input handling.

  2. Game Feel: A Game Designer’s Guide to Virtual Sensation - An excellent book on creating responsive controls in games.

  3. Input Buffering in Games - A deeper exploration of advanced input techniques.

  4. UI Design for Game Developers - Great resource for designing interfaces that respond to player input.

In our next lesson, we’ll explore animations and flipping sprites to bring even more life to your games. Keep practicing with different input methods – responsive controls are the key to creating games that feel satisfying to play!