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 (imperative)
hp = 100
hp -= 30
print(hp) # 70
Ren'Py's screen language is declarative. You describe what the screen should look like.
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
# 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.
| Displayable | Purpose | Example |
|---|---|---|
text | Display text | text "HP: 100" |
add | Display an image | add "icon.png" |
textbutton | Text button | textbutton "OK" action Return() |
imagebutton | Image button | imagebutton idle "btn.png" action Return() |
vbox | Vertical container | Stacks children vertically |
hbox | Horizontal container | Stacks children horizontally |
frame | Framed window | UI area with background |
bar | Bar/slider | bar 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.
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:
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
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 Interaction | Does NOT Cause Interaction |
|---|---|
Say statements (say) | $ variable = value |
Choices (menu) | show image |
pause | hide image |
call screen | show screen |
with transition | Python 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.
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.
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:
# 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
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
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
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
| Action | Behavior |
|---|---|
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.
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:
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 withtext/textbutton/vbox/framedisplayables - Interactions: Screens re-evaluate on interaction.
$lines alone don't trigger screen updates - show vs call:
show screenis non-blocking (for HUDs),call screenis blocking (for menus) - Actions: Pass
SetVariable/Function/Returnobjects 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:
- Screens and Screen Language — screen language reference
- Screens and Python — Action classes, BarValue, and
renpy.restart_interaction() - Screen Actions — full list of built-in Actions
- Screen Language Optimization — prediction and optimization details
- ATL (Animation and Transformation Language) — for screen animations
- Lemma Soft Forums — the original Ren'Py community forum
- r/RenPy — Reddit community
Related articles: