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.

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.

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.