Ren'Py Changing displayed picture in character menu

SPkiller31

Member
Game Developer
Dec 20, 2020
130
159
Hi everyone, I wanted to add simple feature to my character panel.
I would love to add textbutton/imagebutton that will display menu with clothing options for each character (like school, daily, sport etc) but probably my braincells can't find solution to how to implement it in my code.
I use screen that displays certain characters depending on selected panel. I have not idea how could I add choice menu in something like that.

I thought about using very basic if outfit_a = 1 add school_outfit_a or something like that and then displaying choices that will modify that value. No idea how to make it work, without redoing a lot of code or making code very ugly.

I know that 'technically' I should be able to make a ton of labels, use jump option, change each label depending on time of the day player was in that panel but then, by doing it that way I have no idea how could I return to the exactly that character's panel. Plus it sounds super tedious and ineffective.
Probably it all comes down to my little-to-noone knowledge about more advanced Python stuff.

Thanks in advance for any help regarding my problem.

Here is the single character panel code, I deleted all unimportant stuff like text displaying info about character etc. Code is little modified version of the one that one of good folks helped me make. Ofcourse after "test" characters I have bunch of my characters but that's the main working code.
Python:
screen characters():
 
    tag menu

    default character_page_info = "Characters"
 
    use game_menu(_("Characters")):
 
        fixed:
 
            order_reverse True
            text character_page_info + " info":
                style "page_label_text"
                xalign 0.5
 
            if character_page_info == "Characters":
                grid 6 2:
                    style_prefix "character_slot"
                    xalign 0.0 yalign 0.1
 
                    spacing 50
 
                    for i in my_characters_list:
                        vbox:
                            imagebutton:
                                idle "cha_" + str(i.lower()) + "_info_idle.png"
                                hover "cha_" + str(i.lower()) + "_info_hover.png"
                                action SetScreenVariable("character_page_info", i + "'s"))
            else:
                if character_page_info == "test's":
                    text "Affection: [test_affection]" xalign 0.1 yalign 0.35
                    imagebutton xalign 0.1 yalign 0.7:
                        idle "cha_events"
                        hover "cha_test_events"
                        action ShowMenu('test_eventlist')
                    add "cha_replays" xalign 0.5 yalign 0.7
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,302
15,172
I thought about using very basic if outfit_a = 1 add school_outfit_a or something like that and then displaying choices that will modify that value. No idea how to make it work, without redoing a lot of code or making code very ugly.
Rely on dynamism.

By example:
Python:
screen whatever():

    default outfit = "casual"

    hbox:
        vbox:
            xsize 400
            text "Outfits:"
            textbutton "casual":
                action SetScreenVariable( "outfit", "casual" )
            textbutton "school uniform":
                action SetScreenVariable( "outfit", "school" )
            textbutton "sport":
                action SetScreenVariable( "outfit", "sport" )

        vbox:
            add "images/outfit/{}.jpg".format( outfit )
Then it will display "image/outfit/casual.jpg", "image/outfit/school.jpg" or "image/outfit/sport.jpg", depending of the player choice.
 
  • Like
Reactions: SPkiller31

MidnightArrow

Member
Aug 22, 2021
498
423
What you want is "Model-View-Controller" architecture. The model (the data) should be separate from the view (the GUI) and the controller (your input).

Create a class to store all the data about the character.

Code:
init python:

    OUTFIT_NUDE = 0
    OUTFIT_UNDERWEAR = 1
    OUTFIT_SAILORFUKU = 2

    class CharacterData:
        def __init__(self, name, base_image):
            self.name = name
            self.affection = 0
            self.base_image = Image(base_image)
            self.outfit_state = OUTFIT_NUDE
            self.outfit_image = Null()

        get_image(self):
            return Composite((800, 400),
                (0, 0), self.base_image,
                (0, 0), self.outfit_image
            )

        # virtual method, to be overridden by subclass
        def set_outfit_image(self, outfit_enum):
            self.outfit_image = Null()
 
    class Yuki(CharacterData):
        def __init__(self):
            super(CharacterData, self).__init__("Yuki", "yuki_base_image.png")
     
        # overridden from superclass.
        def set_outfit_image(self, outfit_enum):
            if outfit_type == OUTFIT_NUDE:
                # check state
                if is_okay:
                    self.outfit_image = Null()
                    self.outfit_state = OUTFIT_NUDE
                else:
                    # handle problems
            elif outfit_type == OUTFIT_UNDERWEAR:
                # check state
                if is_okay:
                    self.outfit_image = Image("yuki_underwear.png")
                    self.outfit_state = OUTFIT_UNDERWEAR
                else:
                    # handle problems
            elif outfit_type == OUTFIT_SAILORFUKU:
               # check state
                if is_okay:
                    self.outfit_image = Image("yuki_sailorfuku.png")
                    self.outfit_state = OUTFIT_SAILORFUKU
                else:
                    # handle problems
           else:
                # handle problems
