0

For my new game I'd like to have a system in which I have several stages, and each of them has a bunch of levels.

I thought I could have the next stage be unlocked once all the previous stage's levels are cleared, but it doesn't really fit the type of game.

The problem is that I cannot figure out a way to have the levels unlock sequentially within the stage (stage1_1, stage1_2...) without using multiple save files, as the list would go out of order after unlocking a level from another stage. I am currently storing them as a list. Here is how it's currently set up:

public class SaveManager : MonoBehaviour 
{
public static SaveManager manager;
public List<LevelData> levelData;
public GameStats gameStats;

private void Awake()
{
    if(manager == null)
    {
        DontDestroyOnLoad(gameObject);
        manager = this;
    } else if(manager != this)
    {
        Destroy(gameObject);
    }
}

private void OnEnable()
{
    levelData = GetLevelData();
    gameStats = GetGameStats();
}


public List<LevelData> GetLevelData()
{
    string saveFilePath = Application.persistentDataPath + "/levels.dat";
    if (File.Exists(saveFilePath))
    {
        BinaryFormatter bf = new BinaryFormatter();
        FileStream file = File.Open(saveFilePath, FileMode.Open);
        List<LevelData> data = bf.Deserialize(file) as List<LevelData>;

        file.Close();
        Debug.Log("loaded!");
        return data;

        //Debug.Log(Application.persistentDataPath + "/save.dat");
    } else
    {
        Debug.Log("set level defaults!");
        return SetLevelDefaults();

    }
}

public GameStats GetGameStats()
{
    string saveFilePath = Application.persistentDataPath + "/gamestats.dat";
    if (File.Exists(saveFilePath))
    {
        BinaryFormatter bf = new BinaryFormatter();
        FileStream file = File.Open(saveFilePath, FileMode.Open);
        GameStats data = bf.Deserialize(file) as GameStats;

        file.Close();
        Debug.Log("loaded!");
        return data;

        //Debug.Log(Application.persistentDataPath + "/save.dat");
    }
    else
    {
        Debug.Log("set stats defaults!");
        return SetStartingGameStats();

    }
}

public void SaveLevelData()
{
    string saveFilePath = Application.persistentDataPath + "/levels.dat";
    Save(saveFilePath, "levels");
}

public void SaveGameStats()
{
    string saveFilePath = Application.persistentDataPath + "/gamestats.dat";
    Save(saveFilePath, "stats");
}

public List<LevelData> SetLevelDefaults()
{
    // unlock level 1 and create the file
    List<LevelData> ld = new List<LevelData>();
    LevelData levelOne = new LevelData()
    {
        time = 0,
        stars = 0,
        unlocked = true
    };
    ld.Add(levelOne);
    return ld;

}

public GameStats SetStartingGameStats()
{
    return new GameStats()
    {
        money = 0
    };

}

public void Save(string saveFilePath, string type)
{
    BinaryFormatter bf = new BinaryFormatter();
    FileStream file = File.Open(saveFilePath, FileMode.Create);
    switch (type)
    {
        case "levels":
            bf.Serialize(file, levelData);
            break;
        case "stats":
            bf.Serialize(file, gameStats);
            break;
        default:
            break;
    }

    file.Close();
    Debug.Log("saved!");
}

}



[Serializable]
public class LevelData
{
public int time;
public int stars;
public bool unlocked;
}

[Serializable]
public class GameStats
{
public int money;
// todo add powerups
}

Then, when a level is completed:

    // only unlock the next level if it's not been unlocked yet
    if (SaveManager.manager.levelData.Count - 1 == id)
        SaveManager.manager.levelData.Add(nextLevelData);


    SaveManager.manager.SaveLevelData();

And to access them from the menu:

if(SaveManager.manager.levelData.Count > levelID && 
SaveManager.manager.levelData[levelID] != null)
    {
        LevelData levelData = SaveManager.manager.levelData[levelID];
        SetLevelTime(levelData.time);
        SetLevelStars(levelData.stars);

        // todo display best time
        if (levelData.unlocked)
            Unlock();
    }

And that's my situation. It's a little (too) messy, I did not realize the issue with unlocking levels non-sequentially, so my only solution at this point would be to create different variables in SaveManager (stage1_levelData, stage2_levelData...), but it doesn't sound efficient. I already rewrote it twice and really ran out of ideas at this point.

Also as far as I understand dictionaries cannot be binary serialized, am I correct?

Hopefully someone can point me to the right direction :) Thanks in advance!

2
  • Binary data is a byte[]. The value of the dictionary can be a byte[]. The key of the dictionary in your case can be a string which is the level and property. You may want the Level an enumeration (level_A = 1, level_B=2,level_C=3,level_D=4) which can have a name and the value can be cast to a number so you can compare if you are greater than a level in your code. Commented Aug 25, 2019 at 4:12
  • Thanks, I will give that a try. I wonder why in the many threads about similar topics they always vouched against serializing dictionaries. Commented Aug 26, 2019 at 2:35

1 Answer 1

1

You might rather want to use a 2-Dimensional-Array like e.g.

private LevelData[,] levelData = new LevelData[X,Y];

where

  • X: Amount of Stages
  • Y: Amount of Levels per Stage

And later you can access a specific entry using e.g.

LevelData[2,4].unlocked = true;

Now you unlocked the 5th level of the 3rd stage.

I would however already fill the entire array and not only add the levels you already passed. Instead of leaving null entries and compare you should rather add the additional flag

public bool Completed;

and already initialize the entire array with valid LevelData entries (see example below). This avoids NullReferences and additionally also keeps the necessary storage memory the same size from the beginning and thereby kind of already reserves the required storage memory with the moment you serialize for the first time.

SaveManager.manager.levelData.Add(nextLevelData);

would rather become

SaveManager.manager.levelData[stageID, levelID].unlocked = true;

without creating a new instance of LevelData.

and

SaveManager.manager.levelData[levelID] != null

would become

SaveManager.manager.levelData[stageID, levelID].Completed

The advantages:

  • You can easily iterate over this array and check the values

    Before I suggested to use two inputs StageID and LevelID. Since in a multi-dimensional array all entries are again arrays with the same length you can easily calculate both values from using one single flat index:

    // EXAMPLE FOR INITIALLY FILLING THE ARRAY
    for(var i = 0; i < StageAmount * LevelsPerStageAmount; i++)
    {
        // using integer division
        var stageID = i / LevelsPerStageAmount;
    
        // using modulo 
        // (starts over from 0 after exceeding LevelsPerStageAmount - 1)
        var levelID = i % LevelsPerStageAmount;
    
        var newLevelEntry = new LevelData;
    
        newLevelEntry.Completed = false;
        newLevelEntry.stars = -1;
        newLevelEntry.time = -1;
        newLevelEntry.unlocked = false;
    
        SaveManager.manager.levelData[stageID, levelID] = newLevelEntry;
    }
    

    Or in the other direction

    // For example if you want to keep a continues level name
    var overallFlatIndex = stageID * LevelsPerStageAmount + levelID;
    

    so you can still use an overall flat index - you just have to remember how to calculate it.

  • In memory such an array is still in a flat format so it can simply be serialized and deserialized:

    using System.Runtime.Serialization.Formatters.Binary;  
    
    ...
    
    LevelData[,] levelData = new LevelData[X,Y]{ .... };
    BinaryFormatter bf = new BinaryFormatter();  
    MemoryStream ms = new MemoryStream();  
    bf.Serialize(ms, levelData);  
    

Note: For later updates (adding more levels and stages) you might have to rather deserialize the stored data into a temporal array and then rather copy the values from the temporal array into your actual (now bigger) array ;)


What speaks against a Dictionary?

There is no standard way for (de)serializing a Dictionary. Usually you have to serialize the keys and values as separated lists. You can do that in two different files or using a custom type. Than you can decide if you either want to store a List of key-value-pairs or rather store two lists for keys and values. Either way it is a bit of a hickup.

Sign up to request clarification or add additional context in comments.

2 Comments

Wow thanks so much for the explanation! I had no idea this was even possible. One more question, since you mentioned adding stages and levels later (which I will likely do after the release). Would this kind of solution be able to handle stages having a different amount of levels though? For example if I have one "special" stage, unlike the others, with levels that offer unique challenges, it might have just a handful instead of 40 or so. You mentioned them being the same length so I wonder if the reserved (albeit unused) space wouldn't be too much in this case.
well you could either serialize them in a second list independent from the array or use a usual entry in the current array but simply not using the rest of the elements

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.