Skip to content
Feniks Development
  • Start Here
  • Glossary
  • Tools
  • Resources
  • What’s New
  • About
  • Contact
Toggle the button to expand or collapse the Menu

Remapping Controls

  1. Home>
  2. Feniks Tools>
  3. Remapping Controls

Easy Blinking

  • EasyBlink Class
  • EasyBlink Examples

Controller Support Expansion

  • What is the Controller Support Expansion?
  • How do I…? + Common Issues
  • Controller Viewport
  • Controller Bar
  • Virtual Cursor
  • Virtual Keyboard
  • StickEvent
  • KeyController and focused_on
  • FocusDisplayable
  • Remapping Controls
  • Controller and Keyboard Icons
  • Configuration Variables
  • Screen Actions and Values
  • Helper Functions and Classes
  • Engine Override Notes

Sound Disabler and Captions Tool

  • Disabling Sounds and Sound Categorization
View Categories
  • Home
  • Feniks Tools
  • Controller Support Expansion
  • Remapping Controls

Remapping Controls

14 min read

The Controller Support Expansion for Ren’Py also includes the ability for players to remap gamepad buttons, and for developers to add custom events to be used across their game. Pick up the tool from itch.io if you haven’t already:

Contents hide
Adding Custom Events
event_name
title
keysyms
category
compatible_categories
extra_compatibility
required
repeatable
remappable
priority
Examples
Page left/right
Opening the History Screen
Extra Menu Actions
Scroll Shortcut
Input Events
The Default Keymap
Remapping Screens
controller_remap and listen_remap
_gamepad_select and _gamepad_control

Adding Custom Events

You will use the special function pad_remap.add_custom_event to add custom controller events to your game. Let’s look at an example:

init python:
    pad_remap.add_custom_event(
        event_name="cancel",
        title=_("Cancel/Return{#pad_remap}"),
        keysyms=["pad_b_press"],
        category="menu",
        extra_compatibility=["game_menu"],
        required=True,
        repeatable=False,
        remappable=True,
        priority=11,
        )

We’ll walk through the different arguments below.

event_name

The name of the event, which will be used on your buttons. You are in charge of actually handling these events in your game, e.g. by using key or keysym in your screens, or the special icon_button displayable. This event can be remapped as with other events if you mark it as remappable=True. You must then use the method pad_config.get_event("your_event_name") to get the correct event name e.g. key pad_config.get_event("cancel") Note that key "cancel" on its own will not work as it is a custom event.

The event name should be a unique string, typically in snake_case.

e.g. event_name="quest_log"

title

A human-readable name of this event. If this event is remappable, this title will be displayed in the remapping screen, to explain to the player what action they are remapping to a new key. You may want to add a translation tag so you know this is for the gamepad remapping e.g. _("Cancel/Return{#pad_remap}")

e.g. title=_("Open Quest Log")

keysyms

A list of what buttons will trigger this event. These should be strings like “pad_x_press” as seen in DEFAULT_BINDINGS in controller_remap.rpy. You can also check the Ren’Py page on Customizing the Keymap, though note not every possible keysym is listed here (e.g. none of the “release” variants are listed). You should include repeat events in here, if relevant, but only one of press/release is needed for most events.

e.g. keysyms=["pad_rightshoulder_press"]

category

The category is one of “in-game”, “menu”, “situational”, or “always”, with the following meanings:

“in-game”

Events that only occur in-game and won’t conflict with menu events, such as rollback or skip. For example, unless you’re in-game, the skip button won’t start skipping on the main menu/the skip button has no meaning outside of the game.

“menu”

Events that only occur in menus and won’t conflict with in-game events, such as a cancel event, or an event that changes save pages (page_left).

“situational”

Events that only occur in specific situations and won’t conflict with in-game or menu events, such as input events or save deletion.

“always”

Events that can be used anywhere and would conflict with any other button, such as game_menu or button_select.

e.g. category="in-game"

Here is a chart to help assist with categorizing your events. This logic may not perfectly apply to all events, but should serve as a good starting point:

Text version:

Does the event do anything during dialogue? Yes/No

Does the event do anything in the main menu or preferences screens? Yes/No

