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,648 / 1,880
Exp Rank:
38,302
Vote Power:
5.48 votes
Audio Scouts
1
Rank:
Portal Security
Global Rank:
23,497
Blams:
50
Saves:
375
B/P Bonus:
8%
Whistle:
Normal
Medals:
4,619
Supporter:
3y 3m 10d

Using IndexedDB in Phaser3 for a possible Meta-Game project

Posted by 3p0ch - May 4th, 2020


If I were to attempt to run a Meta-Game Collab on NewGrounds where a bunch of games share the same save data and essentially become components of a big meta-game, this would be how I’d recommend setting up Phaser3 to use IndexedDB instead of using localStorage.


If you’re new to Phaser3, visit phaser.io and go to their Learn page under Getting Started and read up. I’m on Windows and I like using XAMPP as the webserver for Phaser, which is also available for Mac and Linux, and you’ll need to install before you do anything else. If you’re using Windows and want to use XAMPP, download XAMPP from apacefriends.org and install it to c:\xampp. Run c:\xampp\xampp-control.exe and tell it to Start the Apache module, and you’ll be good to go when the module light turns green. All of your Phaser stuff should go in c:\xampp\htdocs (sort of the root of what XAMPP considers the website that it’s hosting), and you don’t need the stuff that xampp installs in htdocs so you can just delete the folder or move it to an htdocs_bak folder. Be aware that there can be cache issues, so when you’re making changes to your Phaser game if you’re using Chrome as your browser then you should have the Developer Tools window open (Ctrl-Shift-I) and on the page reload button click, hold, and pick Empty Cache and Hard Reload when you’re making changes. Firefox might generally be better with Phaser and/or XAMPP though.


Then at Phaser.io download it – it’s sufficient to just download the min.js file and put it in a js folder of your game’s directory (like c:\xampp\htdocs\js\phaser.min.js). You can and should then go through the Phaser.io Learn section’s Getting Started including their guide walking you through how to make a game in Phaser3, and remember that to load the phaser.min.js library from your js folder you should have your index.html file’s script line that loads phaser be

<script src="js/phaser.min.js"></script>

instead of anything they might say in the tutorials. Once you’ve made an index.html file in c:\xampp\htdocs you should be able to open a browser and in the URL put localhost/index.html to run the game.


Once you’ve done all that, it’s time to un-learn some of what you just saw. The guide on making your first game did a good job of showing you what the tools are, but they put the entire game’s code in a single .html file with a huge wall of embedded JavaScript, which I consider unadvisable programming practice if you’re going to make an even moderately complex game. Instead, check out @PsychoGoldfish ’s guide on how to really set up a Phaser3 game, and get used to assigning properties to your scenes instead of using variables like in the tutorial. I will assume you’re setting your game up with the structure that he recommends in that guide.


Now’s where the IndexedDB stuff comes in. You can read up on IndexedDB here, here, and some more here, but I’ll break it down for you. First, it helps to see it in action in your browser. If you’re using Chrome, open the Developer Tools window (Ctrl-Shift-I) and go to the Application tab (you might need to click a >> arrow on the top bar to see the Application tab). On the left you should see a list including a Storage section with an IndexedDB item there. If you go to a NewGrounds game’s webpage, then it might be populated with a lot of entries from uploads.ungrounded.net and show you a lot of entries if you click an arrow by IndexedDB to expand it. Each of those entries in IndexedDB is a database, and you might have an /idbfs database from Unity games where the devs lazily used PlayerPrefs to save data. If so and you expand the /idbfs database then you’ll see FILE_DATA under it – that’s an ObjectStore within the idbfs database. The actual values that get saved are within the ObjectStore, and if you click on FILE_DATA then on the right there will probably be a lot of Key / Value pairs if you’ve played a lot of Unity games on NewGrounds. Once you get your game running, this is where you’ll be able to look to verify that stuff is getting saved. Be aware that on Chrome the Key / Value data won’t be updated in real time if you have your game open and the Developer Tools window open, and you may have to click the reload button on the bar just above the Key / Value pairs to make it update.


Let’s get to your code. In the index.html file, I’ve got

<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8">

		<title>My Phaser Game</title>
		<meta name="description" content="This is a Phaser Game!">
		<meta name="author" content="Your Name Here">

		<style>
			body {
				background-color: black;
				margin: 0px;
				padding: 0px;
			}
		</style>

	</head>

	<body>

	<!-- This is where we load our scripts -->
    <script src="js/phaser.min.js"></script>
    <script src="bin/newgroundsio.js" type="text/javascript"></script>
    <script src="js/main.js"></script>

	</body>
</html>

Notice that I’ve installed NewGrounds.io and put it in the bin folder of my build. I highly recommend learning how to implement NewGrounds.io to add medals to your game etc, but if now is not the time then just remove that line. [Side note: if you use NewGrounds.io for Phaser games be aware I noticed that if I do like in the NG.io JS/HTML5 docs and check for nullness of ngio.user then it always gave null even if I was logged in so medals never worked, but if I instead checked ngio.session_id for nullness it worked fine, so try doing that instead.] Otherwise, not much else to see here.


In the js/main.js file I’ve got a bunch of changes to what PsychoGoldfish has

var MainConfig = (()=>{

	// loose reference
	let config = this;

	// path to our scene scripts
	let scene_path = "js/scenes/";

	// class/file names of all our scenes
	config.scene_names = [
		'InitScene',
		'PreloaderScene',
		'GameScene'
	];

	// This will be called when all our scene files have loaded and we are ready to start the Phaser game.
	function startGame() {

		config.game = new Phaser.Game({

			width: 860,
			height: 640,
			type: Phaser.AUTO,	// Render mode, could also be Phaser.CANVAS or Phaser.WEBGL
			scene: config.scene_classes // the code below will set this for us

		});
	}

	// Start NewGrounds.io
	// *************************************************
	// ** Set your game's info for NewGrounds.io here **
	// ** (and un-comment it)                         **
	// *************************************************
	// var ngio = new Newgrounds.io.core("50069:Mc8Q3Qo4", "P0yB53I+JzHKw7PT2YDCkQ==");

	// ****************************************************
	// ** Set the default values for saveData here       **
	// ** (Note that making this "config.saveData" means **
	// ** the rest of your code can access it by calling **
	// ** MainConfig.saveData)                           **
	// ****************************************************
	config.saveData = {
		hp: 1,
		playerName: "yo mama",
	};

	// Open IndexedDB for saving
	// This will be called by the preloader scene
	// *****************************************************************
	// ** Make the indexedDB.open("") target be your game's number on **
	// ** NewGrounds (as a string) or something else appropriate      **
	// *****************************************************************
	config.idb = null;
	config.openIDB = function() {
		return indexedDB.open("testDB");
	}

	// If you ever want to read the saveData while in the middle
	// of the game, use this function but call it from a state that
	// will sort of lock the game until it gets an onsuccess or
	// onerror event like so:
	//
	// var readRequest = readSaveData();
	// readRequest.onsuccess = function(event) {
	//	 if (event.target.result != null) {
	//     MainConfig.saveData = event.target.result;
	//   }
	//   // code to resume the game
	// }
	// readRequest.onerror = function(event) {
	//   // show an error message if you want
	//   // put the code to resume the game here if you want
	// }
	config.readSaveData = function() {
		return config.idb.transaction("thisObjectStore").objectStore("thisObjectStore").get("saveData");
	}

	// Write saveData to the database
	config.writeSaveData = function() {
		if (config.idb != null) {
			config.idb.transaction("thisObjectStore", "readwrite").objectStore("thisObjectStore").put(saveData, "saveData");
		}
	}


	//---------- You shouldn't need to edit anything below here ----------\\

	// this will store references to our scene classes as they are loaded.
	config.scene_classes = [];

	// get the body tag in the HTML document
	let body = document.getElementsByTagName('body')[0];

	// keep track of number of loaded scenes
	let scenes_loaded = 0;

	// Loads a scene file by adding a script tag for it in our page HTML
	function loadScene(scene_name) {

		let script_tag = document.createElement('script');
		script_tag.src = scene_path + scene_name + ".js";

		// wait for the scene file to load, then check if we have loaded them all
		script_tag.addEventListener('load', ()=>{
			scenes_loaded++;
			eval("config.scene_classes.push("+scene_name+")");

			// looks like we can start the game!
			if (scenes_loaded === config.scene_names.length) startGame();
		});

		body.appendChild(script_tag);
	}

	// start loading all of our scene files
	config.scene_names.forEach((scene_name)=> {
		loadScene(scene_name);
	});

	return this;
})();

First is starting the NewGrounds.io core, which you could do here but you don’t need to worry about it for now. Next I’m making a saveData variable, which is an object in MainConfig (accessible by typing MainConfig anywhere else in your Phaser game – as PsychoGoldfish said, this is a good place to put global variables) and giving it default values for HP (MainConfig.saveData.hp) and playerName (MainConfig.saveData.playerName). I would plan to make saveData be a single object that will store all of the data that you plan to save, and the saved data will be put in its object properties. In case you’re wondering, the line at the beginning of

let config = this;

means that the config variable is a reference for “this”, where “this” is MainConfig… I wish I knew about that trick when I was doing the Phaser game jam because I don't have a JavaScript background and I often found myself banging my head against the wall because the thing that "this" refers to changed within functions, methods, event listeners (I think?), and maybe other stuff, although my memory of details is hazy.


Next is the code to open a database in IndexedDB. The MainConfig.idb property will point to the IndexedDB database once it’s opened, and what the config.openIDB function does it a little bit subtle. Actions with IndexedDB don’t work like normal functions. Instead, JavaScript sends a Request to IndexedDB to do something and waits for an Event letting it know that the Request was processed. Here, the line in this function

return indexedDB.open(“testDB”);

will make a request to have IndexedDB open the database named testDB (kind of like that /idbfs database in IndexedDB that we were looking at earlier from Unity games). What it returns will be a Request object that you can attach listeners to. Jumping a little ahead, let’s look at how I modified the create() of js/scenes/PreloaderScene.js where the function is called to open the database.

	create() {
		var thisScene = this;

		let play_btn = this.add.image(this.interact_point.x, this.interact_point.y, 'preloader_button');
		// make the button interactive
		play_btn.setInteractive();
		// add some alpha for the default state
		play_btn.alpha = 0.9;

		// remove alpha on hover
		play_btn.on('pointerover', ()=>{
			play_btn.alpha = 1;
		});

		// add alpha on roll out
		play_btn.on('pointerout', ()=>{
			play_btn.alpha = 0.9;
		});

		// start the GameScene when the button is clicked. Bonus - this will enable audo playback as well!
		play_btn.on('pointerup', ()=>{
			this.scene.start('GameScene');
        });
    
        // Make the Play button inactive until IndexedDB data is loaded
  		play_btn.setActive(false);
  		
  		// First open the IndexedDB database
  		thisScene.idbRequest = MainConfig.openIDB();
  
  		// If IDB can't be opened, throw an error to the browser's console
  		thisScene.idbRequest.onerror = function() {
  			console.log("Error opening IDB");
  		}
  
  		// The "onupgradeneeded" case happens if this is the first time
  		// this game is saving to IndexedDB, so make an object store
  		thisScene.idbRequest.onupgradeneeded = function(event) {
  			thisScene.idbRequest.result.createObjectStore("thisObjectStore");
  		};
  
  		thisScene.idbRequest.onsuccess = function(event) {
  			// Make MainConfig.idb point to the indexedDB object
  			MainConfig.idb = thisScene.idbRequest.result;
  
  			// Now that the IndexedDB database is open, read saveData
  			thisScene.readRequest = MainConfig.readSaveData();
  			thisScene.readRequest.onsuccess = function(event) {
  				// event.target.result will be null if the game has never been played before
  				// and will have save data if it has been played before
  				// So this checks for nullness -- if it's not null then it puts the result
  				// from reading IndexedDB into MainConfig.saveData
  				// And if it is null, then MainConfig.saveData is still the default value,
  				// so go ahead and write that to IndexedDB
  				if (event.target.result != null) {
  					MainConfig.saveData = event.target.result;
  				} else {
  					MainConfig.writeSaveData();
  				}
  				play_btn.setActive(true);
  			}
  			thisScene.readRequest.onerror = function(event) {
  				play_btn.setActive(true);
  			}
  		}
  	}
}

