[SOLVED] Load Quest Game Data from script

Announcements, support questions, and discussion for Quest Machine.
mschoenhals
Posts: 118
Joined: Thu Dec 19, 2019 7:38 am

[SOLVED] Load Quest Game Data from script

Post by mschoenhals »

Hi,

I've been using the AutoSaveLoad for Quest Machine. I've had some issues with inconsistencies with the data being loaded: sometimes the journal would load empty (despite a save present in the save system). After making some adjustments, my player is now jumping to a different Scene then when the game was stopped (when I hit Load Game from saved menu).

Does player positional data get saved with Quest Machine? When I disable Quest Machine and Load a game, the players position and scene is correct.

In my game, I want the player to be able to start a new game or load the previous game (I don't want to use several saved game slots). I was using AutoSaveLoad with the Load on Start checked. I had trouble with using Quest Machine in the main menu, so I had it only in the game play scenes. If I try and use it in the main menu, it automatically loads the previously saved scene bypassing the main menu.

I am saving other data with my own saved game system, would it be better to load from script?
Last edited by mschoenhals on Sat May 02, 2020 8:32 am, edited 3 times in total.
User avatar
Tony Li
Posts: 22107
Joined: Thu Jul 18, 2013 1:27 pm

Re: Load Quest Game Data from script

Post by Tony Li »

Hi,

If you're saving other data with your own saved game system, you might just want to tie Quest Machine's save data into that. Here's how:

1. Remove the Auto Save Load component.

2. On the Save System component, untick 'Save Current Scene'. If this is ticked, the Save System will save the current scene; when loading a saved game, it will load this scene.

3. Tick 'Include in Saved Game Data' on any Quest Journal and Quest Giver components that you want to include in saved games. Tick 'Save Across Scene Changes'.

4. Add any other savers that you want to use, such as PositionSaver on the player to save its position. Tick 'Save Across Scene Changes'.

5. When saving a game, use this code to get Quest Machine's save data as a string:

Code: Select all

string s = PixelCrushers.SaveSystem.Serialize(PixelCrushers.SaveSystem.RecordSavedGameData());
Then you can save the string to your own saved game system. Your Save System GameObject should still have a data serializer (e.g., JsonDataSerializer) and a saved game data storer (PlayerPrefsSavedGameDataStorer) to prevent it from logging a warning message, but it will not use the saved game data storer.

6. When loading a game, use this code to re-apply the string from your saved game system:

Code: Select all

PixelCrushers.SaveSystem.ApplySavedGameData(PixelCrushers.SaveSystem.Deserialize<SavedGameData>(s));
---

If you decide to allow Quest Machine's save system to manage saving and loading, try this setup:

Option A (use AutoSaveLoad):

1. Put the Save System GameObject in the main menu scene. Do not put the AutoSaveLoad component on it.

2. Put the AutoSaveLoad component in the first gameplay scene.

3. If the player chooses to continue, load the first gameplay scene.

4. If the player chooses to start a new game, delete the saved game (e.g., use SaveSystemMethods.DeleteSlot), then load the first gameplay scene.


Option B (do not use AutoSaveLoad):

1. Put the Save System GameObject in the main menu scene. Do not put the AutoSaveLoad component on it.

2. If the player chooses to continue, load the saved game.

3. If the player chooses to start a new game, load the first gameplay scene.
mschoenhals
Posts: 118
Joined: Thu Dec 19, 2019 7:38 am

Re: Load Quest Game Data from script

Post by mschoenhals »

Hi Tony,

Thanks for your reply. The string code you suggest I use, I would need to include it in my saved file, correct? I'm having some difficulty with that part. I'm using a runtime binary formatter - will that work with Quest Machine's save data?

I'm assuming I would include the codeline "string = s..." in my SaveFile Method (see code copied below). That creates the string but I will still need to save that string, correct? Not sure how that will be done here. Does it get added to the dictionary?

Looking at the other options you gave, both of them would create conflicts with my own saved game system, correct? Or can they be used along with my current system? What do you recommend is the simplest solution here? Also, I plan to purchase Pixel Crusher's Dialogue System. Will this solution work for that as well?

Thank you in advance. :D

Code: Select all

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace RPG.Saving
{
    public class SavingSystem : MonoBehaviour
    {

        public IEnumerator LoadLastScene(string saveFile)
        {
            Dictionary<string, object> state = LoadFile(saveFile);
            int buildIndex = SceneManager.GetActiveScene().buildIndex;
            if (state.ContainsKey("lastSceneBuildIndex"))
            {
                buildIndex = (int)state["lastSceneBuildIndex"];
            }
            yield return SceneManager.LoadSceneAsync(buildIndex);
            RestoreState(state);
        }

        public void Save(string saveFile)
        {
            Dictionary<string, object> state = LoadFile(saveFile);
            CaptureState(state);
            SaveFile(saveFile, state);
        }

        public void Load(string saveFile)
        {
            RestoreState(LoadFile(saveFile));
        }

        public void Delete(string saveFile)
        {
            File.Delete(GetPathFromSaveFile(saveFile));
        }

        private Dictionary<string, object> LoadFile(string saveFile)
        {
            string path = GetPathFromSaveFile(saveFile);
            if (!File.Exists(path))
            {
                return new Dictionary<string, object>();
            }
            using (FileStream stream = File.Open(path, FileMode.Open))
            {
                BinaryFormatter formatter = new BinaryFormatter();
                return (Dictionary<string, object>)formatter.Deserialize(stream);
            }
        }

        private void SaveFile(string saveFile, object state)
        {
            string path = GetPathFromSaveFile(saveFile);
            print("Saving to " + path);
            using (FileStream stream = File.Open(path, FileMode.Create))
            {
                BinaryFormatter formatter = new BinaryFormatter();
                formatter.Serialize(stream, state);
                //string s = PixelCrushers.SaveSystem.Serialize(PixelCrushers.SaveSystem.RecordSavedGameData());
            }
        }

        private void CaptureState(Dictionary<string, object> state)
        {
            foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>())
            {
                state[saveable.GetUniqueIdentifier()] = saveable.CaptureState();
            }

            state["lastSceneBuildIndex"] = SceneManager.GetActiveScene().buildIndex;
        }

        private void RestoreState(Dictionary<string, object> state)
        {
            foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>())
            {
                string id = saveable.GetUniqueIdentifier();
                if (state.ContainsKey(id))
                {
                    saveable.RestoreState(state[id]);
                }
            }
        }

        private string GetPathFromSaveFile(string saveFile)
        {
            return Path.Combine(Application.persistentDataPath, saveFile + ".sav");
        }
    }
}
User avatar
Tony Li
Posts: 22107
Joined: Thu Jul 18, 2013 1:27 pm

Re: Load Quest Game Data from script

Post by Tony Li »

If you add these lines to CaptureState and RestoreState, it should work:

(2 lines added to the bottom of each method.)

Code: Select all

        private void CaptureState(Dictionary<string, object> state)
        {
            foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>())
            {
                state[saveable.GetUniqueIdentifier()] = saveable.CaptureState();
            }

            state["lastSceneBuildIndex"] = SceneManager.GetActiveScene().buildIndex;
            string s = PixelCrushers.SaveSystem.Serialize(PixelCrushers.SaveSystem.RecordSavedGameData());
            state["questMachine"] = s;
        }

        private void RestoreState(Dictionary<string, object> state)
        {
            foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>())
            {
                string id = saveable.GetUniqueIdentifier();
                if (state.ContainsKey(id))
                {
                    saveable.RestoreState(state[id]);
                }
            }
            string s = (string)(state["questMachine"]);
            PixelCrushers.SaveSystem.ApplySavedGameData(PixelCrushers.SaveSystem.Deserialize<SavedGameData>(s));
        }
Edit:
1. RestoreState gets state["questMachine"] as string.
2. Made keys consistently "questMachine" in both methods.
mschoenhals
Posts: 118
Joined: Thu Dec 19, 2019 7:38 am

Re: Load Quest Game Data from script

Post by mschoenhals »

Hi Tony,

The capture state appeared to have no errors but I got a "cannot implicity convert type object to string. An explicit conversion exists (are you missing a cast?)" error in the restore state.

I tried using Visual Studio to help solve it and it came up with this:

Code: Select all

PixelCrushers.SaveSystem.ApplySavedGameData(PixelCrushers.SaveSystem.Deserialize<SavedGameData>((string)state["quest_machine"]));
However, that created 2 KeyNotFoundException errors:

KeyNotFoundException: The given key was not present in the dictionary.
System.Collections.Generic.Dictionary`2[TKey,TValue].get_Item (TKey key) (at <df7127ba07dc446d9f5831a0ec7b1d63>:0)
RPG.Saving.SavingSystem.RestoreState (System.Collections.Generic.Dictionary`2[TKey,TValue] state) (at Assets/Scripts/Saving/SavingSystem.cs:92)
RPG.Saving.SavingSystem+<LoadLastScene>d__0.MoveNext () (at Assets/Scripts/Saving/SavingSystem.cs:24)
UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at C:/buildslave/unity/build/Runtime/Export/Coroutines.cs:17)

I did have an issue with the last line of code you gave me:

Code: Select all

PixelCrushers.SaveSystem.ApplySavedGameData(PixelCrushers.SaveSystem.Deserialize<SavedGameData>(s));
But managed to solve it through a using statement:

Code: Select all

using PixelCrushers;
What am I missing?
User avatar
Tony Li
Posts: 22107
Joined: Thu Jul 18, 2013 1:27 pm

Re: Load Quest Game Data from script

Post by Tony Li »

Oops, I typed the key as "questMachine" in one method and "quest_machine" in the other. They should be the same. I'll change my code above to use "questMachine".
mschoenhals
Posts: 118
Joined: Thu Dec 19, 2019 7:38 am

Re: Load Quest Game Data from script

Post by mschoenhals »

Same message:
Cannot implicitly convert type 'object' to 'string.' An explicit conversion exists (are you missing a cast?)

It's referring to this line of code:

Code: Select all

string s = state["questMachine"];
The other lines do not appear to be creating errors.
mschoenhals
Posts: 118
Joined: Thu Dec 19, 2019 7:38 am

Re: Load Quest Game Data from script

Post by mschoenhals »

Ok, I got it working but if you could just check to make sure that this doesn't produce other issues for Quest Machine, I would appreciate it. I edited the line to:

Code: Select all

string s = state["questMachine"] as string;
Adding the "as string" at the end allowed it to compile and appears to be saving in my game.

Do you see any reason for concern on this change?

Thanks.
User avatar
Tony Li
Posts: 22107
Joined: Thu Jul 18, 2013 1:27 pm

Re: Load Quest Game Data from script

Post by Tony Li »

That should be fine. I did something similar in my code above:

Code: Select all

string s = (string)(state["questMachine"]);
mschoenhals
Posts: 118
Joined: Thu Dec 19, 2019 7:38 am

Re: Load Quest Game Data from script

Post by mschoenhals »

Ok, thanks Tony. Much appreciated.
Post Reply