HTML5 Game Development Advancement 5: Creating a Real-time Strategic Game World

Posted by AlexMason on Mon, 20 May 2019 05:35:38 +0200

Define your own game world, architecture, units, and a story line, and create a moving single-player campaign. Then we will use HTML5 WebSocket to enable the game to support real-time multi-player warfare.

Most of the material for this game is provided by Daniel Cook (http://www.lostgarden.com).

When developing the game, we will try to keep the code as general and customizable as possible, so that you can reuse the code to realize your ideas.

5.1 Basic HTML Layout

First, define several layers:

  • Start screen and main menu: The game is displayed at the beginning, allowing players to choose single-player battle mode or multi-player battle mode.
  • Load screen: display when the game loads resources
  • Task Screen: Display before the start of the task, with a brief introduction to the task
  • Game interface: The main screen of the game, including map area and game control panel.

5.2 Create Startup Screen and Main Menu

       index.html

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <title>Last Colony</title>
    <script src="js/common.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/jquery.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/game.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/mouse.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/singleplayer.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/maps.js" type="text/javascript" charset="utf-8"></script>
    <link rel="stylesheet" href="style.css" type="text/css" media="screen" charset="utf-8">
  </head>
  <body>
    <div id="gamecontainer">
      <div id="gamestartscreen" class="gamelayer">
        <span id="singleplayer" onclick="singleplayer.start();">Campaign</span><br>
        <span id="multiplayer" onclick="multiplayer.start();">Multiplayer</span><br>
      </div>
      <div id="missionscreen" class="gamelayer">
        <input type="button" id="entermission" onclick="singleplayer.play();">
        <input type="button" id="exitmission" onclick="singleplayer.exit();">
        <div id="missionbriefing"></div>
      </div>
      <div id="gameinterfacescreen" class="gamelayer">
        <div id="gamemessages"></div>
        <div id="callerpicture"></div>
        <div id="cash"></div>
        <div id="sidebarbuttons"></div>
        <canvas id="gamebackgroundcanvas" height="400" width="480"></canvas>
        <canvas id="gameforegroundcanvas" height="400" width="480"></canvas>
      </div>
      <div id="loadingscreen" class="gamelayer">
        <div id="loadingmessage"></div>
      </div>
    </div>
  </body>
</html>

       style.css

/* Initial style sheets for game containers and layers */
#gamecontainer {
	width: 640px;
	height: 480px;
	background: url(images/splashscreen.png);
	border: 1px solid black;
}


.gamelayer {
	width: 640px;
	height: 480px;
	position: absolute;
	display: none;
}


/* Start screen and main menu */
#gamestartscreen {
	padding-top: 320px;
	text-align: left;
	padding-left: 50px;
	width: 590px;
	height: 160px;
}


#gamestartscreen span {
	margin: 20px;
	font-family: 'Courier New', Courier, monospace;
	font-size: 48px;
	cursor: pointer;
	color: white;
	text-shadow: -2px 0 purple, 0 2px purple, 2px 0 purple, 0 -2px purple;
}


#gamestartscreen span:hover {
	color: yellow;
}


/* Loading screen */
#loadingscreen {
	background: rgba(100, 100, 100, 0.7);
	z-index: 10;
}


#loadingmessage {
	margin-top : 400px;
	text-align: center;
	height: 48px;
	color: white;
	background: url(images/loader.gif) no-repeat center;
	font: 12px Arial;
}
 
