How to set up LayeredImages in Ren'py (& new sprite teaser!)
I don't bring much VN news, other than that I'm actively working still. There's a lot of new sprites and new codes and I'm having a lot of fun working on it. It'll be ready when it's ready.
Until then, I've been asked a few times to help set up LayeredImages so I figured I'd write a short guide out. Feel free to ask me to clarify and I'll update this.
A guide to Ren’py LayeredImages - by @GruntSteel
For this guide I will be using the character Mike from Distant Travels. While the image setup for the character is what I’d suggest most people use, your art style and setup might vary. I suggest reading through the entire guide before starting, to know a few more possibilities before setting up your files and layer structure.
We’ll start at the basics and go all the way to the cool stuff.
So, if you get to a point where you have all the functions you need, you don’t have to follow through the rest.
As another note, there are things that are required and things that might simply help you later, I will mark these optional things out.
File Structure & Setting up the Image Files
To begin setting up our LayeredImage, we first have to decide the layer structure for our art. As a LayeredImage in Ren’Py really just is the same as a .psd, .mbp or .clip file in terms of “structure.” Meaning, we have an order of layers of different images, and they will show up in the order we put them in.
However, before beginning we’ll have to take a look at the base structure of the code in Ren’py.
Simple Code Structure for a LayeredImage
A LayeredImage has a similar setup to your average layered image in your art program (photoshop, csp etc)
They are structured in a folder-like structure using Groups and Attributes. There are also the Always and If structures, but we’ll get to those later.
A group can contain different attributes or images, and only one attribute inside a group can be shown at once. Meaning that from the code snippet below, only one of the attributes shirt and tshirt can be shown at once, because they both reside within the group “shirts”.
Another thing to note is that the images will load from top to bottom in the script.
Here’s an example of code for a simple LayeredImage.
layeredimage mike: group body: attribute body: "sprites/mike/expressions/mike_body.png" group shirts: attribute shirts: "sprites/mike/shirts/mike_shirt.png" attribute tshirt: "sprites/mike/shirts/mike_tshirt.png" group pants: attribute pants: "sprites/mike/expressions/mike_pants.png" attribute shorts: "sprites/mike/expressions/mike_shhorts.png" group expressions: attribute neutral: "sprites/mike/expressions/mike_neutral.png" attribute happy: "sprites/mike/expressions/mike_happy.png" attribute sad: "sprites/mike/expressions/mike_sad.png"
The above snippet of code can be compared to the below picture of a .psd file. Notice how the body is at the top of the code, and the bottom of the image in photoshop? Meaning it’ll be the lowest layer of our image.
So to reiterate:
1) The order of “groups” in your layered image, the further up the attribute/group is in the layered image code, the further down it will be in the actual image.
2) Groups are like folders, only one attribute from a group can be shown at once. We use this for things like different pants or shirts. With this in mind, it’s important that we think in a group sense when deciding the order of layers.
For example, in Mike’s case I apply the shirts before the pants and the image is drawn accordingly. So, if Mike has both a shirt and pants on, the pants go above the shirt.
Outliers to this are completely fine, we’ll get to that after setting up the basics. Put the outliers in their own group for now. Next up, the actual image specifics.
Standardizing Blank Space in Image Files
For our layered image it’s important that the px height and width is the same for every piece of a character.
Every piece of clothing etc will be floating in blank pixels at the right location.
For example, for Mike, here’s his shirt. The entire image fitting Mike is 800x1000 px, so his shirt on its own will be at the position it’d be if he was also there. This is to make sure it’s not floating off randomly to the side.
Of course, we save every layer of the LayeredImage in its own file. I recommend .png, some would recommend .webp to save space.
Please note, since ren’py tries to automatically match images/sprites to file names *if they have a space in them* avoid using spaces in the file name.
e g if I named mike’s shirt “mike shirt.png” and did ‘mike shirt “hi!”’ to show mike with his shirt, it’d *only* show the shirt. To work around this, use underscores instead of spaces in file names. “mike shirt.png” -> “mike_shirt.png”
*Optionally, I would recommend having the same px height/width for all your characters after blank space, as this will help in positioning your characters for scenes etc.
Aside from the aforementioned clothes order being sprite specific (shirt before pants), I really recommend splitting it up in the following way:
- Body on it’s own, without expressions. I wouldn’t recommend including the head here, as tilting a head and so on can change so much if it’s an expression on it’s own.
- Clothes, in order depending on how they fit the best as drawn. I use shirt -> pants -> outerwear
- Expressions (The entire head, in my case.)
- Extras (hats etc)
For the code example, the snippet I used above fits. The images look like this for you visual people.
Congratulations! You have a very basic LayeredImage set up!
Remember to tie your layeredimage to the character in the character definitions by using “image=”
define mike = Character(("Mike"), color="#ff4242", image="mike")
You *could* go ahead and use it in your game, but you’re not yet taking advantage of anything other than the layer structure. To try and explain the usage of layer structures simply, since it’s a LayeredImage with the prefix mike, you can add / remove any attributes with the show statement in your script. Simply add the attribute in if you want to add it in. If you want to remove it, add a minus sign before the attribute.
show mike pants tshirt neutral body mike happy "Oh?" mike -tshirt "It's pretty warm... Don't mind if I just... take my t-shirt off." show mike -pants with dissolve mike "Wha? Where'd my pants go?" show mike pants "Phew, they stayed on." On a side note, if you don’t use minus signs it’ll remember the attribute from the image. E g; I use show mike pants tshirt neutral
at the start, and then I simply want to change his expression to “happy” from “neutral”. As those are in the same group, I can simply write it in the next line.
mike happy "Oh?"
It’ll remember that the attributes “tshirt” and “neutral” are active.
Extended functionality & the cool stuff
It’s time to automate things! For example, I don’t want to have to specify the fact that his body is to be shown every time. For this, we can either use the somewhat (in my eyes) redundant always statement, or use a default.
default has more uses and is faster in my opinion, so we’ll be using that.
First off, default is a tag (*arg for you pythonians) that we can add into the attributes of our LayeredImage.
It will make it so that that attribute is always loaded, unless another attribute from the same group is shown. It will also make it so that if you remove the non-default attribute, it will default back to the default attribute. First, let’s add it to his body so it’s just always loaded.
group body: attribute body default: "sprites/mike/expressions/mike_body.png"
That’s it. Since there’s no other attribute in the body group, it will always show that attribute.
This means we no longer have to specify it when showing the image.
So instead of
show mike pants tshirt neutral body
We can simply do
show mike pants tshirt neutral
Technically, the “attribute body” doesn’t have to be inside a group because it’s on its own, but I suggest using groups anyway for simplicity.
Now, we can apply the same thing to his head/expressions.
group expressions: attribute neutral default: "sprites/mike/expressions/mike_neutral.png" attribute happy: "sprites/mike/expressions/mike_happy.png" attribute sad: "sprites/mike/expressions/mike_sad.png"
We’re yet one attribute lower, it’ll always remember his body and default to his neutral expression.
show mike pants tshirt mike happy "Oh?" otherchar "You're ugly!" mike sad "Oh..." otherchar "I was kidding!" mike -sad "Oh."
This starts with showing mike with his neutral head, changes his expression to happy, then replaces happy with sad, and since neutral is defaulted, removing sad will result in him being neutral again.
If statements (ConditionSwitch lite)
Another thing is that LayeredImages supports its own type of If Logic.
There’s if, if_any, if_not and if_all.
if is much weaker than what you’re probably used to. It’s used outside groups and attributes meaning only one image can be displayed at a time with an if in a LayeredImage. Here’s a code example that shows a tattoo if $ mike_has_tattoo is true.
layeredimage mike: group body: attribute body default: "sprites/mike/expressions/mike_body.png" group shirts: attribute shirts: "sprites/mike/shirts/mike_shirt.png" attribute tshirt: "sprites/mike/shirts/mike_tshirt.png" if mike_has_tattoo: "sprites/mike/mike_tattoo.png" else: Null() group pants: attribute pants: "sprites/mike/expressions/mike_pants.png" attribute shorts: "sprites/mike/expressions/mike_shhorts.png" group expressions: attribute neutral default: "sprites/mike/expressions/mike_neutral.png" attribute happy: "sprites/mike/expressions/mike_happy.png" attribute sad: "sprites/mike/expressions/mike_sad.png"
Overall, I’d highly just recommend using a group, an attribute and ConditionSwitch instead ConditionSwitch official Ren'py Documentation An example using ConditionSwitch would look like this:
layeredimage mike: group body: attribute body default: "sprites/mike/expressions/mike_body.png" group shirts: attribute shirts: "sprites/mike/shirts/mike_shirt.png" attribute tshirt: "sprites/mike/shirts/mike_tshirt.png" group mike_tattoo: attribute tattoo default: ConditionSwitch( 'if mike_has_tattoo == True', "mike/mike_tattoo.png", 'if mike_has_tattoo == False', Null() ) group pants: attribute pants: "sprites/mike/expressions/mike_pants.png" attribute shorts: "sprites/mike/expressions/mike_shhorts.png" group expressions: attribute neutral default: "sprites/mike/expressions/mike_neutral.png" attribute happy: "sprites/mike/expressions/mike_happy.png" attribute sad: "sprites/mike/expressions/mike_sad.png"
The benefit of this is that we still have full access to what I’m to go through next (if_not, if_any, if_all), and we can also use longer if statements with multiple checks.
If_not, if_any and if_all
Finally! Conditions of LayeredImages.
You might have noticed above that Mike’s body is cutting through his shirt in the example image.
This kind of sucks. I’d like to fix this, but I still like the fact that the fluff is there when he’s shirtless. For that, we can use the conditionals for a LayeredImage. They’re used kind of like default, but they’re used in groups, not attributes. Arguably, attributes take these conditionals as well (according to official documentation), but for simplicity we’ll do it on group level.
if_not if_any if_all
I’ll start by preparing a second “body” image, that has no fur sticking out in these places. One that will only be shown if he’s wearing a shirt.
Once that’s done, I’ll use the above attributes to dynamically change the “body” attribute to load the correct body depending on what attributes are shown.
We could do this manually by simply adding another attribute, but with default we’d like to automate it. So, we use if_any on one group, meaning it’ll return true and therefore show up if any of the listed attributes are shown. if_any[“shirt”, “tshirt”]
At the same time, we want to show the shirtless version if there’s none, so we’ll use if_not with the same attributes in the conditionals. Since the attribute is defaulted, it’ll also always show up.
layeredimage mike: group body if_any["shirt", "tshirt"]: attribute body default: "sprites/mike/expressions/mike_body_nofur.png" group body if_not["shirt", "tshirt"]: attribute body default: "sprites/mike/expressions/mike_body.png" group shirts: attribute shirt: "sprites/mike/shirts/mike_shirt.png" attribute tshirt: "sprites/mike/shirts/mike_tshirt.png"
Similarly, if_all will have to match all arguments. We can get pretty advanced with this as well, for example if there’s a variant of a hat that only shows up if the character is wearing both a jacket, and is angry (because the ears change, or such), but never shows up if he’s wearing a coat.
To achieve this, simply add another argument in the same line. The order does not matter.
group body if_any["shirt", "tshirt"] if_not["cloak"]: attribute body default: "sprites/mike/expressions/mike_body.png"
Now, remember what I mentioned about outliers? If I had a shirt that was on a different layer than other shirts, let’s say I randomly want his shirt above his pants, this is what I’d do. I’d add an if_not and list the regular shirt group/attribute there, so both shirts don’t show up at once.
If we want to go further, we can adjust the layer order and then name the attributes and groups the same thing, then add conditionals so for example the shirt is outside the pants only if the attribute “tired” is active. Null Images
I had no idea where to put this. Sometimes it’s been fairly useful to use NullImages. For example,some of my characters have a watch that they almost always have on. I don’t wanna write this every time I show the character in script, so instead I flip it around and show a null image.
group watch: attribute watch default: "sprites/mike/watch.png" attribute no_watch: Null()
So, if I *dont* want the watch, I’ll “show mike no_watch” instead.
You might also have seen me use a NullImage in the condition switch before.
SideImage and LayeredImageProxy Finally, a lot of people like their Side Images. I’m not different. For a side image, we can use something called a LayeredImageProxy, it quite simply copies the LayeredImage and shows it again, for a side image for example.
image side mike = LayeredImageProxy("mike", Transform(crop=(120, 50, 400, 350), xzoom=-1.0))
To explain the above statement, we set the prefix “side” (default in renpy) for image “mike” to use a LayeredImageProxy of the image “mike” (we defined LayeredImage mike when writing our LayeredImage). We then crop it in x, y, width, height. So we remove 120 blank pixels from the side, 50 blank pixels from the top, and then cut a square that’s 400 x 350 px. We then xzoom -1.0 to flip this and use it as our side image.
ATL on groups
LayeredImages take transforms and ATL directly in the code. Let’s say everytime Mike puts on his tie, it’s a magical tie and he calls it in from the void.
group tie: alpha 0.0 linear 1.0 alpha 1.0 attribute tie: "sprites/mike/tie.png"
Jokes aside, the main use for this is for asymmetrical characters. For example, I have a character that has a scar on one side of their face. So if I want to flip them, a xzoom -1.0 mirrors them wrongly.
What I did to solve it was add a group with a NullImage called flip.
group flip: attribute flip: Null()
This let’s us write “show mike flip”, and then automate what image shows up depending on if flip is active. Now, I went all the way and made the body and face etc automatically flip when this happens, but due to blank space it offset the character a bit from the head with the scar image. I fixed this with ATL and if_not and if_any statements, so when writing my script all I have to think about is “show mike flip” or “show mike”.
layeredimage mike: group body if_all["flip"] if_any["shirt", "tshirt"]: xzoom -1.0 xoffset 10 attribute body default: "sprites/mike/expressions/mike_body.png" group body if_all["flip"] if_not["shirt", "tshirt"]: xzoom -1.0 xoffset 10 attribute body default: "sprites/mike/expressions/mike_body_nofur.png" group body if_not["flip"] if_any["shirt", "tshirt"]: attribute body default: "sprites/mike/expressions/mike_body.png" group body if_not["flip", "shirt", "tshirt"]: attribute body default: "sprites/mike/expressions/mike_body_nofur.png"
Get Distant Travels
Leave a comment
Log in with itch.io to leave a comment.