These are the SaveHandler classes that allow games to interact for a Meta-Game collab. Each of them has instructions on how to include it in your game in the comments at the beginning. They fundamentally work by keeping the shared data in a SaveHandler.publicSaveData object and have functions to either read (SaveHandler.readSave()) or write (SaveHandler.writeSave()) the data to Local Storage on the player's computer under the key publicKey. In general, you'll probably want to call SaveHandler.readSave() when your game starts and SaveHandler.writeSave() whenever you change any of the save data, but that's left for you to decide how to execute in case for example you update publicSaveData very frequently (every frame) and it would be a bad idea to write to Local Storage that frequently. The SaveHandlers also have .openSite(url) and .switchToSite(url) functions to open other games in the collab either in a new browser tab or replacing the current game's page.
If you're not familiar with Local Storage: it's a set of key / value pairs stored by the player's browser for the webpage's domain. You can see it in action in Chrome or Firefox by opening a game in NewGrounds, opening the developer tools, and looking under the Application tab on Chrome or Storage tab in Firefox. On the left should be a Local Storage thing to select, and there should be data for newgrounds.com and uploads.ungrounded.net. The ungrounded.net one is where the games' save data is stored, and if you select it then it should show a bunch of key / value pairs with save data. The key that was used in the MetaGame Collab Interface Prototype was NGcollabTest, so you might be able to find it and see its structure. Also note that the Local Storage save data for EVERY GAME YOU'VE EVER PLAYED ON NEWGROUNDS is stored there under ungrounded.net so they all share the same space. A relatively little known fact is that if you save data with Local Storage and happen to save your game's data under the same key that another game uses, then the games can inadvertently read from or write to the same data. If that's done accidentally then that'll probably mess things up, but for a Meta-Game collab that's exactly what we want to do. The SaveHandler scripts just facilitate doing that from any framework, and read-write all of the save data into the publicKey of Local Storage as a JSON-encoded object of the publicSaveData that the games use. The structure of publicSaveData should be maintained by all of the games in the collab, and that's why it's important for the collab to specify the publicKey and the structure of publicSaveData.
The collab will also need one special game that will be the first game that's launched when a player runs the meta-game. It takes advantage of publicSaveData.saveDataVersion in case the structure of publicSaveData ever needs to be changed. When the first game is called, it sets its internal variable SaveHandler.publicSaveData to default values and then attempts to read the publicSaveData from Local Storage -- if there's data already saved in Local Storage then that overrides the defaults, and if not (like if this is the first time the player plays the game) then it proceeds with the default values. If the collab decides to change the structure of publicSaveData, then the default value of publicSaveData.saveDataVersion will be incremented in the first game's code. If a player plays and the first game reads "old" save data from before the update of publicSaveData's structure then it will see an older saveDataVersion from the player's Local Storage and should decide what to do -- whether to keep some of the data and restructure publicSaveData or do anything else that might be appropriate for how the collab is structured. An example of such code, which would be used only by the starting game in the collab (for the prototype this is in Phaser and will just make a new publicSaveData with default values and ignore saved data from the previous version) is:
publicReadSave = function() { if (window.localStorage.getItem(publicKey) != null) { var checkSaveData = JSON.parse(window.localStorage.getItem(publicKey)); if (checkSaveData.saveDataVersion >= publicSaveData.saveDataVersion) { publicSaveData = JSON.parse(window.localStorage.getItem(publicKey)); } } }
Now for the actual code of the SaveHandlers for each framework.
--== Phaser ==--
// Save this file as savehandler.js // In your index.html file, after loading the phaser script, add a // line to load the savehandler.js script like: // <script src="savehandler.js"></script> // This will make a publicly accessible SaveHandler which you can // use in any other script. Public save data (shared between all // the games in the collab) is in a SaveHandler.publicSaveData // object with properties for each of the shared data elements. // SaveHandler.privateSaveData keeps data just for your game that // isn't shared across the collab. // Set the publicKey (for shared data) to the key for the collab // and privateKey (with data only for your game) to whatever you want. // Set up publicSaveData per the collab's specifications and // privateSaveDate can have whatever you want. Both of them are // set up with default values. // Read the save data from disk with SaveHandler.readSave() and write // the current SaveHandler.publicSaveData and SaveHandler.privateSaveData // to disk with SaveHandler.writeSave(). You can also use publicReadSave(), // publicWriteSave(), privateReadSave(), and privateWriteSave() to read // or write just the public or private data. // Open another game in a new tab with SaveHandler.openSite(url) or make // another game take over the current page with SaveHandler.switchToSite(url) let SaveHandler = (function() { publicKey = "NGcollabTest"; privateKey = "NGcollabPhaserTest"; publicSaveData = { saveDataVersion: 0, urlStartPage: "https://www.newgrounds.com/projects/games/1469789/preview/filetype/2", urlPhaserPage: "https://www.newgrounds.com/", urlHaxeFlixelPage: "https://www.newgrounds.com/", urlGodotPage: "https://www.newgrounds.com/", urlUnityPage: "https://www.newgrounds.com/", thisIsaLie: true, hp: 2, pi: 3.14159, pi2: 3.1415926, playerName: "yo mama", floatArray: [1.5, 2.3, 9.2], lotsaData: { dataInt: 8, dataString: "gobble", }, }; privateSaveData = { backgroundColor: "purple", }; readSave = function() {publicReadSave(); privateReadSave();} writeSave = function() {publicWriteSave(); privateWriteSave();} publicReadSave = function() { if (window.localStorage.getItem(publicKey) != null) { publicSaveData = JSON.parse(window.localStorage.getItem(publicKey)); } } privateReadSave = function() { if (window.localStorage.getItem(privateKey) != null) { privateSaveData = JSON.parse(window.localStorage.getItem(privateKey)); } } publicWriteSave = function() {window.localStorage.setItem(publicKey, JSON.stringify(publicSaveData));} privateWriteSave = function() {window.localStorage.setItem(privateKey, JSON.stringify(privateSaveData));} openSite = function(url) {window.open(url);} switchToSite = function(url) {window.open(url, "_parent");} return this; })();
--== HaxeFlixel ==--
// Add this code to your Main.hx file underneath the top // package; line and replacing the import statements. // It will create a SaveHandler class that can be used in // any other scripts, as long as you include the line // import Main.SaveHandler; // in the script that's using it. // Set the publicKey to whatever the collab specifies, and privateKey // to anything you want. // Public save data (accessible by the entire collab) is stored in // SaveHandler.publicSaveData and should be set per the specifications // of the collab, values here would serve as defaults but shouldn't // end up mattering if this game is called from another game that sets // values. Private save data (visible only to your game) is in // SaveHandler.privateSaveData // Call the functions SaveHandler.readSave() or SaveHandler.writeSave() // to read or write the data to the player's computer. You can also use // just the .publicReadSave(), .privateWriteSave(), etc. forms. // Open a new tab with another game in the collab using a function like // SaveHandler.openSite(SaveHandler.publicSaveData.urlPhaserPage); // or to open another game replacing the current page by using // SaveHandler.switchToSite(SaveHandler.publicSaveData.urlPhaserPage); import haxe.Json; import flixel.FlxGame; import openfl.display.Sprite; import js.Browser; class SaveHandler { public static var publicKey = "NGcollabTest"; public static var privateKey = "NGcollabHaxeTest"; public static var publicSaveData = { saveDataVersion: 0, urlStartPage: "https://www.newgrounds.com/projects/games/1469789/preview/filetype/2", urlPhaserPage: "https://www.newgrounds.com/", urlHaxeFlixelPage: "https://www.newgrounds.com/", urlGodotPage: "https://www.newgrounds.com/", urlUnityPage: "https://www.newgrounds.com/", thisIsaLie: true, hp: 2, pi: 3.14159, pi2: 3.1415926, playerName: "yo mama", floatArray: [1.5, 2.3, 9.2], lotsaData: { dataInt: 8, dataString: "gobble", }, }; public static var privateSaveData = { backgroundColor: "green", }; public static function readSave() { publicReadSave(); privateReadSave(); } public static function writeSave() { publicWriteSave(); privateWriteSave(); } public static function publicReadSave() { if (Json.parse(Browser.window.localStorage.getItem(publicKey)) != null) { publicSaveData = Json.parse(Browser.window.localStorage.getItem(publicKey)); } } public static function privateReadSave() { if (Json.parse(Browser.window.localStorage.getItem(privateKey)) != null) { privateSaveData = Json.parse(Browser.window.localStorage.getItem(privateKey)); } } public static function publicWriteSave() { Browser.window.localStorage.setItem(publicKey, Json.stringify(publicSaveData)); } public static function privateWriteSave() { Browser.window.localStorage.setItem(privateKey, Json.stringify(privateSaveData)); } public static function openSite(url:String) { Browser.window.open(url); } public static function switchToSite(url:String) { Browser.window.open(url, "_parent"); } }
--== Godot ==--
extends Node # Save this script as SaveHandler.gd # In Project > Project Settings, AutoLoad tab: add this script and make sure # the Enable checkbox is on # This will make publicly accessible SaveHandler stuff available for other # scripts. Public save data (available to any game in the collab) is in # SaveHandler.publicSaveData with dictionary entries for each data element, # and private save data (available only to your game and not shared across # the collab) is stored in SaveHandler.privateSaveData # Set the publicKey variable to whatever the collab picks # Set the privateKey variable to whatever you want # Make sure publicSaveData is set up per the collab's specifications # Set up privateSaveData however you want # Read the save data with SaveHandler.readSave() and write it with # SaveHandler.writeSave(). You can also use publicReadSave(), # privateWriteSave(), etc to handle just public or private data # If you run this from a browser as an HTML5 build, it will read/write data # to the browser's localStorage. If you run it from within Godot, it will # read/write to files on your hard drive named according to publicKey and # privateKey with .save suffixes, so things should work while you're testing # your game without needing to build to HTML5. # Open another game in a new tab with SaveHandler.openSite(url) or make # another game take over the current page with SaveHandler.switchToSite(url) # It'll look like SaveHandler.openSite(SaveHandler.publicSaveData.urlPhaserPage) # and SaveHandler.switchToSite(SaveHandler.publicSaveData.urlPhaserPage) # Obviously that will only work if you make an HTML5 build. # Otherwise, the console will just show a message saying that it couldn't be # done since it's not an HTML5 build, but you'll know for debugging purposes # that the game attempted to call the function. var publicKey = "NGcollabTest" var privateKey = "NGcollabGodotTest" var publicSaveData = { "saveDataVersion": 0, "urlStartPage": "https://www.newgrounds.com/", "urlPhaserPage": "https://www.newgrounds.com/", "urlHaxeFlixelPage": "https://www.newgrounds.com/", "urlGodotPage": "https://www.newgrounds.com/", "urlUnityPage": "https://www.newgrounds.com/", "thisIsaLie": true, "hp": 2, "pi": 3.14159, "pi2": 3.1415926, "playerName": "yo mama", "floatArray": [1.5, 2.3, 9.2], "lotsaData": { "dataInt": 8, "dataString": "gobble", } } var privateSaveData = { "backgroundColor": "cyan", } func readSave(): publicReadSave() privateReadSave() func writeSave(): publicWriteSave() privateWriteSave() func publicReadSave(): if OS.has_feature('JavaScript'): var JSONstr = JavaScript.eval("window.localStorage.getItem(\"" + publicKey + "\");") if (JSONstr != null): publicSaveData = parse_json(JSONstr) else: var save_file = File.new() if not save_file.file_exists("user://" + publicKey + ".save"): return save_file.open("user://" + publicKey + ".save", File.READ) publicSaveData = parse_json(save_file.get_line()) save_file.close() func privateReadSave(): if OS.has_feature('JavaScript'): var JSONstr = JavaScript.eval("window.localStorage.getItem(\"" + privateKey + "\");") if (JSONstr != null): privateSaveData = parse_json(JSONstr) else: var save_file = File.new() if not save_file.file_exists("user://" + privateKey + ".save"): return save_file.open("user://" + privateKey + ".save", File.READ) privateSaveData = parse_json(save_file.get_line()) save_file.close() func publicWriteSave(): if OS.has_feature('JavaScript'): JavaScript.eval("window.localStorage.setItem(\"" + publicKey + "\", \'" + to_json(publicSaveData) + "\');") else: var save_file = File.new() save_file.open("user://" + publicKey + ".save", File.WRITE) save_file.store_line(to_json(publicSaveData)) save_file.close() func privateWriteSave(): if OS.has_feature('JavaScript'): JavaScript.eval("window.localStorage.setItem(\"" + privateKey + "\", \'" + to_json(privateSaveData) + "\');") else: var save_file = File.new() save_file.open("user://" + privateKey + ".save", File.WRITE) save_file.store_line(to_json(privateSaveData)) save_file.close() func openSite(url): if OS.has_feature('JavaScript'): JavaScript.eval("window.open(\"" + url + "\");") else: print("Could not open site " + url + " without an HTML5 build") func switchToSite(url): if OS.has_feature('JavaScript'): JavaScript.eval("window.open(\"" + url + "\", \"_parent\");") else: print("Could not switch to site " + url + " without an HTML5 build")
--== Unity ==--
using System.Collections; using System.Collections.Generic; using UnityEngine; using Newtonsoft.Json; using System.Runtime.InteropServices; // ================== // === FILE SETUP === // ================== // Save this file as "SaveScript.cs" in Unity // Then attach it to any GameObject in the first scene // Save the following code between /* and */ as "link.xml" in Unity's Assets folder /* <linker> <assembly fullname="System.Core"> <type fullname="System.Linq.Expressions.Interpreter.LightLambda" preserve="all" /> </assembly> </linker> */ // Make an Assets/Plugins subdirectory in Unity // Go to https://www.nuget.org/packages/Newtonsoft.Json/ and click "Download Package" // Rename it to newtonsoft.json.12.0.3.zip (instead of .nupkg) // Put its /lib/netstandard2.0/Newtonsoft.Json.dll file in Assets/Plugins in Unity // Save the following code between /* and */ as "SaveData.jslib" in Assets/Plugins /* mergeInto(LibraryManager.library, { ReadSave: function(key_ptr) { var key = Pointer_stringify(key_ptr); var returnString = window.localStorage.getItem(key) || ""; var bufferSize = lengthBytesUTF8(returnString) + 1; var buffer = _malloc(bufferSize); stringToUTF8(returnString, buffer, bufferSize); return buffer; }, WriteSave: function(key_ptr, saveData_ptr) { var key = Pointer_stringify(key_ptr); var saveData = Pointer_stringify(saveData_ptr); window.localStorage.setItem(key, saveData); }, OpenSite: function(url_ptr) { var url = Pointer_stringify(url_ptr); window.open(url); }, SwitchToSite: function(url_ptr) { var url = Pointer_stringify(url_ptr); window.open(url, "_parent"); }, }); */ // ========================================= // === USING THIS AFTER FILES ARE SET UP === // ========================================= // Make sure the PublicSaveData class is formatted per the collab's specs // Format the PrivateSaveData class to save any data not shared with the collab // Set publicKey to the collab's key and privateKey to whatever you want // Modify the data by changing values of SaveHandler.publicSaveData.property or // SaveHandler.privateSaveData.property from any C# script // Load the data by calling SaveHandler.readSave(); // (If there's no save data, SaveHandler.public/privateSaveData will not change) // Save the data by calling SaveHandler.writeSave(); // (or publicReadSave(), publicWriteSave(), privateReadSave(), privateWriteSave()) // To open another game's page in a new tab: // SaveHandler.openSite(SaveHandler.publicSaveData.urlPhaserPage); // or to switch the current game canvas to a new game: // SaveHandler.switchToSite(SaveHandler.publicSaveData.urlHaxeFlixelGame); // ================================ // ===== End of documentation ===== // ================================ // Structure and default values for collab-wide save data // Note that if it includes JavaScript multi-level parameters, this should be // handled in C# with a new class embedded in PublicSaveData public class PublicSaveData { public int saveDataVersion = 0; public string urlStartPage = "https://www.newgrounds.com/"; public string urlPhaserPage = "https://www.newgrounds.com/"; public string urlHaxeFlixelPage = "https://www.newgrounds.com/"; public string urlGodotPage = "https://www.newgrounds.com/"; public string urlUnityPage = "https://www.newgrounds.com/"; public bool thisIsaLie = true; public int hp = 2; public float pi = 3.14159f; public double pi2 = 3.1415926; public string playerName = "pajama"; public float[] floatArray = {1.4f, 2.2f, 9.1f}; public LotsaData lotsaData = new LotsaData(); } public class LotsaData { public int dataInt = 12; // This is accessed as SaveHandler.publicSaveData.lotsaData.dataInt public string dataString = "gobble"; // Accessed as SaveHandler.publicSaveData.lotsaData.dataString } // Structure and default values for save data only seen by this game public class PrivateSaveData { public string backgroundColor = "blue"; } public static class SaveHandler { // Keys to save under static string publicKey = "NGcollabTest"; static string privateKey = "NGcollabUnityTest"; // Create static variables that will hold the save data // and set them to default values public static PublicSaveData publicSaveData = new PublicSaveData(); public static PrivateSaveData privateSaveData = new PrivateSaveData(); #if (UNITY_WEBGL && !UNITY_EDITOR) // If this is a WebGL build, set up functions from the jslib file [DllImport("__Internal")] private static extern string ReadSave(string key); [DllImport("__Internal")] private static extern void WriteSave(string key, string serializedSaveData); [DllImport("__Internal")] private static extern string OpenSite(string url); [DllImport("__Internal")] private static extern string SwitchToSite(string url); #else // Otherwise, use PlayerPrefs to save the data private static string ReadSave(string key) { return PlayerPrefs.GetString(key); } private static void WriteSave(string key, string serializedSaveData) { PlayerPrefs.SetString(key, serializedSaveData); } private static void OpenSite(string url) { Debug.Log("Cannot open another game except when running WebGL in a browser!"); } private static void SwitchToSite(string url) { Debug.Log("Cannot open another game except when running WebGL in a browser!"); } #endif // These call the jslib or PlayerPrefs functions // and convert between objects and strings for the calls public static void readSave() { publicReadSave(); privateReadSave(); } public static void writeSave() { publicWriteSave(); privateWriteSave(); } public static void publicReadSave() { if (ReadSave(publicKey) != "") { publicSaveData = JsonConvert.DeserializeObject<PublicSaveData>(ReadSave(publicKey)); } } public static void publicWriteSave() { WriteSave(publicKey, JsonConvert.SerializeObject(publicSaveData)); } public static void privateReadSave() { if (ReadSave(privateKey) != "") { privateSaveData = JsonConvert.DeserializeObject<PrivateSaveData>(ReadSave(privateKey)); } } public static void privateWriteSave() { WriteSave(privateKey, JsonConvert.SerializeObject(privateSaveData)); } // These call the functions to open other games public static void openSite(string url) { OpenSite(url); } public static void switchToSite(string url) { SwitchToSite(url); } } // This does nothing, but needs to be present so Unity // will let you attach this script to a GameObject public class SaveScript : MonoBehaviour { }
BobbyBurt
Just got this working in my project last minute as a highscore save feature. I just commented out the public save data stuff, since I'm just using it for private saving. Seems to work great, and it's pretty simple for my simpleton monkey brain to understand. Thanks!