If answered Yes to both: Category “always”

If answered Yes to the first and No to the second: Category “in-game”

If answered No to the first and Yes to the second: Category “menu”

If answered No to both: Category “situational”

compatible_categories

Optional. If provided, this should be a list of category names (as strings) that this event is compatible with (using the event categories as listed above). That is, this event could be mapped to the same button as other events in the provided categories and not cause problems.

If not provided, this will be automatically filled with typical compatible categories (e.g. in-game events are compatible with situational and menu events, but not “always” events). For most events, this can safely be handled automatically if the category is correct. If set to False, no automatic compatibility will be added.

e.g. compatible_categories=["menu", "situational"]

extra_compatibility

Optional. If provided, this should be a list of events that this event is compatible with. This can be used to fine-tune compatibility alongside compatible_categories. The two will be added together.

e.g. extra_compatibility=["save_delete"]

required

Optional. If True, this event is required to have a button mapped to it, and the game will not save a remapped control set which does not have a button mapped to this event. Default is False.

Set this to True if there would be no other way to perform a required action in the game if this event is not mapped. Some things are not required – for example, it’s fine if the player does not have a button mapped to rollback if they don’t want to use rollback. However, it might be impossible to navigate a preferences screen if page_left isn’t mapped, as that’s the only way to switch between the different settings tabs.

e.g. required=True

repeatable

Optional. If True, this event should repeat when the button is held down. Default is False. Some actions, like rollback, should execute multiple times when the rollback button is held down. Most events should only occur once per button press. False by default.

e.g. repeatable=True

remappable

Optional. If True, this event will show up in the remapping screen and can be remapped by the player. Default is False. Ensure you have the other properties set (title, category, compatibility, remappable, repeatable) if the player can remap an event, to ensure they can’t remap themselves into making the game unplayable or remapping conflicting events to the same button. False by default.

e.g. remappable=True

priority

Optional. If provided, this indicates where the event should appear in the remappable events list. Lower priorities appear before higher ones. The default priorities are listed below.

You can use the priority number to ensure your event will appear at the beginning of the list, or between particular events. If not provided, the event will appear at the end of the list.

Default priorities:

REMAPPABLE_EVENTS = [
        (_("Confirm{#pad_remap}"), "button_select", 10),

        ## Feniks note: You can add back the button_alternate event if you
        ## have buttons with alternate actions.
        # (_("Alternate Action{#pad_remap}"), "button_alternate", 20),
        ####

        (_("Advance dialogue{#pad_remap}"), "dismiss", 30),
        (_("Toggle Auto-Advance{#pad_remap}"), "toggle_afm", 40),
        (_("Game Menu{#pad_remap}"), "game_menu", 50),
        (_("Skip{#pad_remap}"), "toggle_skip", 60),

        (_("Rollback{#pad_remap}"), "rollback", 70),
        (_("Roll-Forward{#pad_remap}"), "rollforward", 80),
        (_("Hide UI{#pad_remap}"), "hide_windows", 90),
        (_("Screenshot{#pad_remap}"), "screenshot", 100),

        (_("Delete Saves{#pad_remap}"), "save_delete", 110),
        (_("Accessibility{#pad_remap}"), "accessibility", 120),
        (_("Self-Voicing{#pad_remap}"), "self_voicing", 130),
        (_("Fast Skip{#pad_remap}"), "fast_skip", 140),
        (_("Quit{#pad_remap}"), "quit", 150),

    ]

The third number in the tuple is the priority number (so, the priority of the button_select “Confirm” event is 10).

e.g. priority=65 (this would put it between the Skip and Rollback events).

Examples

Besides the initial example, we’ll briefly look at some of the other examples already included in the pack. Note that, for convenience, the examples below omit the init python: block at the top, so all of these are technically

init python:
    pad_remap.add_custom_event(...)

Page left/right

## These next two are used for custom page left/right actions.
pad_remap.add_custom_event("page_left", _("Page Left{#pad_remap}"),
    ["pad_leftshoulder_press"], "menu", required=True, remappable=True,
    priority=61)
