JavaScript Platformer Level Format

Started by
7 comments, last by Wyrframe 8 years, 6 months ago

I am writing a tile based platformer in JavaScript. I was thinking about storing the level data in a JSON file and then using XMLHttpRequest to grab the file and then load it using XMLHttpRequest.onreadystatechange like this:


TileMap.prototype.loadFile = function(pathToFile) {
	'use strict';
	var xhr = new XMLHttpRequest();
	var that = this;
	xhr.onreadystatechange = function() {
		if(xhr.readyState === 4 && xhr.status === 200) {
                // parse json file and load tilemap stuff
                }
};

PlayState.prototype.init = function() {
	this.tileMap = new TileMap();
	this.tileMap.loadFile('level-1.json');
	this.camera = new Camera(0, 0, this.tileMap.mapLayers[0].width * 
							 this.tileMap.mapLayers[0].tileWidth, canvasWidth,
							 this.tileMap.mapLayers[0].height * 
							 this.tileMap.mapLayers[0].tileHeight, canvasHeight);
};

However, I'm not sure if this is the best way of going about this. Would other parts of my game be running while this is loading? Would it be better if I hardcoded the data into my game? What do you guys think?

Advertisement

I don't see a problem with using this to grab the next level, one thing you could do if you don't want your game running in the background is display some kind of loading screen while the file is being retrieved, something like:

1. show a loading screen in the middle of the page or something (as a DOM element like a div or whatever)

2. send off XMLHttpRequest with an onready callback that goes parses the json and loads the level, and then hides the loading screen when it's done

3. resume running the game...

I don't think hardcoding the level in your game is a good idea. It's okay to prototype but having it on the server allows for plenty of interesting mechanics like letting users upload and share their levels, etc...

“If I understand the standard right it is legal and safe to do this but the resulting value could be anything.”

How are you going to store the tilemap, by the way? I remember that when I used to mess with HTML games, I'd store the tilemap as a string (one character per tile), because otherwise the memory usage would blow up like crazy (it may not seem much given the size of RAM... but remember this is running on a browser on a rather loose scripting language and the data may have to be downloaded every time).

Mind you, that's just regarding the tilemap, object placement is a different issue.

EDIT: incidentally: http://sik.titandemo.de/supercat/supercat/level_map_1.js

And here thinking it'd look pretty horrible since this was a pre-HTML5 experiment, but that format actually looks... quite reasonable, all things said. And it's json too, to boot.

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.
Other things to consider are what other data you need may need to download. If you need to download a level and then also tilemap images, object definitions, sounds, etc. for the level then a single request for a JSON object may be a bad choice. Each individual XMLHttpRequest call has a lot of TCP+HTTP overhead so you want to make as few of those as possible.

You'll possibly want to grab datapack files, same as you might use in a native-code game. e.g., grab a ZIP file or the like and then unpack it in JS using something like zip.js.

Sean Middleditch – Game Systems Engineer – Join my team!

Thanks for the responses! I am loading the level like this:


TileMap.prototype.loadFile = function(pathToFile) {
	'use strict';
	var xhr = new XMLHttpRequest();
	var that = this;
	xhr.onreadystatechange = function() {
		if(xhr.readyState === 4 && xhr.status === 200) {
			console.log('reading file in');
			
			var levelData = JSON.parse(xhr.responseText);
			
			var layers = levelData.layers;
			
                        // a test to see if it's loaded
			console.log(layers[0].height);
			
			Game.levelLoaded = true; 
		}
	};
	xhr.open('GET', pathToFile, true);
	xhr.send(null);
};

PlayState.prototype.init = function() {
	this.tileMap = new TileMap();
	this.tileMap.loadFile('platformer/res/maps/level-1.json');
	
	while(!Game.levelLoaded) {
		clear('#ffffff');
	}
	
	console.log('camera is being created');
	
	this.camera = new Camera(0, 0, this.tileMap.mapLayers[0].width * 
							 this.tileMap.mapLayers[0].tileWidth, canvasWidth,
							 this.tileMap.mapLayers[0].height * 
							 this.tileMap.mapLayers[0].tileHeight, canvasHeight);
};

However, whenever I try to load the PlayState which then loads the TileMap, the page freezes, and I get the option to stop the script from Firefox. It doesn't reach the 'console.log(layers[0].height)'. What's wrong here?

For the resources, I was just planning to grab those using something like this:


this.dotImg = new Image(this.dotHeight, this.dotWidth);
this.dotImg.src = '/platformer/res/grey_dot.png';

Is that a good way to load resources?

And thanks again for the help.

You have a fundamental misunderstanding of how JavaScript works.

JavaScript is single-threaded unless you explicitly use something like WebWorkers. XMLHttpRequest is not run in the background; it's run "in between" event callbacks.

Your while loop on levelLoaded never returns, and hence never gives the browser a chance to pump the event queue, so the request never has a chance to complete. The result is a deadlock; three lip can't exit without the request finishing but the request can't finish until the loop ends.

Sean Middleditch – Game Systems Engineer – Join my team!

Sean is bang on there with your while loop, just hit a callback from the level loaded section where you set Game.levelLoaded = true.

i.e.

TileMap.prototype.loadFile = function(pathToFile, onLoad, onError) {

Thanks for the all the replies! I got the level loading working using a callback, but I have another problem. I also set up a camera to help me render what's supposed to be on screen and what's not. However, when I call the callback to set up the camera, I get this error message: Uncaught TypeError: Cannot read property 'setProperties' of undefined. Here's my code:


function Camera(camX, camY, camWidth, camHeight) {
	this.positionX = camX;
	this.positionY = camY;
	this.xBounds = 0;
	this.yBounds = 0;
	this.width = camWidth;
	this.height = camHeight;
}

Camera.prototype.setBounds = function(camXBounds, camYBounds) {
	 this.xBounds = camXBounds;
	 this.yBounds = camYBounds;
};

TileMap.prototype.loadFile = function(pathToFile, callback) {
	'use strict';
	 var xhr = new XMLHttpRequest();
	 var that = this;
	 xhr.onreadystatechange = function() {
		if(xhr.readyState === 4 && xhr.status === 200) {
			 // parse json and load tilemap ...
			 Game.levelLoaded = true;
			
			 callback();
			
		 }
	};
	 xhr.overrideMimeType("application/json");
	 xhr.open('GET', pathToFile, true);
	 xhr.send(null);
};

function PlayState() {
    'use strict';
    GameState.call();
	
	clear('#ffffff');
	this.camera = new Camera(0, 0, canvasWidth, canvasHeight);
	this.camera.setHeight(canvasHeight);
	this.camera.setWidth(canvasWidth);
	console.log(this.camera);
	
	this.tileMap = new TileMap();
	
	this.tileMap = new TileMap();
	this.tileMap.loadFile('/platformer/res/maps/level-1.json', this.continueLoadingLevel);
		
}

PlayState.prototype.continueLoadingLevel = function() {
        // this.camera is undefined
	this.camera.setBounds(this.tileMap.mapLayers[0].width, this.tileMap.mapLayers[0].height);
};

I don't know why it's undefined. It's defined in the PlayState constructor, and continueLoadingLevel() is a part of the PlayState prototype, so this shouldn't be happening. I also created this.camera with the new keyword. Why is it undefined?

It is undefined because if a little-understood quirk of ECMA script; "this" is a function parameter, not an upvalue. The function reference this.continueLoadingLevel, used by the name callback in onreadystatechange, does not have any memory of what object "this" is.

The normal fix is to replace your call to loadFile with...


var thisobj = this;
this.tileMap.loadFile('/platformer/res/maps/level-1.json', function(aData) {
    thisobj.continueLoadingLevel(aData);
});

I added a parameter aData because you should pass the data from the XHR to the callback function, instead of storing it somewhere fixed to be picked up by the callback.

RIP GameDev.net: launched 2 unusably-broken forum engines in as many years, and now has ceased operating as a forum at all, happy to remain naught but an advertising platform with an attached social media presense, headed by a staff who by their own admission have no idea what their userbase wants or expects.Here's to the good times; shame they exist in the past.

This topic is closed to new replies.

Advertisement