Feniks Tools Archive - Feniks Development https://feniksdev.com/tools/ Coding tutorials, tips, and tricks Fri, 14 Feb 2025 00:33:51 +0000 en-US hourly 1 https://wordpress.org/?v=6.7.5 https://feniksdev.com/wp-content/uploads/2022/05/feniks_shawna-150x150.png Feniks Tools Archive - Feniks Development https://feniksdev.com/tools/ 32 32 Disabling Sounds and Sound Categorization https://feniksdev.com/tool/disabling-sounds-and-sound-categorization/?utm_source=rss&utm_medium=rss&utm_campaign=disabling-sounds-and-sound-categorization https://feniksdev.com/tool/disabling-sounds-and-sound-categorization/#respond Fri, 14 Feb 2025 00:33:50 +0000 https://feniksdev.com/?post_type=docs&p=3548 Sound Disabler and Captions Tool for Ren’Py provides players with a way to toggle off individual in-game sounds to customize their experience. Find the tool here: Sound Disabler and Captions Tool for Ren’Py by Feniks, Just Write Studios This tool was created in collaboration with Alaric from Just Write Studios, who has written the following […]

The post Disabling Sounds and Sound Categorization appeared first on Feniks Development.

]]>
Sound Disabler and Captions Tool for Ren’Py provides players with a way to toggle off individual in-game sounds to customize their experience. Find the tool here:

This tool was created in collaboration with Alaric from Just Write Studios, who has written the following guide to categorizing sounds for your game.

An example screenshot of the Sound Disabler and Captions Tool. It has categories for Animals, Footsteps, Glass, and UI sounds.

Overview

Providing players with the option to disable SFX can go a long way for their quality of life! For individuals with misophonia, hyperacusis, or any other sound-related concerns, providing options at the outset of your game can help them to enjoy playing it without needless distress. Even for individuals without major auditory concerns, this accessibility feature allows any player to enjoy your game without the extreme all-or-nothing option of hearing none of your SFX or dealing with one (or more) that they dislike.

This sort of feature is meaningless if players cannot understand how to recognize problematic SFX (and thereby use this tool to turn them on or off). It is up to you, the developer, to describe sounds accurately. Playtesting your game and its audio is vital to ensure that players understand how you describe and implement this tool.

I strongly encourage you to also do your own research, and look for ways to improve on these tips! There are few games that allow this degree of audio customization, but I look forward to seeing much more innovation in the field, given it is incredibly worth the effort—for many players (myself included), this sort of feature makes or breaks if they can play your game!

Categorizing Sounds

Here are a couple of pointers for clear SFX naming to help you get started:

  • Use clear language. Accessibility is not the place to be vague. “Gore” is not very helpful, because that could mean almost anything. “Crunchy bone break” is specific, evocative, and much more helpful.
  • Describing the intensity of the noise is helpful. “Tinkling glass” easily conveys a light and mellow noise. “Car windshield shattering” conveys a loud and harsh noise. Be specific.
  • Even if you have a category for loud noises, emphasize their volume. “Screaming bloody murder” implies much more volume than a “Quick yelp of pain.”
  • Repetitive noises and long noises can be very bothersome to many individuals. Label them appropriately, even if you have an ambient audio channel. “Birds chirping loop” might be unbearable for a player, whereas a “Single owl hoot” might be fine. Don’t assume, just be clear.
  • When it comes to dynamic or complex ambient noise, such as a crowded city street, clarity is just as vital. A crowd with low voices talking and no other sounds can be described as such. A crowded city street with low, talking voices, as well as a screaming baby occasionally, and vehicles driving by, would benefit from a clear description of those elements too.
  • The bottom line is don’t assume your player’s needs! Give them concise and evocative info that they need to be informed and enjoy your game fully.

Depending on the number of SFX in your game, you might not need categories at all. For games with many SFX, I recommend making clear categories to help players with faster navigation in your accessibility screen. Clarity and ease of use is key!

Below are several suggestions, based on common triggers for misophonia and hyperacusis. The items listed in each category are short examples of what types of sounds would be appropriate for that given category, but be sure to use clarity and evocative language when naming your own SFX! You also may find that your game benefits from other categories, which is great. If there is overlap, how you handle that is up to you—there isn’t a wrong way to do this so long as you are clear.

Category Suggestions

Suggestions for Categories (feel free to innovate and improve on these!):

Animal Noises

  • Birds chirping, dogs barking, crickets, animal grooming, etc.

Bodily Functions

  • Breathing, throat clearing, swallowing, coughing, heart beating, lip smacking, chewing, sniffling, slurping, vomit, etc.

Gore

  • Bones breaking, impact (punches, kicks), visceral sounds (implied mutilation or bleeding), etc.

High-pitched Noises

  • Tinnitus effects, car alarms, sirens, screaming, whistling, etc.

Metal/Mechanical

  • Whirring, humming, screeching, clinking, etc.

Repetitive Sounds

  • Tapping, flicking, ticking, dripping, nail filing or clipping, stomping, etc.

Sudden and/or Loud Noises

  • Car crash, avalanche, door slamming, dropping an object, etc.

Water

  • Rain, dripping, running water (rivers, sinks), flushing toilet, waterfall, splashing, pouring tea, etc.

Voices

  • Many games put voice effects on their own audio channel, or you may have separate code for voice barks, but it is worth mentioning that these also greatly benefit from accessibility options!
  • Laughter, gasping, murmuring, and any other human voice could fall in this category.

Execution

(Section written by Feniks) If you don’t have an AAudioData declaration set up for a sound, the tool will automatically copy a blank version to your clipboard when starting the game. That will generally look like:

AAudioData("footsteps_wet_2", categories=["footsteps"], path="audio/sfx/footsteps/footsteps_wet_2.mp3",
    caption=_(""), description=None)

You can then fill this out with appropriate information. Note that the only field that should be left unchanged is the path field – you are free to update categories, caption, description, and the initial name as well. The name (the first string) is used to declare a variable in the audio namespace. So, the above would declare audio.footsteps_wet_2, so you can do play sound footsteps_wet_2. You may want to rename this to something shorter, like just wet_footsteps.

In the Accessible Audio tool, sounds are labelled for the exclusion system using the description field. For example:

AAudioData("wet_footsteps", categories=["footsteps", "repetitive", "water"], path="audio/sfx/footsteps/footsteps_wet_2.mp3",
    caption=_("footsteps"), description=_("footsteps on a wet surface"))
A screenshot of a page to toggle in-game sounds on and off. There are several categories - Background Music, Footsteps, Repetitive Sounds, Sudden Sounds, and UI. The Footsteps category is open with the sound footsteps on leaves turned off and the sound footsteps on a wet surface checked.

This will appear in the excluded sounds screen with the label “footsteps on a wet surface”. It will appear in the categories for “footsteps”, “repetitive”, and “water” sounds. The description field can be used to provide a more specific description than what might be needed for an audio caption – here, the audio caption is simply “footsteps”.

You can also provide translatable names for the categories with the aaudio.CATEGORY_NAMES dictionary:

define aaudio.CATEGORY_NAMES["repetitive"] = _("Repetitive Sounds")

Final Notes

Feel free to message Alaric on Discord (alaricxx) or Bluesky (@just-write-studios.itch.io) with suggestions on how to improve this info, and to do your own work on improving visual novel design! More people being able to play games is always a good thing.

The post Disabling Sounds and Sound Categorization appeared first on Feniks Development.

]]>
https://feniksdev.com/tool/disabling-sounds-and-sound-categorization/feed/ 0
How do I…? + Common Issues https://feniksdev.com/tool/how-do-i-common-issues/?utm_source=rss&utm_medium=rss&utm_campaign=how-do-i-common-issues https://feniksdev.com/tool/how-do-i-common-issues/#respond Sun, 09 Feb 2025 18:48:28 +0000 https://feniksdev.com/?post_type=docs&p=3304 Learn how to fix common issues and improve controller support for your Ren'Py game with scrolling viewports, virtual cursors, and more.

The post How do I…? + Common Issues appeared first on Feniks Development.

]]>
With something as large as the Controller Support Expansion for Ren’Py, it can be difficult to find the right tool to do what you need in your game. This page is designed to help you find the right resources to solve whatever problem you have. Pick up the tool from itch.io if you haven’t already:

Navigation and Focus Problems

Sometimes Ren’Py will not co-operate when you want to focus a particular button. Maybe it’s skipping over a button in a vbox, or it’s hard to see which button is currently focused.

Issue: Skipping over buttons in a grid or hbox/vbox

If you’re having issues with focus skipping over buttons in a grid or an hbox/vbox, some of the following solutions may work:

  • spacing

First and most basic: negative spacing usually has focus issues. If it’s possible to set the spacing of your box or grid to a positive number, often focus skipping issues will go away.

  • keyboard_focus_insets

A property found in v8.3+ of Ren’Py. Documentation here. This can be used like:

vbox:
    textbutton _("Start") action Start():
        keyboard_focus_insets (0, 10, 0, 10)
    textbutton _("Load") action ShowMenu("load"):
        keyboard_focus_insets (0, 10, 0, 10)

It will reduce the “hitbox” of the button so they aren’t overlapping in Ren’Py’s internal focus calculations (overlapping buttons tend to be skipped when it comes to focusing another button). In the above example, it shrinks the hitbox by 10 pixels at the top and 10 pixels at the bottom (it goes (left, top, right, bottom) in clockwise order, like Frame or padding).

  • penalty property of controller_viewport

If you’re using a Controller Viewport, you can make the penalty property smaller to reduce how much items to the left/right of items in a column, or above/below items in a row, are penalized when Ren’Py is figuring out what to focus next. The default penalty value is 1024, so try something smaller like 128.

If your layout is not symmetrical, you may be having focus issues because of the cache_reverse property of Controller Viewport. Set cache_reverse False to turn this property off.

If you’re having trouble with specific button relationships, you can use the KeyController class (and its screen language version, focused_on) to tell Ren’Py exactly which button should be focused when certain directions are pressed.

Depending on what’s in your screen, a virtual cursor may work best for keyboard and controller users. This is especially helpful for unusual layouts like flow charts or point-and-click investigation.

Issue: Directions focusing the wrong thing

Another common issue is something like “if I hit left while focused on the “Schedule” button, it should select the “Inventory” button”. If these two buttons aren’t exactly aligned in something like a grid or hbox, Ren’Py may have trouble figuring out the relationship between them.

In this case, you can use the focused_on screen element to declare these relationships (see KeyController and focused_on). Otherwise, the Virtual Cursor may also be helpful. See the previous section on skipping over buttons if your buttons are in a container like a grid or vbox/hbox.

Issue: Focusing something I don't want focused

If you’ve got a button or bar that the player shouldn’t be able to navigate to with the keyboard or controller at all, use keyboard_focus False on that element (see the docs here). This is often the case for scrollbars, and sometimes for buttons that can be focused with the mouse but which have a controller or keyboard shortcut.

If you’re using the Controller Viewport, you might need the trap_focus property to prevent focus from leaving the viewport, or the aborb_events property to make sure focus events aren’t passed to the rest of the UI.

You can also use focused_on to specify focus relationships between buttons, or make sure that focus doesn’t leave a button in a particular direction. See KeyController and focused_on. Note that you can also use focused_on to “absorb” an action e.g.

focused_on "desk" key "focus_right" action NullAction()

This means that when right is pressed while focused on the button with the ID “desk”, nothing will happen (NullAction).

Issue: Viewport not scrolling

First, make sure you’re using the Controller Viewport. Regular viewports don’t scroll when focusing the children inside them, or scroll with the controller sticks.

Second, if your viewport has children inside them that you’re trying to focus so that the viewport scrolls, make sure the vscroll_style and/or hscroll_style are set to something besides None. None is the default, which means the viewport will not scroll to reposition itself if a child is focused. vscroll_style "center" and hscroll_style "center" are usually a good value to start with.

Otherwise, if your viewport doesn’t have anything in it but you want to use the controller sticks to scroll it, make sure to set which_stick to one of “left”, “right”, or “both”. Otherwise the sticks will not move it! You can also set arrowkeys to "not sticks" or "keyboard" to make it scroll with the keyboard + dpad or just the keyboard, respectively. This is often helpful in combination with which_stick so keyboard users can scroll as well.

Viewports

How do I make a viewport scroll with the controller sticks/How do I make a viewport scroll when I focus things inside it?

To make a viewport scroll with the controller sticks, you’ll use the Controller Viewport. If the viewport has buttons and other content in it that you can focus, you’ll need to set the vscroll_style and/or hscroll_style properties to something besides None so the viewport will scroll while you navigate it. If the viewport doesn’t have focusable content inside it (e.g. it’s like the History Log viewport), then set which_stick to “left”, “right”, or “both” so that stick can scroll the viewport. You may also want to set focus_scroll False.

User Interface (UI)

How do I change the UI for controllers?

By default, the pad_config.INPUT_TYPE_CALLBACKS (see Configuration Variables) callbacks include a function which will refresh the screen when the input type changes to/from a controller. This means you can wrap parts of your screen with a check for pad_config.is_using_controller() e.g.

screen quick_menu():
    ## Ensure this appears on top of other screens.
    zorder 100

    if pad_config.is_using_controller():
        if quick_menu:
            use quick_menu_controller()

    elif quick_menu:

        hbox:
            style_prefix "quick"

            textbutton _("Back") action Rollback()
            textbutton _("History") action ShowMenu('history')
            textbutton _("Skip") action Skip() alternate Skip(fast=True, confirm=True)
            textbutton _("Auto") action Preference("auto-forward", "toggle")
            textbutton _("Save") action ShowMenu('save')
            textbutton _("Prefs") action ShowMenu('pref_display')

Here, you can see that there’s a check for pad_config.is_using_controller(), which then uses a different screen for the quick menu.

You might also consider taking advantage of Ren’Py’s variant system – see Screen Variants. Of particular note for controller layouts is the "steam_deck" variant.

You can also read up on Controller and Keyboard Icons, which are set up to respond to the currently used input type as well.

How do I show controller icons?

To show controller icons, you’ll usually use the controller_icon displayable, which you can read more about here: Controller and Keyboard Icons. You may also use the functions pad_config.get_icons or pad_config.get_inline_icons to get the icons associated with a particular event.

How do I change controller icon sets?

By default, all of the icons used for controllers will automatically change when the controller layout changes, so you don’t have to write any kind of if/else statement for it. To change which layout is used, however, you’ll typically use the CycleControllerLayout or SetControllerLayout actions.

Virtual Cursor

How do I use a controller/the arrow keys to control a cursor?

To move around a virtual cursor with the controller sticks or keyboard arrow keys, you’ll use the Virtual Cursor.

How do I make the virtual cursor move to a particular location?

You’ll use the MoveVirtualCursor action. If you want the physical cursor to move somewhere too, you may want to use this in a list with MouseMove.

Other

How do pause the game when a controller is disconnected?

Some platforms like Steam want your game to pause when a controller is disconnected. To do this, you’ll use the pad_config.CONTROLLER_CONNECT_CALLBACKS and pad_config.CONTROLLER_DISCONNECT_CALLBACKS (see Callbacks on the Configuration Variables page). In the default template, these are set to functions which will temporarily pause the game with a message that the controller has been disconnected, and dismiss said message if the controller is reconnected.

The post How do I…? + Common Issues appeared first on Feniks Development.

]]>
https://feniksdev.com/tool/how-do-i-common-issues/feed/ 0
What is the Controller Support Expansion? https://feniksdev.com/tool/what-is-the-controller-support-expansion/?utm_source=rss&utm_medium=rss&utm_campaign=what-is-the-controller-support-expansion https://feniksdev.com/tool/what-is-the-controller-support-expansion/#respond Sun, 09 Feb 2025 17:41:26 +0000 https://feniksdev.com/?post_type=docs&p=3510 Learn what the Controller Support Expansion for Ren'Py can do and how to use it to improve controller and keyboard support for your games.

The post What is the Controller Support Expansion? appeared first on Feniks Development.

]]>
Controller Support Expansion for Ren’Py is a set of tools that can be added to an existing Ren’Py project to improve support for controllers (and keyboard navigation). It can be found on itch.io for free:

Below are some common questions about the tool and what it can do!

What features does the controller support expansion have?

There are several major features, as well as many quality-of-life (QoL) upgrades. The main features are:

  • Controller-friendly viewport (Controller Viewport). It can be scrolled smoothly with the controller sticks, and if you navigate to items inside the viewport with a controller or arrow keys, it will automatically scroll the viewport to keep those items in view.
    • By default, if you want to focus an out-of-view item in a viewport in default Ren’Py, you’d need to navigate to a scroll bar, scroll so the item is visible, and then navigate over to it.
  • A virtual cursor that can be moved around with the controller sticks and keyboard arrow keys. It can be used like a regular mouse is to highlight and activate buttons, bars, drag and drop, and more. (Virtual Cursor)
  • Controller-friendly bars (Controller Bar).
    • By default, when you navigate to a bar with a controller or the arrow keys, you have to press the confirm button to “activate” the bar before it can be adjusted, and then press the select button again to deactivate the bar.
    • The controller-friendly version skips this activate/deactivate step and makes the bar immediately available to adjust when it’s focused.
  • An on-screen keyboard that will optionally show up if the player is using a controller to handle input, so players with a controller can type their name without needing a keyboard. (Virtual Keyboard)
  • A method to specify an action that should happen if a key is pressed while a particular button is focused. This includes instructions like “if the dresser is focused and the player hits ‘up’, focus the mirror” to help solve focus issues, and also more complex gameplay features like “if the player hovers ‘Eileen’ and hits ‘i’, bring up the ‘investigate’ dialogue”. (KeyController and focused_on)
  • A displayable that follows the currently focused item to help highlight it. This can be something like a pointer finger or a frame surrounding the currently focused button. It’s particularly helpful when turning a UI made for a mouse & keyboard setup into a controller-friendly one, as it helps the player see where the focus currently is. (FocusDisplayable)
  • The ability for players to remap controller buttons to different actions, and for developers to add custom controller events. (Remapping Controls)
  • A robust system for displaying button icons to the player based on the current input type (mouse/keyboard/controller, with 5 different controller icon sets). (Controller and Keyboard Icons)
  • A displayable that can be used to assign actions to a controller stick. (StickEvent)
  • Many, many other quality of life features for things like saving and restoring focus on menu screens, callbacks for when controllers are disconnected and reconnected, several preferences to customize stick sensitivity and dead zones, and more.

Where would I use this?

The controller support expansion is designed to be dropped into existing or new Ren’Py projects to improve controller and keyboard support. The various tools are designed to work with both controllers and with a mouse, and often have features to switch functionality on the fly depending on the input type (e.g. hiding the Virtual Cursor when a mouse is used, or only showing the Virtual Keyboard when the last input was with a controller).

With a few exceptions, most of the included tools do require you to update your screens to add the new features (e.g. swapping a bar for a controller_bar (Controller Bar)). See the next question for more on that.

What do I need to do to improve controller support with this addon?

There are a few major pitfalls when it comes to controller support for games which can be solved with tools from this expansion:

  • Viewports (Controller Viewport)
    • Swap out your viewport code for controller_viewport so that either the sticks can freely scroll the viewport or focusing items inside it will automatically scroll the viewport
    • Make any of your scrollbars keyboard_focus False so they don’t get in the way of navigation
  • Bars (Controller Bar)
    • Swap out your bar for controller_bar and vbar for controller_vbar (except for scrollbars; see above)
    • Note that this may require some adjustments to your UI layout; see the section on Design in the Controller Bar page.
  • Menu screens
  • Input
    • If you use renpy.input, you can leave it as-is, since it’s automatically overwritten to use the universal_input function instead.
    • If you have a screen with an InputValue on it, swap it out for UniversalInputToggle.
    • Optional – update the styling for the virtual keyboard to match your game.
  • Other
    • Add a section to your preferences screen to adjust things like controller stick inversion, sensitivity, and icon set selection. The pack comes with an example template that has a preferences screen with these options. See also Screen Actions and Values.
    • Add a way to get to the button remapping screen included in the pack, so players can remap the controller button. See also Remapping Controls.
    • If you’ve got any drag and drop features in your game, use the Virtual Cursor.
    • Consider making controller-specific variants of screens, such as the quick menu, which have Controller and Keyboard Icons instead of a set of buttons to navigate to and press. Function calls like pad_config.is_using_controller() will help here – see Helper Functions and Classes.

Above all, be sure to test your game with a controller and keyboard as much as possible to make sure it is simple and intuitive to navigate!

Does this work with the Steam Deck?

Yes, it will work with a Steam Deck! It helps improve the usability of all non-mouse/touchscreen input types; the Steam Deck, being a device with analogue sticks, benefits from these usability improvements.

Of note for Steam releases as well are the controller connect/disconnect callbacks (see Configuration Variables) which can be used to pause the game when a controller is disconnected for better Steam controller support compliance.

Does this expansion only support controllers? What about keyboards?

The Controller Support Expansion improves focus issues for any non-mouse/touch input type, including keyboards. Tools such as the Virtual Cursor include the option to use the keyboard keys to move the cursor, as well as the analogue sticks. Players can readily switch between mouse, keyboard, and controller input at any time during gameplay and the tool has features designed to handle these changes seamlessly.

The post What is the Controller Support Expansion? appeared first on Feniks Development.

]]>
https://feniksdev.com/tool/what-is-the-controller-support-expansion/feed/ 0
Screen Actions and Values https://feniksdev.com/tool/screen-actions-and-values/?utm_source=rss&utm_medium=rss&utm_campaign=screen-actions-and-values https://feniksdev.com/tool/screen-actions-and-values/#respond Sat, 08 Feb 2025 22:35:12 +0000 https://feniksdev.com/?post_type=docs&p=3283 Learn about the various screen actions and bar values used in the controller support expansion for Ren'Py for better controller support.

The post Screen Actions and Values appeared first on Feniks Development.

]]>
There are several screen actions included across the Controller Support Expansion for Ren’Py. They are linked here on this page for convenience.

If you haven’t picked up the tool yet, you can do so here:

Virtual Cursor

MoveVirtualCursor(id, x, y, duration=0, warper=None)

A screen action which will move the virtual cursor to the provided location. See the docs for Virtual Cursors.

Virtual Keyboard

InputLetter(id, letter, layer=None)

InputLetter is a screen action which can be used to add a letter to a ControllerInput. See the docs for the Virtual Keyboard.

ShowOnscreenInput(screen=None, transition=None, *args, **kwargs)

A screen action which will show an onscreen keyboard for controller input. See the docs for the Virtual Keyboard.

UniversalInputToggle(id, **kwargs)

UniversalInputToggle is a screen action which can be used to toggle the provided InputValue if the player is using mouse and keyboard, or if the player is using a controller, it will show the virtual keyboard to enter input via ShowOnscreenInput. See the docs for the Virtual Keyboard.

Controller Configuration & Layout

SetStickInversion(stick, axis, value)

A screen action which sets the provided stick’s axis’s inversion to the given value. In other words, something like SetStickInversion("left", "x", True) inverts the x axis of the left stick.

stick is one of “left” or “right”. Which controller stick to invert the axis of.

axis is one of “x” or “y”. Which axis to invert.

value is one of True or False. True means the axis is inverted (e.g. left becomes right and right becomes left). False means the axis is normal (left is left and right is right).

ToggleStickInversion(stick, axis)

As with SetStickInversion above, except it toggles the inversion on and off rather than setting it to one specific value.

CycleControllerLayout(reverse=False)

An action that cycles between the different controller layouts (“xbox”, “playstation”, “nintendo”, “steam”, and “generic”). reverse=True cycles through in reverse order.

SetControllerLayout(layout)

An action that directly sets the controller layout to the provided layout. layout must be one of “xbox”, “playstation”, “nintendo”, “steam”, or “generic”.

Other

SetFocus(screen, id, layer=”screens”)

A screen action wrapper for renpy.set_focus. Takes the string name of a screen (e.g. "main_menu") and the ID of a displayable to focus on that screen, and focuses that displayable.

Bar Values

StickDeadzoneAdjustment(stick=”left”)

A Bar Value which modifies the dead zone for the provided stick. stick is one of “left” or “right” depending on which stick’s dead zone is being adjusted. This can be provided on a bar e.g.

controller_bar value StickDeadzoneAdjustment("left")

See also: Controller Bar

StickSensitivityAdjustment(stick=”left”)

A Bar Value which modifies the sensitivity multiplier of the provided stick. stick is one of “left” or “right” depending on which stick’s sensitivity multiplier is being adjusted. This can be provided on a bar e.g.

controller_bar value StickSensitivityAdjustment("right")

See also: Controller Bar

The post Screen Actions and Values appeared first on Feniks Development.

]]>
https://feniksdev.com/tool/screen-actions-and-values/feed/ 0
Controller and Keyboard Icons https://feniksdev.com/tool/controller-and-keyboard-icons/?utm_source=rss&utm_medium=rss&utm_campaign=controller-and-keyboard-icons https://feniksdev.com/tool/controller-and-keyboard-icons/#respond Sat, 08 Feb 2025 22:11:22 +0000 https://feniksdev.com/?post_type=docs&p=3452 Learn how to declare icons for controller buttons and keyboard shortcuts in Ren'Py to make navigation easy for players.

The post Controller and Keyboard Icons appeared first on Feniks Development.

]]>
Controller Support Expansion for Ren’Py includes several features to improve controller and keyboard support in Ren’Py. Pick up the tool from itch.io if you haven’t already:

To make it easy to communicate to a player what buttons can be pressed to perform different actions, Controller Support Expansion for Ren’Py comes with a special IconButton class to simplify displaying gamepad or keyboard icons to the player. The gamepad icons come in several layouts – Xbox, PlayStation, Nintendo, Steam, and a generic layout. These can be switched by the player at any time and are automatically handled when using the IconButton class.

Examples

Example 1

First, let’s briefly look at an example of what we’re working towards:

The help screen for keyboards. It shows various keyboard shortcuts. There is a Q and E icon to switch Help pages, and a Back and Select icon in the bottom right corner.
The help screen for the mouse. It shows various keyboard shortcuts. There is a Q and E icon to switch Help pages, and a Back and Select icon in the bottom right corner.
The help screen for gamepads. It shows various keyboard shortcuts. There is a L and R icon to switch Help pages, and a Back and Select icon in the bottom right corner.

From left to right, the screens shown above are what the game looks like when played with a keyboard, with a mouse, and with a controller. Of particular note are the two buttons used for switching tabs (Q and E on mouse/keyboard, and L/R for controller), and the two icons at the bottom right corner of the screen (ESC and Enter for keyboard, ESC and left mouse button for mouse, and B and A for Controller – plus Y for remapping controls). Let’s look at the code for the page tabs first.

hbox:
    ## The icon shortcut to change pages
    icon_button caption _("Previous Page") kind icn.page_left:
        action CycleScreenVariable("device", tabs, reverse=True)
        icon_only True
    ## Buttons to change pages; can be clicked with the mouse, but
    ## not focused with arrow keys
    textbutton _("Keyboard") keyboard_focus False:
        action SetScreenVariable("device", "keyboard")
    textbutton _("Mouse") keyboard_focus False:
        action SetScreenVariable("device", "mouse")
    textbutton _("Gamepad") keyboard_focus False:
        action SetScreenVariable("device", "gamepad")
    ## The icon shortcut to change pages
    icon_button caption _("Next Page") kind icn.page_right:
        action CycleScreenVariable("device", tabs)
        icon_only True

In particular, it uses the special icon_button displayable to display the Q and E/L and R icons. Let’s look at the one for changing the page left:

## The icon shortcut to change pages
icon_button caption _("Previous Page") kind icn.page_left:
    action CycleScreenVariable("device", tabs, reverse=True)
    icon_only True

First is the property caption. This is the text displayed next to the button icon if applicable, or the alt text if the caption is not shown. In this case, the caption is hidden because icon_only True means the icon only has the icon image, not the text caption as well.

Next is the property kind. This is a special kind of property that’s meant to speed up declaring icons of a similar type. If you provide an IconButton to the kind property, the icon_button will inherit properties from it unless they are overwritten by other properties. In this case, the declaration for icn.page_left can be found in controller_icons.rpy near the bottom of the file:

define icn.page_left = IconButton(pad_event="page_left", key_event=["Q", "q"], 
    caption=_("Previous"), mkb_icon="key_q")

First up we have pad_event. This should be set to an event name as found in persistent.pad_bindings – that is, it needs to either be a standard Ren’Py event, or a custom one (as added in Remapping Controls). "page_left" is a custom event that was added for this expansion pack.

Next is key_event – this should be a list of valid keyboard key events as described in Customizing the Keymap. Here, the chosen keyboard key for page left is Q.

caption is as described before – it’s the text shown next to the button icon, or if the text is not shown, the alt text for the icon. Notably, because the icon_button in the screen above uses the caption property, it takes priority over the one declared here.

Finally, mkb_icon stands for “mouse/keyboard icon”. It’s the icon image that will be used for the mouse/keyboard. Images in the pad_config.ICON_FOLDER (see Configuration Variables) that start with "mouse_" or "key_" are automatically declared. Here, we want the icon for the Q key.

So, back to the icon_button – due to kind icn.page_left, it inherits the pad_event, key_event, and mkb_icon properties from that Iconbutton. The last property we haven’t covered is action, which is the action to perform when the correct shortcut is pressed, or the icon button itself is clicked. So, in this case, it’s the action to perform when the Q key on the keyboard is pressed, or the page_left button for a controller (which is the left shoulder button by default). In this case, it cycles the Help screen tabs in reverse.

Example 2

The help screen for gamepads. It shows various keyboard shortcuts. There is a L and R icon to switch Help pages, and a Back and Select icon in the bottom right corner.

Also in the screens for Example 1 are button prompts found in the bottom right corner. Those are handled with icon_button, and organized using a special screen called key_footer:

use key_footer():
    if GamepadExists():
        icon_button kind icn.reset suffix "small":
            caption _("Remap Controls")
            action ShowMenu("controller_remap")
    icon_button kind icn.menu_return suffix "small"
    icon_button kind icn.select suffix "small"

All the key_footer class does is have an hbox with styling to put the icons in the bottom right corner of the screen.

First up is the icon button for the “Remap Controls” shortcut. This one inherits from icn.reset, which is also found in controller_icons.rpy with the following definition:

define icn.reset = IconButton("extra_menu", ["r", "R"], _("Reset"), "key_r")

In particular, it uses the custom “extra_menu” event, and is mapped to the R key on a keyboard.

The icon_button overwrites the caption to be more specific to what this button will do on this screen – namely, that it will take the player to the control remapping screens. The action property is here as well, with the action to go to the controller_remap screen. The last new property is suffix "small" – this appends a suffix onto the icon image names. By default, all mouse/keyboard/controller icons have a _small variant that is a suitable size for these kinds of footers.

The second icon_button is the Back button. Nearly all the properties are inherited from the icn.menu_return IconButton, which uses the escape keyboard key and the custom “cancel” event. It also has the action MenuReturn() which is a special action that helps manage saving and restoring focus between the game/main menus and in-game.

The final icon_button is the Select button. It inherits from icn.select, whose declaration is as follows:

define icn.select = IconButton("button_select",
    ## The key event doesn't use button_select because it would absorb
    ## mouseup events, interfering with using screens with a mouse.
    key_event=['K_RETURN', 'K_KP_ENTER', 'K_SELECT'],
    caption=_("Select"),
    mkb_icon="mkb_select")

In particular, the mkb_icon is an icon called “mkb_select”, which is declared earlier in the controller_icons.rpy file as:

image mkb_select = FocusTypeDisplayable("mouse_left", "key_enter")

