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" 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;
});
},
}
相關文章
- HTML5遊戲開發(三):使用webpack構建TypeScript應用HTML遊戲開發WebTypeScript
- 阿里開源HTML5小遊戲開發框架Hilo實戰教程阿里HTML遊戲開發框架
- 悠遊世界/遊戲/系統技術開發/悠遊世界養成遊戲開發解析遊戲開發
- HTML5遊戲開發過程中的二三事HTML遊戲開發
- 悠遊世界合成遊戲系統技術開發解析/合成遊戲/小遊戲遊戲
- HTML5遊戲開發(二):使用TypeScript編寫程式碼HTML遊戲開發TypeScript
- 遊戲的戰略(五):為什麼做遊戲難【完】遊戲
- Mac經典戰略策略遊戲:Mac遊戲
- <題解>幻想鄉戰略遊戲遊戲
- 即時戰略遊戲(RTS)是否已經沒落?遊戲
- HTML5遊戲開發(四):飛機大戰之顯示場景和元素HTML遊戲開發
- HTML5遊戲開發(五):飛機大戰之讓所有元素動起來HTML遊戲開發
- 實戰Flash遊戲開發遊戲開發
- 幽冥世界鏈遊/闖關/系統開發/合成卡牌遊戲/幽冥世界遊戲玩法遊戲
- 完美世界遊戲召開戰略釋出會,釋出近30款重磅新遊及IP遊戲
- 德國發行商Assemble:與輕語遊戲展開戰略合作遊戲
- 團隊即時戰略遊戲《A YEAR OF RAIN》封測版今日開啟遊戲AI
- “遊戲中的遊戲世界”——遊戲副本的起源與發展史遊戲
- 給獨立遊戲製作人的進階建議遊戲
- HTML5遊戲開發(一):3分鐘建立一個hello worldHTML遊戲開發
- 坦克世界開發商Wargaming進軍手機遊戲市場GAM遊戲
- 【Unity3D開發小遊戲】《戰棋小遊戲》Unity開發教程Unity3D遊戲
- 使用 .NET 進行遊戲開發遊戲開發
- HTML5版的String Avoider小遊戲HTMLIDE遊戲
- python遊戲開發實戰:網路遊戲Demo(客戶端)Python遊戲開發客戶端
- 遊戲免下載 開啟網頁輕鬆玩!網易雲遊戲帶你領略遊戲新時代遊戲網頁
- 環遊世界/合成/遊戲/系統技術開發案例遊戲
- 遊戲開發入門(一)遊戲開發概述遊戲開發
- 第 1 天|基於 AI 進行遊戲開發:5 天建立一個農場遊戲!AI遊戲開發
- 網易遊戲宣佈對Behaviour Interactive進行戰略投資遊戲
- 網易遊戲宣佈戰略投資《黎明殺機》開發商Behavior遊戲
- 遊戲的戰略(二)——選擇性的戰略與落地的挑戰遊戲
- 在Unity中為即時戰略遊戲實現戰爭迷霧(上)Unity遊戲
- 在Unity中為即時戰略遊戲實現戰爭迷霧(下)Unity遊戲
- NFT遊戲系統開發/遊戲開發技術遊戲開發
- 太平洋雄風Victory At Sea Pacific for mac(即時戰略遊戲)Mac遊戲
- 前端開發入門到實戰:HTML5進階FileReader的使用前端HTML
- 小遊戲改變世界——談談內建小遊戲的設計之道遊戲
- 磊友黃何:HTML5遊戲開發成本低 盈利模式清晰HTML遊戲開發模式