32blogby Studio Mitsu

Ren'Py Screen Language: Why Your UI Won't Update

Learn Ren'Py 8.5 screen language from scratch. Covers the declarative paradigm, interaction mechanics, show vs call screen, and Action-based UI control.

by omitsu10 min read
On this page

When you try to build a status display or custom menu in Ren'Py, you'll almost certainly think: "I changed the variable, but the screen didn't update."

It's not a bug. Ren'Py's screen language is designed to be declarative. If you're used to imperative Python programming, this feels counterintuitive at first.

This guide covers the fundamentals of screen language in Ren'Py 8.5.2 and explains the mechanics behind "why it won't update."

Screen Language Is Declarative

Python is imperative. You write step-by-step instructions: "do this, then do that."

python
# Python (imperative)
hp = 100
hp -= 30
print(hp)  # 70

Ren'Py's screen language is declarative. You describe what the screen should look like.

renpy
screen stats_hud():
    frame:
        text "HP: [player_hp]"

This text "HP: [player_hp]" isn't a command saying "display HP now." It's a declaration: "the screen should show the HP value." Ren'Py reads this declaration at the appropriate time and builds the screen.

This "declarative" nature is the key to understanding screen update behavior.

Basic Screen Syntax

Defining and Showing a Screen

renpy
# Define a screen
screen greeting():
    frame:
        xalign 0.5
        yalign 0.5

        vbox:
            text "Hello"
            textbutton "Close" action Hide("greeting")

label start:
    # Show the screen
    show screen greeting

    "A window is visible on screen."

    # Hide the screen
    hide screen greeting

Define UI structure with the screen statement, then display it with show screen.

Key Displayables

The building blocks (displayables) you use inside screens.

DisplayablePurposeExample
textDisplay texttext "HP: 100"
addDisplay an imageadd "icon.png"
textbuttonText buttontextbutton "OK" action Return()
imagebuttonImage buttonimagebutton idle "btn.png" action Return()
vboxVertical containerStacks children vertically
hboxHorizontal containerStacks children horizontally
frameFramed windowUI area with background
barBar/sliderbar value player_hp range 100

For the full list, see Screens and Screen Language.

Conditions and Loops

You can use if and for inside screen language.

renpy
screen inventory():
    frame:
        vbox:
            text "Inventory"

            for item in inventory_list:
                textbutton "[item]" action NullAction()

            if len(inventory_list) == 0:
                text "No items"

These look like Python's if/for, but they behave differently. Screen if/for are re-evaluated every time the screen is re-evaluated. They're not one-time imperative control flow.

Why Your Screen Won't Update

This is the core concept. Look at this code:

renpy
default player_hp = 100

screen hp_display():
    text "HP: [player_hp]"

label start:
    show screen hp_display

    "HP is [player_hp]."

    $ player_hp -= 30

    "HP dropped to [player_hp]."

This code works fine. When player_hp changes, the screen updates.

So why do people say "my screen won't update"? Here's when it actually happens.

The Case: No Interaction

renpy
label start:
    show screen hp_display

    $ player_hp -= 30
    # ← Screen has NOT updated here

    $ player_hp -= 20
    # ← Still NOT updated

    "HP is [player_hp]."
    # ← Screen finally updates at this say statement (interaction)

Ren'Py screens are re-evaluated when an interaction occurs. An interaction is any point where the engine waits for player input.

Causes InteractionDoes NOT Cause Interaction
Say statements (say)$ variable = value
Choices (menu)show image
pausehide image
call screenshow screen
with transitionPython statements ($ lines)

No matter how many variables you change with $ lines, the screen won't redraw until the next interaction.

The Fix: Add an Interaction

To update the screen after changing variables, trigger an interaction with a say statement or pause.

renpy
label start:
    show screen hp_display

    $ player_hp -= 30
    pause 0.5
    # ← pause triggers an interaction, screen updates

    $ player_hp -= 20
    "HP dropped to [player_hp]."
    # ← say statement is also an interaction, screen updates here too

When changing variables through buttons inside a screen, Actions (covered below) automatically restart the interaction, so you don't need to manually trigger one.

Watch Out for Screen Prediction

Ren'Py predicts screens before displaying them. This optimization enables smooth transitions, but it means Python code inside screens can run at unexpected times.

renpy
screen dangerous():
    python:
        # This runs during prediction too!
        store.gold -= 100

    text "Gold: [gold]"

The python: block inside a screen runs during prediction, before the screen is actually shown. Never write side effects (variable changes, file operations) in screen python: blocks. Use Actions for variable changes instead.

If your screen takes arguments that have side effects, use the nopredict clause on show screen or call screen to prevent prediction:

renpy
# Prevents Ren'Py from predicting (and pre-evaluating) this screen
call screen my_screen(some_function()) nopredict

show screen vs call screen

Two ways to display a screen, with very different behavior.

show screen: Label Keeps Running

renpy
label start:
    show screen hp_display

    "HP display is visible during this dialogue."
    "Still visible here."

    hide screen hp_display

    "HP display is gone."

show screen displays the screen but doesn't stop label execution. Use it for always-visible HUDs (status bars, minimaps, etc.).

call screen: Waits for Player Input