Briefly, this is a special kind of displayable whose image is updated in real-time without requiring a screen refresh whenever the input type changes. It takes three arguments – a mouse_img, a keyboard_img, and a controller_img (any of which can be omitted, as in this case where it’s only used for mouse and keyboard). By default, the screen is refreshed only when switching to and from controller input, so switching from mouse to keyboard or vice-versa won’t update the icons. If using a FocusTypeDisplayable, however, the image will update when the focus type changes. In particular, this image switches between a left mouse button icon to the enter key.

Properties

IconButton (and its screen language version, icon_button) take several properties to define the behaviour of the button and its appearance.

pad_event

Required. This should be the string name of a gamepad event, either a custom one or a built-in one. See Remapping Controls for more on adding custom events.

e.g. pad_event "screenshot"

key_event

Similar to pad_event, this should be the string name of a key, or a list of such key names, as described in Customizing the Keymap. These can also be event names as used in config.keymap.

e.g. key_event ['K_ESCAPE', 'mouseup_3']

e.g. key_event "game_menu"

caption

The text to display alongside the button icon, if icon_only is False. In that case, the caption is used for the alt text of the icon.

e.g. caption _("Return")

mkb_icon

Short for “mouse and keyboard icon”. The icon that will be used when the mouse or keyboard is the last used input type. Should be a string corresponding to an image name, or a list of such strings. In order to work with the suffix property, this needs to be a declared image which can have suffixes added onto the end of it.

e.g. mkb_icon "key_escape"

action

The action to perform when this button is clicked, or one of the various keys is pressed (e.g. if key_event="K_ESCAPE", then this action will happen when the Escape key is pressed, or if the icon_button on the screen is clicked with the mouse.

e.g. action Return()

activate_sound

A sound to play when the action is executed.

e.g. activate_sound "boop.ogg"

suffix

A suffix to add onto the end of the images used for this icon. Most common is simply "small". If mkb_icon="key_q" then this will use the image "key_q_small" for the mouse and keyboard.

e.g. suffix "small"

use_keysym

If True, the default, the icon_button will automatically listen for and handle key presses corresponding to the event(s) it is listening for. If False, the icon may be pressed with the mouse to execute its action, but will not respond to any of its shortcuts being pressed. In most cases this should stay as True, unless there is some kind of redundancy in your screen or the key presses are situational in some way.

e.g. use_keysym False

icon_only

If True, the icon_button’s caption will not be shown next to the button icon.

e.g. icon_only True

keymap_only

If True, the icon and caption will not be shown, but the icon_button will still listen for events that match the pad_event or key_event values given to it, and execute its action where appropriate.

e.g. keymap_only True

hide_on_mouse

If True, the icon_button will just be a Null displayable when the mouse is being used (i.e. it will be invisible). False by default.

e.g. hide_on_mouse True

hide_on_keyboard

If True, the icon_button will just be a Null displayable when the keyboard is being used (i.e. it will be invisible). False by default.

e.g. hide_on_keyboard True

hide_on_controller

If True, the icon_button will just be a Null displayable when a controller is being used (i.e. it will be invisible). False by default.

e.g. hide_on_controller True

kind

kind takes another IconButton object and will inherit all the properties from it, except where those properties have been overwritten in the declaration for this icon button.

e.g. kind icn.select

hbox_ properties

You may also pass in properties prefixed with hbox_, which will be passed to the hbox that’s used to contain the icon and text. See Box Style Properties.

e.g. hbox_spacing 50

text_ properties

You may also pass in properties prefixed with text_, which will be passed to the caption text. See Text Style Properties.

e.g. text_color "#FFF"

button properties

You may also pass in button style properties, which will be passed to the containing button. See Button Style Properties.

e.g. padding (20, 20)

FocusTypeDisplayable

This is a special kind of displayable which will change its image in real-time based on the last used input type. It has three arguments:

mouse_img

The image used when the mouse is the last used input type. Can be any kind of displayable.

keyboard_img

The image used when the keyboard is the last used input type. Can be any kind of displayable.

controller_img

The image used when the controller is the last used input type. Can be any kind of displayable.

e.g. image cancel_img = FocusTypeDisplayable("mouse_right", "key_escape", "pad_b")

The post Controller and Keyboard Icons appeared first on Feniks Development.

]]>
https://feniksdev.com/tool/controller-and-keyboard-icons/feed/ 0
Remapping Controls https://feniksdev.com/tool/remapping-controls/?utm_source=rss&utm_medium=rss&utm_campaign=remapping-controls https://feniksdev.com/tool/remapping-controls/#comments Fri, 07 Feb 2025 01:47:13 +0000 https://feniksdev.com/?post_type=docs&p=3398 Learn how to let players remap controls for the controller in Ren'Py, and how to set up custom events for remapping.

The post Remapping Controls appeared first on Feniks Development.

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

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.

The post Remapping Controls appeared first on Feniks Development.

]]>
https://feniksdev.com/tool/remapping-controls/feed/ 5
StickEvent https://feniksdev.com/tool/stickevent/?utm_source=rss&utm_medium=rss&utm_campaign=stickevent https://feniksdev.com/tool/stickevent/#respond Thu, 06 Feb 2025 04:19:37 +0000 https://feniksdev.com/?post_type=docs&p=3384 Learn how to assign actions to controller sticks in Ren'Py and get information on the sticks to use for minigames, UI elements, and more.

The post StickEvent appeared first on Feniks Development.

]]>
Controller Support Expansion for Ren’Py includes several features to improve controller and keyboard support in Ren’Py. Pick up the tool from itch.io if you haven’t already:

The Controller Support Expansion for Ren’Py adds in support for granular stick control for controllers. This means that you can take advantage of the full 360 degrees of stick rotation, and varying degrees of tilt depending on how far the stick is pushed from the center. The built-in Controller Viewport scrolling and Virtual Cursor take advantage of these features. You can also inherit from or use the general StickEvent class to add in your own functionality.

Note: this feature is intended for intermediate to advanced Ren’Py users.

Examples

Example 1

This first example demonstrates how to set up a StickEvent that will display the distance of the controller stick on-screen.

A gif demonstrating the code below. A numerical readout to the left goes from 0 while the stick is at rest to 1 while the stick is fully pushed to the limit.
init python:
    def get_stick_distance(st, at):
        """A DynamicDisplayable to show the stick distance in real-time."""
        global stick_distance
        return Text(str(stick_distance)), 1.0/60.0

    def stick_distance_callback(x, y, stick):
        """A stick_event callback which saves the distance to a variable."""
        global stick_distance
        stick_distance = stick.distance
        return

default stick_distance = 0
image stick_distance_text = DynamicDisplayable(get_stick_distance)

screen stick_distance_screen():
    add "stick_distance_text" align (0.5, 0.5)

    stick_event:
        which_stick "left"
        changed stick_distance_callback
        refresh_rate 1.0/60.0

This stick is very simple: it’s at the bottom of the screen, so it gets events first. It pays attention to the left stick – which_stick "left". When the stick value changes, it calls the stick_distance_callback with the changed property. It refreshes 60 times a second (refresh_rate 1.0/60.0).

In the changed callback, stick_distance_callback, we can see it receives 3 values: x, y, and the StickEvent itself (named stick in the callback). In this case we’re only interested in the StickEvent’s distance property, which is saved to a global variable. The distance is from 0.0 to 1.0 where 1.0 is as far away from the dead zone as possible, and 0.0 is in the dead zone.

The other part of this screen involves a DynamicDisplayable to display the saved value in real-time (see DynamicDisplayable in the Ren’Py docs for more).

Example 2

The second example is in the form of a short minigame, where the player must hold the stick at a certain angle for a period of time.

