Migrating Level Manager to Dialogue System

Announcements, support questions, and discussion for the Dialogue System.
Mackerel_Sky
Posts: 111
Joined: Mon Apr 08, 2019 8:01 am

Migrating Level Manager to Dialogue System

Post by Mackerel_Sky »

Hi there,

I'm currently refactoring all the old garbage code in my project and was looking at integrating my level/scene manager code with Dialogue System's functionality. Currently, each of my scenes has a level manager that does the following.
  • Receives references to the player and other important gameObjects components from a DontDestroyOnLoad game manager object and assigns them where needed.
  • Saves and loads the contents of lootable chests and containers within the scene, so if the chest has been modified it remembers its contents between loads
  • Remembers the state of special enemies and bosses and prevents them from respawning if required (say they have been defeated)
The game is a metroidvania-style platformer, so the player might be moving back and forth between scenes regularly.

I will probably have to expand the functionality of the manager in the future, especially once I start looking into fleshing out a 'story' manager and having to spawn and edit NPCs into different areas to play cutscenes and dialogue, etc.

I have a couple of questions regarding the save system and what it does and doesn't support:
  • I understand we can make our own custom savers from the provided templates and plan to do so for saving chest contents. The documentation also says we need to assign unique keys for each saved object. If I have Chest 1, Chest 2 and Chest 3 in Scene 1, and then Chest 4, Chest 5, Chest 6 in Scene 2, would Assign Unique Keys remember the chests in Scene 1 and automatically assign unique names, or would it assign 1, 2 and 3 again and overwrite the chests in the first scene once I save the 2nd scene?
  • This is more out of curiosity but does each saved object generate a new file, or does the save manager package everything together into a single file?
  • I am guessing we need to use the Spawned Object Manager for spawning unique enemies/bosses that are spawned once or disappear in certain conditions. In the example screenshot it looks like we only need to specify the prefab. How do we specify the position or other parameters for the prefab to be spawned?
  • What sort of functionality would I have to look into for implementing 'progression' along a linear story? For example, maybe some important characters will appear in a zone after the player defeats a boss or completes a quest, or a new part of a level is opened up, etc.
Thanks for your help!
User avatar
Tony Li
Posts: 22051
Joined: Thu Jul 18, 2013 1:27 pm

Re: Migrating Level Manager to Dialogue System

Post by Tony Li »

Hi,
Mackerel_Sky wrote: Wed Jul 15, 2020 6:04 amI understand we can make our own custom savers from the provided templates and plan to do so for saving chest contents. The documentation also says we need to assign unique keys for each saved object. If I have Chest 1, Chest 2 and Chest 3 in Scene 1, and then Chest 4, Chest 5, Chest 6 in Scene 2, would Assign Unique Keys remember the chests in Scene 1 and automatically assign unique names, or would it assign 1, 2 and 3 again and overwrite the chests in the first scene once I save the 2nd scene?
It uses GameObject instance IDs, which are generally unique.
Mackerel_Sky wrote: Wed Jul 15, 2020 6:04 amThis is more out of curiosity but does each saved object generate a new file, or does the save manager package everything together into a single file?
The save system consists of 4 types of components:
  • Save System: Coordinates everything. Holds the current save data in memory (SavedGameData).
  • Savers: When saving, records the state of something (RecordData). When loading, applies the previously saved state (ApplyData).
  • Data Serializer (e.g., JsonDataSerializer): Serializes SavedGameData into a form that can be written to permanent storage. DS ships with JsonDataSerializer and BinaryDataSerializer, and you can also write your own.
  • Saved Game Data Storer (e.g., DiskSavedGameDataStorer): Saves serialized data to permanent storage. DS ships with DiskSavedGameDataStorer, which stores each saved game in a separate local disk file [finally answering your question ;)], and PlayerPrefsSavedGameDataStorer, which stores each saved game in a PlayerPrefs key. You can also write your own. The Easy Save integration includes a Saved Game Data Storer that writes using Easy Save.
Mackerel_Sky wrote: Wed Jul 15, 2020 6:04 amI am guessing we need to use the Spawned Object Manager for spawning unique enemies/bosses that are spawned once or disappear in certain conditions. In the example screenshot it looks like we only need to specify the prefab. How do we specify the position or other parameters for the prefab to be spawned?
The prefab should have a Spawned Object component. When it's spawned, the instance registers itself with the Spawned Object Manager. (When the instance is destroyed, it unregisters.) When saving a game, the Spawned Object Manager records the positions, etc., of all Spawned Object instances so it knows where to instantiate them again when loading the game.
Mackerel_Sky wrote: Wed Jul 15, 2020 6:04 amWhat sort of functionality would I have to look into for implementing 'progression' along a linear story? For example, maybe some important characters will appear in a zone after the player defeats a boss or completes a quest, or a new part of a level is opened up, etc.
I'll usually use Dialogue System variables for that. It makes it easy to reference in conversations and quests. You can set up a Dialogue System Trigger, or your own script, that checks the variable to expose new doors, etc.
Mackerel_Sky
Posts: 111
Joined: Mon Apr 08, 2019 8:01 am

