Ren'Py Tutorial [How-To] Variables and save compatibility...

Solunsky

New Member
Sep 8, 2022
1
0
Hello! I have a question.
I apologize if this is off topic, but the topic is about variables))

I have code:
Python:
default cur_location = None

init python:
    def changeLocation(location_id: str):
        for location in locations:
            if location.id == location_id:
                cur_location = location
        return

label change_location(location_id = None):
    "location_id = [location_id]"
    $ changeLocation(location_id = location_id)
    return
But cur_location is None when i call label change_location(location_id = "home") in script.rpy
1662938265272.png

and i have other code, his work:
Python:
init python:
    def changeLocation(location_id: str):
        for location in locations:
            if location.id == location_id:
                return location
        return

label change_location(location_id = None):
    "location_id = [location_id]"
    $ cur_location = changeLocation(location_id = location_id)
    return
1662938421322.png

Why first code is not work and why location_id is DELETED? :(
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,110
14,787
Why first code is not work
For a question of scope.

To limit the risk of collision between variables (two variables with the same name, but a different meaning), language like Python create space in which the variables are, or not, relevant.

By default there's only one scope, the global one. It's the lowest possible level, where everything is located. Then Python create a local scope every time it's relevant ; globally speaking, every time you create a function/method, you also create a local scope.

When you'll try to assign a value to a variable, Python will always works with the current scope, that is the smallest and highest local scope ; therefore, still globally speaking, the body of the current function/method.
Note that the same isn't totally the same when Python get a value from a variable. Python will first look if the variable exist in the current scope, and if it don't, Python will look at the global scope if it find it.

Python:
init python:

    def loop1( idx ):
        what = "not a test"
        for i in range( 1, 3 ):
            loop2( idx, i )

    def loop2( idx1, idx2 ):
        for i in range( 1, 3 ):
            print( "{} - {} - {} - {}".format( what, idx1, idx2, i ) )

label start:
    $ what = "my test"

    python:
        for i in range( 1, 3 ):
            loop1( i )

    "Open the console [[shift] + [[o] to see what have been printed."
    "END"
The result will be:
my test 1 - 1 - 1
...
my test 2 - 1 - 2
...
my test 2 - 2 - 2
Each one of the three variables named "i" are defined in their own scope, reason why you can use them without problem. In each scope, the variable have a different value, relevant only for this scope.

In the same time, the variable "what" is defined two times. Firstly in the global scope, and secondly in the scope local for "loop1". But it's only in "loop2" that we use it, from the point of view of "loop2", the two only scopes that exist are global and the current scope.
Like it don't find a variable named "what" in the current scope, it look for (and find) it in the global scope. But like you can see, it totally ignore what exist in the "loop1" local scope.

As seen from Ren'Py, the global scope is the script of the game.
Python:
init python:
    myVar1 = None

define myVar2 = None
default myVar3 = None

label whatever:
    $ myVar4 = None
The four variables will be in the global scope. But obviously, if you define a function/method, then it will have its own local scope, as expected.
The only local scope that exist in Ren'Py is for screens:
Python:
screen whatever():

    default myLocalVar = None

    #  Note: I don't guaranty that this one haven't changed a bit recently,
    # and that there isn't times when it will act as a local variable, especially
    # when there's embedded (/use/d) screens.
    $ myGlobalVar = None


Now, in your first version, you assign the value inside the "changeLocation" function:
Python:
   def changeLocation(location_id: str):
        [...]
                cur_location = location
This mean that you are giving a value to a variable named "cur_location" and that exist in the current scope. When you'll quit the function, this scope will disappear, and the variable with it.

This while in your second version, you assign the value inside the "change_location" label:
Python:
label change_location(location_id = None):
    [...]
    $ cur_location = changeLocation(location_id = location_id)
Like I said, labels works in the global scope, therefore you assign the value to the same "cur_location" than the one created with your previous default cur_location = None

There's a way to deal with this kind of issues. To simplify the explanation, I'll present it as "tell Python that you want to address the 'Ren'Py' scope" ; but keep somewhere in your mind that it's a simplification, not the effective truth.
For this, you just need to prefix the variable by store.. Therefore, for your first version to works, you just need a small bit of change:
Python:
   def changeLocation(location_id: str):
        for location in locations:
            if location.id == location_id:
                #  Now you are addressing the Ren'Py variable named 'cur_location' and
                # not anymore the local variable named like that.
                store.cur_location = location
        return



and why location_id is DELETED?
In a way, it's still a question of scope.

When you pass a value as argument to a function/method, Python create the variable in the current scope. But Ren'Py cannot do this, since it do not have local scope (except those particular case with screens).
Therefore, it create the variable in the global scope, then delete it at the end of the label, because it lost all relevance.


Now a bit of "all I said above isn't totally true".

Ren'Py really don't have the notion of scopes, but it can more or less fake a "local scope". It's possible to tell it that a given variable will only be relevant for the current label. This is done with the function.
Python:
label start:

    $ myVar = "ABCD"
    "[[start] The content of {i}myVar{/i} is '[myVar]'"

    call calledLabel
    "[[start] The content of {i}myVar{/i} is back to '[myVar]'"

    "END"
    return

label calledLabel:
    $ renpy.dynamic( "myVar" )
    $ myVar = "A larch"
    "[[calledLabel] The content of {i}myVar{/i} is now '[myVar]', but locally."

    call calledLabel2
    jump jumpedLabel

label calledLabel2:
    "[[calledLabel2] The content of {i}myVar{/i} is still '[myVar]' because we are a subcontext of {i}calledLabel2{/i}."
    return

label jumpedLabel:
    "[[jumpedLabel] The content of {i}myVar{/i} is still '[myVar]' because we are in the same context than {i}calledLabel2{/i}."
    return
 
  • Red Heart
Reactions: Solunsky

dev_muffin

Member
Game Developer
Jul 25, 2022
132
194
is it possible to change the initialized python class's field name and save the save compatibility simultaneously?
should I init the python class in the label also? just trying to refactor some old classes and need to change the field name.
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,110
14,787
is it possible to change the initialized python class's field name and save the save compatibility simultaneously?
should I init the python class in the label also? just trying to refactor some old classes and need to change the field name.
Hmm, if I understand correctly what you want to do, there's two way to do.


Firstly, effectively getting rid of the previous name of the attribute.
Since I need names, I assume that the class is used twice, by "obj1" and "obj2", and that you want to change the attribute "love" into "relationship".
Python:
#  Will be automatically called by Ren'Py each time it load a save file.
label after_load:

    #  Declare the variables /toUpdate/ and /obj/ as local to this label. It will exist only 
    # here, and disappear once you'll leave the label. It's not mandatory but cleaner.
    $ renpy.dynamic( "toUpdate", "obj" )

    #  Ren'Py don't have a /for/ statement, so you have to adapt.

    #  List of the objects depending of the class you want to update.
    $ toUpdate = [ obj1, obj2 ]

    #  While there's still at least one object in the /toUpdate/ list...
    while toUpdate:
        #  Get the next available object from /toUpdate/ ; will remove it from
        # the list in the same time.
        $ obj = toUpdate.pop()

        #  If the object still have an attribute with the previous name...
        if hasattr( obj, "love" ):
            #  Add an attribute named /relationship/ to the object, and give it
            # the value actually stored in the /love/ attribute.
            $ setattr( obj, "relationship", obj.love )
            #  Then delete the now useless /love/ attribute.
            $ delattr( obj, "love" )

    #  You finished the update, time to let Ren'Py finish the loading process.
    return
Side note: If you think that you'll have to often use the "after_load" label, it's probably better to split the code by update:
Python:
label after_load:

    #  Changes made by the version 0.3.x
    call update03

    #  Changes made by the version 0.4.x  
    call update04

    #  All changes have been proceeded.
    return

label update03:
    [The code I gave above]
    return

label update04:
    [whatever you can possibly need at this time]
    return
This will lead to a cleaner code.


Now, as I said, there's a second way to deal with what you want to do, and it's to not change the attribute.

Python have what is called "properties", it's like attributes but with an interface. And this can be used to solve your issue.
Once again, I assume that you want to change "love" into "relationship".
Python:
init python:

    class myClass( renpy.python.RevertableObject ):

       #  The method that Python will use when you get the value.
       # Here it will simply return the value of the /love/ attribute.
       def relationshipGetter( self ):
           return self.love

       #  The method that Python will use when you change the value.
       # Here, it will simply assign this value to the /love/ attribute.
       def relationshipSetter( self, value ):
           self.love = value

        #  And now you tell Python that the class have a property named
        # /relationship/, that use "relationshipGetter" as *getter*, and 
        # "relationshipSetter" as *setter*.
        relationship = property( relationshipGetter, relationshipSetter )

        [rest of your class]
And you use it like if it was a regular attribute:
Code:
label whatever:
    $ obj.relationship += 1
    "You relationship with her is now [obj.relationship]"
    [...]
When Ren'Py will load a save file, it will automatically use the new version of the class, and therefore have both a "love" and a "relationship" value. The first being the old direct attribute, and the second being a property build over the old attribute.


Up to you to choose the solution you want, both having their pros and cons.