00:00
00:00
3p0ch
If you like hard games try my Daxolissian System series

plasmid @3p0ch

Cat

Scientist

Read the manual & try stuff

So Cal

Joined on 2/13/10

Level:
13
Exp Points:
1,638 / 1,880
Exp Rank:
38,468
Vote Power:
5.48 votes
Audio Scouts
1
Rank:
Portal Security
Global Rank:
23,697
Blams:
50
Saves:
371
B/P Bonus:
8%
Whistle:
Normal
Medals:
4,611
Supporter:
3y 2m 22d

3p0ch's News

Posted by 3p0ch - February 18th, 2020


--== Update ==--


This implementation solves some of the issues with the previous implementation I posted.

If you haven't published your game yet and want to make sure save data in PlayerPrefs doesn't get wiped out when you publish updates, or you've already published but you're willing to make one update that will wipe out everyone's save data right now but make it possible to post future updates without wiping out everyone's save data, then here are step-by-step instructions. (See the very end of this post for a discussion about the underlying problem with PlayerPrefs and how this handles it.)


1) This step is optional* but to be on the safe side: Make sure your game has an opening scene right after the preloading that essentially just has a "Play" button. Graphics and text are fine to include but nothing in the scene should attempt to read or write PlayerPrefs data. (You should do this anyway to force the user to interact with the screen before really running the game so music starts properly.)

2) Save the code below in a C# script named StartScript.

3) Set the savePathName in the C# code to be your NewGrounds project number (look at the URL of your game's upload page for the number).

4) At the end of the C# code, make

SceneManager.LoadScene("Level Select", LoadSceneMode.Single);

load whatever scene is appropriate for your game if it doesn't happen to be named Level Select.

5) Add this script to the Play button as a component, and in the Play button's On Click make its target be the Play button game object and the function be this script's startGame() function.


*If you don't want to make a start screen like this, try just putting the top part of the code (excluding the StartScript class definition at the very end) into any C# script that you're already using in the first scene of the game, and do step #3 while disregarding the other steps.