Re: Migrating Level Manager to Dialogue System

Post by Mackerel_Sky »

Thanks for the clarification!

I've started testing saving the contents of my chests between scenes. I generated keys for the test chests I'm working with, and I need to use them to access the saved data for each chest, correct? How do I get the chests to remember which key they are assigned between scenes?

I also assume we need to call ApplyScene manually? I called it on Start() at the beginning but obviously this didn't work out because there was no save data and it bugged out. Do you know how I would distinguish whether or not a chest has had data saved, so that it would correctly use its starting inventory when it's supposed to?

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

Re: Migrating Level Manager to Dialogue System

Post by Tony Li »

Hi,

Here are some different approaches you can take:

1. Use the Dialogue System's save system as the primary save system. Refactor your other save code to work with the save system (e.g., write Saver subclasses and possibly a SavedGameDataStorer subclass).

2. Or use the Dialogue System's save system, serialize it to a string, and save that string in your own save code. A rough example:

Code: Select all

using PixelCrushers;
void SaveMyGame()
{
    var dsSaveData = SaveSystem.Serialize(SaveSystem.RecordSavedGameData());
    var someOtherData = // Save some other data in a different way.
    // Now save dsSaveData and someOtherData using your own save code.
}

void LoadMyGame()
{
    var dsSaveData = // Retrieve the serialized save system data from your own save code.
    SaveSystem.ApplySavedGameData(SaveSystem.Deserialize<SavedGameData>(dsSaveData));
}
SaveSystem.RecordSavedGameData() tells all Savers to record their data into the save system's in-memory saved game data. If your own save code keeps track of which scene to restore, untick the Save System component's Save Current Scene checkbox. Otherwise the save system will also record the scene to load when applying saved game data.

3. Or bypass most of the save system's functionality and manually call each saver's RecordData and ApplyData methods:

Code: Select all

var someChestSaver = someChest.GetComponent<Saver>();

// Record chest's current state:
string key = someChestSaver.key;
string data = someChestSaver.RecordData(); // This just return the chest's state in a string.

// Restore chest's saved state:
someChestSaver.ApplyData(data);
This doesn't answer your question about how to retrieve a specific saver's data from the save system's in-memory saved game data if you're using #1 or #2 above. To do that:

Code: Select all

string data = SaveSystem.currentSavedGameData.GetData(someChestSaver.key);
---

Note: If a saver's Save Across Scene Changes checkbox is ticked, then its data will stay in the save system's in-memory saved game data. Otherwise it will be dropped when you leave the saver's scene.

This way you can save detailed information about the player's current scene, but reduce saved game sizes by dropping some data for other scenes. For example, you'll want to keep Save Across Scene Changes ticked for your chests.

However, if a bullet is flying through the air in the player's current scene, you'll want to save that, since it might be heading right toward the player. But if you change scenes, you no longer have to keep track of the bullet, so there's no sense bloating the saved game data with its info.
Mackerel_Sky
Posts: 111
Joined: Mon Apr 08, 2019 8:01 am

Re: Migrating Level Manager to Dialogue System

Post by Mackerel_Sky »

I was actually trying to go with Option 1 and replace all my current save data with Dialogue System's.

