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,734 / 1,880
Exp Rank:
38,286
Vote Power:
5.51 votes
Audio Scouts
1
Rank:
Portal Security
Global Rank:
23,493
Blams:
50
Saves:
378
B/P Bonus:
8%
Whistle:
Normal
Medals:
4,932
Supporter:
4y 2m 8d

3p0ch's News

Posted by 3p0ch - April 6th, 2020


If you're like me and a newbie to HaxeFlixel working on the Game Jam, and if you're like me and have been looking for some fancy spritesheets to make level maps, then you might've run into this problem too. I found this really nice spritesheet at https://opengameart.org/content/exterior-32x32-town-tileset. But it's a little too nice to use the same sort of approaches that were covered in the HaxeFlixel tutorial, and the issues would become immediately obvious if you tried to work with something like this, so I'm going to break down how I approached it.


First things first, if you look at the spritesheet for a while then you might see that it makes sense to classify tiles into one of three categories. 1) Stuff like the ground, floors, etc that characters should be able to walk over unimpeded. 2) Stuff like tree trunks and building walls that characters shouldn't be able to walk on. 3) Stuff like the tops of trees that should be in the foreground -- characters should be able to walk through those tiles unimpeded, but the tiles should be rendered "above" the characters so it looks like the character is underneath the treetops. Also notice that you've dealt with the first two types of things in the tutorial -- there was a "floor" tile and a "wall" tile and you manually set it where you could walk over floor tiles but were blocked by wall tiels -- but this new slick spritesheet I'm working with is huge and I'm not gonna sit there manually typing out which tiles should be walls and which should be floors. Finally, notice that some tiles like treetops have transparent areas within the tile, and are intended to be placed "on top of" other tiles like grass or a road or whatever so you see the appropriate ground behind the treetop, and the tutorial sure didn't teach how to handle that sort of thing.


So here's what you do. In Ogmo3, go to the Layers screen and add some Tile layers until you have (at least) three layers - I'll call mine "floor", "walls", and "foreground" for each of the three categories I talked about above. Depending on your spritesheet, if you have something like tiles of grass and also tiles with flowers with transparent area around the flowers so the grass can show up behind the flower but they should both still be part of the "floor" layer (rendered behind the characters and not blocking their movement), then you might need to get fancy and do things like have a "floorFront" and "floorBack" layer, but I'm gonna just stick with the three types to illustrate the concept in this post. For the spritesheet I'm working with, all sorts of tiles (floor, walls, and foreground) are scattered around the single spritesheet, so for each of the layers I'm going to have its Tileset be that spritesheet (so they're all looking at the same spritesheet). Also, once you've made the layers, you can drag them up or down on the list of layers. You should make foreground be on the top of the list, then walls, then floor so that Ogmo will show things how you'll ultimately want them to look and make stuff easier to see. The Entities layer can be right next to the walls layer, doesn't matter whether it's just above or just below.


Now when you go to edit levels in Ogmo, you'll see that you can pick which layer you're editing on the far left. You should place a tile on every square of the floor layer, the wall layer should have tiles placed only where you want to block character movement -- characters will be blocked at a tile IF AND ONLY IF there is a tile (any tile) placed there in the walls layer (remember you can delete a tile by right-clicking), and the foreground should have tiles placed only if you want that stuff to be rendered in front of the characters. Now save your level and get to your PlayState.hx file.


Here are key parts of my PlayState() class (note that if the code doesn't all fit in your window there's a horizontal scroll bar at the bottom of the code section), and I'll walk you through it

  var player:Player;
  var map:FlxOgmo3Loader;
  var ground:FlxTilemap;
  var walls:FlxTilemap;
  var foreground:FlxTilemap;
  var dudes:FlxTypedGroup<Dude>;

  override public function create():Void {
    /* Get the level's Ogmo3 tilemap
       In Ogmo3 I made a layer called floor with stuff like grass that should just be background,
       a layer called walls with stuff that the player and enemies should collide with and not walk through,
       and a layer called foreground with stuff like treetops that should be rendered in front of the player
       and enemies but shouldn't block them from moving on that tile.
    */
    map = new FlxOgmo3Loader("assets/data/haxeFlixelJam.ogmo", "assets/data/" + Main.currentLevel.x + "-" + Main.currentLevel.y + ".json");
    ground =     map.loadTilemap("assets/images/tileset_town_multi_v002.png", "floor");
    walls =      map.loadTilemap("assets/images/tileset_town_multi_v002.png", "walls");
    foreground = map.loadTilemap("assets/images/tileset_town_multi_v002.png", "foreground");

    // Add the layers to the scene in the correct order (draw ground, then walls on top of ground,
    // then foreground on top of everything else)
    add(ground);
    add(walls);

    // Add the characters to the level
    player = new Player(Main.playerSpawn.x, Main.playerSpawn.y);
    add(player);
    dudes = new FlxTypedGroup<Dude>();
    map.loadEntities(placeEntities, "entities");
    add(dudes);

    // Add the foreground last (so it's rendered on top of the player & enemies)
    add(foreground);

    super.create();
  }

  override public function update(elapsed:Float):Void {
    super.update(elapsed);

    // Make anything in the walls layer block movement
    FlxG.collide(player, walls);
  }

You'll have a map:FlxOgmo3Loader variable to put the Ogmo3 loader into just like in the tutorial. Then we make three calls so the ground, walls, and foreground FlxTilemap variables are set by using map.loadTilemap(), where the second parameter is the name of the layer from Ogmo3 that we're reading in.


Next thing to notice is that it matters what order you list things when you use the add() function. HaxeFlixel will draw things in the order that it sees them, so you want the ground to be drawn first, then walls and characters drawn next so they end up being drawn on top of the ground, then foreground drawn last so it's on top of everything else. If you have more stuff to add like textboxes that should be in front of even the foreground, then of course add them later.


Last thing to notice is that all you've got to do to make walls block movement is to add lines to the update() function specifying what should be blocked by walls. In this case I'm showing it for the player, but you would also want to have similar lines making the walls block enemies and projectiles and anything else that should be blocked. Notice that you don't need to specify which tiles block movement and which ones don't, by default HaxeFlixel will make every non-empty tile in a layer block movement, which is why I stressed to only put tiles in the walls layer in Ogmo3 if you want those tiles to block movement.


And that's it, now you know how to work with a slick spritesheet like this in Ogmo3 and bring it into your HaxeFlixel game!


Tags:

1

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.