How to make resizeable backgrounds in Ren’Py with Frame

How to make resizeable backgrounds in Ren’Py with Frame

It’s common for UI to reuse the same background for various UI elements, just sizing it differently based on its contents. Often this background has a repeating or solid pattern on the inside so it can be stretched or tiled to accommodate many sizes.

The most basic example of this is a messenger bubble in a chat app. The rounded corners and little speech bubble triangle will be the same size for all messages, but depending on how long the text is, the bubble sides may get longer or taller to accommodate the contents.

Luckily, Ren’Py has a built-in displayable you can use to achieve this effect for your UI as well without creating a whole bunch of additional images. It’s called Frame (not to be confused with frame, which we covered in Ren’Py Screen Language Basics – Frames – though Frame displayables are often used as backgrounds for frame containers). If you’ve done any development in Android, you might also know this technique as “9-patch”, for reasons which will become clear shortly.

Difficulty Level: Beginner

This tutorial is designed to be understood as a standalone guide to Frame. In particular, the knowledge in this tutorial will be sufficient to let you swap out some images in the default GUI without needing to know anything else about screen language. That said, you may find it useful to read through more of my tutorials on screen language, in which case I recommend starting with Getting Started with Ren’Py Screen Language.

What is Frame?

At its core, Frame takes a displayable of some kind (generally an image) and slices it up in a 3×3 grid based on numbers you provide – hence the name “9-patch” for the 9 squares. The corners of the 3×3 grid (squares 1, 3, 7, and 9 in the image below) are preserved exactly as-is regardless of how big the Frame displayable will be, but the inner “+” of grid squares (squares 2, 4, 5, 6, and 8) will be either stretched (the default) or tiled to make the Frame as tall and wide as it needs to be.

This makes it incredibly powerful for UI, since you can reuse the same Frame for the background of dozens of UI elements regardless of their size, since the Frame will handle resizing the background to whatever you need.

How to set up Frame

Frame takes as few as 2 arguments to set up. An argument is just a value of some kind that you’re going to provide to Frame so it can slice up your image properly.

The one consistent argument you will provide a Frame is an image or displayable of some kind. This is the displayable you’re trying to stretch/tile to fill out a background. You should be able to slice this displayable up into a 3×3 grid to make it work for a Frame.

Next, you have three main options for providing the numbers for the 3×3 grid.

Method 1: Two numbers

If you provide two numbers after the displayable, those are interpreted as the x border and the y border. So, take the following example:

image my_background = Frame("myframe.png", 25, 30)

For this Frame, the left border will be 25 pixels from the left edge of the image and the right border will be 25 pixels from the right edge of the image. Similarly, the top border will be 30 pixels from the top edge and the bottom border will be 30 pixels from the bottom edge. Ren’Py will use these numbers to slice up the image “myframe.png” to turn it into a resizable image you can use as a background.

Method 2: Four numbers

If you provide four numbers after the displayable, those are interpreted as the (left, top, right, bottom) borders, respectively. You remember it by thinking of it going clockwise, starting at the left. For example:

A floral frame is shown on the right with text. The left shows a smaller rounded square the frame is constructed from.
image floral_background = Frame("flowers.png", 214, 230, 206, 217)

For this example, the left border is 214 pixels from the left edge, the top border is 230 pixels from the top edge, the right border is 206 pixels from the right edge, and the bottom border is 217 pixels from the bottom edge.

Method 3: A Borders object

You may also provide a Borders object right after the displayable, in which case the four numbers you provide act exactly like just providing 4 regular numbers would (so, left/top/right/bottom; see Method 2 above).

image speech_bubble = Frame("bubble.png", Borders(53, 47, 28, 29))

In this case, the left border is 53, the top border is 47, the right border is 28, and the bottom border is 29. Borders are used a lot in the default gui styles as an easy way of holding four numbers for padding or Frame borders, though personally I don’t use them myself and generally prefer to directly provide numbers as in methods 1 and 2.

Note

Note that the numbers you provide to Frame to split up the displayable for stretching/tiling do NOT have to correspond to the padding for the displayable. Sometimes they are the same or similar, but you can use Frame to stretch unusual and asymmetrical displayables as well. In such a case, often at least one of the borders is much higher than the others (e.g. messenger bubbles with a little triangle. The side with the triangle has a bigger border than the other sides.).

Tiling

By default, the displayable provided to Frame will be stretched to fit the required area. However, there are some background designs that should be tiled instead of stretched, for example, frames with a dotted line border, frames with a repeating pattern on the inside, frames with a particular outline (e.g. with wavy edges for a thought cloud), etc.