by someChestSaver.key, do you mean the private string m_key in the Saver abstract class? Is it safe to make this protected? Here's what my current code looks like, I don't think m_runtimeKey is what I was supposed to be using :(

Code: Select all

        
using UnityEngine;
using System.Collections.Generic;

namespace PixelCrushers
{
    public class ChestSaveTemplate : Saver // Rename this class.
    {
        private List<ItemsContained> itemSave;
        LootChest lootChest;

        public override void Start()
        {
            base.Start();
            lootChest = GetComponent<LootChest>();
            ApplyData(SaveSystem.currentSavedGameData.GetData(m_runtimeKey));
        }

        public void RecordContents()
        {
            if (SaveSystem.instance != null)
            {
                itemSave = lootChest.itemList;
            }
        }
        public override string RecordData()
        {
            Debug.Log("saving data");
            return SaveSystem.Serialize(itemSave);
        }

        public override void ApplyData(string data)
        {
            Debug.Log("applying data");
            List<ItemsContained> savedItems = SaveSystem.Deserialize<List<ItemsContained>>(data);
            if (savedItems != null)
            {
                Debug.Log("saved chest data detected, loading saved data");
                lootChest.itemList = savedItems;
            }
        }
 }
I get a ArgumentNullException: Value cannot be null error on starting and reentering the scene.
User avatar
Tony Li
Posts: 22051
Joined: Thu Jul 18, 2013 1:27 pm

Re: Migrating Level Manager to Dialogue System

Post by Tony Li »

Use the key property, not m_key or m_runtimeKey.

You don't need to override Start().

When you change scenes or load a game, the save system will automatically call ApplyData(), passing in the appropriate save data.

If there is no save data, the string will be empty. Check if it's null or empty first:

Code: Select all

public override void ApplyData(string data)
{
    if (string.IsNullOrEmpty(data)) return;
    // The rest of your code here...
Note that the save system will normally wait a number of frames (specified on the Save System component) before calling ApplyData(). This allows scripts in the newly-loaded scene to initialize in Start(). It will wait for the end of the frame before calling ApplyData(). So if none of your scripts run coroutines that take more than one frame to initialize, you can set the Save System's Frames To Wait Before Apply Data to zero.

In addition, if you really need to set the data immediately after the scene is loaded (and before scripts' Start() methods have run), you can implement the ApplyDataImmediate() method. In this method, use SaveSystem.currentSavedGameData.GetData(key) to get the save data string.
Mackerel_Sky
Posts: 111
Joined: Mon Apr 08, 2019 8:01 am

Re: Migrating Level Manager to Dialogue System

Post by Mackerel_Sky »

Hey Tony,

I checked the original start method in Saver and it seems to only call ApplyData() when the restoreStateOnStart bool is true?

Code: Select all

        public virtual void Start()
        {
            if (restoreStateOnStart)
            {
                ApplyData(SaveSystem.currentSavedGameData.GetData(key));
            }
        }
I don't want chests to save inbetween sessions, only when the player manually reaches a save point so I don't think selecting this is correct for me. I removed it anyway and confirmed ApplyData() hasn't been called using prints when I reload the scene with the chests inside. I also think I need to override start anyway because I need to get my reference to the loot chest component in there.

Maybe I need to transfer my scene transition/load to Dialogue Manager? I reverted everything back and noticed Start() wasn't being called when I returned to the scene so maybe I am doing something else wrong.
User avatar
Tony Li
Posts: 22051
Joined: Thu Jul 18, 2013 1:27 pm

Re: Migrating Level Manager to Dialogue System

Post by Tony Li »

Normally you'll leave the Restore State On Start checkbox unticked (i.e, restoreStateOnStart value is false). Only in very special circumstances will you use it.

Normally ApplyData() is called when you load a game using PixelCrushers.SaveSystem.LoadFromSlot() or change scenes using SaveSystem.LoadScene() (or LoadAdditiveScene).

If you're not using the Save System to change scenes or load games, you can still do it manually. For example, to change scenes some other way:

1. Call PixelCrushers.SaveSystem.BeforeSceneChange() to inform Savers that you'll be changing scenes, in case any Savers care about that.

2. Call SaveSystem.RecordSavedGameData(). This will call Savers' RecordData() methods and store the info in the Save System's memory.

3. Change the scene.

4. Call SaveSystem.ApplySavedGameData(). This will call Savers' ApplyData() methods.
Mackerel_Sky
Posts: 111
Joined: Mon Apr 08, 2019 8:01 am

Re: Migrating Level Manager to Dialogue System

Post by Mackerel_Sky »

Thanks for the tips. I remembered I had the DS demo in my project so I opened it up to take a look at what was going on and how to implement the scene transitions correctly.

I can see that you used a LoadLevel sequence like this:

Image

Copying the same structure across, I don't seem to spawn at the intended location like the demo, the player transform remains the same as in the first scene.

Image

I'm guessing this is because the dialogue system doesn't know which game object is the player? I took another look through the demo and couldn't see anything like this- maybe I've got it wrong. Either way I think migrating the scene changes from my old system to dialogue system fixed any issues I had with saving (I think).

I looked through the manual and tried LoadSceneAtSpawnpoint, but that didn't change the scene. Do you have any ideas?
User avatar
Tony Li
Posts: 22051
Joined: Thu Jul 18, 2013 1:27 pm

Re: Migrating Level Manager to Dialogue System

Post by Tony Li »

Hi,

To use spawnpoints, your player GameObject should have a Position Saver component whose Use Player Spawnpoint checkbox is ticked.
Post Reply