您的位置:首页 > Web前端 > HTML5

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

2018-02-12 14:57 591 查看
将定义自己的游戏世界、建筑、单位,以及一个故事主线,并建立一个动人的单人战役。接着我们还要利用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" 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

/* 游戏容器和图层的初始样式表 */
#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;
});
},
}

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: