HTML5游戏开发进阶 5 :创建即时战略游戏世界

     将定义自己的游戏世界、建筑、单位,以及一个故事主线,并建立一个动人的单人战役。接着我们还要利用HTML5 WebSocket使游戏支持多人实时对战。

     这款游戏的大部分素材有Daniel Cook(http://www.lostgarden.com)提供。

     开发该游戏时,我们会尽可能保持代码的通用性和可定制性,这样你就可以重新使用这些代码来实现自己的想法了。

5.1 基本HTML布局

     先来定义几个图层:

  • 启动画面和主菜单:游戏开始时显示,允许玩家选择单人战役模式或多人对战模式。
  • 加载画面:游戏加载资源时显示
  • 任务画面:任务开始前显示,带有一段任务简介
  • 游戏界面:游戏的主画面,包括地图区域和游戏控制面板。

5.2 创建启动画面和主菜单

       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" οnclick="singleplayer.start();">Campaign</span><br>
        <span id="multiplayer" οnclick="multiplayer.start();">Multiplayer</span><br>
      </div>
      <div id="missionscreen" class="gamelayer">
        <input type="button" id="entermission" οnclick="singleplayer.play();">
        <input type="button" id="exitmission" οnclick="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

/* 游戏容器和图层的初始样式表 */
#gamecontainer {
	width: 640px;
	height: 480px;
	background: url(images/splashscreen.png);
	border: 1px solid black;
}


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


/* 启动画面与主菜单 */
#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;
}


/* 加载画面 */
#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样式 */
#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;
}
/* 游戏界面 */
#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

/* 设置requestAnimationFrame和图像加载器 */
(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,   //目前已被加载的资源数
	totalCount: 0,    //需要加载的资源总数
	init: function() {
		//检查声音格式支持
		var mp3Support, oggSupport;
		var audio = document.createElement('audio');
		if (audio.canPlayType) {
			// 目前canPlayType()方法返回:"", "maybe"或"probably"
			mp3Support = "" != audio.canPlayType('audio/mpeg');
			oggSupport = "" != audio.canPlayType('audio/ogg; codecs="vorbis"');
		} else {
			//浏览器不支持audio标签
			mp3Support = false;
			oggSupport = false;
		}
		// 检查是否支持ogg, mp3格式,若都不支持,设置soundFileExtn为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 = {
	// 开始预加载资源
	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();
	},
	// 地图被分割成20像素x20像素的方形网格
	gridSize: 20,
	// 记录背景是否移动了,是否需要被重绘
	backgroundChanged: true,
	// 控制循环,运行固定的时间
	animationTimeout: 100,  // 100ms
	offsetX: 0, //地图平移偏移量,X和Y
	offsetY: 0,
	panningThreshold: 60, //与canvas边缘的距离,在此距离范围内拖拽鼠标进行地图平移
	panningSpeed: 10,     //每个绘画循环平移的像素数
	handlePanning: function() {
		//如果鼠标离开canvas,地图不再平移
		if (!mouse.insideCanvas) {
			return;
		}
		if (mouse.x <= game.panningThreshold) { //鼠标在最左边
			if (game.offsetX >= game.panningSpeed) {
				game.refreshBackground = true;
				game.offsetX -= game.panningSpeed;
			}
		} else if (mouse.x >= game.canvasWidth - game.panningThreshold) {//鼠标在最右边
			if (game.offsetX + game.canvasWidth + game.panningSpeed <= game.currentMapImage.width) {
				game.refreshBackground = true;
				game.offsetX += game.panningSpeed;
			}
		}
		if (mouse.y<=game.panningThreshold) {
			//鼠标在最上边
			if (game.offsetY >= game.panningSpeed) {
				game.refreshBackground = true;
				game.offsetY -= game.panningSpeed;
			}
		} else if (mouse.y>=game.canvasHeight - game.panningThreshold) {
			//鼠标在最下边
			if (game.offsetY + game.canvasHeight + game.panningSpeed <= game.currentMapImage.height) {
				game.refreshBackground = true;
				game.offsetY += game.panningSpeed;
			}
		}
		if (game.refreshBackground) {
			//基于平移偏移量,更新鼠标坐标
			mouse.calculateGameCoordinates();
		}
	},
	animationLoop: function() {
		// 执行游戏中每个物体的动画循环
	},
	drawingLoop: function() {
		// 处理地图平移
		game.handlePanning();

		// 绘制背景地图是一项庞大的工作,我们仅仅在地图改变()时重绘
		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;
		}
		//清空前景canvas
		game.foregroundContext.clearRect(0,0,game.canvasWidth,game.canvasHeight);
		//绘制前景上的物体

		//绘制鼠标
		mouse.draw();

		// 下一次绘图循环
		if (game.running) {
			requestAnimationFrame(game.drawingLoop);
		}
	},
}

    主菜单目前提供了两个选项:战役项,基于故事线的单玩家模式;多人对战选项,玩家对玩家模式。

5.3 地图与关卡

    有很多可行的方法为游戏定义地图和关卡。其中一个方法是,将地图的信息作为元数据存储起来,游戏运行时,浏览器根据这些元数据动态地生成并绘制出地图。

     另一种略简单一些的方法是,利用自己的关卡设计工具软件,将地图保存为一张较大的图片。只需要存储地图图片的路径和另外一些元数据,如游戏中的物体、关卡任务的目标等。使用Tiled(www.mapeditor.org)---一款通用的地图编辑软件。

      定义maps对象,js/maps.js,

      地图被分割成20px宽和20px高的格网。目前,我们使用“调试”模式在地图上绘制一层格网,这样在调试游戏的时候,就很容易确定游戏中物体的位置。

      初始位置坐标是基于地图格网坐标系统的,它用来决定在游戏开始时,视野位于地图上的哪一块区域。

/* 定义基本的关卡元数据 */
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.",
			/* 地图细节 */
			"mapImage": "images/maps/level-one-debug-grid.png",
			"startX": 4,
			"startY": 4,
		},
	],
};

5.4 加载任务简介画面

     在index.html中加入任务画面

     singleplayer.js

/* 实现基本的Singleplaer对象 */
var singleplayer = {
	// 开始单人战役
	start: function() {
		// 隐藏开始菜单图层
		$('.gamelayer').hide();
		// 从第一关开始
		singleplayer.currentLevel = 0;
		game.type = "singleplayer";
		game.team = "blue";
		// 最后,开始关卡
		singleplayer.startCurrentLevel();
	},
	exit: function() {
		//显示开始菜单
		$('.gamelayer').hide();
		$('#gamestartscreen').show();
	},
	currentLevel: 0,
	startCurrentLevel: function() {
		//获取用来构建关卡的数据
		var level = maps.singleplayer[singleplayer.currentLevel];
		// 加载资源完成之前,禁用”开始任务“按钮
		$("#entermission").attr("disabled", true);
		//加载用来创建关卡的资源
		game.currentMapImage = loader.loadImage(level.mapImage);
		game.currentLevel = level;
		// 设置地图偏移量
		game.offsetX = level.startX * game.gridSize;
		game.offsetY = level.startY * game.gridSize;
		// 加载资源完成后,启用”开始任务“按钮
		if (loader.loaded) {
			$("#entermission").removeAttr("disabled");
		} else {
			loader.onload = function() {
				$("#entermission").removeAttr("disabled");
			}
		}
		// 加载任务简介画面
		$('#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 制作游戏界面

     在index.html中加入游戏界面 gameinterfacescreen

     游戏界面层包含以下几个区域:

  • 游戏区域:玩家在该区域查看地图,与建筑、单位及游戏中的其他物体进行交互。该区域有两个canvas元素组成。
  • 消息区域:玩家可以在该区域看到系统提示与故事驱动消息
  • 图像区域:玩家在该区域可以看到故事驱动信息发送者的图像
  • 资金栏:玩家可以在此区域查看资金余额。
  • 侧边栏按钮:此区域包含了玩家用来创建单位和建筑的按钮。

5.6 实现地图平移

     创建mouse对象mouse.js

     init()方法中,设置了所有必要的事件响应函数:

var mouse = {
	// 鼠标相对于canvas左上角的x、y坐标
	x: 0,
	y: 0,
	// 鼠标相对于游戏地图左上角的坐标
	gameX: 0,
	gameY: 0,
	// 鼠标在游戏网格中的坐标
	gridX: 0,
	gridY: 0,
	// 鼠标左键当前是否被按下
	buttonPressed: false,
	// 是否按下鼠标左键并进行拖拽
	dragSelect: false,
	// 鼠标是否在canvas区域内
	insideCanvas: false,
	//
	click: function(ev, rightClick) {
		// 在canvas内单击鼠标
	},
	//
	draw: function() {
		//是否拖拽
		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);
		}
	},
	// 将鼠标的坐标转换为游戏坐标
	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");
		//鼠标移动时,计算鼠标的位置坐标并存储起来。
		//检查鼠标按键是否被按下,以及按下按键的鼠标是否被拖拽超过4像素,
		//如果是,则将dragSelect置为true。
		//4像素的阀值用来阻止游戏将每一次单击操作都转化为拖拽操作
		$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;
			}
		});
		//单击操作完成后
		$mouseCanvas.click( function(ev) {
			mouse.click(ev, false);
			mouse.dragSelect = false;
			return false;
	    });
	    //
	    $mouseCanvas.mousedown(function(ev) {
	    	//鼠标左键被按下时
	    	if (ev.which == 1) {
	    		mouse.buttonPressed = true;
	    		mouse.dragX = mouse.gameX;
	    		mouse.dragY = mouse.gameY;
	    		//阻止浏览器默认的单击行为
	    		ev.preventDefault();
	    	}
	    	return false;
	    });
	    //右键弹出浏览器上下文菜单
	    $mouseCanvas.bind('contextmenu', function(ev){
	    	mouse.click(ev, true);
	    	return false;
	    });
	    $mouseCanvas.mouseup(function(ev) {
	    	var shiftPressed = ev.shiftKey;
	    	//左键释放时
	    	if (ev.which == 1) {
	    		// Left key was released
	    		mouse.buttonPressed = false;
	    		mouse.dragSelect = false;
	    	}
	    	return false;
	    });
	    //鼠标离开canvas区域
	    $mouseCanvas.mouseleave(function(ev) {
	    	mouse.insideCanvas = false;
	    });
	    //鼠标进入canvas区域
	    $mouseCanvas.mouseenter(function(ev) {
	    	mouse.buttonPressed = false;
	    	mouse.insideCanvas = true;
	    });
	},
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值