pad_remap.add_custom_event("page_right", _("Page Right{#pad_remap}"),
    ["pad_rightshoulder_press"], "menu", required=True, remappable=True,
    priority=62)

These events are used across the default template in order to switch tabs on menu screens. For example, they are used on the Preferences screen:

A screenshot of the preferences screen in a Ren'Py game. Four tabs are seen at the top, with L1 to the left and R1 to the right, as shortcuts for cycling to the previous and next tabs respectively.

By default, L1 and R1 switch between the different preferences tabs. These are required, since without a button mapped to these, controller users wouldn’t be able to switch tabs. They are “menu” category events because they occur out-of-game, so they won’t conflict with an in-game event like rollback.

Opening the History Screen

## This is a custom event for opening the history screen. See the quick
## menu in dialogue_screens.rpy for the shortcut.
pad_remap.add_custom_event("history", _("Open History{#pad_remap}"),
    ["pad_lefttrigger_pos"], "in-game", required=False, remappable=True,
    priority=51)

This is a custom event to open the history log screen. It’s in-game only, since it won’t do anything while on a menu screen. It isn’t required to be mapped to a button; if a player gets rid of their ability to open the history log, they can still progress the game.

Extra Menu Actions

## This is a custom event for extra menu actions, like syncing save data
## or resetting preferences to the defaults.
pad_remap.add_custom_event("extra_menu", _("Sync Save Data/Reset to Default"),
    ["pad_y_press"], "menu", required=True, remappable=True,
    priority=115)

This is a special button which is used for extra actions in menu screens. For example, in the default template, it is used to sync save data, reset preferences to their defaults, and also to open the remapping screen (the latter is why it’s required to be mapped to something). It’s only used in menu screens.

Scroll Shortcut

## This is a custom event for viewport scrolling shortcuts. It is not
## remappable. It is used in 01_controller_vp.rpy to jump the viewport
## scrolling to the top or bottom.
pad_remap.add_custom_event("scroll_shortcut", _("Scroll Shortcut{#pad_remap}"),
    ["pad_rightshoulder_press"], "situational", required=False, remappable=False)

This is a custom button which must be held down to jump to the beginning or end of a Controller Viewport. By default, it is not remappable, nor required.

Input Events

pad_remap.add_custom_event("input_shift", _("Shift{#pad_remap}"),
    ["pad_lefttrigger_pos"], "situational", required=False, remappable=False)
pad_remap.add_custom_event("input_page", _("Switch Input Page{#pad_remap}"),
    ["pad_leftstick_press"], "situational", required=False, remappable=False)
pad_remap.add_custom_event("input_space", _("Spacebar{#pad_remap}"),
    ["pad_y_press"], "situational", required=False, remappable=False,
    repeatable=True)

There are also three custom events for the Virtual Keyboard – namely, the button which activates the shift key, the button which inputs a space, and the button which switches between input sets (by default, the qwerty keyboard and a page of symbols). None of these are required, as there are buttons directly on the virtual keyboard which can be navigated to and pressed instead. They are not remappable for this reason, though you could make them remappable if you so desire.

The Default Keymap

Controller Support Expansion for Ren’Py adjusts the default mapping to be slightly different from the default found in Ren’Py (see the Ren’Py docs). The defaults are as follows:

A visual representation of the button assignments seen in the DEFAULT_BINDINGS declaration.
A visual representation of the default bindings in the Controller Support Expansion
A visual representation of the button assignments seen in default Ren'Py as of version 8.3
A visual representation of the default bindings in Ren’Py as of version 8.3

Controller image courtesy of LambdaLighthouse on itch.io.

