GameLevels Lesson - Local Storage
Local Storage / Level Selection
Selection and Local Storage
Background
During our lesson we will explain the basics about window.localStorage property and Model-View-Controllers. At the end of our lesson you should be able to:
- Navigate between levels
- Save your current level
- Learn how to change your game speed
- Save your current game speed
a. The Local Storage Property
“The localStorage read-only property of the window interface allows you to access a Storage object for the Document’s origin; the stored data is saved across browser sessions”(MDN Web Docs).
In simpler terms it is a way to store data when you exit a web page. It works in as a key-value database on your web page for storing data
- data values are saved to an identifiyer called a key
example (the name variable is not a key):
# | name | age |
---|---|---|
b. Model-View-Controllers (MVC)
“MVC is short for Model, View, and Controller. MVC is a popular way of organizing your code. The big idea behind MVC is that each section of your code has a purpose, and those purposes are different. Some of your code holds the data of your app, some of your code makes your app look nice, and some of your code controls how your app functions”(codecademy).
In simpler terms, it is a code organization method for user interaction with some set of data. We roughly created code around this concept.
- the model is window.localstorage (LocalStorage.js)
- the view is the ui (sidebar with options)
- the controller is our controller.js class
Step 1: The Local Storage class
in LocalStorage.js
export class LocalStorage{
constructor(keys){
this.keys = keys;
console.log("browser local storage available: "+String(this.storageAvailable));
}
get storageAvailable(){ //checks if browser is able to use local storage
let type = "localStorage";
let storage;
try {
storage = window[type];
const x = "__storage_test__";
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch (e) {
return (
e instanceof DOMException &&
// everything except Firefox
(e.code === 22 ||
// Firefox
e.code === 1014 ||
// test name field too, because code might not be present
// everything except Firefox
e.name === "QuotaExceededError" ||
// Firefox
e.name === "NS_ERROR_DOM_QUOTA_REACHED") &&
// acknowledge QuotaExceededError only if there's something already stored
storage &&
storage.length !== 0
);
}
}
save(key){ //save a particular key
if(!this.storageAvailable){return}; //check if local storage is possible
window.localStorage.setItem(key,this[key]);
}
load(key){//load a particular key
if(!this.storageAvailable){return}; //check if local storage is possible
this[key] = window.localStorage.getItem(key);
}
saveAll(){ //saves data for all keys in this.keys
if(!this.storageAvailable){return}; //check if local storage is possible
Object.keys(this.keys).forEach(key => {
window.localStorage.setItem(key,this[key]);
});
}
loadAll(){//loads data from all keys in this.keys
if(!this.storageAvailable){return}; //check if local storage is possible
Object.keys(this.keys).forEach(key => {
this[key] = window.localStorage.getItem(key);
});
}
}
export default LocalStorage;
This is the code for the LocalStorage class. It is comprised of 4 major functions, and 1 getter.
- getters
- the getter: storageAvailable, this getter returns true if your browser is capable of using Local Storage, otherwise it returns an error message.
- functions
- the function save(key), this function saves the value this.key to the local storage with the respective key
- the function load(key), this function loads the value from the local storage with the respective key to this.key
- the function saveAll(), this function saves all of the values of this.key from the class’ this.keys to local storage
- the function loadAll(), this function load all of the values from the local storage from the class’ this.keys to their respective this.key
Step 2: Creating the Controller Class
in Controller.js First, import LocalStorage, GameEnv, and GameControl, and export the Controller class
import LocalStorage from "./LocalStorage.js";
import GameEnv from "./GameEnv.js";
import GameControl from "./GameControl.js";
export class Controller extends LocalStorage{
constructor(){
var keys = {currentLevel:"currentLevel",gameSpeed:"gameSpeed"}; //default keys for localStorage
super(keys); //creates this.keys
}
Run the initialization after a different time than the constructor. This is used to update the current level and start the eventListeners.
//separated from constructor so that class can be created before levels are added
initialize(){
this.loadAll(); // load data
if(this[this.keys.currentLevel]){ //update to active level
GameControl.transitionToLevel(GameEnv.levels[Number(this[this.keys.currentLevel])]);
}
else{ //if not currentLevel then set this.currentLevel to 0 (default)
this[this.keys.currentLevel] = 0;
}
if(this[this.keys.gameSpeed]){ //update to custom gameSpeed
GameEnv.gameSpeed = Number(this[this.keys.gameSpeed]);
}
else{ //if not gameSpeedthen set this.gameSpeed to GameEnv.gameSpeed (default)
this[this.keys.gameSpeed] = GameEnv.gameSpeed;
}
window.addEventListener("resize",()=>{ //updates this.currentLevel when the level changes
this[this.keys.currentLevel] = GameEnv.levels.indexOf(GameEnv.currentLevel);
this.save(this.keys.currentLevel); //save to local storage
});
window.addEventListener("speed",(e)=>{ //updates this.gameSpeed when a speed event is fired
this[this.keys.gameSpeed] = e.detail.speed();
GameEnv.gameSpeed = this[this.keys.gameSpeed]; //reload or change levels to see effect
this.save(this.keys.gameSpeed); //save to local storage
})
}
The code below is a getter and creates the table. The table is used to include level tags and the level number. This will show up once we finish Step 3.
get levelTable(){
var t = document.createElement("table");
//create header
var header = document.createElement("tr");
var th1 = document.createElement("th");
th1.innerText = "#";
header.append(th1);
var th2 = document.createElement("th");
th2.innerText = "Level Tag";
header.append(th2);
t.append(header);
//create other rows
for(let i = 0;i < GameEnv.levels.length;i++){
var row = document.createElement("tr");
var td1 = document.createElement("td");
td1.innerText = String(i);
row.append(td1);
var td2 = document.createElement("td");
td2.innerText = GameEnv.levels[i].tag;
td2.addEventListener("click",()=>{ // when player clicks on level name
GameControl.transitionToLevel(GameEnv.levels[i]); //transition to that level
})
row.append(td2);
t.append(row);
}
return t; //returns <table> element
}
The code is used to define speedDiv. This helps change the gameSpeed in the game. This will be essential later for the homework.
get speedDiv(){
var div = document.createElement("div"); //container
var a = document.createElement("a"); //create text
a.innerText = "Game Speed";
div.append(a);
var input1 = document.createElement("input"); //create inputfeild
input1.type = "number"; //type number (1234...)
const event = new CustomEvent("speed", { detail: {speed:()=>input1.value} });
input1.addEventListener("input",()=>{ //after input feild is edited
window.dispatchEvent(event); //dispatch event to update game speed
})
div.append(input1);
return div; //returns <div> element
}
}
export default Controller;
Step 3 the game.md file changes
In style at the top:
#gameBegin, #controls, #gameOver, #settings {
position: relative;
z-index: 2; /*Ensure the controls are on top*/
}
for adding an extra button to the game settings
.sidenav {
position: fixed;
height: 100%; /* 100% Full-height */
width: 0px; /* 0 width - change this with JavaScript */
z-index: 3; /* Stay on top */
top: 0; /* Stay at the top */
left: 0;
overflow-x: hidden; /* Disable horizontal scroll */
padding-top: 60px; /* Place content 60px from the top */
transition: 0.5s; /* 0.5 second transition effect to slide in the sidenav */
background-color: black;
}
This is for adding a side navigation bar. For more information see w3 Schools page
in the html
<div id="mySidebar" class="sidenav">
<a href="javascript:void(0)" id="toggleSettingsBar1" class="closebtn">×</a>
</div>
this will be the side bar
<div id="canvasContainer">
<div id="gameBegin" hidden>
<button id="startGame">Start Game</button>
</div>
<div id="controls"> <!-- Controls -->
<!-- Background controls -->
<button id="toggleCanvasEffect">Invert</button>
</div>
<div id="settings"> <!-- Controls -->
<!-- Background controls -->
<button id="toggleSettingsBar">Settings</button>
</div>
<div id="gameOver" hidden>
<button id="restartGame">Restart</button>
</div>
</div>
we are adding the settings button into this division
In the script
import Controller from '/csse-er/assets/js/platformer/Controller.js';
place with the other imports
var myController = new Controller();
myController.initialize();
the object creation can be placed whereever, but initialization needs to be placed after the game loop has started GameControl.gameLoop()
var table = myController.levelTable;
document.getElementById("mySidebar").append(table);
var toggle = false;
function toggleWidth(){
toggle = !toggle;
document.getElementById("mySidebar").style.width = toggle?"250px":"0px";
}
document.getElementById("toggleSettingsBar").addEventListener("click",toggleWidth);
document.getElementById("toggleSettingsBar1").addEventListener("click",toggleWidth);
place this after initialization
Homework:
- Implement the LocalStorage code in the game (Is the code fully implemented? Does the game save your data after closing the tab/window)
- Add an extra setting (examples: gameSpeed, keybinds for movement, etc)
Challenge:
- Implement storage and controls for gravity (Hint: Use the GameEnv.gravity variable from GameEnv)