ephread / inkgd

Implementation of inkle's Ink in pure GDScript for Godot, with editor support.
MIT License
305 stars 33 forks source link

Implementing save/load #25

Closed mcoorlim closed 4 years ago

mcoorlim commented 4 years ago

I thought I had saving and loading the state of the ink figured out, but it's only working if I haven't made any choices yet. My game uses multiple ink story jsons, and If I try loading while the same ink story is in the runner, the passage text displays the very first passage in the story (incorrect, it should display the loaded passage), displays the correct choices for the loaded passage, and continues from that point.

If I load from a different story (load a state from json B while json A is currently playing), it displays the correct choices but the passage text is blank and clicking just makes the choices vanish.

Here's my code:

func save_game():
    var save_game = File.new()
    var dir = Directory.new()
    dir.remove("user://savegame.save")
    save_game.open("user://savegame.save", File.WRITE)
    save_game.store_line(inkRunner.myStory) # current loaded ink json reference
    save_game.store_line(inkRunner.saveState()) # function to save the story state
    save_game.close()

func saveState():
    var savedJson = story.state.to_json()
    return (savedJson)

func load_game():
    var save_game = File.new()
    if not save_game.file_exists("user://savegame.save"):
        return # Error! We don't have a save to load.
    save_game.open("user://savegame.save", File.READ)
    var loadStory = save_game.get_line()
    var data = save_game.get_line()
        inkRunner.myStory = loadStory 
    inkRunner.wipeChoices() # function to clear the current choice buttons
    inkRunner.on_new_story_start(loadStory)
    inkRunner.loadState(data)
    save_game.close()

func loadState(state):
    story.state.load_json(state)
    wipeChoices()
    continue_story()

# Without calling wipeChoices() the new buttons just pop up under the old ones.

func wipeChoices():
    StoryVBoxContainer.remove_child(_current_choice_container)
    if is_instance_valid(_current_choice_container):
        _current_choice_container.queue_free()

here's my continue_story():

func continue_story():
    while story.can_continue:
        var text = story.continue()
        var label = LineLabel.instance()
        label.bbcode_text = text

        StoryVBoxContainer.add_child(label)
        var scrollbar = StoryScrollContainer.get_v_scrollbar()

        scrollbar.max_value += 1000
        StoryScrollContainer.scroll_vertical = scrollbar.max_value

        if story.current_tags.size() > 0:
            check_tags()

    if story.current_choices.size() > 0:

        _current_choice_container = ChoiceContainer.instance()
        StoryMarginContainer.add_child(_current_choice_container)

        _current_choice_container.create_choices(story.current_choices)
        _current_choice_container.connect("choice_selected", self, "_choice_selected")
ephread commented 4 years ago

@mcoorlim it's a bit tricky to know what's going on without being able to fiddle with the project.

Can you show me the code for on_new_story_start()? Does it just replace inkRunner.story?

If I load from a different story (load a state from json B while json A is currently playing), it displays the correct choices but the passage text is blank and clicking just makes the choices vanish.

ink tries to be as tolerant as possible when loading incompatible states, but if you use a state save from another story, you're likely to meet undefined behaviours.

mcoorlim commented 4 years ago

ink tries to be as tolerant as possible when loading incompatible states, but if you use a state save from another story, you're likely to meet undefined behaviours.

on_new_story_start() exists to load in the new ink file.

func on_new_story_start(inkToLoad):
    match chapter:
        1:
            myStory = chapter1Story
            _load_story(inkToLoad)
            _bind_externals_1()
    continue_story()

'chapter' here is a variable set to the current story... which I don't believe I store when I save!

Fixing that solves the problem where loading from a different file breaks the game, but I still have the issue where upon loading I'm given the first ink passage in the file. I'm given the loaded choice buttons, though, and after I click a choice the game proceeds correctly.

ephread commented 4 years ago

The last line in on_new_story_start() is probably why you get the first passage printed upon loading the new story. It's called before inkRunner.loadState(data), so the story starts from scratch.

As for displaying the right passage before hitting the choices, do you export the state before said passage or after it was outputted by story.continue()?

mcoorlim commented 4 years ago

I export when the player hits the save button on the godot level (which only sends a signal to run save_game()) so that would be after.

ephread commented 4 years ago

Alright, then you need to keep track of the last passage shown on screen at any given time. I suppose you can then save it alongside the state when the user press the save button¹ and put it back on screen right before loadState(). ink doesn't backtrack, so once a line is outputted, it's gone.

¹ I noticed that you currently store both the story and the state as two different lines in your savegame. Saving the last passage may break your logic, as lines returned by story.continue() usually (but not always) end with newline characters.

mcoorlim commented 4 years ago

Hm. Is there a way I can save the children of the VBoxContainer where I'm keeping all my instanced story passages, and then reload them? These are the actual label nodes, and preserving them would enable the player to read back and see where in the story they're at. (I'm not entirely sure how to write them to file, then read them back in where they're supposed to go)

mcoorlim commented 4 years ago

I was able to save and re-import those labels using this tutorial: https://docs.godotengine.org/en/stable/tutorials/io/saving_games.html

It's all working fine now. Thanks so much for the continued assistance!

ephread commented 4 years ago

No worries, happy to help!