A red square is to the left of a controller. As the left controller stick swivels down, the square turns green. After a few seconds while the square is green, the message "You win!" appears.
init python:
    def correct_stick_angle(st, at):
        """
        A DynamicDisplayable which is green while the player is holding the
        stick in a valid zone, and red otherwise.
        """
        global stick_angle, valid_stick_range
        if valid_stick_range[0] <= stick_angle <= valid_stick_range[1]:
            return Solid("#0F0"), 1.0/60.0
        else:
            return Solid("#F00"), 1.0/60.0

    def stick_angle_callback(x, y, stick):
        """
        A callback which will return if the player holds the stick in the
        designated area for a particular amount of time.
        """
        global stick_angle, stick_start_time, success_timing, valid_stick_range
        stick_angle = stick.angle
        if valid_stick_range[0] <= stick_angle <= valid_stick_range[1]:
            if stick_start_time is None:
                ## In the valid area; start the timer
                stick_start_time = stick.st
            elif stick.st - stick_start_time >= success_timing:
                ## Held the stick in the right area for success_timing seconds.
                ## Returning a non-None value will end the interaction.
                return True
        else:
            ## Reset the timer, since it's out of the valid area
            stick_start_time = None

default stick_angle = 0
default stick_start_time = None
define success_timing = 2.0
define valid_stick_range = (180-30, 180+30)
image stick_angle_indicator = DynamicDisplayable(correct_stick_angle)

screen stick_angle_minigame():
    add "stick_angle_indicator" align (0.5, 0.5) xysize (300, 300)

    stick_event:
        which_stick "left"
        changed stick_angle_callback
        refresh_rate 1.0/60.0

This second example has a timing element to it. The stick must be held at the correct angle for 2 seconds for the interaction to end. First, we set things up similarly to the first example, with a DynamicDisplayable and a callback. The DynamicDisplayable shows green when the stick is held in the right location, and red otherwise. The correct angles are declared as a range with the line define valid_stick_range = (180-30, 180+30). This means the correct angle is between 5:00 and 7:00, since 180 degrees is 6:00.

Next is the callback. As with the previous example, it is provided the x and y values and the StickEvent itself. In the callback, we first check if the current stick angle is within the valid range. If it is, and the timer has not been started (stick_start_time), we start the timer by setting it to stick.st, which is the amount of time the stick has been on-screen for. If the timer has already been started, we check if the difference between now (stick.st) and the start time (stick_start_time) is greater than or equal to success_timing, aka how long it has to be held down for to win the minigame. If so, we return True to end the interaction. Otherwise, if the angle is not in the valid range, we reset the countdown timer.

And that’s all! The stick_event is added to the bottom of the screen as before, with the stick_angle_callback callback.

Properties

The StickEvent class (and its screen language equivalent, stick_event) can be passed the following properties as keyword arguments.

x

An Adjustment object (see ui.adjustment in the Ren’Py docs). This tracks the x-axis movement of the stick, and by default goes from -1 to 1. Typically rather than providing this directly, you will use the x_min, x_max, and start_x properties to customize the Adjustment object.

y

As for x above, but tracking the y-axis movement.

x_min

The minimum x-axis value for this stick. By default, this is -1.0. This will be used along with x_max to create an Adjustment object if x is not directly provided.

x_max

The maximum x-axis value for this stick. By default, this is 1.0. This will be used along with x_min to create an Adjustment object if x is not directly provided.

start_x

The starting x axis value tracked by this stick. A float. This should be within the range of x_min and x_max for the stick. By default it is set to halfway between x_max and x_min.

y_max, y_min, start_y

As for the above x_ equivalents, but for the y axis value.

event_type

There are two main ways the StickEvent class can be used. If event_type is "range", the speed attribute will be used to adjust the x and y values over time, relative to how far the sticks are pressed. So, positive y axis movement increases y (holding down) and negative y axis movement decreases y (holding up). Suitable for events such as scrolling, cursor movement within a range, etc.

If event_type is "axis", the default, the x and y attributes record the relative position of the stick along that axis (compared to the full range of motion). This is suitable for things that require the angle of the stick such as power selection wheels or circular UI.

Note that the x and y values for both “range” and “axis” will be multiplied by the stick sensitivity; for “range”, it means the movement is faster, and for “axis”, it means the provided values may be higher or lower than the usual range of x/y. For a non-multiplied number, see the raw_x and raw_y properties.

which_stick

Which controller stick to listen to for events. One of “left”, “right”, or “both”.

changed

A callable or list of callables which are called when the stick position changes. It should take three arguments, the x Adjustment and y Adjustment and the StickEvent object itself.

absorb_events

If True, the StickEvent will absorb stick events and prevent them from being passed to other displayables. Which events are absorbed depends on the value of which_stick. Default is False.

refresh_rate

The number of seconds before polling the stick coordinates again, if it’s outside of a dead zone. 0.0 by default, aka as often as possible. Setting this to higher numbers can help with performance, at the cost of less accurate/frequent stick information.

The following properties are read-only; that is, they are intended only to retrieve information from in a changed callback or similar.

raw_x

The raw x position of the stick, as a percentage from -1.0 to 1.0 depending on how far it is from the dead zone (the dead zone is 0). Read-only.

raw_y

The raw y position of the stick, as a percentage from -1.0 to 1.0 depending on how far it is from the dead zone (the dead zone is 0). Read-only.

angle

An angle from 0 to 359 where 12:00 is 0 degrees, based on the current stick position. Read-only.

distance

The distance the stick is from the dead zone, as a percentage from 0.0 to 1.0 with 1.0 being as far away as possible and 0.0 being in the dead zone. Read-only.

last_used_stick

A tuple of (ID, which_stick) where ID is the index of the controller whose stick was last used, and which_stick is which stick (i.e. “left” or “right”) was last used. Read-only.

st

A float counting up from 0 for how long this StickEvent has been displayed for. Can be checked against for things like timing-based minigames. Read-only.

The post StickEvent appeared first on Feniks Development.

]]>
https://feniksdev.com/tool/stickevent/feed/ 0
FocusDisplayable https://feniksdev.com/tool/focusdisplayable/?utm_source=rss&utm_medium=rss&utm_campaign=focusdisplayable https://feniksdev.com/tool/focusdisplayable/#respond Sun, 02 Feb 2025 21:18:21 +0000 https://feniksdev.com/?post_type=docs&p=3332 Learn an easy way to highlight focused buttons in Ren'Py using FocusDisplayable to improve keyboard and controller navigation.

The post FocusDisplayable appeared first on Feniks Development.

]]>
Controller Support Expansion for Ren’Py includes several features to improve controller and keyboard support in Ren’Py. Pick up the tool from itch.io if you haven’t already:

The FocusDisplayable class is a special kind of displayable which will follow the currently focused button/bar/other focusable element. It can be used to draw attention to the focused item by using a cursor to point to it or a frame to highlight it.

Examples

Example 1

The first example is a simple arrow which will point to the left side of the currently focused item.

A main menu screen. The Start button is in a column on the left, highlighted with orange text and a white arrow on its left side pointing to it.
## Declare the FocusDisplayable
image focus_right_arrow = FocusDisplayable(
    Transform("right_arrow", anchor=(1.0, 0.5), pos=(0.0, 0.5)))

## Note: the main menu screen has been slightly simplified for brevity
screen main_menu():
    tag menu
    add "main_menu_background"

    vbox:
        xpos 60 yalign 0.5 spacing 6
        textbutton _("Start") id 'start' action Start() default_focus 10
        textbutton _("Load") id 'load' action ShowMenu("load")
        textbutton _("Preferences") id 'preferences' action ShowMenu("pref_display")
        textbutton _("About") id 'about' action ShowMenu("about")
        textbutton _("Help") id 'help' action ShowMenu("help")
        textbutton _("Quit") id 'quit' action Quit(confirm=not main_menu)

    use key_footer():
        icon_button kind icn.select suffix "small"

    ## Add the FocusDisplayable from earlier
    add 'focus_right_arrow'

Let’s break down the declaration:

image focus_right_arrow = FocusDisplayable(    
    Transform("right_arrow", anchor=(1.0, 0.5), pos=(0.0, 0.5)))

The first thing passed to the FocusDisplayable is a displayable. Namely, the image of the arrow. In particular, it is wrapped in a Transform so we can tell Ren’Py where it will be positioned relative to the button it’s pointing to. If you’re not familiar with the pos and anchor properties, Ren’Py Position Properties – Pos and Anchor will help you with those! Basically, this is saying that the right side of the arrow should be centered along the left side of the button it’s pointing to.

