[Solved] Class Instance Variables Revert to Class Defaults on Load

Discuss how to use the Ren'Py engine to create visual novels and story-based games. New releases are announced in this section.
Forum rules
This is the right place for Ren'Py help. Please ask one question per thread, use a descriptive subject like 'NotFound error in option.rpy' , and include all the relevant information - especially any relevant code and traceback messages. Use the code tag to format scripts.
Post Reply
Message
Author
Loneclaw
Newbie
Posts: 5
Joined: Thu Jun 29, 2023 7:28 am
Contact:

[Solved] Class Instance Variables Revert to Class Defaults on Load

#1 Post by Loneclaw »

Hello, everyone!
I have created a class that is intended to handle the various characters in the game, with each character being created as an instance of that class.
Here's an extremely slimmed down version of the game containing a simplified version of the code in question:

Code: Select all

define elaine = Actor("elaine")
define e = Character("[elaine.display_name]", who_prefix="{color=[elaine.color]}", who_suffix="{/color}")
define mary = Actor("mary")
define m = Character("[mary.display_name]", who_prefix="{color=[mary.color]}", who_suffix="{/color}")

init -1 python:
    ActorList = []
    import renpy.store as store

    class Actor(store.object):
        def __init__(self, characterName):
            
            #Mechanics
            self.initialized = False
            self.enabled = True
            self.location = "UNINITIALIZED_LOCATION"
            self.last_location = "UNINITIALIZED_LAST_LOCATION"
            self.room = "UNINITIALIZED_ROOM"
            self.is_home = True
            self.is_asleep = False
            self.home_name_location = "UNINITIALIZED_HOME_NAME"
            self.has_shower = False
            self.inventory = []

            #Identity
            self.name = characterName
            self.display_name = "UNINITIALIZED_PERSONAL_NAME"
            self.thing_name = "UNINITIALIZED_THING_NAME"
            self.has_name = False
            self.color = "#89CFF0"

            ActorList.append(self)


label instantiate_characters:
    python:
        for item in ActorList:
            if not item.initialized:
                renpy.call("instantiate_" + item.name)
    return


label instantiate_elaine:
    if not elaine.initialized:

        #Mechanics
        $ elaine.initialized = True
        $ elaine.enabled = True
        $ elaine.location = "Elaine's Home"
        $ elaine.last_location = "Elaine's Home"
        $ elaine.room = "Elaine's Living Room"
        $ elaine.is_home = True
        $ elaine.is_asleep = False
        $ elaine.home_name_location = "Elaine's Home"
        $ elaine.has_shower = True
        $ elaine.inventory = []

        #Identity
        # $ elaine.name = characterName ### Set upon actor creation.
        $ elaine.display_name = "Nurse"
        $ elaine.thing_name = "deer girl"
        $ elaine.has_name = False
        $ elaine.color = "#ff8ca3"
    
    jump instantiate_characters


label instantiate_mary:
    if not mary.initialized:

        #Mechanics
        $ mary.initialized = True
        $ mary.enabled = True
        $ mary.location = "Mary's Home"
        $ mary.last_location = "Mary's Home"
        $ mary.room = "Mary's Garden"
        $ mary.is_home = True
        $ mary.is_asleep = False
        $ mary.home_name_location = "Mary's Home"
        $ mary.has_shower = False
        $ mary.inventory = []

        #Identity
        # $ mary.name = characterName ### Set upon actor creation.
        $ mary.display_name = "Mary"
        $ mary.thing_name = "sheep girl"
        $ mary.has_name = True
        $ mary.color = "#8cc2ff"
    
    jump instantiate_characters


# The game starts here.

label start:
    call instantiate_characters

    scene bg room
    show eileen happy

    e "Why hello there!"

    m "Hi! Could you give us some directions? We're lost."

    e "Where are you heading?"

    m "The library!"

    e "Oh, just continue down this street and then take the third left."

    m "Thank you, kindly! What was your name, again?"

    if not elaine.has_name:
        $ elaine.display_name = "Elaine"
        $ elaine.has_name = True
    
    e "*giggles* The name's [elaine.display_name]! Pleasure to meet you!"

    m "Pleased to meet you, [elaine.display_name]! I'm [mary.display_name]!"

    e "Pleased to meet you too, [mary.display_name]!"

    m "We gotta get going now! Bye!"

    e "Bye!"

    # This ends the game.

    return