Then pass that object to the stats screen as an argument. The view (the screen) will only look at the abstract base class, leaving the model (each individual character subclass) to handle their own internal logic.

Code:
screen character_stats(character):
    vbox:
        hbox:
            text "Affection: "
            text str(character.affection)
        fixed:
            add character.get_image()
        vbox:
            text "Outfits"
            textbutton "Naked":
                action Function(character.set_outfit_image, [OUTFIT_NUDE])
            textbutton "Underwear":
                action Function(character.set_outfit_image, [OUTFIT_UNDERWEAR])
            textbutton "Sailor Fuku":
                action Function(character.set_outfit_image, [OUTFIT_SAILORFUKU])
        textbutton "Return":
            action Return()

screen character_select:
    default current_character = None

    # grid to set current_character variable from portraits

    if current_character:
        use character_stats(current_character)
Not sure if that's 100% correct, since Python and screen language are both shit and love screwing you over at every chance just when you think you've got a handle on them. But from a general programming perspective, that's a solid foundation for model-view-controller architecture.

Edit: Re-reading your question, it sounds like maybe you want to store different variants of outfits, rather than hardcoded "types" of outfits? In that case, the best solution would be to create an array of tuples storing the variants inside the character class and then fetch them when the character is passed to the screen.

Code:
class Yuki(CharacterData):
    def get_outfit_variants(self, outfit_type):
        all_outfits = []
        if outfit_type == OUTFIT_CASUAL:
            all_outfits.append(
                ("Sundress", "yuki_sundress.png", "yuki_sundress_icon.png")
            )
            all_outfits.append(
                ("Sunday best", "yuki_sunday_best.png", "yuki_sunday_best_icon.png")
            )
        return all_outfits
Then you can simply feed that array of tuples to a for loop inside the stats screen and pull the data from the tuples to populate each outfit button. Obviously the exact implementation will depend on your use case.
 
Last edited:
  • Like
Reactions: SPkiller31

SPkiller31

Member
Game Developer
Dec 20, 2020
130
159
Code:
class Yuki(CharacterData):
    def get_outfit_variants(self, outfit_type):
        all_outfits = []
        if outfit_type == OUTFIT_CASUAL:
            all_outfits.append(
                ("Sundress", "yuki_sundress.png", "yuki_sundress_icon.png")
            )
            all_outfits.append(
                ("Sunday best", "yuki_sunday_best.png", "yuki_sunday_best_icon.png")
            )
        return all_outfits
Then you can simply feed that array of tuples to a for loop inside the stats screen and pull the data from the tuples to populate each outfit button. Obviously the exact implementation will depend on your use case.
To be honest tuples is thing that I hear about first time in my life but thank you very much for both version of the code, in my free time I'll do my best to check this in practice. Appreciate your time, cheers.
 

MidnightArrow

Member
Aug 22, 2021
498
423
To be honest tuples is thing that I hear about first time in my life but thank you very much for both version of the code, in my free time I'll do my best to check this in practice. Appreciate your time, cheers.
A tuple is basically an array that you can't change. You group a set of data together inside the parentheses and pass them around under one variable name. But unlike a struct in other, better programming languages, you can't access the data by name. You need to access it by index, and just remember which slot you put the data into.

How Pythonic!
 
  • Red Heart
Reactions: SPkiller31

SPkiller31

Member
Game Developer
Dec 20, 2020
130
159
Rely on dynamism.

By example:
Python:
screen whatever():

    default outfit = "casual"

    hbox:
        vbox:
            xsize 400
            text "Outfits:"
            textbutton "casual":
                action SetScreenVariable( "outfit", "casual" )
            textbutton "school uniform":
                action SetScreenVariable( "outfit", "school" )
            textbutton "sport":
                action SetScreenVariable( "outfit", "sport" )

        vbox:
            add "images/outfit/{}.jpg".format( outfit )
Then it will display "image/outfit/casual.jpg", "image/outfit/school.jpg" or "image/outfit/sport.jpg", depending of the player choice.
I wanted to try both versions of solution, yours and MidnightArrow's and so firstly I decided to keep it simple and modify your version so it can be easily applied to each character.
I came up with something like that:

Python:
                elif character_page_info == "Shinobu's":
                    default shinobu_outfit = "shinobu_casual"
                    hbox:
                        vbox:
                            xpos 800
                            ypos 70
                            xsize 400
                            text "Outfits:"
                            textbutton "Casual":
                                action SetScreenVariable( "shinobu_outfit", "shinobu_casual" )
                            if shinobu_event1_flag == True:     
                                textbutton "Store Clerk":
                                    action SetScreenVariable( "shinobu_outfit", "shinobu_clerk" )
                        vbox:
                            add "images/outfit/shinobu/{}.png".format( shinobu_outfit )  xpos 0.95 ypos 0.01