pad_remap.DEFAULT_BINDINGS = {
    ## SHOULDER BUTTONS
    ## LEFT SHOULDER (L1)
    "pad_leftshoulder_press" : ["rollback", "input_left"],
    "repeat_pad_leftshoulder_press" : ["rollback", "input_left"],
    "pad_leftshoulder_release" : [],

    ## RIGHT SHOULDER (R1)
    "pad_rightshoulder_press" : ["rollforward", "input_right"],
    "repeat_pad_rightshoulder_press" : ["rollforward", "input_right"],
    "pad_rightshoulder_release" : [],

    ## TRIGGERS
    ## LEFT TRIGGER (L2)
    "pad_lefttrigger_pos" : [], # Used for the custom history log event
    "repeat_pad_lefttrigger_pos" : [],
    "pad_lefttrigger_zero" : [],

    ## RIGHT TRIGGER (R2)
    "pad_righttrigger_pos" : ["toggle_skip", "input_enter"],
    "repeat_pad_righttrigger_pos" : [],
    "pad_righttrigger_zero" : [],

    ## BUTTONS
    ## A BUTTON
    "pad_a_press" : ["dismiss", "button_select", "bar_activate", "bar_deactivate", "drag_activate", "drag_deactivate"],
    "repeat_pad_a_press" : [],
    "pad_a_release" : [],

    ## B BUTTON
    "pad_b_press" : [], # Used for the custom cancel event
    "repeat_pad_b_press" : [],
    "pad_b_release" : [],

    ## X BUTTON
    "pad_x_press" : ["hide_windows", "save_delete", "input_backspace"],
    "repeat_pad_x_press" : ["input_backspace"],
    "pad_x_release" : [],

    ## Y BUTTON
    "pad_y_press" : ["toggle_afm"],
    "repeat_pad_y_press" : [],
    "pad_y_release" : [],

    ## D-PAD
    ## LEFT
    "pad_dpleft_press" : [ "focus_left", "bar_left", "viewport_leftarrow" ],
    "repeat_pad_dpleft_press" : [ "focus_left", "bar_left", "viewport_leftarrow" ],
    "pad_dpleft_release" : [],

    ## RIGHT
    "pad_dpright_press" : ["focus_right", "bar_right", "viewport_rightarrow"],
    "repeat_pad_dpright_press" : ["focus_right", "bar_right", "viewport_rightarrow"],
    "pad_dpright_release" : [],

    ## UP
    "pad_dpup_press" : ["focus_up", "bar_up", "viewport_uparrow"],
    "repeat_pad_dpup_press" : ["focus_up", "bar_up", "viewport_uparrow"],
    "pad_dpup_release" : [],

    ## DOWN
    "pad_dpdown_press" : ["focus_down", "bar_down", "viewport_downarrow"],
    "repeat_pad_dpdown_press" : ["focus_down", "bar_down", "viewport_downarrow"],
    "pad_dpdown_release" : [],

    ## STICKS
    ## LEFT STICK
    "pad_leftstick_press" : ["accessibility"],
    "repeat_pad_leftstick_press" : [],
    "pad_leftstick_release" : [],

    "pad_leftx_pos" : ["focus_right", "bar_right", "viewport_rightarrow"],
    "repeat_pad_leftx_pos" : ["focus_right", "bar_right", "viewport_rightarrow"],
    "pad_leftx_neg" : ["focus_left", "bar_left", "viewport_leftarrow"],
    "repeat_pad_leftx_neg" : ["focus_left", "bar_left", "viewport_leftarrow"],
    "pad_lefty_pos" : ["focus_down", "bar_down", "viewport_downarrow"],
    "repeat_pad_lefty_pos" : ["focus_down", "bar_down", "viewport_downarrow"],
    "pad_lefty_neg" : ["focus_up", "bar_up", "viewport_uparrow"],
    "repeat_pad_lefty_neg" : ["focus_up", "bar_up", "viewport_uparrow"],

    ## RIGHT STICK
    "pad_rightstick_press" : ["fast_skip"],
    "repeat_pad_rightstick_press" : [],
    "pad_rightstick_release" : [],

    "pad_rightx_pos" : ["focus_right", "bar_right", "viewport_rightarrow"],
    "repeat_pad_rightx_pos" : ["focus_right", "bar_right", "viewport_rightarrow"],
    "pad_rightx_neg" : ["focus_left", "bar_left", "viewport_leftarrow"],
    "repeat_pad_rightx_neg" : ["focus_left", "bar_left", "viewport_leftarrow"],
    "pad_righty_pos" : ["focus_down", "bar_down", "viewport_downarrow"],
    "repeat_pad_righty_pos" : ["focus_down", "bar_down", "viewport_downarrow"],
    "pad_righty_neg" : ["focus_up", "bar_up", "viewport_uparrow"],
    "repeat_pad_righty_neg" : ["focus_up", "bar_up", "viewport_uparrow"],

    ## SELECT
    "pad_back_press" : ["screenshot"],
    "repeat_pad_back_press" : [],
    "pad_back_release" : [],

    ## HOME
    "pad_guide_press" : [],
    "repeat_pad_guide_press" : [],
    "pad_guide_release" : [],

    ## START
    "pad_start_press" : ["game_menu"],
    "repeat_pad_start_press" : [],
    "pad_start_release" : [],
}