There are two possible methods Ren’Py will use for tiling a provided image.

Method 1: Regular tiling

To make the displayable tile, simply add tile=True to the end of your Frame declaration e.g.

image polka_dots = Frame("gui/dots.png", 0, 0, tile=True)

For this example, we have a polka-dot background that should be tiled when it’s resized to fit a larger area. Notably the borders are 0; this is perfectly valid if you don’t need to preserve the shape or style of the edges when the displayable is resized. In this case we’re using Frame primarily to tile the polka dots to fill out an area.

If you set tile=True for your Frame, Ren’Py will simply repeat the provided sections as many times as possible to fill out the area.

Method 2: Integer tiling

This method is very similar to the previous one, except instead of tile=True you’ll use tile="integer" e.g.

image thought_bubble = Frame("gui/thought.png", 20, 10, 20, 30, tile="integer")

As opposed to regular tile=True, tile="integer" means that Ren’Py will calculate how many repetitions of each “tile” it can create to fill the given area, and then rounds that to the nearest integer.

So, for example, if your image is 50×50 and you provided borders of (20, 20, 20, 20), then the tiles will be 10×10 (since 20 pixels are taken up on each side of the displayable). If you then use this Frame to fit an area that’s 67×92 pixels wide, there will be (68-(20*2))/10 = 2.7 tiles along the top and bottom edges, which will be rounded up to 3. Similarly, there will be (92-(20*2))/10 = 5.2 tiles along the left and right edges, which will be rounded down to 5.

To get the final displayable, Ren’Py calculates these numbers and then squashes or stretches the tiles to fit. So, for the earlier example, three 10×10 tiles along the top and bottom edges would usually take up 30×10, making for a final width of 30+20*2=70. But if you recall, the provided width was 67, not 70. So Ren’Py will slightly squish each of the tiles to be 9×10 instead, so the final width is 20*2+9*3=67.

Similarly, the five 10×10 tiles along the left and right sides would normally take up an area of 10×50, giving the displayable a final height of 20*2+50=90 instead of 92. Ren’Py will slightly stretch the tiles vertically to fill out this space, so each tile is 10×10.4 instead of 10×10.

Unlike with tile=True, tile="integer" will look different depending on how large the tiled sections are. Generally, you should try to reduce your image to have the smallest repeating section possible for the best adaptability.

Which tiling method to use

In short, if it’s okay to have half a tiled square as part of your tiled background, just do tile=True. If each tile needs to contain the same contents and can’t be cropped/split, then use tile="integer" (and ideally either have each tile be fairly small or use displayable sizes that are close to a round number of tiles for your tiled Frame).

Using Frame for buttons and frames

To use Frame in-game, you can declare it using the image statement and then use that as the background, or declare a Frame directly as the background property of a style or container.

Two scrolls are side by side. The first reads "By royal decree, you shall be executed on the morrow." The second reads "Signed, the King".
image history_scroll = Frame("scroll.png", 10, 120)
screen royal_decree():
    frame:
        # Uses your resizable scroll background
        background "history_scroll"
        # Since the scroll curls are fairly tall, this container
        # looks best with a minimum size
        yminimum 300
        text "By royal decree, you shall be executed on the morrow."

Note the background "history_scroll" line; this uses the history_scroll image declared just above the screen, which is a Frame.

A screenshot with a rectangle in the middle that has a dotted outline. Text in the rectangle reads "How to play. Rock beats scissors. Scissors beat paper. Paper beats rock."
screen information():
    frame:
        background Frame("big_outline.png", 25, 25, tile="integer")
        has vbox
        text "How to play" size 50
        text "Rock beats scissors"
        text "Scissors beat paper"
        text "Paper beats rock"

In this example, the background for the frame directly uses a Frame as the value rather than declaring it as an image and using that.

Summary

  • Frame allows you to declare an image which will resize itself to fit whatever space it is given. You can provide information to Frame to customize how the image is stretched or squished to fit the available space.
  • Frame images are either stretched (default) or tiled (tile=True or tile="integer") to expand to fill additional space. tile=True can result in half tiles; tile="integer" will guarantee full tiles but may squish or stretch the tiles to fit the space.
  • You can use Frame anywhere a displayable is used – so, as the displayable for background properties, in styles, added directly to screens with add, etc.

Bonus tool

You may have noticed I’m using some kind of tool in the various screenshots for this tutorial – this tool is available on my itch.io:

You can use the tool to find the borders, tiling, and padding for a Frame image, and then copy the result to your clipboard to use in your game.

Leave a Reply