And that’s all! The only thing left to do is add it to the main menu screen where we wanted to use it, which was done with the line add "focus_right_arrow" at the bottom. It’s important that the FocusDisplayable goes at the very bottom of the screen (or close to it) so it appears on top of any buttons it’s highlighting.

Example 2

Now let’s look at an example that has some animation. FocusDisplayable has several properties that can be used to add some movement.

A main menu screen with the Start, Load, Preferences, About, Help, and Quit buttons in a column on the left. Each button is selected starting from the top. A white arrow follows the selected button on the left.
image focus_right_arrow = FocusDisplayable(
    Transform("right_arrow", anchor=(1.0, 0.5), pos=(0.0, 0.5)),
    xtime=0.1, xwarper="ease", ytime=0.1, ywarper="ease", 
    hide_on_mouse=True)

We’ve added several new properties to this declaration. Let’s look at what they do.

xtime and ytime are how long the displayable should take to animate from one focus to another, in seconds. Usually this is pretty short, so the displayable isn’t spending several seconds floating across the screen to the next button.

Next, xwarper and ywarper are warpers to use to animate that movement with. By default, this is "linear", so it just moves at a consistent speed. Here we’ve set it to "ease", which starts slow, speeds up, and ends slow again. You can read more about the existing warpers in the Ren’Py documentation here. This can also be a function that takes in a number from 0-1 and returns a float.

Finally, the property hide_on_mouse=True means that if the mouse is used, the FocusDisplayable will disappear. It reappears when the keyboard or a controller is used to focus something. This can be useful if you’re using the FocusDisplayable to draw additional attention to the focused item, but it would be too distracting for a mouse user who already has the cursor to draw their attention.

You can see from the animation above that this means the arrow spends a bit of time animating to the next focused item rather than simply appearing there.

Example 3

Now let’s look at an example with a Frame rather than a fixed image, which is restricted to a particular area.

An audio preferences screen. The highlighted buttons and bars have a pink rectangle around them.
screen pref_audio():
    tag menu
    default f1 = FocusDisplayable(Frame("pink_rect", 5, 5),
        hide_on_mouse=True, padding=(10, 8),
        active_area=(350, 200, config.screen_width-350, config.screen_height-200))

    use preferences_common("pref_audio"):
        side 'c r':
            style_prefix 'pref'
            controller_viewport:
                id 'pref_display_vp'
                vscroll_style "center" 
                ## IMPORTANT!
                focus_displayables f1
                scroll_delay (0.1, 0.1)
                mousewheel True draggable renpy.variant("touch")
                has vbox
                ## Contents omitted for brevity

            vbar value YScrollValue("pref_display_vp") keyboard_focus False

    add f1 ## IMPORTANT!

First, let’s break down the FocusDisplayable declaration. Here it’s declared as a variable rather than an image. Both work well!

default f1 = FocusDisplayable(Frame("pink_rect", 5, 5),
    hide_on_mouse=True, padding=(10, 8),
    active_area=(350, 200, config.screen_width-350, config.screen_height-200))

First is the displayable – in this case, it’s a pink rectangle using Frame so it’s expandable. See How to make resizeable backgrounds in Ren’Py with Frame for more on Frame!

Next, hide_on_mouse=True is as we saw before – it means the displayable doesn’t appear when the mouse is used.

padding=(10, 8) is new. Each button and bar that can be focused has a “hitbox” – a rectangle around its coordinates that defines the area of the button or bar. The padding property lets us define some extra padding around this hitbox so the rectangle isn’t right up against the button/bar area. You can provide it either as an (xpadding, ypadding) pair, or as (left_padding, top_padding, right_padding, bottom_padding). It works the same way as the padding property of frame – see Ren’Py Screen Language Basics – Frames for more on that. So, there is 10 extra pixels of space to the left/right of the buttons and bars, and 8 pixels to the top/bottom.

Lastly, there’s active_area. This declares an (x, y, width, height) tuple defining the area where the FocusDisplayable should appear. In this case, it’s the area where the viewport is. We don’t want the FocusDisplayable appearing over the menu options to the left, or the tabs at the top, just the viewport with the audio sliders and buttons.

The other thing to note is that this screen uses a controller_viewport (see Controller Viewport). As a result, when scrolling the focus coordinates (hitbox) of the focused button can change over time. To make sure the FocusDisplayable is updated as the hitbox coordinates are changing, you should provide the FocusDisplayable to the controller_viewport with the focus_displayables property. In this case we declared the FocusDisplayable as default f1 = FocusDisplayable(...), so the property is set to focus_displayables f1. If we’d instead declared this outside the screen as image f1 = FocusDisplayable(...) then we’d pass it in as a string e.g. focus_displayables "f1".

Finally, note the add f1 at the bottom of the screen as before, so the FocusDisplayable appears on top of everything.

Properties

Now that you’ve seen several examples of the FocusDisplayable in use, let’s look at all the properties available.

d

A displayable which will follow the currently focused displayable.

e.g. d="my_pointer"

d=Transform("bouncy_arrow", anchor=(0.0, 1.0), pos=(1.0, 0.0))

d=Frame("my_rect", 10, 10)

padding

Padding which will be added around the “hitbox” of the currently focused displayable. (0, 0) by default (no padding). Positive numbers increase the hitbox size away from the center, and negative numbers shrink it towards the center. You can provide padding either as an (xpadding, ypadding) tuple or as (left_padding, top_padding, right_padding, bottom_padding).

e.g. padding=(10, 8)

padding=(5, 5, 5, 5)

hide_on_mouse

If True, the FocusDisplayable will be hidden while the mouse is used. This is in real-time – it will reappear as soon as the keyboard or controller are used to focus something and disappear as soon as the mouse is moved. False by default.

e.g. hide_on_mouse=True

linger_on_focused

If True, the FocusDisplayable will stay on the last-focused item until a new one is focused, at which point it will animate over to the newly focused item. This is only relevant when hide_on_mouse is False, because the keyboard and controllers can only go from one focused item to the next/there is no such thing as hovering over something that doesn’t have focus. False by default.

e.g. linger_on_focused True

active_area

An (x, y, width, height) tuple where the FocusDisplayable should be active in. If buttons or bars etc. are focused outside of this area, the FocusDisplayable will not highlight them. A tuple like (100, 200, 400, 500) means that the area starts at (100, 200) and is 400 pixels wide and 500 pixels tall.

e.g. active_area=(100, 200, 400, 500)

This will take floats, integers, absolutes, and position as seen elsewhere in Ren’Py; see Position in the Ren’Py docs.

xwarper

The warper to use for scrolling animations in the x direction. Can be a string like "linear", in which case it must be the name of one of the built-in warpers (see https://www.renpy.org/doc/html/transforms.html#warpers). Otherwise, it can be a callable which will be passed a value between 0.0 and 1.0 and is expected to return a float. Default value is "linear".

e.g. xwarper="easein"

ywarper

The warper to use for scrolling animations in the y direction. Takes the same arguments as xwarper above.

e.g. ywarper="linear"

xtime

How long it takes to animate to the new position in the x direction, in seconds. A float. Set to 0.0 (the default) for no animation.

e.g. xtime=0.2

ytime

How long it takes to animate to the new position in the y direction, in seconds. A float. Set to 0.0 (the default) for no animation.

e.g. ytime=0.1

recheck_period

How long to wait before checking for a focus position change, in seconds. Default is 0.25. If you’re passing your FocusDisplayable to any controller_viewport that use it (see the focus_displayables property of Controller Viewport), you usually won’t need to adjust this.

e.g. recheck_period=0.1

displayables

Optionally, the FocusDisplayable can change based on the mouse property of the focused item. See the mouse property docs on Ren’Py. If provided, this is a dictionary of event name : Displayable pairs which correspond to cursors which should be used for the provided event. If the d property is not provided, this dictionary must have at least the "default" key in it.

The events are as for the built-in Hardware Mouse Cursor as seen in the Ren’Py documentation: https://www.renpy.org/doc/html/mouse.html#hardware-mouse-cursor

e.g.

image focus_rect = FocusDisplayable(
    displayables=dict(
        default=Frame("pink", 5, 5),
        pressed_default=Frame("red", 5, 5),
        investigate=Frame("yellow", 5, 5),
    )
)

The post FocusDisplayable appeared first on Feniks Development.

]]>
https://feniksdev.com/tool/focusdisplayable/feed/ 0
KeyController and focused_on https://feniksdev.com/tool/keycontroller-and-focused_on/?utm_source=rss&utm_medium=rss&utm_campaign=keycontroller-and-focused_on https://feniksdev.com/tool/keycontroller-and-focused_on/#respond Sat, 01 Feb 2025 18:04:44 +0000 https://feniksdev.com/?post_type=docs&p=3311 Learn how to fix focus problems with keyboard and controller, and how to add extra functionality to your UI in Ren'Py.

