This page is out of date

You've reached a page on the Ren'Py wiki. Due to massive spam, the wiki hasn't been updated in over 5 years, and much of the information here is very out of date. We've kept it because some of it is of historic interest, but all the information relevant to modern versions of Ren'Py has been moved elsewhere.

Some places to look are:

Please do not create new links to this page.


テキストの履歴と音声のリプレイ

以下のスクリプトによって、会話の履歴画面(Readback: 日本製のゲームで従来から使われている方式)を Ren'Py のゲームに追加できます。音声ファイルがゲーム中で使われている場合は、以前の会話をクリックすることで再生できます。音声がない場合は、会話が一行ずつテキストとして画面に表示されます。 (訳注: extendを使用した時の不具合やnvl clearで行間をあける等の改善をしたものがこちらで配布されています。

使い方

基本的には、マウスホイールが上にスクロールされると、履歴画面が表示されます。ボタンを使って履歴画面を表示させたい場合は、screen ブロックに以下のコードを追加してください。

textbutton '会話履歴' action [SetVariable("yvalue", 1.0), ShowMenu('text_history')]

例えば、ナビゲーター(画面右クリック時に表示されるボタン欄)にボタンを追加したい場合は、screen navigation ブロックのスクリプトを以下のように編集します。

#screens.rpy
screen navigation:
 
    # The background of the game menu.
    window:
        style "gm_root"
 
    # The various buttons.
    frame:
        style_group "gm_nav"
        xalign .98
        yalign .98
         
        has vbox
 
        textbutton _("Return") action Return()

        ## 履歴ボタンの追加
        textbutton _("Text History") action [SetVariable("yvalue", 1.0), ShowMenu("text_history")]
        #.....

スクリプト

以下のスクリプトをコピーし、readback.rpy などの名前で保存してください。

# readback.rpy
# 中ボタンスクロールによる会話履歴(リードバック)画面への切り替え
# this file is licensed under the terms of the WTFPL
# see http://sam.zoy.org/wtfpl/COPYING for details

# voice_replay function added by backansi from Lemma soft forum.
# Ren'Py 6.12 以上の環境向け

init -3 python:

    # スタイル
    style.readback_window.xmaximum = 760
    style.readback_window.ymaximum = 500
    style.readback_window.align = (.5, .5)

    style.readback_frame.background = None
    style.readback_frame.xpadding = 10
    style.readback_frame.xmargin = 5
    style.readback_frame.ymargin = 5
    
    style.readback_text.color = "#fff"

    style.create("readback_button", "readback_text")
    style.readback_button.background = None
    
    style.create("readback_button_text", "readback_text")
    style.readback_button_text.selected_color = "#f12"
    style.readback_button_text.hover_color = "#f12"
    
    style.readback_label_text.bold = True
    
    # コンフィグレーション変数の新規追加
    config.locked = False 
    
    # 会話履歴のコンフィグレーション変数 
    config.readback_buffer_length = 100 # 履歴に保存される最大行数
    config.readback_full = True # True = ロールバックの完全な置き換え, False = ゲームメニューのみからの表示 (デバッグモード)
    config.readback_disallowed_tags = ["size"] # 履歴内で無視されるテキストタグ
    config.readback_choice_prefix = ">> "   # ユーザーが選択したメニュー項目の前に付くテキスト
    
    # 追加終了
    config.locked = True
    
