Phaser 3 and Server Side Pathfinding

I spent some time working on server side pathfinding with Phaser 3. I’m not an expert, but this is how I did it. I wanted to share how I did it because I spent a long time on it and want to save others some time and energy. Hopefully it helps you with your project. I would love to hear any suggestions on how to improve the code. E-mail me at matt@drennansoftware.com or comment below!

In Dead Haven, I wanted the zombies in the game to detect nearby players and then start moving towards the players location. I needed this all to be server side.

Firstly, I found a pathfinding library which works with node.js. You can find it here. You will need to download and set it up on your node.js server.

Next I had to add some code to my server JavaScript file. Here is the pathfinding code for the server side:

function ServerGameLoop()
{
	// Zombies
	
	for(var i = 0; i < zombie_array.length; i++)
	{
		// Fixes a bug where it would cause game to crash and repeat inside the dist if check
		if(zombie_array[i] != undefined)
		{
			var zombie = zombie_array[i];
			var followPlayerX = 0;
			var followPlayerY = 0;
			var getPlayerX = 0;
			var getPlayerY = 0;
			var playerDetected = false;
			var myMap = zombie_array[i].myMap;
			var myMapCoords = zombie_array[i].myMapCoords;

			// Check if this bullet is close enough to hit any player 
			for(var playerId in players)
			{
				// And your own bullet shouldn't kill you
				var dx = players[playerId].x - zombie_array[i].x; 
				var dy = players[playerId].y - zombie_array[i].y;
				var dist = Math.sqrt(dx * dx + dy * dy);
				
				// Determine distance
				if(dist < 160000 && zombie_array[i].path.length == 0)
				{
					followPlayerX = players[playerId].x;
					followPlayerY = players[playerId].y;

					var zombieCoords = [(zombie_array[i].x - 320), (zombie_array[i].x + 320), (zombie_array[i].y - 320), (zombie_array[i].y + 320)];
					getPlayerX = Math.floor((followPlayerX - zombieCoords[0])/32);
					getPlayerY = Math.floor((followPlayerY - zombieCoords[2])/32);

					if(getPlayerX < 0 || getPlayerX > 20 || getPlayerY < 0 || getPlayerY > 20)
					{
						zombie_array[i].isFollowingPlayer = false;
					}
					else
					{
						zombie_array[i].isFollowingPlayer = players[playerId];
						playerDetected = true;
					}
				}
			}

			// Is following a player, lets do something about that
			if(zombie_array[i].isFollowingPlayer != false)
			{
				// No path defined, so lets figure this out
				if(zombie_array[i].path.length == 0)
				{	
					// For looping though tiles around zombie
					var loopC = 0;

					// Clear variables
					zombie_array[i].myMap = [];
					zombie_array[i].myMapCoords = [];

					// Start loop through tiles around zombie and put them in an array
					for(y = -10; y < 10; y++)
					{
						zombie_array[i].myMap[loopC] = new Array();
						
						for(c = Math.floor((zombie_array[i].x/32) * 32) - 320; c < Math.floor((zombie_array[i].x/32) * 32) + 320; c += 32)
						{
							var yOffset = Math.floor(zombie_array[i].y/32) + (y);
							var getValue = (yOffset * 100) + ((Math.floor(c/32)));
							if(map[getValue] == undefined)
							{
								zombie_array[i].myMap[loopC].push(1);
							}
							else
							{
								zombie_array[i].myMap[loopC].push(map[getValue]);
							}
							// Fixes a bug by doing it this way - calculate the y value
							var calcBefore = parseInt(zombie_array[i].y) + (y * 32);
							zombie_array[i].myMapCoords.push(c + "," + calcBefore);
						}
						loopC++;
					}
								
					// Let's find a path for the zombie
		    		var grid = new PF.Grid(20, 20, zombie_array[i].myMap);
					var finder = new PF.AStarFinder({
    					allowDiagonal: true
					});

					// Just in case there is an exception
					try
					{
						if(playerDetected)
						{
							zombie_array[i].path = finder.findPath(10, 10, parseInt(getPlayerX), parseInt(getPlayerY), grid);
						}
					}
					catch(error)
					{
						// This failed, put some code here later.
					}
				}

				if(zombie_array[i].path.length > 0)
				{
					// Zombie path NPM cords
					var zArray = zombie_array[i].path[zombie_array[i].pathI];

					try
					{
						// Calculate which tile to travel to based on zombie path position
						var calcBeforeCoords = (zArray[0]) + ((zArray[1]) * 20);

						// Translate
						var getCoords = myMapCoords[calcBeforeCoords].split(',');
						var gX = parseInt(getCoords[0]);
						var gY = parseInt(getCoords[1]);

						// Prevent zombie from going out of bounds
						if(getCoords[0] < 0)
						{
							gX = 32;
						}

						if(getCoords[1] < 0)
						{
							gY = 32;
						}
						// End of Prevent zombie from going out of bounds

						// Let's check and see if the zombie moved
						var moved = 0;

						// Move zombie
						if(zombie_array[i].x < gX)
						{
							zombie_array[i].x++;
							moved++;
						}
						if(zombie_array[i].x > gX)
						{
							zombie_array[i].x--;
							moved++;
						}
						if(zombie_array[i].y > gY)
						{
							zombie_array[i].y--;
							moved++;
						}
						if(zombie_array[i].y < gY)
						{
							zombie_array[i].y++;
							moved++;
						}

						// Did not move, let's go to the next index
						if(moved == 0)
						{
							zombie_array[i].pathI++;
						}
						
						if(zombie_array[i].pathI >= zombie_array[i].path.length)
						{
							zombie_array[i].path = [];
							zombie_array[i].isFollowingPlayer = false;
							zombie_array[i].pathI = 0;
						}
					}
					catch(error)
					{
						// Found player
						//zombie_array[i].path = [];
						//zombie_array[i].isFollowingPlayer = false;
						//zombie_array[i].pathI = 0;
					}
				}
			}
		}
	}
	
	// Tell everyone where all the zombies are by sending the whole array
	io.emit("zombie", zombie_array);
}

The code above detects if a player is nearby. If a player is nearby and the zombie is not following a player, it gets the player’s coordinate on the pathfinding grid system. It also calculates the coordinates of all the tiles on the pathfinding grid system (myMap – an array of all the tiles in the grid system) to the Phaser 3 grid system, and put it into the array myMapCoords. The pathfinding library then gets a path to the player, but the pathfinding library gets a path based on the pathfinding library grid system. We then convert the pathfinding library path array to the Phaser 3 coordinates. The server then send data to the zombie to move towards those Phaser 3 coordinates.

Next here is the code in my client side JavaScript file which utilizes Phaser 3:

	this.socket.on('zombie', function(server_zombie_array)
	{
		for(var i = 0; i < server_zombie_array.length; i++)
		{
			if(zombie_array[i] == undefined)
			{
				zombie_array[i] = self.add.sprite(server_zombie_array[i].x, server_zombie_array[i].y, 'zombie').setOrigin(0, 0);
				zombie_array[i].spriteid = server_zombie_array[i].spriteid;
			}
			else
			{
				// Otherwise, just update it! 
				zombie_array[i].x = server_zombie_array[i].x; 
				zombie_array[i].y = server_zombie_array[i].y;
			}
		}
	});

The code above creates a sprite if one does not exist or updates the sprites location if it exists.

I did this in a rush, and I know I didn’t post a ton of info, but I will update this later. Please feel free to send me questions.

This entry was posted in Dev Log and tagged , , , , , . Bookmark the permalink.

Leave a Reply