/* CSS Style of Task Screen */
#missionscreen {
	background: url(images/missionscreen.png) no-repeat;
}
#missionscreen #entermission {
	position: absolute;
	top: 79px;
	left: 6px;
	width: 246px;
	height: 68px;
	border-width: 0px;
	background-image: url(images/buttons.png);
	background-position: 0px 0px;
}
#missionscreen #entermission:disabled, #missionscreen #entermission:active {
	background-image: url(images/buttons.png);
	background-position: -251px 0px;
}
#missionscreen #exitmission {
	position: absolute;
	top: 79px;
	left: 380px;
	width: 98px;
	height: 68px;
	border-width: 0px;
	background-image: url(images/buttons.png);
	background-position: 0px -76px;
}
#missionscreen #exitmission:disabled, #missionscreen #exitmission:active {
	background-image: url(images/buttons.png);
	background-position: -103px -76px;
}
#missionscreen #missionbriefing {
	position: absolute;
	padding: 10px;
	top: 160px;
	left: 20px;
	width: 410px;
	height: 300px;
	color: rgb(130, 150, 162);
	font-size: 13px;
	font-family: 'Courier New', Courier, monospace;
}
/* Game interface */
#gameinterfacescreen {
	background: url(images/maininterface.png) no-repeat;
}
#gameinterfacescreen #gamemessages {
	position: absolute;
	padding-left: 10px;
	top: 5px;
	left: 5px;
	width: 450px;
	height: 60px;
	color: rgb(130, 150, 162);
	overflow: hidden;
	font-size: 13px;
	font-family: 'Courier New', Courier, monospace;	
}
#gameinterfacescreen #gamemessages span {
	color: white;
}
#gameinterfacescreen #callerpicture {
	position: absolute;
	top: 154px;
	left: 498px;
	width: 126px;
	height: 88px;
	overflow: none;
}
#gameinterfacescreen #cash {
	position: absolute;
	top: 256px;
	left: 498px;
	width: 120px;
	height: 22px;
	color: rgb(130, 150, 162);
	overflow: hidden;
	font-size: 13px;
	font-family: 'Courier New', Courier, monospace;	
	text-align: right;
}
#gameinterfacescreen canvas {
	position: absolute;
	top: 79px;
	left: 0px;
}
#gameinterfacescreen #foregroundcanvas {
	z-index: 1;
}
#gameinterfacescreen #backgroundcanvas {
	z-index: 0;
}

       common.js

/* Setting up request Animation Frame and image loader */
(function () {
   var lastTime = 0;
   var vendors = ['ms', ';', 'webkit', 'o'];
   for (var x=0; x<vendors.length && !window.requestAnimationFrame; ++x){
   	   window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
   	   window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] 
   	     || window[vendors[x] + 'CancelRequestAnimationFrame'];
   }
   if (!window.requestAnimationFrame) {
   	   window.requestAnimationFrame = function(callback, element) {
   	   	  var currTime = new Date().getTime();
   	   	  var timeToCall = Math.max(0, 16 - (currTime - lastTime));
   	   	  var id = window.setTimeout(function() { callback(currTime + timeToCall);}, timeToCall);
   	   	  lastTime = currTime + timeToCall;
   	   	  return id;
   	   };
   }
   if (!window.cancelAnimationFrame) {
   	   window.cancelAnimationFrame = function(id) {
   	   	  clearTimeout(id);
   	   };
   }
}());

var loader = {
	loaded: true,
	loadedCount: 0,   //Number of resources currently loaded
	totalCount: 0,    //Total number of resources to load
	init: function() {
		//Check voice format support
		var mp3Support, oggSupport;
		var audio = document.createElement('audio');
		if (audio.canPlayType) {
			// Currently the canPlayType() method returns: "," "maybe" or "probably"
			mp3Support = "" != audio.canPlayType('audio/mpeg');
			oggSupport = "" != audio.canPlayType('audio/ogg; codecs="vorbis"');
		} else {
			//Browsers do not support audio tags
			mp3Support = false;
			oggSupport = false;
		}
		// Check if ogg, mp3 format is supported, if not, set soundFileExtn to undefined
		loader.soundFileExtn = oggSupport?".ogg":mp3Support?".mp3":undefined;
	},
	loadImage: function(url) {
		this.totalCount++;
		this.loaded = false;
		$('#loadingscreen').show();
		var image = new Image();
		image.src = url;
		image.onload = loader.itemLoaded;
		return image;
	},
	soundFileExtn: ".ogg",
	loadSound: function(url) {
		this.totalCount++;
		$('#loadingscreen').show();
		var audio = new Audio();
		audio.src = url+loader.soundFileExtn;
		audio.addEventListener("canplaythrough", loader.itemLoaded, false);
		return audio;
	},
	itemLoaded: function() {
		loader.loadedCount++;
		$('#loadingmessage').html('Loaded ' + loader.loadedCount + ' of ' + loader.totalCount);
		if (loader.loadedCount === loader.totalCount) {
			loader.loaded = true;
			$('#loadingscreen').hide();
			if (loader.onload) {
				loader.onload();
				loader.onload = undefined;
			}
		}
	},
}

       game.js