init -2 python:

    # 発言を記録する2つのカスタムキャラクター
    class ReadbackADVCharacter(ADVCharacter):
        def do_done(self, who, what):
            store_say(who, what)
            store.current_voice = ''
            return

    class ReadbackNVLCharacter(NVLCharacter):
        def do_done(self, who, what):
            store_say(who, what)
            store.current_voice = ''
            return
            
    # this enables us to show the current line in readback without having to bother the buffer with raw shows
    def say_wrapper(who, what, **kwargs):
        store_current_line(who, what)
        return renpy.show_display_say(who, what, **kwargs)
    
    config.nvl_show_display_say = say_wrapper
    
    adv = ReadbackADVCharacter(show_function=say_wrapper)
    nvl = ReadbackNVLCharacter()
    NVLCharacter = ReadbackNVLCharacter
    
    # voice 関数を上書きし、会話履歴上のボタンをクリックした時に音声をリプレイできるようにする。
    def voice(file, **kwargs):
        if not config.has_voice:
            return
        
        _voice.play = file
        
        store.current_voice = file

    # menu 関数を上書きしてユーザーのメニュー選択を記録できるようにする。
    def menu(items, **add_input): 
        
        newitems = []
        for label, val in items:
            if val == None:
                narrator(label, interact=False)
            else:
                newitems.append((label, val))
                
        rv = renpy.display_menu(newitems, **add_input)
        
        # メニュー選択の記録
        for label, val in items:
            if rv == val:
                store.current_voice = ''
                store_say(None, config.readback_choice_prefix + label)
        return rv
        
    def nvl_screen_dialogue(): 
        """
         Returns widget_properties and dialogue for the current NVL
         mode screen.
         """

        widget_properties = { }
        dialogue = [ ]
        
        for i, entry in enumerate(nvl_list):
            if not entry:
                continue

            who, what, kwargs = entry

            if i == len(nvl_list) - 1:
                who_id = "who"
                what_id = "what"
                window_id = "window"

            else:
                who_id = "who%d" % i
                what_id = "what%d" % i
                window_id = "window%d" % i
                
            widget_properties[who_id] = kwargs["who_args"]
            widget_properties[what_id] = kwargs["what_args"]
            widget_properties[window_id] = kwargs["window_args"]

            dialogue.append((who, what, who_id, what_id, window_id))
        
        return widget_properties, dialogue
        
    # nvl menu 関数の上書き
    def nvl_menu(items):

        renpy.mode('nvl_menu')
        
        if nvl_list is None:
            store.nvl_list = [ ]

        screen = None
        
        if renpy.has_screen("nvl_choice"):
            screen = "nvl_choice"
        elif renpy.has_screen("nvl"):
            screen = "nvl"
            
        if screen is not None:

            widget_properties, dialogue = nvl_screen_dialogue()        

            rv = renpy.display_menu(
                items,
                widget_properties=widget_properties,
                screen=screen,
                scope={ "dialogue" : dialogue },
                window_style=style.nvl_menu_window,
                choice_style=style.nvl_menu_choice,
                choice_chosen_style=style.nvl_menu_choice_chosen,
                choice_button_style=style.nvl_menu_choice_button,
                choice_chosen_button_style=style.nvl_menu_choice_chosen_button,
                type="nvl",                      
                )
                
            for label, val in items:
                if rv == val:
                    store.current_voice = ''
                    store_say(None, config.readback_choice_prefix + label)
            return rv
            
        # Traditional version.
        ui.layer("transient")
        ui.clear()
        ui.close()

        ui.window(style=__s(style.nvl_window))
        ui.vbox(style=__s(style.nvl_vbox))

        for i in nvl_list:
            if not i:
                continue

            who, what, kw = i            
            rv = renpy.show_display_say(who, what, **kw)

        renpy.display_menu(items, interact=False,
                           window_style=__s(style.nvl_menu_window),
                           choice_style=__s(style.nvl_menu_choice),
                           choice_chosen_style=__s(style.nvl_menu_choice_chosen),
                           choice_button_style=__s(style.nvl_menu_choice_button),
                           choice_chosen_button_style=__s(style.nvl_menu_choice_chosen_button),
                           )

        ui.close()

        roll_forward = renpy.roll_forward_info()

        rv = ui.interact(roll_forward=roll_forward)
        renpy.checkpoint(rv)

        for label, val in items:
            if rv == val:
                store.current_voice = ''
                store_say(None, config.readback_choice_prefix + label)
        return rv
        
    ## readback
    readback_buffer = []
    current_line = None
    current_voice = None
    
    def store_say(who, what):
        global readback_buffer, current_voice
        new_line = (preparse_say_for_store(who), preparse_say_for_store(what), current_voice)
        readback_buffer = readback_buffer + [new_line]
        readback_prune()

    def store_current_line(who, what):
        global current_line, current_voice
        current_line = (preparse_say_for_store(who), preparse_say_for_store(what), current_voice)

    # 発言内容からのテキストタグの除去
    disallowed_tags_regexp = ""
    for tag in config.readback_disallowed_tags:
        if disallowed_tags_regexp != "":
            disallowed_tags_regexp += "|"
        disallowed_tags_regexp += "{"+tag+"=.*?}|{"+tag+"}|{/"+tag+"}"
    
    import re
    remove_tags_expr = re.compile(disallowed_tags_regexp) # remove tags undesirable in readback
    def preparse_say_for_store(input):
        global remove_tags_expr
        if input:
            return re.sub(remove_tags_expr, "", input)

    def readback_prune():
        global readback_buffer
        while len(readback_buffer) > config.readback_buffer_length:
            del readback_buffer[0]

    # 会話履歴を表示するためのキーマップの上書き
    def readback_catcher():
        ui.add(renpy.Keymap(rollback=(SetVariable("yvalue", 1.0), ShowMenu("text_history"))))
        ui.add(renpy.Keymap(rollforward=ui.returns(None)))

    if config.readback_full:
        config.rollback_enabled = False
        config.overlay_functions.append(readback_catcher)    
    
init python:
    yvalue = 1.0
    class NewAdj(renpy.display.behavior.Adjustment):
        def change(self,value):

            if value > self._range and self._value == self._range:
                return Return()
            else:
                return renpy.display.behavior.Adjustment.change(self, value)
                
    def store_yvalue(y):
        global yvalue
        yvalue = int(y)

# 会話履歴画面
screen text_history:

    #use navigation
    tag menu 
    
    if not current_line and len(readback_buffer) == 0:
        $ lines_to_show = []
        
    elif current_line and len(readback_buffer) == 0:
        $ lines_to_show = [current_line]
        
    elif current_line and not ( current_line == readback_buffer[-1] or False 
            if len(readback_buffer) == 1 else (current_line == readback_buffer[-2]) ):   
        $ lines_to_show = readback_buffer + [current_line]
        
    else:
        $ lines_to_show = readback_buffer
    
    
    $ adj = NewAdj(changed = store_yvalue, step = 300)
    
    window:
        style_group "readback"
    
        side "c r":
            
            frame:
                
                $ vp = ui.viewport(mousewheel = True, offsets=(0.0, yvalue), yadjustment = adj)

                vbox:
                    null height 10
                    
                    for line in lines_to_show:
                        
                        if line[0] and line[0] != " ":
                            label line[0] # name

                        if line[1]:
                            # 音声がない場合は会話を表示するのみ
                            if not line[2]:
                                text line[1] 
                            
                            # 音声がある場合は、会話をボタンとして表示し、クリック時に再生する
                            else: 
                                textbutton line[1] action Play("voice", line[2] )
                        
                        null height 10
                
            bar adjustment adj style 'vscrollbar'
        textbutton _("Return") action Return() align (.97, 1.0)