Ren'Pyでステータス表示やカスタムメニューを作ろうとすると、ほぼ全員がこう思う。「変数を変えたのに、画面が更新されない。」
これはバグではない。Ren'Pyのscreen言語が 宣言的 に設計されているからだ。Pythonの命令型プログラミングに慣れていると、最初は直感に反する。
この記事では、Ren'Py 8.5.2 のscreen言語の基本と、「なぜ更新されないのか」の仕組みを解説する。
screen言語は宣言的に書く
Pythonは 命令型 だ。「この順番でこれをやれ」と手続きを書く。
# Python(命令型)
hp = 100
hp -= 30
print(hp) # 70
一方、Ren'Pyのscreen言語は 宣言的 だ。「画面はこういう状態であるべき」と結果を書く。
screen stats_hud():
frame:
text "HP: [player_hp]"
この text "HP: [player_hp]" は「HPを表示しろ」という命令ではない。「HPが表示されている状態であるべき」という宣言だ。Ren'Pyは必要なタイミングでこの宣言を読み取り、画面を構築する。
この「宣言的」という性質が、画面更新の挙動を理解する鍵になる。
スクリーンの基本構文
screenの定義と表示
# screenを定義する
screen greeting():
frame:
xalign 0.5
yalign 0.5
vbox:
text "こんにちは"
textbutton "閉じる" action Hide("greeting")
label start:
# screenを表示する
show screen greeting
"画面にウィンドウが表示されている。"
# screenを非表示にする
hide screen greeting
screen 文でUIの構造を定義し、show screen でそれを画面に表示する。
主要なdisplayable
screenの中で使う部品(displayable)の一覧。
| displayable | 用途 | 例 |
|---|---|---|
text | テキスト表示 | text "HP: 100" |
add | 画像表示 | add "icon.png" |
textbutton | テキストボタン | textbutton "OK" action Return() |
imagebutton | 画像ボタン | imagebutton idle "btn.png" action Return() |
vbox | 縦並びコンテナ | 子要素を縦に並べる |
hbox | 横並びコンテナ | 子要素を横に並べる |
frame | 枠付きウィンドウ | 背景付きのUI領域 |
bar | バー/スライダー | bar value player_hp range 100 |
全displayableの一覧は Screens and Screen Language を参照。
条件分岐とループ
screen言語の中でも if と for は使える。
screen inventory():
frame:
vbox:
text "アイテム一覧"
for item in inventory_list:
textbutton "[item]" action NullAction()
if len(inventory_list) == 0:
text "アイテムがありません"
ただし、これらはPythonの if/for とは見た目が同じでも動作が違う。screenの if/for は 画面が再評価されるたびに実行される。一度だけ実行される命令型の制御フローではない。
なぜ画面が更新されないのか
ここが核心だ。次のコードを見てほしい。
default player_hp = 100
screen hp_display():
text "HP: [player_hp]"
label start:
show screen hp_display
"HPは [player_hp] です。"
$ player_hp -= 30
"HPが [player_hp] に下がった。"
このコードは 問題なく動く。player_hp が変わると画面のHP表示も更新される。
では、なぜ「更新されない」という問題が起きるのか。次のケースを見よう。
更新されないケース:インタラクションの不在
label start:
show screen hp_display
$ player_hp -= 30
# ← ここでは画面が更新されていない
$ player_hp -= 20
# ← ここでも更新されていない
"HPは [player_hp] です。"
# ← このセリフ表示(インタラクション)で初めて画面が更新される
Ren'Pyのscreenは、インタラクション が発生するタイミングで再評価される。インタラクションとは、プレイヤーの入力を待つ処理のことだ。
| インタラクションが発生するもの | インタラクションが発生しないもの |
|---|---|
セリフ表示(say) | $ variable = value |
選択肢(menu) | show image |
pause | hide image |
call screen | show screen |
with transition | Python文($ 行) |
$ 行でいくら変数を変更しても、次のインタラクションまで画面は再描画されない。
解決策:インタラクションを挟む
変数を変えた後に画面を更新したければ、セリフや pause でインタラクションを発生させればいい。
label start:
show screen hp_display
$ player_hp -= 30
pause 0.5
# ← pause がインタラクションを発生させ、画面が更新される
$ player_hp -= 20
"HPが [player_hp] に下がった。"
# ← セリフもインタラクションなので、ここでも更新される
screen内のボタンで変数を変える場合は、後述する Action を使えば自動的にインタラクションが再スタートされるため、手動でインタラクションを挟む必要はない。
screenの予測実行に注意
Ren'Pyはscreenを 表示前にも予測実行 する。これはスムーズな画面遷移のための最適化だが、screen内に副作用のあるPythonコードを書くと問題になる。
screen dangerous():
python:
# これは表示前の予測でも実行される!
store.gold -= 100
text "ゴールド: [gold]"
screen内の python: ブロックは表示前の予測でも実行されるため、変数の変更やファイル操作のような副作用を書いてはいけない。変数の変更は Action を通じて行うこと。
show screen と call screen の違い
screenを画面に出す方法は2つある。挙動が大きく異なる。
show screen:表示したままlabelが進む
label start:
show screen hp_display
"このセリフの間もHP表示が見えている。"
"次のセリフでも見えている。"
hide screen hp_display
"HP表示が消えた。"
show screen はscreenを表示するだけで、labelの実行を止めない。常時表示のHUD(ステータスバー、ミニマップ等)に使う。
call screen:プレイヤーの操作を待つ
screen choice_menu():
vbox:
xalign 0.5
yalign 0.5
textbutton "戦う" action Return("fight")
textbutton "逃げる" action Return("flee")
label start:
call screen choice_menu
# _return に選択結果が入る
if _return == "fight":
"戦いを選んだ!"
else:
"逃げ出した!"
call screen はscreenを表示し、Return() が呼ばれるまでlabelの実行を停止する。選択結果は _return 変数に格納される。一時的なメニューやダイアログに使う。
Actionで画面を操作する
ボタンの action に渡すのは、Pythonの関数呼び出しではなく Actionオブジェクト だ。
間違いやすいパターン
init python:
def do_heal():
store.player_hp += 20
screen heal_button():
# 間違い — do_heal() を即座に実行してしまう
# textbutton "回復" action do_heal()
# 正しい — Function() で包む
textbutton "回復" action Function(do_heal)
do_heal() と書くと、screen評価時に関数が即座に実行されて、その戻り値(None)が action に渡される。Function(do_heal) と書けば、ボタンがクリックされたときに実行される。
主要な組み込みAction
| Action | 動作 |
|---|---|
SetVariable("name", value) | グローバル変数を変更 |
SetScreenVariable("name", value) | screenローカル変数を変更 |
ToggleVariable("name") | bool変数を反転 |
Function(callable) | 任意の関数を実行 |
Show("screen_name") | 別のscreenを表示 |
Hide("screen_name") | screenを非表示 |
Return(value) | call screen に値を返す |
NullAction() | 何もしない(ボタンを有効にするだけ) |
組み込みActionは実行後に 自動的にインタラクションを再スタート する。SetVariable でHPを変えれば、HPを表示しているscreenは自動的に更新される。これが「renpy.restart_interaction() を直接呼ぶ必要がほとんどない」理由だ。
全Actionの一覧は Screen Actions を参照。
screenローカル変数
screen内だけで使う変数は default で宣言する。
screen counter():
default count = 0
vbox:
text "カウント: [count]"
textbutton "+1" action SetScreenVariable("count", count + 1)
textbutton "リセット" action SetScreenVariable("count", 0)
default はscreenが表示されたときに1回だけ初期化される。screen内の python: ブロックで変数に代入すると、再評価のたびにリセットされてしまうので注意。
まとめ
この記事で解説したこと:
- 宣言的パラダイム: screenは「こうあるべき」という状態を書く。「こうしろ」という手続きではない
- 基本構文:
screen文で定義、text/textbutton/vbox/frame等のdisplayableで構築 - インタラクション: screenはインタラクション発生時に再評価される。
$行だけでは画面は更新されない - show vs call:
show screenは非ブロッキング(HUD向き)、call screenはブロッキング(メニュー向き) - Action:
SetVariable/Function/Return等のオブジェクトをボタンに渡す。自動的に画面更新される
Ren'Pyの基本は 入門ガイド を、画像表示は 画像表示ガイド を参照してほしい。
公式リソース:
- Screens and Screen Language — screen言語の公式リファレンス
- Screen Actions — 全Actionの一覧
- Screen Language Optimization — 予測実行と最適化の詳細
- r/RenPy — コミュニティ