$(window).load(function() {
	game.init();
});

var game = {
	// Start preloading resources
	init: function() {
		loader.init();
		mouse.init();

		$('.gamelayer').hide();
		$('#gamestartscreen').show();

		game.backgroundCanvas = document.getElementById('gamebackgroundcanvas');
		game.backgroundContext = game.backgroundCanvas.getContext('2d');

		game.foregroundCanvas = document.getElementById('gameforegroundcanvas');
		game.foregroundContext = game.foregroundCanvas.getContext('2d');

		game.canvasWidth = game.backgroundCanvas.width;
		game.canvasHeight = game.backgroundCanvas.height;
	},
	start: function() {
		$('.gamelayer').hide();
		$('#gameinterfacescreen').show();
		game.running = true;
		game.refreshBackground = true;
		game.drawingLoop();
	},
	// Maps are segmented into 20-pixel x-20-pixel square grids
	gridSize: 20,
	// Does the record background move and need to be redrawn?
	backgroundChanged: true,
	// Control cycle, run for a fixed time
	animationTimeout: 100,  // 100ms
	offsetX: 0, //Map translation offset, X and Y
	offsetY: 0,
	panningThreshold: 60, //Distance from the edge of canvas, drag the mouse to translate the map
	panningSpeed: 10,     //Number of Pixels Translated by Each Painting Cycle
	handlePanning: function() {
		//If the mouse leaves canvas, the map will no longer pan
		if (!mouse.insideCanvas) {
			return;
		}
		if (mouse.x <= game.panningThreshold) { //Mouse on the leftmost side
			if (game.offsetX >= game.panningSpeed) {
				game.refreshBackground = true;
				game.offsetX -= game.panningSpeed;
			}
		} else if (mouse.x >= game.canvasWidth - game.panningThreshold) {//Mouse on the far right
			if (game.offsetX + game.canvasWidth + game.panningSpeed <= game.currentMapImage.width) {
				game.refreshBackground = true;
				game.offsetX += game.panningSpeed;
			}
		}
		if (mouse.y<=game.panningThreshold) {
			//Mouse at the top
			if (game.offsetY >= game.panningSpeed) {
				game.refreshBackground = true;
				game.offsetY -= game.panningSpeed;
			}
		} else if (mouse.y>=game.canvasHeight - game.panningThreshold) {
			//The mouse is at the bottom
			if (game.offsetY + game.canvasHeight + game.panningSpeed <= game.currentMapImage.height) {
				game.refreshBackground = true;
				game.offsetY += game.panningSpeed;
			}
		}
		if (game.refreshBackground) {
			//Updating mouse coordinates based on translation offset
			mouse.calculateGameCoordinates();
		}
	},
	animationLoop: function() {
		// Execute animation loops for each object in the game
	},
	drawingLoop: function() {
		// Processing map translation
		game.handlePanning();

		// Drawing background maps is a huge task. We only redraw when the map changes.
		if (game.refreshBackground) {
			game.backgroundContext.drawImage(game.currentMapImage, game.offsetX,
				game.offsetY, game.canvasWidth, game.canvasHeight, 0, 0, game.canvasWidth,
				game.canvasHeight);
			game.refreshBackground = false;
		}
		//Clear Outlook canvas
		game.foregroundContext.clearRect(0,0,game.canvasWidth,game.canvasHeight);
		//Drawing objects in the foreground

		//Draw mouse
		mouse.draw();

		// Next drawing cycle
		if (game.running) {
			requestAnimationFrame(game.drawingLoop);
		}
	},
}

