Posts
Search
Contact
Cookies
About
RSS

Godot, persistent states when revisiting scenes

Added 24 Mar 2024, 8:55 a.m. edited 24 Mar 2024, 10:12 a.m.

It a fairly common practice to use an autorun singleton to keep hold of certain player related info for example health, score and so forth that needs to persist from scene to scene.

However, it may well be that your game allows the player to move boxes etc around (even leaving them on pressure plates), in this scenario what you really don't want is for all these object the player has carefully moved around to end up resetting themselves when they next revisit the "level" (or scene)

Equally however they may very well be some that you do want to reset while allowing others to persist their "state"

I find that using a nodes "groups" are great for just this sort of scenario, in effect this allows you to "tag" each rigid body you want a persistent state with the "persist" group.

We really also want to only be persisting an individual scene at a time, however the GDScript API is both extensive and powerful - and to be honest I have the feeling I have barely scratched the surface so far...

Every level in my project has an instance of the player, and its here that there is a single function that is responsible for loading the next scene (multiple portals have file path properties pointing at different scenes)

Its here that we need to make sure we save the states of the object we want to persist

    func complete_level(file_path: String):
        var root = get_node("..")
        playerVars.save_states(root)
        # other unrelated stuff removed...
        get_tree().change_scene_to_file(file_path)

Here playerVars is a reference to the players global singleton, as multiple different scenes need to persist, and its the player instance that is arbitrating when the scenes transition this is a useful place to keep the persistence code.  The complementary code to this, needs to be added to each levels script where you want object to persist.

    func _ready():
        var pv = get_node("/root/PlayerVars")
        pv.restorePdata(get_node("."))   

other than tagging nodes "persist" is as arduous as it gets, once you're happy with what properties you're persisting.

var Pbody = preload("res://player/Pbody.gd").Pbody
var levelsPdata = {}

func save_states(root: Node3D):
    var st: SceneTree = root.get_tree()
    var nodelist = st.get_nodes_in_group("persist")
    var pdata = Array()
    for node in nodelist:
        var pd = Pbody.new()
        pd.pos = node.position
        pd.rot = node.rotation
        pd.lvel = node.linear_velocity
        pd.avel = node.angular_velocity
        pd.nodePath =  str(node.get_path())
        pdata.append(pd)
    levelsPdata[str(root.get_path())] = pdata

Pbody is a very simple class that is little more than a stand in for a struct, its obvious what its properties are from the save restore code (Pbody contains no code)

From the root node it is fairly easy to get a "list" (array) of the node we need to persist - Ironically after finishing coding this I found a remarkably similar idea on the Godot documentation site, but it never hurts to have multiple source and even slightly different takes on implementing a solution...

I'm manually creating a "Pbody" object that holds just a very few specific properties I'm currently interested in.  Once we have an array of all the scenes persisting objects we add it to a dictionary of different level potentially each with its own unique list of persistence data.  Having this data is also handy as at a later date if you decide you want to implement save games, you've already got a good bit of the data ready to be saved.

    func restorePdata(root: Node3D):
        var st: SceneTree = root.get_tree()
        var path = str(root.get_path())
        if path in levelsPdata:
            var pd = levelsPdata[path]
            for node in pd:
                var dest = root.get_node(node.nodePath)
                dest.position = node.pos
                dest.rotation = node.rot
                dest.linear_velocity = node.lvel
                dest.angular_velocity = node.avel


As you can see from the "restore" code we filter for just the current scene we're restoring and then just replacing the properties we're interested in.

Its worth noting that usually you shouldn't directly manipulate a physics objects position etc as this can cause the physics engine to behave oddly, however in this case the objects have only just been set up and this is happening before we are even executing physics steps.

Things to remember only ever put a "persist" group on a rigid node, make sure root nodes have a unique name and don't forget to restore on each levels _ready function.  Theres a lot more that could be done here, however its a simple enough solution to be easy to understand, and easy to tailor for your own needs

Enjoy!

Post Attachments

split-terminal.png