Important things about the DEFAULT_BINDINGS dictionary (expanded on below):

  • Nearly every button aside from navigation differs from the default Ren’Py mappings
  • DEFAULT_BINDINGS can only include valid default Ren’Py events, not custom ones
  • It does not include a button mapped to alternate button actions
  • The toggle_skip and drag_activate/drag_deactivate events are special so they can be used with the preference options which remove press-and-hold requirements (see Configuration Variables)

Basically every button except for the d-pad, stick directions, and confirm button has been adjusted from the default Ren’Py version. pad_remap.DEFAULT_BINDINGS must only include valid Ren’Py events, and CANNOT include custom events (hence why buttons such as the B button and left trigger are left blank, as custom actions are mapped to those). A backend system handles combining the custom and default events into one dictionary that is saved and remembered in persistent across game launches. As long as you’ve set up your custom events with pad_config.add_custom_event, they will be included in the combined mappings dictionary.

Also of note is that the default mapping does not include a button for alternate button actions (i.e. the action that runs when the alternate property is used). If your game includes alternate button actions that are required to be able to play the game, you should map a button to the "button_alternate" event.

Finally, the toggle_skip and drag_activate/drag_deactivate events are handled specially. The variable persistent.hold_to_skip controls whether the button with the event "toggle_skip" must be held down to skip, or if it simply needs to be pressed to turn on skipping, and pressed again to turn it off.

Similarly, "drag_activate" and "drag_deactivate" should be assigned to the same button (in this case, "pad_a_press"). These will automatically be handled according to the value of persistent.hold_to_drag – if True, that button must be held down to drag, and releasing the button drops the drag. If False, the button can be pressed to pick up the drag, and pressed again to release it.

Remapping Screens

controller_remap and listen_remap

A screenshot of the in-game remapping screen. It has a column of event names beside three spaces to remap a button to. A button at the top says "Calibrate Gamepad Buttons" and below it "Change Icon Set".

There are several screens used as part of the user-facing remapping process. The first of these is accessed through Help -> Gamepad tab -> Pressing the “extra menu” button (controller) or the “Remap Controls” button at the bottom of the screen. This screen is called controller_remap, and it can be found in controller_remap_screens.rpy. You can restyle this screen however you like. It is crucial, however, that you give each of the remap buttons in the grid a unique ID and use the same actions for remapping in order for focus to be saved and restored properly.

The second screen used as part of the remapping process is the listen_remap screen. This screen prompts the player to press a button to use for remapping. The process is generally:

  • Click a slot next to the event you’d like to remap on the controller_remap screen
  • The listen_remap screen appears, telling you to press the button you want to remap that action to
  • You press a new button, which is then added to that event, and are returned to the controller_remap screen

_gamepad_select and _gamepad_control

A calibration screen. The textbox reads "Calibrating PS4 Controller (1/21)". "Press or move the Cross button."

These screens are relocated from the engine to controller_remap_screens.rpy so they may be styled to suit your game. They appear when you click “Calibrate Gamepad Buttons” on the controller_remap screen. The first of these, _gamepad_select, prompts the player to choose which controller they wish to calibrate. The second is the screen seen above, which guides the player through pressing each of the buttons on the controller to calibrate it. The text shown to the player is also adjusted and available in the REMAP_DESCRIPTIONS just above the screen declaration.

Updated on February 9, 2025
FocusDisplayableController and Keyboard Icons

Leave a Reply Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© Copyright – Feniks with OceanWP
Close Menu