HTML5游戏开发进阶 5 :创建即时战略游戏世界
2018-02-12 14:57
591 查看
将定义自己的游戏世界、建筑、单位,以及一个故事主线,并建立一个动人的单人战役。接着我们还要利用HTML5 WebSocket使游戏支持多人实时对战。
这款游戏的大部分素材有Daniel Cook(http://www.lostgarden.com)提供。
开发该游戏时,我们会尽可能保持代码的通用性和可定制性,这样你就可以重新使用这些代码来实现自己的想法了。
启动画面和主菜单:游戏开始时显示,允许玩家选择单人战役模式或多人对战模式。
加载画面:游戏加载资源时显示
任务画面:任务开始前显示,带有一段任务简介
游戏界面:游戏的主画面,包括地图区域和游戏控制面板。
style.css
common.js
game.js
主菜单目前提供了两个选项:战役项,基于故事线的单玩家模式;多人对战选项,玩家对玩家模式。
另一种略简单一些的方法是,利用自己的关卡设计工具软件,将地图保存为一张较大的图片。只需要存储地图图片的路径和另外一些元数据,如游戏中的物体、关卡任务的目标等。使用Tiled(www.mapeditor.org)---一款通用的地图编辑软件。
定义maps对象,js/maps.js,
地图被分割成20px宽和20px高的格网。目前,我们使用“调试”模式在地图上绘制一层格网,这样在调试游戏的时候,就很容易确定游戏中物体的位置。
初始位置坐标是基于地图格网坐标系统的,它用来决定在游戏开始时,视野位于地图上的哪一块区域。
singleplayer.js
游戏界面层包含以下几个区域:
游戏区域:玩家在该区域查看地图,与建筑、单位及游戏中的其他物体进行交互。该区域有两个canvas元素组成。
消息区域:玩家可以在该区域看到系统提示与故事驱动消息
图像区域:玩家在该区域可以看到故事驱动信息发送者的图像
资金栏:玩家可以在此区域查看资金余额。
侧边栏按钮:此区域包含了玩家用来创建单位和建筑的按钮。
init()方法中,设置了所有必要的事件响应函数:
这款游戏的大部分素材有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.jsinit()方法中,设置了所有必要的事件响应函数:
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; }); }, }
相关文章推荐
- HTML5游戏开发进阶 2 :创建基本的游戏世界
- HTML5游戏开发进阶 8 :添加更多的游戏元素
- Cocos2d-xna : 横版战略游戏开发实验6 CCAnimate创建角色动画
- Unity3D游戏开发 创建简单的游戏世界(三)
- 从现在开始开发一款即时战略游戏——BigForest!
- HTML5游戏开发进阶 4 :物理引擎集成
- HTML 5游戏开发的世界:最佳五大实践
- Unity3D游戏开发 创建简单的游戏世界(三)
- 创建等距世界:游戏开发入门
- Android基于box2d开发弹弓类游戏[三]-------------创建游戏世界
- 游戏开发创建游戏世界(8)
- Android基于box2d开发弹弓类游戏[三]-------------创建游戏世界
- HTML5游戏开发进阶 3 :物理引擎基础
- 日本游戏开发公司谈:如何在激烈的APP世界立足
- 跨平台移动APP开发进阶(二)HTML5+、mui开发移动app教程
- 我的Android进阶之旅------>Android疯狂连连看游戏的实现之开发游戏界面(二)
- iOS游戏开发一:App申请创建与证书的申请创建
- 游戏开发世界的Lua语言
- Cocos2d-x创建android项目(cocos2d-x游戏开发三)
- 网站开发进阶(十八)js获取html标签中的值