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:
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:

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:


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
anddrag_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

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

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.