Ren'Py Add extra properties to character object?

ozbozoz

Newbie
Jun 6, 2018
17
16
Is there a (not overly complicated) way to attach additional, arbitrary properties to a character object?

E.g. I'd like my characters to sometimes refer to each other by nicknames or last names, and I thought it would be convenient to have them all grouped together on the same object. So I could for instance have:

Code:
mc "My name is [mc.lastname], [mc.name] [mc.lastname]. Though my friends call me [mc.nick]."
->
"My name is Doe, John Doe. Though my friends call me Johnny."
Now, it would be just as easy and practically viable to use regular variables:

Code:
mc "My name is [mc_lastname], [mc.name] [mc_lastname]. Though friends call me [mc_nick]."
...but I'm wondering what can and can't be done with the engine.
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,581
2,219
I have a vague memory that the person most responsible for RenPy (PyTom) has said that doing this would be a bad idea.
There's also something at the back of my head that Character() is a function, not a class/object.

Edit: Found it:

Though honestly, I've no clue what the alternative might be.
It is something I've wondered about too.

I'm not sure that helps you much, but gets the basics out of the way before someone swings in with a better solution.

I do have an example of adding a NAME property to the existing Character(): thingy (well, ADVCharacter(): which I believe might be it's actual name).

Python:
init python:
    def upperName( self ):
        return renpy.substitutions.substitute( renpy.python.py_eval( self.name ) if self.dynamic else self.name )[0].upper()
    ADVCharacter.NAME = property( upperName )

label start:

    sis "[mc], are you listening to me?"
    mc "What?"
    sis "I SAID: ARE YOU LISTENING TO ME, [mc.NAME]?"

    return
This example returns the mc's name in UPPERCASE when used as mc.NAME.
I'd imagine the solution is something similar to that.

I'm sure the "real" answer will be along shortly...
 
Last edited:
Apr 24, 2020
192
257
Why not just do it the other way around?
Create a custom class that contains the Character() class for the given character, along with any other variables and information you wish to store.
 

ozbozoz

Newbie
Jun 6, 2018
17
16
Why not just do it the other way around?
Create a custom class that contains the Character() class for the given character, along with any other variables and information you wish to store.
Not sure what that would look like, or how it would work? Can you provide an example of what you mean?
 
Apr 24, 2020
192
257
This is how I would set up the NPC class itself, or whatever you would call it.
Code:
init python:
    class NPC(object):
        def __init__(self, _name, _lastName, _nickName ):
            self.name = _name.capitalize()
            self.lastName = _lastName.capitalize()
            self.nickName = _nickName .capitalize()
            self.say = Character(self.name)
The creation of the character would look something like this.
Code:
default mc = NPC("John","Doe","Johnny")
Because your original Character() class is now a part of the new class, your dialogue will have to look something like this.
Code:
mc.say "My name is [mc.lastName], [mc.name] [mc.lastName]. Though friends call me [mc.nickName]."
 
  • Like
Reactions: yasenib and ozbozoz

ozbozoz

Newbie
Jun 6, 2018
17
16
Okay, I get it, thanks. Does seem kind of awkward though. Just having Character be a class instead of a function and letting inheritance/polymorphism handle it would be a lot more convenient, but I'm sure there's a reason for why you can't do that.

I actually think I'll rather have the extra properties as loose variables than having to add ".say" to each line of dialogue. There'll be thousands of lines of dialogue, wheras the nickname and lastname are only used a handful of times...
 
Apr 24, 2020
192
257
Yeah, if you just have a single character that needs special treatment then this wouldn't be the way to go. It's something you would do when you have multiple characters that all needs the same type of extra data attached to them.

Likewise I wish there were a way to edit the Character class as well, but I'm guessing there's a very good reason, especially considering the programming experience of your average Ren'Py user.

As for adding ".say" every time, you get used to it. Not to mention that you can always do a search and replace just to be sure.

But I agree, if you just have the main character that needs special treatment then it will be hell to remember when you should and should not add ".set" to the dialogue.
 

Rich

Old Fart
Modder
Donor
Respected User
Game Developer
Jun 25, 2017
2,490
7,035
Why not just do it the other way around?
Create a custom class that contains the Character() class for the given character, along with any other variables and information you wish to store.
Using "composition" (i.e. building a class that contains the other stuff you need/want" will work. Deriving a class, however, poses some non-trivial problems.

Just to summarize the issue with derived classes:

So, to start out with, Character is not a class. It's a function that instantiates the correct underlying class based on whether you're in "ADV-mode" (what we normally think of) and "NVL-mode." So, you'd probably be deriving from ADVCharacter.

It gets twisty, however, because there's some convoluted code in the "say" processing. Essentially, under certain circumstances, when your character says something, Ren'py won't use your specific character class, but will, instead, re-create another character class and use that one. It has to do with the behavior of some of the character attributes. Thus, depending on exactly what you try to do with customizing a class derived from ADVCharacter, those can go missing because of the way that the character class gets re-created. This is one of the reasons PyTom recommends against this.

On the other hand, you CAN create your own class, and use it as if it was a Ren'py Character class. The "trick" is that when you say
Code:
    my_character "This is something I'm saying"
this is implemented by the Python operation
Code:
    my_character("This is something I'm saying")
The character object is called, passing the appropriate text, etc. So, what you can do is make YOUR class callable by implementing
Code:
    def __call__(self, *args, **kwargs):
        self.the_real_character_object(*args, **kwargs)
In other words, when Ren'py tries to implement the "say" by calling your pseudo-character object, you turn around and pass that into the nested "real Character" object. There still are some limitations on this, but for the most part you can make it work.

anne O'nymous can probably chime in, however, on some of the subtle underlying issues related to this - we had a conversation on various approaches to customizing Character processing a while back.
 

DuniX

Well-Known Member
Dec 20, 2016
1,203
797
You can just use the character store/namespace for things like the say command and whatnot since that will be used first by renpy.
define character.mc= Character("Player")
default mc = YourClass("mc", stuff)

See also:
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,355
15,269
Is there a (not overly complicated) way to attach additional, arbitrary properties to a character object?
Well, all depend what you intend to do. Both 79flavors and InCompleteController approach are valid, as long as you don't need to use parameters/arguments with your dialogs. But note that at anytime you can break Ren'py because you'll redefine an attribute/method of the ADVCharacter object.

But if you have to use some, even just time to time, then it will not works ; for the reason explained by Rich. Then the best option is to have a secondary object to store the lastname, nickname and all. Something like that by example :
Code:
init python:
    class StoringObject( renpy.python.RevertableObject ):
        def __init__( self, **kwargs ):
            for k in kwargs:
                setattr( self, k, kwargs[k] )

define character.mc = Character( "the mc" )
default mc = StoringObject( name="the mc", lastname="whatever", nickname="something else" )
Then you'll be able to write things like mc "my name is [mc.lastname], [mc.name] [mc.lastname], but call me [mc.nickname]" and it should works fine.
Not necessarily my preferred solution, this said.
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,581
2,219
The more I read about this, the more a separate structure seems like the best solution.

Classes and such really aren't my thing. But I can see the possibility of a solution, even if I can't actually write the code for it.
(Well, I probably could these days... but I'd need longer to think about it).

Something that looks like...

Python:
default player_name = "Unnamed"

define mc = Character("[player_name]")
define m = Character("Mom")
define e = Character("Emily", color="#04B486")
define k = Character("Karen", color="#04B486")
define c = Character("Claire", color="#04B486")

define ex_mc = ExtraChar ("Williams", age=18)
define ex_m = ExtraChar ("Williams", age=44)
define ex_e = ExtraChar ("Miller", age=19)
define ex_k = ExtraChar ("Johnson", age=18)
define ex_c = ExtraChar ("Garcia", age=24)

label start:

    $ player_name = renpy.input("What is your name? {i}(Press ENTER for 'John'){/i}")
    $ player_name = player_name.strip() or "John"

    mc "Hi. My name is [mc] [ex_mc.surname] and I'm [ex_mc.age] years old."
    "*** THE END ***"

    return
I really want to write something like ex.mc and ex.e - but that is more of a programming style thing... I suspect I'd need to stick to something simpler like ex_mc and ex_e, without the class hierarchy of mc being part of ex.

I'm using define here, because all that information "feels" fixed. Plus, as the mc object shows, it's possible to use dynamic (redirected) variables.

It might need a slightly different solution if you wanted a character to have a birthday or wanted to store other information like ex_e.love, ex_e.corruption, ex_e.bjs or ex_e.resistance...

...using default instead of define - since you'll want that data saved as part of the savegame.
default ex_c = ExtraChar ("Garcia", age=24, love=5, corruption=0.

Obviously the bit I'm missing is the actual creation of the ExtraChar(): class. But I think other examples in this thread would cover that... I hope.

I'm also wondering if you could also implement (in part) the solution I included earlier, where you add additional properties to an existing structure. In that case, it was to add mc.NAME - which used a function to return the mc's name in uppercase.
I'm thinking of something where you refer to mc.age, but it goes to retrieve the value from ex_mc.age using py_eval or something similar.
The problem I foresee is that you could do all that and "read" variables like mc.surname and mc.age - but if you wanted to "write" to those variables, you still need to access ex_mc.surname and ex_mc.age. I think that sort of "mixing" of how you access the same data would be just bad programming.

Edit: Beyond all that... forget classes and do what everyone else does. Simple variables with names that make sense.
You don't have permission to view the spoiler content. Log in or register now.
 
Last edited:
Apr 24, 2020
192
257
The more I read about this, the more a separate structure seems like the best solution.
This was my go to approach as well.

The only reason why I did the wonky stuff I've done is because I wanted to make a sandbox. As such, I needed to handle a cast that wasn't set in stone, as well as generic scenes, which I'm sure Ren'Py wasn't meant for.
 

ozbozoz

Newbie
Jun 6, 2018
17
16
The character object is called, passing the appropriate text, etc. So, what you can do is make YOUR class callable by implementing
Code:
    def __call__(self, *args, **kwargs):
        self.the_real_character_object(*args, **kwargs)
I actually thought of that trick myself, but couldn't get it to work and gave up. But since you mentioned it I gave it another shot. It seems that if instead write the call method like this:

Code:
def __call__(self, what, *args, **kwargs):
    self.say(what, args, kwargs)
...it works exactly as I wanted from the start! :)

Still debating whether I should actually do it, though, or just use regular variables. It is my first Ren'Py project after all, and I suppose it would be prudent to keep it dead simple when it's still all new to me. :D
 

Rich

Old Fart
Modder
Donor
Respected User
Game Developer
Jun 25, 2017
2,490
7,035
I actually thought of that trick myself, but couldn't get it to work and gave up. But since you mentioned it I gave it another shot. It seems that if instead write the call method like this:

Code:
def __call__(self, what, *args, **kwargs):
    self.say(what, args, kwargs)
...it works exactly as I wanted from the start! :)

Still debating whether I should actually do it, though, or just use regular variables. It is my first Ren'Py project after all, and I suppose it would be prudent to keep it dead simple when it's still all new to me. :D
Apologies - I should have included the "what" argument. (Was doing it from memory.) Not sure why it doesn't work without it, however. Possibly there's some Ren'py quirkiness involved.