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.
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.
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
- Screen Actions — full list of Actions
- Screen Language Optimization — prediction and optimization details
- r/RenPy — community