I made two major changes compared to the original version based on @Rallyx suggesting conditional compilation, and the tactic of making the new emulating class be called PlayerPrefs so it overrides the built-in PlayerPrefs function if you're running it from a WebGL build but otherwise just uses the regular Unity PlayerPrefs function. That way it doesn't write save game data to an /idbfs/ directory on your hard drive when you run your game from the Unity editor (or the player's drive if you make a non-WebGL build), and you don't have to find/replace the PlayerPrefs statements throughout your code.


using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using UnityEngine.EventSystems;

// ***************************************************
// ** PUT YOUR SAVE PATH NAME IN THE VARIABLE BELOW **
// ***************************************************

// This class acts sort of as an emulator for
// PlayerPrefs, but saves to a specified
// place in IndexedDB so saved games don't
// get wiped when game updates are uploaded

// PlayerPrefs is similar (but not identical)
// to a hash of ints, floats, and strings, so
// this uses a Dictionary in C# (like a hash)

// Note that in this implementation all of
// the values are stored as strings even if
// they're really int or float, but they
// get parsed when values are returned

// Also note that I call Save() any time
// a value gets changed, because I think
// the risk of not saving stuff leading to
// bugginess outweighs the computational
// cost of saving more frequently.
// That could potentially be changed fairly
// easily as long as you know it's happening
// and that you just need to comment out the
// Save() statements at the end of the Set
// methods.

#if UNITY_EDITOR
#elif UNITY_WEBGL
public static class PlayerPrefs {
// **********************************
// ** PUT YOUR SAVE PATH NAME HERE **
// **********************************  
  static string savePathName = "12345";
  static string fileName;
  static string[] fileContents;
  static Dictionary<string, string> saveData = new Dictionary<string, string>();
  
  // This is the static constructor for the class
  // When invoked, it looks for a savegame file
  // and reads the keys and values
  static PlayerPrefs() {
    fileName = "/idbfs/" + savePathName + "/NGsave.dat";
    
    // Open the savegame file and read all of the lines
    // into fileContents
    // First make sure the directory and save file exist,
    // and make them if they don't already
    // (If the file is created, the filestream needs to be
    // closed afterward so it can be saved to later)
    if (!Directory.Exists("/idbfs/" + savePathName)) {
      Directory.CreateDirectory("/idbfs/" + savePathName);
    }
    if (!File.Exists(fileName)) {
      FileStream fs = File.Create(fileName);
      fs.Close();
    } else {
      // Read the file if it already existed
      fileContents = File.ReadAllLines(fileName);
      
      // If you want to use encryption/decryption, add your
      // code for decrypting here
      //   ******* decryption algorithm ********
      
      // Put all of the values into saveData
      for (int i=0; i<fileContents.Length; i += 2) {
        saveData.Add(fileContents[i], fileContents[i+1]);
      }
    }
  }
  
  // This saves the saveData to the player's IndexedDB
  public static void Save() {
    // Put the saveData dictionary into the fileContents
    // array of strings
    Array.Resize(ref fileContents, 2 * saveData.Count);
    int i=0;
    foreach (string key in saveData.Keys) {
      fileContents[i++] = key;
      fileContents[i++] = saveData[key];
    }
    
    // If you want to use encryption/decryption, add your
    // code for encrypting here
    //   ******* encryption algorithm ********
    
    // Write fileContents to the save file
    File.WriteAllLines(fileName, fileContents);
  }
  
  // The following methods emulate what PlayerPrefs does
  public static void DeleteAll() {
    saveData.Clear();
    Save();
  }
  
  public static void DeleteKey(string key) {
    saveData.Remove(key);
    Save();
  }
  
  public static float GetFloat(string key) {
    return float.Parse(saveData[key]);
  }
  public static float GetFloat(string key, float defaultValue) {
    if (saveData.ContainsKey(key)) {
      return float.Parse(saveData[key]);
    } else {
      return defaultValue;
    }
  }
  
  public static int GetInt(string key) {
    return int.Parse(saveData[key]);
  }
  public static int GetInt(string key, int defaultValue) {
    if (saveData.ContainsKey(key)) {
      return int.Parse(saveData[key]);
    } else {
      return defaultValue;
    }
  }
  
  public static string GetString(string key) {
    return saveData[key];
  }
  public static string GetString(string key, string defaultValue) {
    if (saveData.ContainsKey(key)) {
      return saveData[key];
    } else {
      return defaultValue;
    }
  }
  
  public static bool HasKey(string key) {
    return saveData.ContainsKey(key);
  }
  
  public static void SetFloat(string key, float setValue) {
    if (saveData.ContainsKey(key)) {
      saveData[key] = setValue.ToString();
    } else {
      saveData.Add(key, setValue.ToString());
    }
    Save();
  }
  
  public static void SetInt(string key, int setValue) {
    if (saveData.ContainsKey(key)) {
      saveData[key] = setValue.ToString();
    } else {
      saveData.Add(key, setValue.ToString());
    }
    Save();
  }
  
  public static void SetString(string key, string setValue) {
    if (saveData.ContainsKey(key)) {
      saveData[key] = setValue;
    } else {
      saveData.Add(key, setValue);
    }
    Save();
  }
}
#endif

public class StartScript : MonoBehaviour {
  public void startGame() {
    SceneManager.LoadScene("Level Select", LoadSceneMode.Single);
  }
}

In case you're wondering, the reason why it's


#if UNITY_EDITOR

#elif UNITY_WEBGL

<code>

#endif


instead of just


#if UNITY_WEBGL

<code>

#endif


is because if you run a game made for WebGL from the Unity editor then both UNITY_EDITOR and UNITY_WEBGL are true, so the version with just #if UNITY_WEBGL will still run while you're in editor mode and write files to your hard drive.


--== Original ==--


Brief instructions: If you’re using C# scripting and the NewGrounds.io API you can 1) pop this code into the top of core.cs just after the “using” statements, 2) change the savePathName variable to be the number that shows up on NewGrounds in your game’s project page URL so it’s unique to your game if you haven't already published it yet*, 3) find/replace PlayerPrefs with NGData throughout your game's code.


*If you've already published your game then people's saves will be wiped out when you make this update, but will persist during any future updates. Note that PlayerPrefs doesn't save data in the same stringified format as this code, so simply looking in the same directory the PlayerPrefs function had been pointing to won't work.


Be aware that if you test your game out locally within the Unity environment, then it’ll save your game data to your hard drive under /idbfs/[game number]/NGsave.dat. It’s not really a particularly bad thing, but you should be aware that it'll happen so it doesn't take you off guard. I have not yet found a way to change that behavior.


// ***************************************************
// ** PUT YOUR SAVE PATH NAME IN THE VARIABLE BELOW **
// ***************************************************

// This class acts sort of as an emulator for
// PlayerPrefs, but saves to a specified
// place in IndexedDB so saved games don't
// get wiped when game updates are uploaded

// PlayerPrefs is similar (but not identical)
// to a hash of ints, floats, and strings, so
// this uses a Dictionary in C# (like a hash)

// Note that in this implementation all of
// the values are stored as strings even if
// they're really int or float, but they
// get parsed when values are returned

// Also note that I call Save() any time
// a value gets changed, because I think
// the risk of not saving stuff leading to
// bugginess outweighs the computational
// cost of saving more frequently.
// That could potentially be changed fairly
// easily as long as you know it's happening
// and that you just need to comment out the
// Save() statements at the end of the Set
// methods.

public static class NGData {
// **********************************
// ** PUT YOUR SAVE PATH NAME HERE **
// **********************************  
  static string savePathName = "12345";
  static string fileName;
  static string[] fileContents;
  static Dictionary<string, string> saveData = new Dictionary<string, string>();
  
  // This is the static constructor for the class
  // When invoked, it looks for a savegame file
  // and reads the keys and values
  static NGData() {
    fileName = "/idbfs/" + savePathName + "/NGsave.dat";
    
    // Open the savegame file and read all of the lines
    // into fileContents
    // First make sure the directory and save file exist,
    // and make them if they don't already
    // (If the file is created, the filestream needs to be
    // closed afterward so it can be saved to later)
    if (!Directory.Exists("/idbfs/" + savePathName)) {
      Directory.CreateDirectory("/idbfs/" + savePathName);
    }
    if (!File.Exists(fileName)) {
      FileStream fs = File.Create(fileName);
      fs.Close();
    } else {
      // Read the file if it already existed
      fileContents = File.ReadAllLines(fileName);
      
      // If you want to use encryption/decryption, add your
      // code for decrypting here
      //   ******* decryption algorithm ********
      
      // Put all of the values into saveData
      for (int i=0; i<fileContents.Length; i += 2) {
        saveData.Add(fileContents[i], fileContents[i+1]);
      }
    }
  }
  
  // This saves the saveData to the player's IndexedDB
  public static void Save() {
    // Put the saveData dictionary into the fileContents
    // array of strings
    Array.Resize(ref fileContents, 2 * saveData.Count);
    int i=0;
    foreach (string key in saveData.Keys) {
      fileContents[i++] = key;
      fileContents[i++] = saveData[key];
    }
    
    // If you want to use encryption/decryption, add your
    // code for encrypting here
    //   ******* encryption algorithm ********
    
    // Write fileContents to the save file
    File.WriteAllLines(fileName, fileContents);
  }
  
  // The following methods emulate what PlayerPrefs does
  public static void DeleteAll() {
    saveData.Clear();
    Save();
  }
  
  public static void DeleteKey(string key) {
    saveData.Remove(key);
    Save();
  }
  
  public static float GetFloat(string key) {
    return float.Parse(saveData[key]);
  }
  public static float GetFloat(string key, float defaultValue) {
    if (saveData.ContainsKey(key)) {
      return float.Parse(saveData[key]);
    } else {
      return defaultValue;
    }
  }
  
  public static int GetInt(string key) {
    return int.Parse(saveData[key]);
  }
  public static int GetInt(string key, int defaultValue) {
    if (saveData.ContainsKey(key)) {
      return int.Parse(saveData[key]);
    } else {
      return defaultValue;
    }
  }
  
  public static string GetString(string key) {
    return saveData[key];
  }
  public static string GetString(string key, string defaultValue) {
    if (saveData.ContainsKey(key)) {
      return saveData[key];
    } else {
      return defaultValue;
    }
  }
  
  public static bool HasKey(string key) {
    return saveData.ContainsKey(key);
  }
  
  public static void SetFloat(string key, float setValue) {
    if (saveData.ContainsKey(key)) {
      saveData[key] = setValue.ToString();
    } else {
      saveData.Add(key, setValue.ToString());
    }
    Save();
  }
  
  public static void SetInt(string key, int setValue) {
    if (saveData.ContainsKey(key)) {
      saveData[key] = setValue.ToString();
    } else {
      saveData.Add(key, setValue.ToString());
    }
    Save();
  }
  
  public static void SetString(string key, string setValue) {
    if (saveData.ContainsKey(key)) {
      saveData[key] = setValue;
    } else {
      saveData.Add(key, setValue);
    }
    Save();
  }
}

Now the TL,DR

It looks like a vast majority of Unity programmers use PlayerPrefs to store save game data since it’s easy. But when they do, if they ever need to update their game, everyone ends up having their savegame data wiped out.


That’s because Unity saves PlayerPrefs in IndexedDB, and when it does it creates a directory for the save files with the format /idbfs/[some huge hex string]/filename. The [some huge hex string] is based on the URL of the game’s files. If you upload an update to NewGrounds (or pretty much any hosting site) it might look like it’s still being hosted at the same URL in your browser, but will actually be stored at another URL and get a new hex string. So the update won’t know which directory to look in for the previous save files.


That’s kinda discussed in this thread

https://forum.unity.com/threads/persistentdatapath-returns-different-paths-for-differet-builds.526776/

And they talk about a solution of saving data not to /idbfs/[some huge hex string]/filename but instead to /idbfs/[a constant name]/filename. But they don’t explain how to do it.


It is possible to save files to arbitrary locations like that if you plan things out from the beginning, but if you’re like most Unity newbies like me and wrote a ton of code to store data in PlayerPrefs, then going back and redesigning your game to store info differently would be suck city. If you can just use PlayerPrefs instead of building your own save system then that's way easier. But there’s no good way to change where PlayerPrefs saves because Unity forces its behavior, and there’s no good way of pulling the data from PlayerPrefs out and putting it into a variable to save because PlayerPrefs doesn’t have a Keys() function to list all its keys so you can pull out all the data without worrying about whether you’re forgetting anything.


So I wrote my own class to emulate PlayerPrefs behavior, so you could literally just find/replace PlayerPrefs everywhere in your code and replace it with NGData and have all the PlayerPrefs functions work like they should, but you can specify what directory in /idbfs it saves into. It’s a static class so can’t be instantiated (and therefore can’t just be attached to a GameObject), but it can be embedded in any other code that’s placed on a GameObject, like the NewGrounds.io API. I imagine it would be possible to make the save directory be a public variable that could be set from the NewGrounds.io GameObject to be more user-friendly than making people edit the C# code since the App ID and encryption key can be set from the GameObject. The savePathName variable within the NGData class is static, so it can’t be a public variable attached to an object, but I imagine you could make a public variable that can be set from a GameObject and then make NGData read that instance variable into the static savePathName variable when it calls its constructor.


It has crossed my mind that you could use this approach to make a game series and have each game look in the same place for save data so you can see what players have done in different games in the series. Or you could have a collab of Unity devs create a mega-meta-game where each game in the collab interacts with the others through this save data.


10

Posted by 3p0ch - February 14th, 2020


This post pertains to Phaser 3; I haven't tried this with Phaser 2.


While working on the Phaser Game Jam, I discovered that if you use localStorage to save game data on NewGrounds then bad things can happen. Every game on NewGrounds saves to the same space in the player's computer's memory because all the games come from the same host site. So if one game saves a key/value pair of "MaxHP" = 5279 because they have FF7 style hit points and the player goes and plays another game that sets "MaxHP" = 11 because it's old school D&D, then the second game will overwrite the first game's MaxHP of 5279 and leave the player with 11 HP. I've tested that out and confirmed that two different games can read each others' values set in localStorage.


You could work around this by making your keys in localStorage all start with your game number (if you view your game in preview after it's uploaded to NewGrounds and look for the number at the end of the URL) so no other games are likely to overwrite your values. But, there still exists a function called localStorage.clear() that a well-meaning but unaware programmer might put in their game to let the player do what they think will just be reset the save data for their own game, and that scares me.


Using IndexedDB is I think a better solution because it allows you to keep all of your game's data in its own area in the IndexedDB structure on the player's computer and avoids these issues. But I haven't seen any good instructions online describing how to implement IndexedDB saving in a Phaser game. I admit that I'm a hack who just started learning JavaScript when the Jam started and am no pro, but I still managed to get an implementation of IndexedDB saving working in my game and am sharing it because using localStorage for NewGrounds saves seems like an innocuous looking thing that underneath the surface is unacceptably prone to badness.


If you don't want to read all the TL,DR stuff, then just put this into your index.html file just after starting JavaScript and you'll be able to store data you want to save in the saveData object and save it by calling writeSaveData() at appropriate points in your game; the data will be loaded whenever the player starts the game.

// The number below should be your game's number on NewGrounds
// But if people forget to change it, I'ma have u spam 12345
var idbRequest = indexedDB.open("12345");
var idb = null;
var saveData;
idbRequest.onupgradeneeded = newIdb;
idbRequest.onsuccess       = openIdb;
function newIdb() {
  idbRequest.result.createObjectStore("saveData");
}
function openIdb() {
  idb = idbRequest.result;
  idb.transaction("saveData").objectStore("saveData").get(1).onsuccess = function(event) {
    saveData = event.target.result;
    if (saveData == null) {saveData = new Object();}
  }
}
function writeSaveData() {
  if (idb != null) {
    idb.transaction("saveData", "readwrite").objectStore("saveData").put(saveData, 1);
  }
}

Be aware that if you try to access a property of saveData that hasn't been saved to yet (for example if you're lazy like I was and only wrote values to a LevelNumber property after the player finished the level without bothering to initialize them to zero or smth when the game starts) then you'll get an error, but you can workaround that by looking for values with statements like

completion[1] = saveData.FirstLevel || null;

instead of just

completion[1] = saveData.FirstLevel;


If you want to write to an object property based on the value of a variable and don't know how to refer to saveData.variableName, it's done by saying saveData[variableName]


If you want to read all the TL,DR stuff on IndexedDB then look here, here, and even here just for good measure. The most important thing to be aware of in case you try to make a bunch of fancy modifications to what I'm showing is that IndexedDB acts asynchronously, so if you're used to programs that just run straight through and/or think that you'll be able to insert a

while(stillLoading) { } // Not that a genius like myself would ever actually try that and not like the outcome :)

statement, then consider yourself warned to read up on asynchronous processes. In particular, don't try to save data right when the game loads -- put the code above at the very beginning of the JavaScript in index.html and wait until all the preloading stuff is done before you try to write saveData to the player's computer and you should be fine.


To be on the safe side, don't call writeSaveData() consecutively without leaving a little bit of time for the write to happen before trying to write again. There are probably more elegant ways of handling IndexedDB's asynchronous processing, but I don't know them and probably won't learn them in enough time to be useful for this Game Jam.


Finally, in case you have a Unity background and have noticed that using localStorage with Phaser doesn't have the same issues as Unity's implementation of playerPrefs when you update your game: the problem with updating is not inherent to IndexedDB and is something specific to Unity, and the implementation I'm showing here for Phaser doesn't have the same problem of wiping out save data if you update, so you can safely update your game without wiping out everyone's save data. I'ma post a solution to the problem Unity has in not too long (I hope) but I got distracted by the Phaser Game Jam.