Automatic Scaling in Ren’Py

Ren’Py is an authoring system for visual novels. Last semester, I was involved in the creation of such a novel („Felix, Gaijin“ – German only, sorry) together with several fellow students. We stumbled upon a „little problem“: Out of the box, images in Ren’Py can only be of one size, but we wanted to use the maximum possible resolution. This article describes how I extended our story with an automatic scaling system.

One little disclaimer: This is the first time that I came in contact with python – the language behind Ren’Py – so my terminology might be awkward. 🙂

The basic approach is simple: Create the images in an as high resolution as possible (or reasonable). Then find the optimal fullscreen resolution and scale the images and game interface to that size.

Scaling images

The scaling of images itself is easy, just create your image variable in the script using im.Scale()

image myimage = im.Scale("images/whatever.jpg", <width>, <height>)

The tricky part is determining the optimal resolution to use. I finally came up with the following function which does all this:

Finding the best resolution


      # get_target_resolution(source_x, source_y)
      #   Function for retrieving scaling data for the images.
      #
      # Parameters:
      #   - horizontal size of the source images
      #   - vertical size of the source images
      # 
      # Returns:
      #   - horizontal resolution
      #   - vertical resolution
      #   - scale factor
      #   - aspect ratio of the original system resolution
      #       if the system resolution cannot be found, the ratio of the source
      #       image is used
      def get_target_resolution(source_x, source_y):
      
      
          # default return values (change nothing)
          factor = 1
          target_x = source_x
          target_y = source_y
          source_factor = float(source_x) / float(source_y)
          screen_factor = float(source_x) / float(source_y)
          
          # At first, try to detect the system resolution. If successful, use
          # this one. (currently works only on windows systems)
          import os
          if os.name == 'nt':
            from win32api import GetSystemMetrics
            # Only use the system resolution if it seems to make sense and isn't
            # a dual-monitor resolution. It makes sense if the aspect ratio
            # is that of a normal monitor. 
            if 1.25 < = (float(GetSystemMetrics(0)) / float(GetSystemMetrics(1))) < 1.8:
              factor = float(GetSystemMetrics(0)) / float(source_x)
              target_x = GetSystemMetrics(0)
              target_y = int(float(GetSystemMetrics(0)) / source_factor)
              screen_factor = float(GetSystemMetrics(0)) / float(GetSystemMetrics(1))
            return target_x, target_y, factor, screen_factor
          

          # if the system resolution could not be used, try the largest
          # resolution that pygame provides (again, only if it makes sense).
          # Also try this only if it was requested via parameter, because
          # too high resolutions can damage poorly constructed CRT screens.
          
          try: # try-except is necessary because not all renpy versions support parameters
            if len(config.args) > 0:
              if config.args[0] == "autoscale":

                # get a list of display modes, sorted by resolution
                import pygame
                pygame.display.init()
                modes = pygame.display.list_modes()
                
                # find the appropriate scaling factor, 1 being the default
                factor = 1 #default
                for i in range(len(modes)):
                  aspect_ratio = modes[i][0] / modes[i][1]
                  # if the aspect ratio is somewhat reasonable (not some multi-screen setup
                  # which *should* be recognizable by unusal aspect ratios) set the scale
                  # factor to scale to this resolution => the highest resolution should be
                  # chosen
                  if 1.25 < = aspect_ratio < 1.8: 
                    factor = modes[0][0] / float(source_x)
                    target_x = modes[0][0]
                    target_y = int(float(target_x) / source_factor)
                    break
                
                    
                return target_x, target_y, factor, screen_factor

          except:
            pass  
          
          # if everything failed, just use the default resolution of 800x600 which
          # should be supported by everyone and should not be able to destroy and
          # monitors.
          return 800, int(800.0 / source_factor), (800.0 / float(source_x)), (800.0 / 600.0)

There are a few restrictions, unfortunately. This function can check which video modes are available and use the largest possible. In theory, this is great, but it has a massive drawback: Not all systems know what’s good for them. For example, my CRT monitor would happily switch to full 1920×1080, although I never used a resolution higher than 1280×1024 on it. The result is a high-resolution high-distortion image which looks crappy. Worse than that: Some cheap CRT monitors can break if they receive too hight resolutions.

For Windows systems, I found a solution: The function queries the current system resolution and always sticks with that one.

For other systems, I do not know how to query the system resolution. The default behaviour on such systems is to fall back to 800×600 (that should not be able to kill any monitor). The user can decide to switch the safety off, by starting the story with the parameter “autoscale”. Then, it uses the largest video mode available.

Using it

I suggest putting the function in options.rpy right under “init -1”.

Then, wherever you need information about your optimal resolution, you do this:

target_data = get_target_resolution(1920, 1080)

1920 and 1080 are the width and height of your source material.

target_data includes the following information:

  1. horizontal resolution
  2. vertical resolution
  3. scale factor
  4. aspect ratio of the original system resolution
    (if the system resolution cannot be found, the ratio of the source image is used)

So now you can set the optimal size of your game in options.rpy:


        # width = target width
        config.screen_width = target_data[0]
        # height = target width / screen aspect ratio
        config.screen_height = int(float(target_data[0]) / target_data[3])

And scale your pictures accordingly in script.rpy:

image myimage = im.Scale("images/whatever.jpg", target_data[0], target_data[1])

Using backgrounds with different aspect ratios

When using your images, you need to consider that the monitor might have a different aspect ratio than your image. That means that you cannot use the scene command to display your backgrounds. Displaying a 16:9 image on a 4:3 monitor using scene would lead to “undefined” areas (usually visible as black bars). If you display text in these freak zones, it’ll “burn in”, meaning that it won’t go away when you display the next line of text.

So you need to use a real black background: image black = "#000000"

You can then use this background as scene (scene black) and display your images using show: show myimage at truecenter

Note the parameter “at truecenter” which makes sure that the image is always centered and not at the bottom of the screen or something.

Usually you do not need to hide your backgrounds when you want to display the next one. The new one will be placed above the old one. You only need to be careful if you want to switch back to an image that you already used. In this case, you need to hide it first, because otherwise it won’t come to the top again.

Scaling the interface

Now that your images are scaled, you might want to scale the interface as well. If you do not scale the font size, for example, then the text will either be tiny on large resolutions, or huuuuge on small resolutions.

Scaling the text box

I recommend a black, semi-transparent text box:

style.window.background = Solid((0, 0, 0, 128))

This is how you define the minimum height, depending on aspect ratio and resolution:


        if ((target_data[3]) <= 1.4):
            style.window.yminimum = int(float(config.screen_height) * 0.125)
        else:
            style.window.yminimum = int(float(config.screen_height) * 0.20)

Scaling the font size

style.default.size = int(target_data[0]/(1920/40))

Scaling the bookmark thumbnail size


    # 120 pixels high at resolution of  x*800. Scales linear at other resolutions.
    $ config.thumbnail_height = int(target_data[1]/(800/120))
    $ config.thumbnail_width = int(config.thumbnail_height * target_data[3])

Scaling menu and button font sizes


    $ style.menu_choice.size = int(target_data[0]/(1920/44))
    $ style.button_text.size = int(target_data[0]/(1920/44))
    $ style.button.xminimum = int(target_data[0]/(1280/300))

Scaling the settings menu

Scaling the settings menu turned out to be painful. Somehow it does not accept continuous sizes, so I had to use some stepping:


    $ style.prefs_label.size = int(target_data[0]/(1280/25))
    $ style.prefs_left.xpos = 0.2
    $ style.prefs_center.xpos = 0.5
    $ style.prefs_right.xpos = 0.8
    
    if target_data[0] >= 1920:
        $ style.prefs_column.xmaximum = 550
    elif target_data[0] >= 1600:
        $ style.prefs_column.xmaximum = 450
    elif target_data[0] >= 1280:
        $ style.prefs_column.xmaximum = 375
    elif target_data[0] >= 1024:
        $ style.prefs_column.xmaximum = 275
    else:
        $ style.prefs_column.xmaximum = 225

Note that there might still be some steps missing, but I hope I covered the most important resolutions.

Final Words

Feel free to use any code that I published in this article. I hereby put all fragments under an GPLv3 license.

Also feel free to ask questions in the comments if you have trouble understanding my gibberish. 🙂

If anyone has worked out any improvements => leave a comment.

One reply on “Automatic Scaling in Ren’Py”

Comments are closed.