The main menu currently offers two options: campaign items, story-line-based single player mode, multiplayer combat options, and player-to-player mode.

5.3 Maps and Checkpoints

There are many possible ways to define maps and checkpoints for games. One of the methods is to store the map information as metadata. When the game runs, the browser dynamically generates and draws the map based on these metadata.

Another slightly simpler approach is to use your own level design tool software to save the map as a larger picture. It only needs to store the path of map pictures and other metadata, such as the object in the game, the target of the checkpoint task and so on. Use Tiled (www.mapeditor.org) -- a general map editing software.

Define maps objects, js/maps.js,

Maps are divided into 20 PX wide and 20 PX high grids. At present, we use the "debugging" mode to draw a layer of grid on the map, so when debugging the game, it is easy to determine the position of the object in the game.

The initial position coordinates are based on the map grid coordinate system, which is used to determine which area of the map the field of view is located at the beginning of the game.

/* Define basic level metadata */
var maps = {
	"singleplayer": [
		{
			"name": "Introduction",
			"briefing": "In this level you will learn how to pan across the map.\n\nDon't worry! We will be implementing more features soon.",
			/* Map details */
			"mapImage": "images/maps/level-one-debug-grid.png",
			"startX": 4,
			"startY": 4,
		},
	],
};

5.4 Loading Task Brief Screen

Add task screen to index.html

     singleplayer.js

/* Implementing basic Singleplaer objects */
var singleplayer = {
	// Start a single-player campaign
	start: function() {
		// Hide Start Menu Layer
		$('.gamelayer').hide();
		// Starting from the first pass
		singleplayer.currentLevel = 0;
		game.type = "singleplayer";
		game.team = "blue";
		// Finally, start checkpoints
		singleplayer.startCurrentLevel();
	},
	exit: function() {
		//Display the Start Menu
		$('.gamelayer').hide();
		$('#gamestartscreen').show();
	},
	currentLevel: 0,
	startCurrentLevel: function() {
		//Get the data used to build checkpoints
		var level = maps.singleplayer[singleplayer.currentLevel];
		// Disable the Start Task button before loading resources
		$("#entermission").attr("disabled", true);
		//Load the resources used to create checkpoints
		game.currentMapImage = loader.loadImage(level.mapImage);
		game.currentLevel = level;
		// Setting map offset
		game.offsetX = level.startX * game.gridSize;
		game.offsetY = level.startY * game.gridSize;
		// After loading resources, enable the Start Task button
		if (loader.loaded) {
			$("#entermission").removeAttr("disabled");
		} else {
			loader.onload = function() {
				$("#entermission").removeAttr("disabled");
			}
		}
		// Load Task Profile Screen
		$('#missionbriefing').html(level.briefing.replace(/\n/g, '<br><br>'));
		$('#missionscreen').show();
	},
	play: function() {
		game.animationLoop();
		game.animationInterval = setInterval(game.animationLoop, game.animationTimeout);
		game.start();
	},
}

5.5 Making Game Interface

Add Game Interface creen to index.html

The game interface layer contains the following areas:

  • Game area: Players view maps in this area and interact with buildings, units and other objects in the game. The region consists of two canvas elements.
  • Message area: Players can see system prompts and story-driven messages in this area
  • Image area: Players can see the image of the story-driven message sender in this area
  • Funds column: Players can view the balance of funds in this area.
  • Side Bar Button: This area contains buttons that players use to create units and buildings.

5.6 Realizing Map Translation

Create mouse.js

In the init() method, all necessary event response functions are set up:

var mouse = {
	// x and y coordinates of the mouse relative to the upper left corner of canvas
	x: 0,
	y: 0,
	// The coordinates of the mouse relative to the upper left corner of the game map
	gameX: 0,
	gameY: 0,
	// Mouse coordinates in game grids
	gridX: 0,
	gridY: 0,
	// Is the left mouse button currently pressed?
	buttonPressed: false,
	// Whether to press the left mouse button and drag it
	dragSelect: false,
	// Is the mouse in the canvas area?
	insideCanvas: false,
	//
	click: function(ev, rightClick) {
		// Click the mouse in canvas
	},
	//
	draw: function() {
		//Dragging or dragging
		if (this.dragSelect) {
			var x = Math.min(this.gameX, this.dragX);
			var y = Math.min(this.gameY, this.dragY);
			var width = Math.abs(this.gameX - this.dragX);
			var height = Math.abs(this.gameY - this.dragY);
			game.foregroundContext.strokeStyle = 'white';
			game.foregroundContext.strokeRect(x-game.offsetX, y-game.offsetY, width, height);
		}
	},
	// Convert mouse coordinates to game coordinates
	calculateGameCoordinates: function() {
		mouse.gameX = mouse.x + game.offsetX;
		mouse.gameY = mouse.y + game.offsetY;
		mouse.gridX = Math.floor((mouse.gameX)/game.gridSize);
		mouse.gridY = Math.floor((mouse.gameY)/game.gridSize); 
	},
	//
	init: function() {
		var $mouseCanvas = $("#gameforegroundcanvas");
		//When the mouse moves, it calculates the position coordinates of the mouse and stores them.
		//Check whether the mouse button is pressed and whether the mouse that pressed the button is dragged more than 4 pixels.
		//If so, set dragSelect to true.
		//A 4-pixel threshold is used to prevent the game from converting every click into a drag-and-drop operation.
		$mouseCanvas.mousemove(function(ev) {
			var offset = $mouseCanvas.offset();
			mouse.x = ev.pageX - offset.left;
			mouse.y = ev.pageY - offset.top;
			mouse.calculateGameCoordinates();
			if (mouse.buttonPressed) {
				if ((Math.abs(mouse.dragX - mouse.gameX)>4 || 
					 Math.abs(mouse.dragY - mouse.gameY)>4)) {
					mouse.dragSelect = true;
				}
			} else {
				mouse.dragSelect = false;
			}
		});
		//When the click operation is complete
		$mouseCanvas.click( function(ev) {
			mouse.click(ev, false);
			mouse.dragSelect = false;
			return false;
	    });
	    //
	    $mouseCanvas.mousedown(function(ev) {
	    	//When the left mouse button is pressed
	    	if (ev.which == 1) {
	    		mouse.buttonPressed = true;
	    		mouse.dragX = mouse.gameX;
	    		mouse.dragY = mouse.gameY;
	    		//Prevent the default click behavior of browsers
	    		ev.preventDefault();
	    	}
	    	return false;
	    });
	    //Right-click to pop up the browser context menu
	    $mouseCanvas.bind('contextmenu', function(ev){
	    	mouse.click(ev, true);
	    	return false;
	    });
	    $mouseCanvas.mouseup(function(ev) {
	    	var shiftPressed = ev.shiftKey;
	    	//When the left key is released
	    	if (ev.which == 1) {
	    		// Left key was released
	    		mouse.buttonPressed = false;
	    		mouse.dragSelect = false;
	    	}
	    	return false;
	    });
	    //Mouse out of the canvas area
	    $mouseCanvas.mouseleave(function(ev) {
	    	mouse.insideCanvas = false;
	    });
	    //Mouse into the canvas area
	    $mouseCanvas.mouseenter(function(ev) {
	    	mouse.buttonPressed = false;
	    	mouse.insideCanvas = true;
	    });
	},
}

Topics: Javascript html5 JQuery