The IndexedDB handling starts at the

thisScene.idbRequest = MainConfig.openIDB();

line, which uses that function so the Request to open the IndexedDB database is stored in thisScene.idbRequest. Just after that are listener statements telling it what to do when the Request is finished. If there’s an error, it logs an error message to the console. Next it has an “onupgradeneeded” handler. If IndexedDB doesn’t already have a testDB database (or if it has one with an obsolete version – something I’m not going to go into detail about right now) then it creates it and should add ObjectStores to it (remember that Unity games had the /idbfs database and the FILE_DATA ObjectStore under it – same thing here). So the onupgradeneeded handler creates a creatively named “thisObjectStore” ObjectStore. The next handler is onsuccess, which fires if the database is successfully opened, and if this is the first time you run the program then onupgradeneeded happens and then onsuccess happens. In onsuccess it sets the MainConfig.idb property to point to the result of the Request to open the database, so now MainConfig.idb is a reference to the thisDB database. We’ll get to the rest of the code after that in a moment.


Now going back to the main.js file, there are two more functions that I’ve added to PsychoGoldfish’s code. The first reads the saved data and the next writes it. Note all the comments above readSaveData. Just like opening the database was a matter of sending a Request and waiting for an event to let you know it’s done, the same is true of reading save data. You probably don’t want to tell your game to read save data and then have the player continue playing for a while before the save data actually gets loaded, so you probably want to have your game pause or something while it’s reading save data (maybe with a popup window that pauses the game and lets the player click a Load or Cancel button, and if the player clicks Load then the game doesn’t unpause and close the popup window until loading is finished). But when it comes to writing the save data I don’t sweat it so much – if your game sends a command to save the data then it’s probably fine to let the player keep playing and the save data can finish writing whenever it finishes writing with no impact on anything.


Going back to the PreloaderScene, the way I handled it is that I deactivated the play button while we’re opening IndexedDB and reading the save data. That’s the

play_btn.setActive(false);

command just before the code to open the database, and we’re not going to activate it again until we’re done. Picking up where we left off after opening the database, next we send a request to read the saveData which should be in testDB under thisObjectStore if it was saved from a previous play. Note that this is all embedded within the onsuccess event from opening the database, which ensures we won’t try to read from the database until after it’s open. Then we have event handlers for the readRequest. If it read a non-null value (it would be null if this were the first time we ran the game and there was no saveData stored yet) then it sets MainConfig.saveData to be the result from the call. If it read null, then remember that we set default values for saveData in main.js, so we’ll write those default values to IndexedDB for now. Either way, we’ll go ahead and activate the Play button so the player can get on with the game. And if there was an error reading saveData then I’ll still let the player play because I’m such a nice guy.


I didn’t make any real changes to InitScene.js and it still has this.load.image commands for the background and button that PsychoGoldfish had if you finished his post, so I’ll skip showing it. And we already went through all the significant changes in PreloaderScene.js. So to wrap up, we’ll look at GameScene.js.

class GameScene extends Phaser.Scene {
  
  constructor () {
  // Sets the string name we can use to access this scene from other scenes
	super({key:'GameScene'});
  }

  init() {
  }

  create() {
    var thisScene = this;
    this.cameras.main.setBackgroundColor(0x00AAFF);

    this.hpText = this.add.text(5, 5, "HP: " + MainConfig.saveData.hp + " Click this to increment");
    this.hpText.setInteractive();
    this.hpText.on("pointerup", function() {
      MainConfig.saveData.hp++;
      MainConfig.writeSaveData();
      this.text = "HP: " + MainConfig.saveData.hp + " Click this to increment";
    });

    this.playerNameText = this.add.text(5, 55, "Player Name: " + MainConfig.saveData.playerName);

    this.readButton = this.add.text(5, 105, "Click here to read saveData");
    this.readButton.setInteractive();
    this.readButton.on("pointerup", function() {
      thisScene.readRequest = MainConfig.readSaveData();
      thisScene.readRequest.onsuccess = function(event) {
        if (event.target.result != null) {
          MainConfig.saveData = event.target.result;
        }
        // Ordinarily you would have the game paused while
        // reading saveData (like have it be in a state that
        // doesn't process any actions) and resume it here
        // after the read returns a result and saveData
        // (any any relevant game variables) get updated
        thisScene.hpText.text = "HP: " + MainConfig.saveData.hp + " Click this to increment";
        thisScene.playerNameText.text = "Player Name: " + MainConfig.saveData.playerName;
      }
      thisScene.readRequest.onerror = function(event) {
        playerNameText = "There was an error reading the save data"
      }
    });
  }
  
  update() {
    // This is your main game loop. Code in here runs on every frame tick.
  }
}

I have

var thisScene = this;

to use that handy trick. Then I add the hpText object which will show the HP from MainConfig.saveData.hp (you have my permission to go ahead and use variables directly from MainConfig.saveData without pulling the values into variables / class properties within your game scene classes if you’re careful about it) and be clickable so you can click on the text to increase the HP by one per click, and it will write the updated HP to IndexedDB after each click. Remember that just because you change a value in saveData doesn’t mean it’s immediately written to IndexedDB and you’ll have to call MainConfig.writeSaveData() at appropriate times. It will also show a text box with MainConfig.saveData.playerName but it won’t be editable (for now). Last it will have a button to read the data from IndexedDB – if you click it, it will send a read request and set MainConfig.saveData to be what was read from IndexedDB, and then update the text boxes. For now it won’t really accomplish much because the values in IndexedDB will always be equal to the game’s current values, but it will become neat in not too long.


Now if you run your index.html file via XAMPP (with the Apache server on and by typing localhost/index.html in your web browser) the game should work. When it’s running you should be able to open the Developer Tools window and look in the Application tab for the IndexedDB entry named testDB that has an ObjectStore named thisObjectStore, and if you click the thisObjectStore then you should see the key / value pair of saveData with a structure that can be expanded to show you the hp and playerName values. And you should be able to click the HP text box to increment the HP by one per click and the value in IndexedDB should be updated – but remember that at least with Chrome you’ll need to click the Refresh button just above the key / value pairs in the Developer Tools window to make it update.


That’s fine and everything, but not too mind blowing yet. I went ahead and uploaded that project here. The really cool part is the other project that I uploaded here. It looks pretty similar, but clicking its HP button will decrement the HP and clicking the Player Name will change the name.


Now try opening both games from the same browser (they can be open simultaneously in different tabs of the same browser), clicking around with them, and try out that button to read saveData from IndexedDB in one game right after you’ve changed the values in the other game. Neat, huh? The details of exactly how I made that second project are left as an exercise for the reader, but it’s not much of a change from what I just showed you.


A key step in making a Meta-Game collab possible is in place. As an aside, this could be done with localStorage without having to go through IndexedDB, but localStorage is gross for reasons I discussed in previous posts. IndexedDB doesn’t completely eliminate all of the issues of potentially getting save data messed up but it makes it far less likely.


Tags:

Comments

Comments ain't a thing here.