The post KeyController and focused_on appeared first on Feniks Development.

]]>
Controller Support Expansion for Ren’Py includes several features to improve controller and keyboard support in Ren’Py. Pick up the tool from itch.io if you haven’t already:

The KeyController is a special class which helps declare relationships between buttons. It can be provided instructions like “if the player is focused on the button with ID "desk" and presses right, focus on the button with the ID "bed". It helps to fix focus problems, like Ren’Py focusing the wrong button when you’re navigating the UI, or skipping over buttons in hbox or vbox. If you’re wondering how to fix focus issues or add extra functionality to your buttons, the KeyController is a great solution.

Examples

The format to set up a KeyController is:

focused_on "DISPLAYABLE_ID" key "SOME_EVENT" action SomeAction()

In this case, "DISPLAYABLE_ID" should be the ID of a displayable in the screen. You assign IDs to displayables with the id property e.g. textbutton "Hello" id "my_hello_id". In order for the rest of the code to run, the displayable with the provided ID must be the currently focused one.

Next, "SOME_EVENT" will be an event or list of events as seen in Customizing the Keymap. Some examples are event names like "focus_left" (which covers any keyboard or controller presses which focus something to the left) or direct key presses like "K_DELETE". It can also be a list of either of these e.g. [ "toggle_skip", "pad_leftshoulder_press" ].

Finally, SomeAction() is any screen action you want to execute when the provided conditions are met (aka that Ren’Py is both currently focused on the displayable with the provided ID, and the provided event occurred).

Example 1

Let’s look at a full example, with the screen and everything. Say you have an image like the following:

An image of a gray bedroom with a bed on the left and a desk on the right.
Image from sigelus on itch.io

You can find the image from sigelus on itch.io here.

Let’s say that the horse picture above the desk and the bed on the left are buttons that can be interacted with.

screen bedroom():
    add "backgrounds/bedroom_day.jpg"
    imagebutton:
        id "horse"
        auto "bedroom/horse_image_%s.png"
        action Jump("view_horse_picture")
    imagebutton:
        id "bed"
        auto "bedroom/bed_%s.png"
        action Jump("go_to_bed")
An image of a bedroom. The bed on the left and horse image on the right are highlighted in red.

Let’s say that Ren’Py does not seem to want to focus the bed when you hit left while focused on the horse image. You can use focused_on to define that relationship:

screen bedroom():
    add "backgrounds/bedroom_day.jpg"
    imagebutton:
        id "horse"
        auto "bedroom/horse_image_%s.png"
        action Jump("view_horse_picture")
    imagebutton:
        id "bed"
        auto "bedroom/bed_%s.png"
        action Jump("go_to_bed")

    focused_on "horse" key "focus_left" action SetFocus("bedroom", "bed")

Example 2

You can also use the KeyController to do other special actions when particular buttons are pressed; say, for example, you have it set up so that pressing the “i” key on the keyboard or Y key on a controller while focused on a button will investigate it:

screen bedroom():
    add "backgrounds/bedroom_day.jpg"
    imagebutton:
        id "horse"
        auto "bedroom/horse_image_%s.png"
        action Jump("view_horse_picture")
    imagebutton:
        id "bed"
        auto "bedroom/bed_%s.png"
        action Jump("go_to_bed")

    focused_on "horse" key "focus_left" action SetFocus("bedroom", "bed")

    focused_on "horse" key ["K_i", "pad_y_press"] action Jump("investigate_horse")
    focused_on "bed" key ["K_i", "pad_y_press"] action Jump("investigate_bed")

You can add multiple focused_on key + action combos for the same ID, as seen above where there are two for the “horse” ID. This works because the events they are listening for are different.

Example 3

screen preferences():
    ## ... full screen omitted
    focused_on "pick_icons" key "focus_left" action CycleControllerLayout(reverse=True)
    focused_on "pick_icons" key "focus_right" action CycleControllerLayout(reverse=False)

This means that when the button with the ID "pick_icons" is focused, and the left arrow key or controller D-pad/stick is moved, it will run the action CycleControllerLayout(reverse=True) which cycles through the different controller layouts in reverse. The same happens for hitting right while focused on the "pick_icons" button, but it cycles normally (not in reverse order).

Properties

The full list of properties used by the KeyController are listed below.

key

This should be a string or a list of such strings corresponding to event names. See Customizing the Keymap in the docs for valid event names.

Note that this class does not convert custom gamepad events to their remapped format, so you may need to use the pad_config.get_event function to convert it e.g. key pad_config.get_event("investigate").

action

The action or a list of actions to execute if the provided event occurs while focused on a displayable with the given ID. e.g. action Notify("You pressed the button")

activate_sound

Optional. A sound to play when the action is triggered. e.g. activate_sound "oof.ogg"

capture

Optional. If True, the default, the KeyController will ignore the event afterwards, preventing it from being passed along to the rest of the UI. If False, the event will be passed along to the rest of the UI after executing the action. e.g. capture False

The post KeyController and focused_on appeared first on Feniks Development.

]]>
https://feniksdev.com/tool/keycontroller-and-focused_on/feed/ 0
Engine Override Notes https://feniksdev.com/tool/engine-override-notes/?utm_source=rss&utm_medium=rss&utm_campaign=engine-override-notes https://feniksdev.com/tool/engine-override-notes/#respond Sat, 01 Feb 2025 03:13:11 +0000 https://feniksdev.com/?post_type=docs&p=3297 This page explains any engine functions which were overwritten in the controller support expansion for Ren'Py to add functionality.

The post Engine Override Notes appeared first on Feniks Development.

]]>
As a tool designed to expand on Ren’Py’s existing functionality to add better controller support, sometimes functions and features need to be overwritten to provide a better experience. Where practical, I will endeavour to get these changes into the engine (and in the case of the bugs I discovered, those fixes are incorporated but only in newer engine versions, so they are backported here). In the meantime, my goal is to have a tool override to get that functionality sooner. This article is a brief overview of the engine functionalities which needed to be overwritten rather than built upon, and thus have the potential to be updated in the future.

Pick up the tool from itch.io if you haven’t already:

Most of the overwritten content can be found in controller_override.rpy. There are comments denoting where the overwritten code is.

renpy.display.controller.start

This function runs when a new controller is connected. It has been overwritten to support callbacks.

renpy.display.controller.quit

This function runs when a controller is disconnected. It has been overwritten to support callbacks.

renpy.map_event

This function has been overwritten if the user’s version is below 8.3.x, as there was a bug preventing lists of controller events from working. In 8.3.x+ it is untouched.

GamepadCalibrate

This screen action is overwritten to a version which ensures that the custom controller connect/disconnect callbacks (introduced in the start/quit functions earlier) are not called when calibrating the controller.

Hide Windows Key Event

The default Hide Windows event causes focus to be dropped while in menu screens. An additional hide_windows_without_focus_drop function is added to the keymap underlay to avoid this behaviour.

renpy.display.controller.event

This function is called to process and categorize pygame controller events. It has been overwritten to pass along CONTROLLERAXISMOTION events when a stick enters or leaves its dead zone (to enable granular controller stick movement).

renpy.display.focus.before_interact

This function is called before a new focusable element is selected. It is overwritten only if the user’s version is below 8.3.6, as there’s a bug whereby the default_focus displayable gets focus even if it has been explicitly set by the user to be something else.

The post Engine Override Notes appeared first on Feniks Development.

]]>
https://feniksdev.com/tool/engine-override-notes/feed/ 0