Which works flawlessly, including fact that I can easily set very different outfits for each character and make outfits "unlockable" by doing it that way. The only problem I realized is the fact that choice of the outfit is not saved and resets after leaving characters screen, not to mention saving/loading the game. I think that easiest way of fixing that issue would be using persistent values? But I have no idea how to implement it as I never needed/wanted in some cases using persistent. I tried using persistent. but each time I got errors regarding "=" in code or other things.

Currently checking MidnightArrow's version of solution but I think I would still love to learn how to fix that issue for future problems.
 

MidnightArrow

Member
Aug 22, 2021
498
423
Which works flawlessly, including fact that I can easily set very different outfits for each character and make outfits "unlockable" by doing it that way. The only problem I realized is the fact that choice of the outfit is not saved and resets after leaving characters screen, not to mention saving/loading the game. I think that easiest way of fixing that issue would be using persistent values? But I have no idea how to implement it as I never needed/wanted in some cases using persistent. I tried using persistent. but each time I got errors regarding "=" in code or other things.

Currently checking MidnightArrow's version of solution but I think I would still love to learn how to fix that issue for future problems.
Persistent is for something else entirely, so don't use that.

Whenever Ren'py closes a screen, it wipes the current execution state. So when you set something with SetScreenVariable(), that's lost on exit. There's a few ways to fix this. Simplest way is to declare the variable outside the screen, in the global scope, and sets its state with SetVariable().

Code:
default shinobu_outfit = None

screen outfit_screen:
    textbutton "Casual":
        action SetVariable("shinobu_outfit", "shinobu_casual")
A more advanced way is to create an object that holds all a character's variables (like in my earlier example) and use actions to change its state.

Also I don't recommend using string comparisons for something like this. If there's a misspelling anywhere, then your screen won't work and you'll never know why. It's better to pass an object holding the character's state as an argument instead, which fetches the data from method calls on the object.

Code:
default shinobu = Chara("Shinobu")


screen outfit_select(character):
    hbox:
        vbox:
            xpos 800
            ypos 70
            xsize 400
            text "Outfits:"
            for outfit in character.unlocked_outfits():
                textbutton outfit.get_name():
                    action Function(character.set_current_outfit, [ outfit.get_outfit() ])
        vbox:
            add character.get_current_outfit():
            xpos 0.95
            ypos 0.01      


screen character_select:
    default current_chara = None
   
    textbutton "Shinobu":
        action SetScreenVariable("current_chara", shinobu)
   
    if current_chara:
        use outfit_select(current_chara)
 
  • Red Heart
Reactions: SPkiller31

SPkiller31

Member
Game Developer
Dec 20, 2020
130
159
Persistent is for something else entirely, so don't use that.

Whenever Ren'py closes a screen, it wipes the current execution state. So when you set something with SetScreenVariable(), that's lost on exit. There's a few ways to fix this. Simplest way is to declare the variable outside the screen, in the global scope, and sets its state with SetVariable().

Code:
default shinobu_outfit = None

screen outfit_screen:
    textbutton "Casual":
        action SetVariable("shinobu_outfit", "shinobu_casual")
A more advanced way is to create an object that holds all a character's variables (like in my earlier example) and use actions to change its state.

Also I don't recommend using string comparisons for something like this. If there's a misspelling anywhere, then your screen won't work and you'll never know why. It's better to pass an object holding the character's state as an argument instead, which fetches the data from method calls on the object.

Code:
default shinobu = Chara("Shinobu")


screen outfit_select(character):
    hbox:
        vbox:
            xpos 800
            ypos 70
            xsize 400
            text "Outfits:"
            for outfit in character.unlocked_outfits():
                textbutton outfit.get_name():
                    action Function(character.set_current_outfit, [ outfit.get_outfit() ])
        vbox:
            add character.get_current_outfit():
            xpos 0.95
            ypos 0.01     


screen character_select:
    default current_chara = None
  
    textbutton "Shinobu":
        action SetScreenVariable("current_chara", shinobu)
  
    if current_chara:
        use outfit_select(current_chara)
Oh wow, I tried to set variable outside of the screen before as it sounded like most reasonable way but I simply forgot to change SetScreenVariable to normal SetVariable, now I'm ashamed of myself :HideThePain:. I will try to redo code closer to your second way with passing the object, tomorrow after waking up but I'm really glad I got this to work. Big thanks to both of you for your time and trying to make code much cleaner and automatized. Hopefully this post will save someone's time in the future.