So there are some issues that I have been unable to resolve on my own.
Firstly, any changed instance variables remain changed on rollback or when loading without having exited the game. For example, in this simplified code, if you save before Elaine gives you her name, then load that save after she's given it, her dialogue tag will say "Elaine" instead of "Nurse".
Secondly, if you exit the game, start it back up, and load a save, all instance variables will have reverted back to the class defaults. So for example, Elaine's dialogue tag will say "UNINITIALIZED_PERSONAL_NAME" instead of "Nurse" or "Elaine".

Does anyone know how to solve these issues? Any help would be greatly appreciated! <3
Last edited by Loneclaw on Fri Feb 16, 2024 7:08 am, edited 1 time in total.

User avatar
m_from_space
Miko-Class Veteran
Posts: 975
Joined: Sun Feb 21, 2021 3:36 am
Contact:

Re: Class Instance Variables Revert to Class Defaults on Load

#2 Post by m_from_space »

Loneclaw wrote: Thu Feb 15, 2024 1:49 pm Does anyone know how to solve these issues? Any help would be greatly appreciated! <3
Hey there,

so here is the answer to your problem first: You're using "define" to create your Actor objects (instead of using "default"). This way you're telling Renpy that it's supposed to be a constant and Renpy won't save any changes. In fact it won't even save the variable, just initialize it on the next startup (I mean why should it save it, it's constant, right?)

Use "default" for variables and "define" for constants!

But let me also give you some advice on issues I have with your code in general. Take it or leave it. ;)

#1 When creating an object out of a class, you usually initialize it at the time of creation and not randomly after. It feels like a waste of (CPU) time to do it your way. Also your global "ActorList" should be created using "default", so Renpy knows about it. I'm not sure what this importing of renpy.store is about or deriving the class from store.object. It's not necessary at all in my opinion.

So here is an example:

Code: Select all

init python:
    class Actor:
        def __init__(self, name, age, location):
            self.name = name
            self.age = age
            self.location = location
            store.ActorList.append(self)
            
default ActorList = []

# make sure to use default if you ever want to make Renpy save any changes!
default elaine = Actor("elaine", 18, "Elaine's Home")
Now, if you don't want a long list of arguments inside the class definition, you can use keyword arguments (kwargs) instead...

Code: Select all

init python:
    class Actor:
        def __init__(self, name, **kwargs):
        self.name = name
        # read a keyword argument named "age" and if it's not present, default it to None
        self.age = kwargs.get("age", None)
        self.location = kwargs.get("location", None)
    
default elaine = Actor(
    "elaine",
    age=18,
    location="Elaine's Home"
)
#2 Your "instantiate_" labels are called using "call", but they do not return, instead they jump elsewhere. That's really not a good idea. A label that is supposed to be called should always end with "return".

#3 Your whole init code should really be a python function in my opinion, not some labels being called. (But I wouldn't use such a code in the first place, see issue #1)

#4 If your Actor's don't have names, you just can set their names to None, instead of using another boolean variable like "has_name" to check for it.

Code: Select all

# assuming the display_name is None or an empty string like ""
if not elaine.display_name:
    ...

# alternative, if it's really None
if elaine.display_name is None:
    ...

Loneclaw
Newbie
Posts: 5
Joined: Thu Jun 29, 2023 7:28 am
Contact:

Re: Class Instance Variables Revert to Class Defaults on Load

#3 Post by Loneclaw »

Thank you so much for your response and your suggestions! However, my attempts to implement your suggestions just result in an error on start-up.

This is what the code now looks like:

Code: Select all

define e = Character("[elaine.display_name]", who_prefix="{color=[elaine.color]}", who_suffix="{/color}")
define m = Character("[mary.display_name]", who_prefix="{color=[mary.color]}", who_suffix="{/color}")

default ActorList = []

init python:
    class Actor:
        def __init__(self, characterName):
            
            #Mechanics
            self.initialized = False
            self.enabled = True
            self.location = "UNINITIALIZED_LOCATION"
            self.last_location = "UNINITIALIZED_LAST_LOCATION"
            self.room = "UNINITIALIZED_ROOM"
            self.is_home = True
            self.is_asleep = False
            self.home_name_location = "UNINITIALIZED_HOME_NAME"
            self.has_shower = False
            self.inventory = []

            #Identity
            self.name = characterName
            self.display_name = "UNINITIALIZED_PERSONAL_NAME"
            self.thing_name = "UNINITIALIZED_THING_NAME"
            self.has_name = False
            self.color = "#89CFF0"

            ActorList.append(self)


