Timer counting number of seconds played resets and starts over from 0

Hi!

Since releasing our game we had issues trying to keep track of how many total seconds a player has been playing (For analytical purposes). Over the last couple of weeks, we have seen a decrease in the total number of hours players have collectively spent playing our game. This should basically be impossible since we store every game save ever made in the cloud.

What seems to be happening is that sometimes the integer counting seconds is reset to 0 overwriting the previous value. Meaning that a player can go from having played 4 hours all the way back to 5 minutes played in our statistics. The code is pretty simple and I can´t for the life of me understand why this happens.

  • We use a singleton that is never destroyed to keep track of time.

  • No other save data is affected. Players are not simply deleting their saves starting over.

  • The integer keeping track of seconds is not written to anywhere outside of this script.

  • It’s an iOS game.

  • Edit: Persistent data is serialized and deserialized between sessions.

Any ideas?

using UnityEngine;
using System.Collections;
using System;

public class TimeManagerSingleton : MonoBehaviour {

    public static TimeManagerSingleton instance;

    private void Awake()
    {
        if(instance != null && instance != this)
        {
            Destroy(this.gameObject);
            return;
        }
        else instance = this;

        DontDestroyOnLoad(this.gameObject);
    }

    private void Start()
    {
        StartCoroutine(CountSecounds());
    }

    private IEnumerator CountSecounds()
    {  
        while(true)
        {
            yield return new WaitForSecondsRealtime(1f);
            SaveManager.persistentData.totalPlaytimeInSecounds++;
        }
    }
}

The only thing I can think of is that if SaveManager doesn’t load the data from the previous session before starting a new one, then it would reset.

That would indeed make sense on its own, but then any other persistent data would also have been unavailable during Start(). The SaveManager deserializes the persistantData object during Awake().

If you can show your SaveManager, then it would a lot easier to find the issue.

I’ve written a simplified version of the SaveManager to increase readability and amplify the signal to noise ratio:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using VoxelBusters.RuntimeSerialization;
using System.Linq;
using System;

public class SaveManager : MonoBehaviour {

    public static PersistentData persistentData;

    string saveGameKey;

    private void Awake()
    {
        TryLoad();
    }

    public void TryLoad()
    {
        DefaultPersistentData.LoadDefaultValues();

        saveGameKey = SaveGameKey.GetKey((SaveGame) PlayerPrefs.GetInt("currentProfile"));

        if(RSManager.ContainsKey(saveGameKey))
        {
            //Load
            byte[]     _serializationData    = null;
            if(RSManager.TryGetSerializationData(saveGameKey, out _serializationData))
            {
                //Load Exsisting savegame
                persistentData = new PersistentData(LoadFromDisk(saveGameKey));
            }
            else
            {
                //Create new savegame
                CreateNewProfile(saveGameKey);
            }
        }
        else
        {
            //Create new savegame
            CreateNewProfile(saveGameKey);
        }
    }

    private Hashtable LoadFromDisk(string key)
    {
        Hashtable result = RSManager.Deserialize<Hashtable>(key);
        return result;
    }

    public void SaveToDisk()
    {
        RSManager.Serialize<Hashtable>( persistentData.GetDataAsHashtable(), saveGameKey, eSaveTarget.FILE_SYSTEM);
    }

    private void CreateNewProfile(string saveGameKey)
    {
        this.saveGameKey = saveGameKey;
        persistentData = new PersistentData(null);
        SaveManager.instance.SaveToDisk();
    }   
}

public class PersistentData
{
    public float totalPlaytimeInSecounds;
   
    public PersistentData() : this(null)
    {
    }
   
    public PersistentData(Hashtable hash)
    {
        sourceHash = hash;
        totalPlaytimeInSecounds = (float) GetValue("totalPlaytimeInSecounds");
    }

    //This method is used to extract values from the deserialized hashtable
    private System.Object GetValue(string key)
    { 
        System.Object result = null;

        if(sourceHash == null)
        {
            //if sourcehash not set; / non-exsistant, load defaultvalue.
            result = DefaultPersistentData.data[key]; //<-- All other persistantData variables would also be set to their default value if this was run.
            return result;
        }

        result = sourceHash[key];
       
        if(result == null)
        {
            Debug.LogWarning("Uninitalized Value Found: " + key); //<-- This message would have shown if the default value was loaded.
            result = DefaultPersistentData.data[key];
        }

        return result;
    }

    //This method returns a hashtable that is serialized to disk
    public Hashtable GetDataAsHashtable()
    {
        Hashtable result = new Hashtable();
        result.Add("totalPlaytimeInSecounds", totalPlaytimeInSecounds);
    }
}

//This class stores hardcoded default values for all savegame variables.
//These are used when a new variable has been added to savegame in a patch and when starting a new savegame.
public class DefaultPersistentData
{
    public static Hashtable data;

    public static void LoadDefaultValues()
    {
        data = new Hashtable();
        data.Add("totalPlaytimeInSecounds", 0f);
    }
}
  • public static void LoadDefaultValues()
  • {
  • data = new Hashtable();
  • data.Add(“totalPlaytimeInSecounds”, 0f);
  • }

This is where I see the issue. Try actually removing that.
Instead of calling that function just do a check at the beginning once you serialize the player.
if(playerdata.timeplayer != null){addToItHere}else{createNewTimeTracker}

I think you might have mixed DefaultPersistentData.data with PersistentData.sourceHash. If you have a look at PersistentData.GetValue(string key), you can see that DefaultPersistentData.data[“totalPlaytimeInSecounds”] is not “extracted” from the hashtable unless “totalPlaytimeInSecounds” is absent from PersistentData.sourceHash. PersistentData.sourceHash is the Hashtable actually being serialized and deserialized from disk.

DefaultPersistentData.LoadDefaultValues() is executed every time the game starts, so if it were indeed overwriting the saved variable it would also overwrite all other saved variables. (All the other variables have been removed from the script above).

Your time and patience are greatly appreciated. It has helped me exclude some options :slight_smile: