My History of Game Programming (Part 8 – javascript/node.js/html/css)

This project is my favorite that I have done and I plan to turn the framework into some form of completed game. For now though it is on ice but the learning experience was priceless and my brain had a blast trying to solve all the complex problems.  One aspect of video games I enjoy the most is multiplayer but trying to make a multiplayer game defiantly makes development more complicated. However after learning how to code while thinking about interactions between client and server, I defiantly am confident in my work for further multiplayer endeavors.

The idea for this project is to make a team based survival 2d game in html, css, javascript and node.js. This game has a randomly generated world with some basic survival elements which includes gathering resources, building, crafting, cooking, killing monsters and other players. The current working features are a randomly generated map(bilinear/bicubic interpolation), resource gathering, hit detection (rectangular), collision detection (spatial hash), sprinting, blood splatter, dropping items from inventory, dropping all items on death, picking up items, killing other players, a continuous world where the edges of the map connect to each other(complicates detection at edges), health, stamina, text chat, sending debug commands via client to server, one monster type, multiplayer, simple AI, a sprite animation system, seamless scrolling viewport and a draw manager to handle layering sprites.

Below is a gif of the game play currently in the game. The purple lines are map edges where the sides of the square map connect back to their opposing side. (The art used for terrain is from this github project https://github.com/idlesync/dimensionMason and the rest is from https://opengameart.org/)

GIF normal gameplay

Below is a gif of just player vs players fights in the game.

GIF player vs player

Server (node.js)

I found node.js easy to work with and very well supported which made my experience using it more enjoyable. I first used express for sending html, css, js and images to the client. I then used sockets(socket.io) for sending packets of data back and forth from the client and server. The below code is the main loop on the server. Each class player, wolf, grounditems and trees run their update and the new information is then sent to each player/client. If nothing new is to be sent then these packets of data are just empty. The current code structure is probably a bit off of how it would look in a more finalized version. For instance I believe a lot of  multiplayer games like this have longer intervals for when the server responds to clients for specific parts of the game. For example Runescape sends data approximately every 0.6 seconds for much of the game. I also imagine this would depend on the maximum player count on the server as the server might not be able to keep up with say sending 60 packets a second.

let delta;
let lastFrameTimeStamp = new Date().getTime();
//global loop
setInterval(() => {
    delta = new Date().getTime() - lastFrameTimeStamp;

	//each class is updated and tracks when items are created, changed or deleted and this is put into packs to send to the clients
    initPack = { players: player.InitNewPlayers(), wolves: wolf.InitNewWolves(), grounditems: groundItem.InitNewItems(), 
                 trees: tree.InitNewTrees() };
    updatePack = { players: player.PlayerUpdate(delta), wolves: wolf.WolfUpdate(delta), grounditems: groundItem.ItemUpdate(delta), 
                   trees: tree.TreeUpdate(delta) };
    removePack = { players: player.RemovePlayersCheck(), wolves: wolf.RemoveWolvesCheck(), grounditems: groundItem.RemoveItemsCheck(),
                   trees: tree.RemoveTreesCheck() };

	//loops through each connected player to send the current data
    let PLAYER_LIST = player.GetPlayerList();
    for (var i in SOCKET_LIST) {
        var socket = SOCKET_LIST[i];
        socket.emit('init', initPack);
        socket.emit('update', updatePack);
        socket.emit('remove', removePack);
        socket.emit('inventory', PLAYER_LIST[i].inventory.getUpdatePack());
    }

	//we set these to nothing because we sent the data already in this loop cycle so we clear it for the next.
    updatePack = { players: [], wolves: [], grounditems: [], trees: [] };
    player.ClearInitNewPlayers();
    wolf.ClearInitNewWolves();
    groundItem.ClearInitNewItems();
    tree.ClearInitNewTrees();
    removePack = { players: [], wolves: [], grounditems: [], trees: [] };

    lastFrameTimeStamp = delta + lastFrameTimeStamp;

}, 1000 / 60); //runs the loop at this time interval.

One thing that was interesting to do in node.js was the use of the module system which I liked since it resembles the c# class structure.  However for the browser to support this I needed to bundle the modules together with browserify, babelfiy and react, which compiled the es6(es2015) code into es5 with module support. Being able to do code similarity with module support on both client and server side made the code more organized and the logical line of thinking was more in line with c# which I am more familiar with. I actually got stuck on implementing this for quite some time because I did not realize you only need to point browserify to the entry javascript file and it makes all the connections by following the module system links between files. The below code is how to get it to work in app.js for node.js.

let fs = require("fs");
let stream = fs.createWriteStream("./dist/bundle.js");
let browserify = require("browserify");
browserify(["./client/js/main.js"])
  .transform("babelify", {presets: ["es2015", "react"]})
  .bundle()
  .pipe(stream);

Collision Detection (Spatial Hash)

One of the most exciting things I learned during this project was the implantation and use of a spatial hash. Other than the world generation I wanted the game not to be grid based which so many games are partly because it makes the math easier. In order to accomplish this I needed to find a way to do collision detection with rectangles of different sizes that are placed anywhere, which is where spatial hash shines. The concept is just a hash table where the keys are an area of the map and each key is to a list or what they call a bucket that contains every object that is within this location. It is very useful since we can have a huge number of objects but all we need to do is check what is close to us by looking at what is in the bucket we are in using our location as the key to the correct bucket.

How it is implemented is first the spatial hash initializes by going through all the objects in the world on server startup and puts them in the hash. During this process each object is assigned the key of which bucket or buckets they are in. When objects move the hash will update if the object moves into a new bucket or the object is destroyed or a new object is created. The extra math I needed to do is when an object is at the world edge then it needs to check the bucket that this edge connects to.

This website has a very nice visualization of what is going on with a spatial hash.

http://zufallsgenerator.github.io/2014/01/26/visually-comparing-algorithms/

Below is a gif showing off the collision detection working on the map edges (purple lines) where the left/right or top/bottom of the square world meet.

GIF collision detection

World Generation

The world  is randomly generated off a seed and chunks are created with a x/y position, width/height and corner values. Then the rest of the chunk is generated using bilinear interpolation. After that bicubic interpolation is used to stitch the chunks together. One nice part of this technique is that the server only needs to send the client the seed for the client to generate the world itself and the client does not need to generate the entire world but just the surrounding chunks at the players location. The below link is to a post that explains this rather well and where I got the knowledge to get this done.

https://codepen.io/clindsey/post/procedural-world-generation

The great part of this I found was that this kind of random generation also makes it so the world edges connect to their opposite sides (left/right and top/bottom of the square). This gives the world a sense of being round although it is not.

The above post is also where I found out about bitmasking which is a really cool way to assign the corresponding tile in the world with the correct area on a spritesheet. This post has a great explanation of how this is done.

http://www.angryfishstudios.com/2011/04/adventures-in-bitmasking/

Client

The client in a multiplayer game is responsible to show the current representation of the server state. There are many things that happen only on client side that the server doesn’t need to waste time handling. For instance drawing of objects which the server does none of except tell the client where some objects are. In the current state of this game the most notable examples of the clients duties that the server does not handle are the blood effects, the terrain and non interactive objects like bushes. Since the world is randomly generated the only thing the server needs to send the client is a seed for the client to be able to draw the grass, sand and water where the server has it.

The below code is the blood module which illustrates how a lot of the game is coded.  In this module there is a list of the blood class objects, the blood class object, and functions to manage and draw the list. Since the project is set up as modules in order to use this elsewhere we would need to declare it like so, const blood = require(‘./blood.js’); and in the main game loop we would then use it like so blood.UpdateBloods(delta);.
 
(Sorry the spacing is off a bit, this widget to display code seems to be counting tab spacing differently)

let BLOOD_LIST = {};
let index = 0;

class Blood {
	constructor(x, y, index) {
		this.x = x;
		this.y = y;
		this.endX;
		this.endY;
		this.spriteSize = 4;
		this.endSpriteSize;
		this.index = index;
		this.existTimeCount = 0;
		this.existTimeMax = 30000;
		this.moveTime = 0;
		this.moveTimeMax = 250;
	}
	
	loadBloodImage() {
		const bloodSprite = document.getElementById("bloodSprite");
		bloodSprite.src = document.getElementById("bloodSprite").src;
		this.bloodSprite = bloodSprite; 
	}
	
	init() {
		this.endX = this.x + GetRandomIntInclusive(-32, 32);
		this.endY = this.y + GetRandomIntInclusive(-16, 28);
		this.endSpriteSize = this.spriteSize + GetRandomIntInclusive(1, 8);
		this.existTimeMax -= GetRandomIntInclusive(0, 25000);
	}
	
	initDouble() {
		this.endX = this.x + GetRandomIntInclusive(-32, 64);
		this.endY = this.y + GetRandomIntInclusive(-32, 60);
		this.endSpriteSize = this.spriteSize + GetRandomIntInclusive(1, 8);
		this.existTimeMax -= GetRandomIntInclusive(0, 25000);
	}

	update(delta) {
		this.moveTime += delta;
		if(this.moveTime <= this.moveTimeMax) {
			let point = this.interpolate({x: this.x, y: this.y}, {x: this.endX, y: this.endY}, 0.05);
			this.x = point.x;
			this.y = point.y;
			this.spriteSize = this.sizeInterpolate(this.spriteSize, this.endSpriteSize, 0.02);
		}
		this.existTimeCount += delta;
		if(this.existTimeCount >= this.existTimeMax) {
			RemoveBlood(this.index);
		}
	}
	
	draw(ctx, x, y) {
		ctx.drawImage(this.bloodSprite, 0, 0, this.bloodSprite.width,  this.bloodSprite.height,
			x - (this.spriteSize / 2), y - (this.spriteSize / 2), this.spriteSize, this.spriteSize);
	}
	
	interpolate(a, b, frac) {
		let nx = Math.round(a.x+(b.x-a.x)*frac);
		let ny = Math.round(a.y+(b.y-a.y)*frac);
		return {x:nx,  y:ny};
	}
	
	sizeInterpolate(a, b, frac) {
		return Math.round((a+(b-a)*frac));
	}
}

let	GetRandomIntInclusive = (min, max) => {
	min = Math.ceil(min);
	max = Math.floor(max);
	return Math.floor(Math.random() * (max - min + 1)) + min;
}

let UpdateBloods = (delta) => {
	for(var i in BLOOD_LIST) {
		BLOOD_LIST[i].update(delta);
	}
}

let DrawAll = (ctx, canvasXworld, canvasYworld, visibleCanvasX, visibleCanvasY, worldPixelWidth, worldPixelHeight) => {
	let drawx = 0, 
		drawy = 0;
	for(var i in BLOOD_LIST) {
		drawx = (BLOOD_LIST[i].x) - canvasXworld - visibleCanvasX;
		drawy = (BLOOD_LIST[i].y) - canvasYworld - visibleCanvasY;
		//if the blood is over the world edge than mod it to draw right
		if (drawx < -1) {
			drawx += worldPixelWidth;
		}
		else if(drawx > worldPixelWidth) {
			drawx -= worldPixelWidth
		}
		if(drawy < -1) {
			drawy += worldPixelHeight;
		}
		else if(drawy > worldPixelHeight)
		{
			drawy -= worldPixelHeight;
		}

		BLOOD_LIST[i].draw(ctx, drawx, drawy);
	}
}

let CreateBlood = (x, y) => {
	for(var i = 1; i < 5; i++){ 
		BLOOD_LIST[index+i] = new Blood(x, y, index+i);
		BLOOD_LIST[index+i].loadBloodImage();
		BLOOD_LIST[index+i].init();
		index++;
	}
	index += 4;
}

let CreateDoubleBlood = (x, y) => {
	for(var i = 1; i < 10; i++){ 
		BLOOD_LIST[index+i] = new Blood(x, y, index+i);
		BLOOD_LIST[index+i].loadBloodImage();
		BLOOD_LIST[index+i].initDouble();
		index++;
	}
	index += 9;
}

let RemoveBlood = (index) => {
	delete BLOOD_LIST[index];
}

let GetBloodList = () => {
	return BLOOD_LIST;
}

module.exports = {UpdateBloods, DrawAll, CreateBlood, CreateDoubleBlood, GetBloodList};

Hit Detection

The hit detection this game uses is rectangular detection. When an enemy or player attacks we use a detection rectangle of the area we want to hit in the direction they are facing and test if another objects collision rectangle intersects with that hit detection rectangle. That part is fairly easy but the problem I ran into is if a hit detection rectangle is on a world edge. Since the world left/right and top/bottom connect to each other, the math gets more complicated. To solve this I ended up splitting the detection rectangle into two rectangles (one on each side of the edge) when the rectangle overlaps a world edge. This splitting up of rectangles also happens with the collision detection. This happens when we test if the hit detection rectangle is less than zero on the x or y axis or greater than the world width or height. If this is true then we split it into two rectangles and test each of them. It works great but requires a bit more math.

Below is a gif depicting the hit detection in action, especially on the map edges (purple lines) where the left/right or top/bottom of the square world meet.

GIF hit detection

 

Draw Manager

One cool feature in the game is hiding behind trees or bushes. In order to do this I had to implement a draw manager that orders the draw calls by the y-position of the objects. When an object is above another it will draw first which will put it behind any object that is drawn later.

Below is a gif how the draw manager module looks in game.

GIF

Below is the code for the draw manager. I have not gone back to optimize this code yet and it is recreating the array of objects that is sorted every time it is called. The code also attempts to draw objects not in the clients viewport/the canvas although. A clear optimization would be to check if the image is inside the viewport before drawing it.
 
(Sorry the spacing is off a bit, this widget to display code seems to be counting tab spacing differently)

const player = require('./playerclient.js'); 
const wolf = require('./wolfclient.js');
const blood = require('./blood');
const groundItem = require('./grounditemclient');
const tree = require('./treeclient');
const shrub = require('./shrub.js');

let ALLDRAW_LIST = [];

let UpdateDrawOrders = () => {
	ALLDRAW_LIST = [];
	SetListOfAllDraws();

	ALLDRAW_LIST = quicksort(ALLDRAW_LIST, 0, ALLDRAW_LIST.length - 1);	
}

let DrawAll = (entityCtx, canvasXworld, canvasYworld, visibleCanvasX, visibleCanvasY, worldPixelWidth, worldPixelHeight) => {
    entityCtx.clearRect(0, 0, entityCtx.canvas.width, entityCtx.canvas.height);
	let drawx = 0, 
	drawy = 0;
	for(let i = 0; i < ALLDRAW_LIST.length; i++) {
		drawx = 0;
		drawy = 0;
		drawx = (ALLDRAW_LIST[i].x) - canvasXworld - visibleCanvasX;
		drawy = (ALLDRAW_LIST[i].y) - canvasYworld - visibleCanvasY;
		//just glancing at this I forget why we check -1000 which seems arbitrary but I marked it as important when I first coded this
		if (drawx < -1000) { 
			drawx += worldPixelWidth;
		}
		if(drawy < -1000) {
			drawy += worldPixelHeight;
		}
		ALLDRAW_LIST[i].draw(entityCtx, drawx, drawy);
	}
}

let SetListOfAllDraws = () => {
	let PLAYER_LIST = player.GetPlayerList();
	for(var i in PLAYER_LIST) {
		ALLDRAW_LIST.push(PLAYER_LIST[i]);
	}
	let WOLF_LIST = wolf.GetWolfList();
	for(var i in WOLF_LIST) {
		ALLDRAW_LIST.push(WOLF_LIST[i]);
	}
	ALLDRAW_LIST = ALLDRAW_LIST.concat(groundItem.GetItemList());  //grounditems class returns the items lists to a single array
	ALLDRAW_LIST = ALLDRAW_LIST.concat(shrub.GetShrubList()); //shrubs are in an array
	let TREE_LIST = tree.GetTreeList();
	for(var i in TREE_LIST) {
		ALLDRAW_LIST.push(TREE_LIST[i]);
	}
	let BLOOD_LIST = blood.GetBloodList();
	for(var i in BLOOD_LIST) {
		ALLDRAW_LIST.push(BLOOD_LIST[i]);
	}
}

let compareNumbers = (a, b) => {
	return ((a.y) - (b.y));
}

// classic implementation 
function quicksort(array, left, right) {
  left = left || 0;
  right = right || array.length - 1;

  var pivot = partitionLomuto(array, left, right); 
  if(left < pivot - 1) {
    quicksort(array, left, pivot - 1);
  }
  if(right > pivot) {
    quicksort(array, pivot, right);
  }
  return array;
}

/*function partitionHoare(array, left, right) {
  var pivot = Math.floor((left + right) / 2 );

  while(left <= right) {
    while(array[left].y < array[pivot].y) {
      left++;
    }
    while(array[right].y > array[pivot].y) {
      right--;
    }
    if(left <= right) {
      swap(array, left, right);
      left++;
      right--;
    }
  }
  return left;
}*/

function partitionLomuto(array, left, right) {
  var pivot = right;
  var i = left;

  for(var j = left; j < right; j++) {
    if((array[j].y+(array[j].spriteSize/2)) <= array[pivot].y+((array[pivot].spriteSize/2))) {
      swap(array, i , j);
      i = i + 1;
    }
  }
  swap(array, i, j);
  return i;
}

// swap function helper
function swap(array, i, j) {
  var temp = array[i];
  array[i] = array[j];
  array[j] = temp;
}

module.exports = {UpdateDrawOrders, DrawAll};

AI + Pathfinding

The game currently uses simple AI with no pathfinding.  The AI just goes at a players direction when he is within a certain range and then attacks when he is close enough. Since I am not using any grid based math, making the AI smarter is difficult. I put in this section because I spent a long while trying to figure out a good way to do efficient accurate dynamically updating pathfinding without using a grid. I did find a solution which involved navigation meshes that efficiently dynamically update when objects move. However I never found any code examples of this that I could use as a base to implement this. I did find a very good paper on it that has very high level psudocode which helps but making something like this from scratch would take me a long time. The papers solution is something some game engines use but I don’t think the code is available to the public, at least I couldn’t find any. Basically their solution was to break up the world into navigation meshes similar to the spacial hash and then dynamically modify the navmeshes as objects move, get removed or get added. If anyone knows a good solution to this problem with some code examples then please let me know on my contact page.