Welcome to the sponsor-exclusive content for the Ren'Py Patreon. Sponsors like you ensure this page exists. Thank you.

_images/2021-11.jpg

Ren’Py Developer Update - November 2021 link

Welcome to this month’s Ren’Py developer update. As we go into the last month of the year, I’ve been successfully solving issues found in 7.4, which will lead to the next release shortly. After a quick update on 7.4, I’ll describe the new gui.variant decorator, and then give a few ways to make a text marquee.

As always, thank you for supporting Ren’Py.

November 2021 link

I spent most of November working on 7.4.11, the next maintenance release of Ren’Py. So far, there have been three prereleases that have come out, and the reports from people that have been trying the nightlies is that the current version is the best yet. So I’m hoping to get that released early in December, giving a version of Ren’Py people can work with for the next few months.

One of the most complex problems in 7.4.10 was an issue with scope in screens, where interpolated text (like text "the_variable=[the_variable]") wasn’t updating when it should. That took quite a bit of work to track down, and while what’s in the current prerelease is my second try, I’m sure we got it.

Live2D also got a bit of work in this release. Now that we’re getting a bit of use of that technology, I’m getting some reports of how Ren’Py differs from the reference implementation, so I’ve been changing Ren’Py to better match it.

A user reported an issue with how the default games react when the gui was rebuilt, when a variant is used. (For example, how the default gui changes size when it’s run on a small device, like a phone.) That lead to the new gui.variant decorator, which I’ll explain how to use below.

These are just some of the fixes that went in this month. Including what was done last month, I’m counting 27 fixes that are going into Ren’Py 7.4.11, due out soon.

Decorators and gui.variant link

This month saw the introduction of a new decorator, gui.variant, into Ren’Py. Since this is one of the first decorators in the public Ren’Py API (another is renpy.atl_warper(), but I don’t think that’s used much), I thought I’d describe what a decorator is and how they’re used.

A decorator is a function that takes one parameter, a function, and returns a function. Python has a special syntax for calling this - you begin the decorator with an @, and put it on a line before the function it’s decorating. When called with this syntax, the function it returns is assigned to the original name.

For example,

init python:

    @wrapper
    def myfunction():
        return None

is equivalent to:

init python:

    def myfunction():
        return None

    myfunction = wrapper(myfunction)

One good use of this is as a way to register functions to be called by a larger system. When this is done, you can use the __name__ property to get the name of a function to be called.

For example, you could use a decorator to register an event, and a function that should be called to check to see if the event should occur. A simple version of this might be:

init python:

    # A list of (name, function) tuples.
    daily_events = [ ]

    def daily_event(f):
        """
        Register an event that uses a function to run on a given day.
        """

        daily_events.append((f.__name__, f))
        return f

This can then do things like register events along with a function that can be called to determine if the event should run.

init python:

    import datetime

    @daily_event
    def christmas():
        now = datetime.datetime.now()
        return now.month == 12 and now.day == 25

    @daily_event
    def christmas():
        now = datetime.datetime.now()
        return now.month == 11 and now.day == 1

    @daily_event
    def always():
        return True

This could be used to implement a system that calls a label on a certain date. For example:

init python:

   def find_daily_event():
       """
       Finds an event to run on a given day.
       """

       for name, function in daily_events:
           if function():
               return name

   label start:
       jump expression find_daily_event()

While Python lets you return a different object from the wrapper, in Ren’Py that’s probably a bad idea - the load and save system means that functions have to keep the same name. This might not be the case all the time, but for functions that can show up in save files, make sure any decorators applied return the original function.

The gui.variant Decorator link

The newest decorator is gui.variant, and it’s meant for use in gui.rpy. What it does is register a function to be called when the game first starts, and then each time the gui is rebuilt. This is something that happens when the language of the game is changed, when a gui preference is changed, or when gui.rebuild() is called.

As a reminder, variants are selected by Ren’Py based on what platform and hardware it’s run on. For example, the “pc” variant is present when running on Windows, macOS, or Linux, the “mobile” version on Android and iOS and so on. You can find out more about variants here.

This fixes a problem that’s been in the default Ren’Py games for the past few years. If your game was created before Ren’Py 7.4.11, and you haven’t changed it, it has at the bottom of gui.rpy:

init python:

    ## This increases the size of the quick buttons to make them easier to touch
    ## on tablets and phones.
    if renpy.variant("touch"):

        gui.quick_button_borders = Borders(40, 14, 40, 0)

    ## This changes the size and spacing of various GUI elements to ensure they
    ## are easily visible on phones.
    if renpy.variant("small"):

        ## Font sizes.
        gui.text_size = 30
        gui.name_text_size = 36
        gui.notify_text_size = 25
        gui.interface_text_size = 36
        gui.button_text_size = 34
        gui.label_text_size = 36

        ## Adjust the location of the textbox.
        gui.textbox_height = 240
        gui.name_xpos = 80
        gui.dialogue_xpos = 90
        gui.dialogue_width = 1100

        ## Change the size and spacing of items in the game menu.
        gui.choice_button_width = 1240

        gui.navigation_spacing = 20
        gui.pref_button_spacing = 10

        gui.history_height = 190
        gui.history_text_width = 690

        ## File button layout.
        gui.file_slot_cols = 2
        gui.file_slot_rows = 2

        ## NVL-mode.
        gui.nvl_height = 170

        gui.nvl_name_width = 305
        gui.nvl_name_xpos = 325

        gui.nvl_text_width = 915
        gui.nvl_text_xpos = 345
        gui.nvl_text_ypos = 5

        gui.nvl_thought_width = 1240
        gui.nvl_thought_xpos = 20

        gui.nvl_button_width = 1240
        gui.nvl_button_xpos = 20

        ## Quick buttons.
        gui.quick_button_text_size = 20

Perhaps with some of the numbers different, depending on what size you selected for your game.

This has the problem that it only runs once, at init-time, and then never again. By replacing it with:

init python:

    ## This increases the size of the quick buttons to make them easier to touch
    ## on tablets and phones.
    @gui.variant
    def touch():

        gui.quick_button_borders = Borders(40, 14, 40, 0)

    ## This changes the size and spacing of various GUI elements to ensure they
    ## are easily visible on phones.
    @gui.variant
    def small():

        ## Font sizes.
        gui.text_size = 30
        gui.name_text_size = 36
        gui.notify_text_size = 25
        gui.interface_text_size = 36
        gui.button_text_size = 34
        gui.label_text_size = 36

        ## Adjust the location of the textbox.
        gui.textbox_height = 240
        gui.name_xpos = 80
        gui.dialogue_xpos = 90
        gui.dialogue_width = 1100

        ## Change the size and spacing of items in the game menu.
        gui.choice_button_width = 1240

        gui.navigation_spacing = 20
        gui.pref_button_spacing = 10

        gui.history_height = 190
        gui.history_text_width = 690

        ## File button layout.
        gui.file_slot_cols = 2
        gui.file_slot_rows = 2

        ## NVL-mode.
        gui.nvl_height = 170

        gui.nvl_name_width = 305
        gui.nvl_name_xpos = 325

        gui.nvl_text_width = 915
        gui.nvl_text_xpos = 345
        gui.nvl_text_ypos = 5

        gui.nvl_thought_width = 1240
        gui.nvl_thought_xpos = 20

        gui.nvl_button_width = 1240
        gui.nvl_button_xpos = 20

        ## Quick buttons.
        gui.quick_button_text_size = 20

the gui.variant decorator will only call the functions if the variant given by the function name is present, and then it will call it whenever the gui is rebuilt.

Marquee link

A discord user contacted me this month asking if there was a way to create a marquee in Ren’Py - text that scrolls back and forth if it’s bigger than the area allotted to it. After a little bit of work, I figured out how to make it happen, and here’s the result:

transform marquee_move(t):
    xalign 0.0
    pause t / 4
    linear t xalign 1.0
    pause t / 4
    linear t xalign 0.0
    repeat

screen marquee(text, width=100, size=100, t=2.0):
    fixed:
        yfit True
        xsize width
        at Transform(crop=(0, 0, 1.0, 1.0), crop_relative=True)

        text text:
            layout "nobreak"
            size size
            at marquee_move(t)

screen test():
    vbox:
        use marquee("Hello, world.", 300)
        use marquee("Hello, alternate universe.", 300)

This works by using a fixed layout of a certain size to contain the text, and then a transform that crops the fixed layout to make sure the text can’t be shown outside it. The text itself is shown using layout "nobreak" to disable line wrapping, and then is moved with a transform to provide the marquee effect.

This default marquee bounces the text back and forth if it’s too big, because that’s what I was asked for. An alternative would be to move it from offscreen on the right to offscreen on the left:

transform marquee_move(t):
    xanchor 0.0 xpos 1.0
    linear t xanchor 1.0 xpos 0.0
    pause t / 4
    repeat

I like to put a pause in all ATL transforms, to make sure Ren’Py has time to load images it’s predicted. Of course, you can change the marquee_move transform to be anything you want.

Thanks link

Thanks to everyone that’s supported Ren’Py. Your support is very motivating as I try to bring you the best visual novel engine I can.