renpy
screen choice_menu():
    vbox:
        xalign 0.5
        yalign 0.5

        textbutton "Fight" action Return("fight")
        textbutton "Flee" action Return("flee")

label start:
    call screen choice_menu

    # The result is stored in _return
    if _return == "fight":
        "You chose to fight!"
    else:
        "You ran away!"

call screen displays the screen and halts label execution until Return() is called. The return value goes into _return. Use it for temporary menus and dialogs.

Controlling Screens with Actions

What you pass to a button's action isn't a Python function call — it's an Action object.

A Common Mistake

renpy
init python:
    def do_heal():
        store.player_hp += 20

screen heal_button():
    # Wrong — calls do_heal() immediately during screen evaluation
    # textbutton "Heal" action do_heal()

    # Right — wraps it in Function()
    textbutton "Heal" action Function(do_heal)

Writing do_heal() calls the function immediately during screen evaluation, passing its return value (None) to action. Writing Function(do_heal) makes it execute when the button is clicked.

Key Built-in Actions

ActionBehavior
SetVariable("name", value)Change a global variable
SetScreenVariable("name", value)Change a screen-local variable
ToggleVariable("name")Toggle a bool variable
Function(callable)Execute any function
Show("screen_name")Show another screen
Hide("screen_name")Hide a screen
Return(value)Return a value to call screen
NullAction()Do nothing (makes a button clickable)

Built-in Actions automatically restart the interaction after execution. Change HP with SetVariable, and any screen displaying HP updates automatically. That's why you rarely need renpy.restart_interaction() directly.

For the full list, see Screen Actions.

Screen-Local Variables

Variables used only within a screen are declared with default.

renpy
screen counter():
    default count = 0

    vbox:
        text "Count: [count]"
        textbutton "+1" action SetScreenVariable("count", count + 1)
        textbutton "Reset" action SetScreenVariable("count", 0)

default initializes once when the screen is first shown. Assigning variables in a python: block inside a screen resets them on every re-evaluation — a subtle bug that the Screens and Python docs call out specifically.

Creating Custom Actions

When built-in Actions aren't enough, you can create your own by subclassing Action:

renpy
init python:
    class HealAction(Action):
        def __init__(self, amount):
            self.amount = amount

        def __call__(self):
            store.player_hp = min(store.player_hp + self.amount, store.max_hp)
            renpy.restart_interaction()
            return None

        def get_sensitive(self):
            return store.player_hp < store.max_hp

screen heal_button():
    textbutton "Heal +20" action HealAction(20)

get_sensitive controls whether the button is clickable. When player_hp is already at max, the button grays out. Calling renpy.restart_interaction() inside your custom Action forces screens to re-evaluate — built-in Actions do this automatically, but custom ones need it explicitly.

FAQ

What's the difference between show screen and call screen?

show screen displays the screen without blocking — the label continues executing, making it ideal for HUDs and persistent overlays. call screen pauses label execution until a Return() Action is triggered, storing the result in _return. Use call screen for menus and dialogs where you need the player's choice before continuing.

Why does my screen variable reset every time the screen updates?

You're likely assigning the variable in a python: block instead of using default. Code in python: blocks runs on every screen re-evaluation, resetting the value. Declare screen-local variables with default count = 0 at the top of your screen — this initializes only once when the screen first appears.

How do I pass data from a screen back to a label?

Use call screen with a Return(value) Action on your buttons. The returned value is stored in the special variable _return in your label code. For example, textbutton "Yes" action Return(True) followed by if _return: in the label.

Can I use regular Python functions inside screen action?

Yes, but you must wrap them with Function(). Writing action my_func() calls the function immediately during screen evaluation and passes None to the action. Writing action Function(my_func) defers execution until the button is clicked. If your function takes arguments, use action Function(my_func, arg1, arg2).

How do I make a screen update continuously (like a timer or animation)?

For animations, use ATL transforms inside your screen — they run independently of interactions. For timers, use the timer screen statement: timer 1.0 action SetVariable("seconds", seconds + 1) repeat True. The timer triggers the Action at the specified interval and restarts the interaction automatically.

What's the difference between SetVariable and SetScreenVariable?

SetVariable modifies global variables (those in the store), visible across all screens and labels. SetScreenVariable modifies variables declared with default inside that specific screen, scoped only to that screen instance. Using SetVariable on a screen-local variable won't work, and vice versa.

How do I create a custom Action class?

Subclass renpy.store.Action and implement __call__ (what happens on click) and optionally get_sensitive (whether the button is clickable) and get_selected (whether it appears toggled on). Call renpy.restart_interaction() at the end of __call__ to refresh screens. See the Screen Actions docs for the full API.

Wrapping Up

What we covered:

  • Declarative paradigm: Screens describe what should be shown, not step-by-step instructions
  • Basic syntax: Define with screen, build with text/textbutton/vbox/frame displayables
  • Interactions: Screens re-evaluate on interaction. $ lines alone don't trigger screen updates
  • show vs call: show screen is non-blocking (for HUDs), call screen is blocking (for menus)
  • Actions: Pass SetVariable/Function/Return objects to buttons. They automatically trigger screen updates

For Ren'Py basics, see the getting started guide. For image display, see the image basics guide.

Official resources:

Related articles: