32blogby StudioMitsu
renpy12 min read

Ren'Pyのscreen言語入門:画面が更新されない理由

Ren'Py 8.5のscreen言語を基礎から解説。宣言的パラダイム、インタラクションの仕組み、show/callの違い、Actionによる画面操作まで。

renpyvisual-novelgame-devpython
目次

Ren'Pyでステータス表示やカスタムメニューを作ろうとすると、ほぼ全員がこう思う。「変数を変えたのに、画面が更新されない。」

これはバグではない。Ren'Pyのscreen言語が 宣言的 に設計されているからだ。Pythonの命令型プログラミングに慣れていると、最初は直感に反する。

この記事では、Ren'Py 8.5.2 のscreen言語の基本と、「なぜ更新されないのか」の仕組みを解説する。

screen言語は宣言的に書く

Pythonは 命令型 だ。「この順番でこれをやれ」と手続きを書く。

python
# Python(命令型)
hp = 100
hp -= 30
print(hp)  # 70

一方、Ren'Pyのscreen言語は 宣言的 だ。「画面はこういう状態であるべき」と結果を書く。

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

この text "HP: [player_hp]" は「HPを表示しろ」という命令ではない。「HPが表示されている状態であるべき」という宣言だ。Ren'Pyは必要なタイミングでこの宣言を読み取り、画面を構築する。

この「宣言的」という性質が、画面更新の挙動を理解する鍵になる。

スクリーンの基本構文

screenの定義と表示

renpy
# 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言語の中でも iffor は使える。

renpy
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画面が再評価されるたびに実行される。一度だけ実行される命令型の制御フローではない。

なぜ画面が更新されないのか

ここが核心だ。次のコードを見てほしい。

renpy
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表示も更新される。

では、なぜ「更新されない」という問題が起きるのか。次のケースを見よう。

更新されないケース:インタラクションの不在

renpy
label start:
    show screen hp_display

    $ player_hp -= 30
    # ← ここでは画面が更新されていない

    $ player_hp -= 20
    # ← ここでも更新されていない

    "HPは [player_hp] です。"
    # ← このセリフ表示(インタラクション)で初めて画面が更新される

Ren'Pyのscreenは、インタラクション が発生するタイミングで再評価される。インタラクションとは、プレイヤーの入力を待つ処理のことだ。

インタラクションが発生するものインタラクションが発生しないもの
セリフ表示(say$ variable = value
選択肢(menushow image
pausehide image
call screenshow screen
with transitionPython文($ 行)

$ 行でいくら変数を変更しても、次のインタラクションまで画面は再描画されない。

解決策:インタラクションを挟む

変数を変えた後に画面を更新したければ、セリフや pause でインタラクションを発生させればいい。

renpy
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コードを書くと問題になる。

renpy
screen dangerous():
    python:
        # これは表示前の予測でも実行される!
        store.gold -= 100

    text "ゴールド: [gold]"

screen内の python: ブロックは表示前の予測でも実行されるため、変数の変更やファイル操作のような副作用を書いてはいけない。変数の変更は Action を通じて行うこと。

show screencall screen の違い

screenを画面に出す方法は2つある。挙動が大きく異なる。

show screen:表示したままlabelが進む

renpy
label start:
    show screen hp_display

    "このセリフの間もHP表示が見えている。"
    "次のセリフでも見えている。"

    hide screen hp_display

    "HP表示が消えた。"

show screen はscreenを表示するだけで、labelの実行を止めない。常時表示のHUD(ステータスバー、ミニマップ等)に使う。

call screen:プレイヤーの操作を待つ

renpy
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オブジェクト だ。

間違いやすいパターン

renpy
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 で宣言する。

renpy
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の基本は 入門ガイド を、画像表示は 画像表示ガイド を参照してほしい。

公式リソース: