---
title: StickEvent
description: "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."
url: "https://feniksdev.com/tool/stickevent/"
updated: "2025-02-09"
category: Controller Support Expansion
---

# StickEvent

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:

[https://itch.io/embed/2766391?bg_color=21212d&fg_color=f7f7ed&link_color=f93c3e&border_color=ff8335](https://itch.io/embed/2766391?bg_color=21212d&fg_color=f7f7ed&link_color=f93c3e&border_color=ff8335)

   **Contents**  [hide](#)     [Examples](#Examples)  [Example 1](#Example_1)  [Example 2](#Example_2)  [Properties](#Properties)  [x](#x)  [y](#y)  [x_min](#x_min)  [x_max](#x_max)  [start_x](#start_x)  [y_max, y_min, start_y](#y_max_y_min_start_y)  [event_type](#event_type)  [which_stick](#which_stick)  [changed](#changed)  [absorb_events](#absorb_events)  [refresh_rate](#refresh_rate)  [raw_x](#raw_x)  [raw_y](#raw_y)  [angle](#angle)  [distance](#distance)  [last_used_stick](#last_used_stick)  [st](#st)

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](https://feniksdev.com/tool/controller-viewport/) scrolling and [Virtual Cursor](https://feniksdev.com/tool/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.](https://feniksdev.com/wp-content/uploads/2025/02/stick_distance.gif)

```renpy
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](https://www.renpy.org/doc/html/displayables.html#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.](https://feniksdev.com/wp-content/uploads/2025/02/stick_angle.gif)

```renpy
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](https://www.renpy.org/doc/html/screen_python.html#ui.adjustment)). 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.