default elaine = Actor(
    "elaine",
    #Mechanics
    enabled=True,
    location="Elaine's Home",
    last_location="Elaine's Home",
    room="Elaine's Living Room",
    is_home=True,
    is_asleep=False,
    home_name_location="Elaine's Home",
    has_shower=True,
    inventory=[],
    #Identity
    display_name="Nurse",
    thing_name="deer girl",
    has_name=False,
    color="#ff8ca3"
)


default mary = Actor(
    "mary",
    #Mechanics
    enabled=True,
    location="Mary's Home",
    last_location="Mary's Home",
    room="Mary's Garden",
    is_home=True,
    is_asleep=False,
    home_name_location="Mary's Home",
    has_shower=False,
    inventory=[],
    #Identity
    display_name="Mary",
    thing_name="sheep girl",
    has_name=True,
    color="#8cc2ff"
)


# The game starts here.

label start:
    scene bg room
    show eileen happy

    e "Why hello there!"

    m "Hi! Could you give us some directions? We're lost."

    e "Where are you heading?"

    m "The library!"

    e "Oh, just continue down this street and then take the third left."

    m "Thank you, kindly! What was your name, again?"

    if not elaine.has_name:
        $ elaine.display_name = "Elaine"
        $ elaine.has_name = True
    
    e "*giggles* The name's [elaine.display_name]! Pleasure to meet you!"

    m "Pleased to meet you, [elaine.display_name]! I'm [mary.display_name]!"

    e "Pleased to meet you too, [mary.display_name]!"

    m "We gotta get going now! Bye!"

    e "Bye!"

    # This ends the game.

    return
And this is the error I get when I try to run the game:

Code: Select all

I'm sorry, but an uncaught exception occurred.

While running game code:
  File "renpy/common/00start.rpy", line 192, in script
    python:
  File "renpy/common/00start.rpy", line 193, in <module>
    renpy.execute_default_statement(True)
  File "game/script.rpy", line 32, in set_default
    default elaine = Actor(
  File "game/script.rpy", line 48, in <module>
    color="#ff8ca3"
TypeError: __init__() got an unexpected keyword argument 'has_shower'

-- Full Traceback ------------------------------------------------------------

Full traceback:
  File "renpy/common/00start.rpy", line 192, in script
    python:
  File "renpy/ast.py", line 1131, in execute
    renpy.python.py_exec_bytecode(self.code.bytecode, self.hide, store=self.store)
  File "renpy/python.py", line 1061, in py_exec_bytecode
    exec(bytecode, globals, locals)
  File "renpy/common/00start.rpy", line 193, in <module>
    renpy.execute_default_statement(True)
  File "renpy/exports.py", line 3797, in execute_default_statement
    i.set_default(start)
  File "game/script.rpy", line 32, in set_default
    default elaine = Actor(
  File "renpy/python.py", line 1085, in py_eval_bytecode
    return eval(bytecode, globals, locals)
  File "game/script.rpy", line 48, in <module>
    color="#ff8ca3"
TypeError: __init__() got an unexpected keyword argument 'has_shower'

Windows-10-10.0.19041 AMD64
Ren'Py 7.5.3.22090809
Code Lab 1.0
Fri Feb 16 11:23:48 2024
I'm not sure what I'm doing wrong here, so if you (or anyone else) is willing to provide some more help, that would be greatly appreciated. <3

User avatar
m_from_space
Miko-Class Veteran
Posts: 975
Joined: Sun Feb 21, 2021 3:36 am
Contact:

Re: Class Instance Variables Revert to Class Defaults on Load

#4 Post by m_from_space »

Loneclaw wrote: Fri Feb 16, 2024 6:27 am I'm not sure what I'm doing wrong here, so if you (or anyone else) is willing to provide some more help, that would be greatly appreciated. <3
Well, you're not sticking to my code. ;)

If you create class objects with keyword-arguments, the __init__() method should also handle the keyword arguments. So here is my relevant code example again:

Code: Select all

class Actor:
        def __init__(self, name, **kwargs):
        self.name = name
        # read a keyword argument named "age" and if it's not present, default it to None
        self.age = kwargs.get("age", None)
        self.location = kwargs.get("location", None)

Loneclaw
Newbie
Posts: 5
Joined: Thu Jun 29, 2023 7:28 am
Contact:

Re: Class Instance Variables Revert to Class Defaults on Load

#5 Post by Loneclaw »

Whoops, my bad!
Everything seems to be working as it should now, so thank you so much for the help! <3

Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot], Google [Bot], Kia