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; }); }, }