這是關于初學者的文章,他們希望通過一些簡單的方法開始在網絡中開發游戲開發,而不使用任何重型工具。本文提供了一個簡單的步驟,開始使用html / css和javascript的2d游戲開發。在這里,我將介紹如何創建一個圖像拼圖游戲,您可以在其中拖放圖像部分進行交換和重新排列零件以形成完整的圖像。
你可以在線玩這個游戲:http://www.ikinsoft.com/3ddemo/puzzle/puzzle.html
游戲的規則很簡單。你只需要拖放破碎的圖像來交換它。您需要以形成正確圖像的方式交換它們。將拖放圖像部件所需的步驟數。所以,您可能希望考慮并嘗試以盡可能最小的步驟進行。右側提供正確的圖像供您參考。
游戲畫面如下所示:
游戲畫面截圖
我們將游戲的代碼分成3部分:Html,Css和Javascript。Html部分包含形成游戲布局的簡單標簽。CSS提供了一些響應式設計,Javascript部分包含游戲的主要邏輯。游戲的幾個重要步驟如下:
打破圖像
對于圖像看起來像分為nxn不同的部分,每側的部件n數量在哪里,nxn li元素已被使用在一個ul。每個的顯示屬性li已設置為內嵌塊,以使其顯示為網格。每個背景圖像li被設置為僅顯示圖像的1 /(n×n)部分,并且相應地設置背景圖像的位置。data-value屬性已被分配給每個li以標識該片段的索引。
代碼如下:
打碎圖片代碼
在這里,您可以看到使用簡單background-image和background-position風格實現了破壞的效果。在已經設置了博爾森圖像之后,按照正確的順序,隨機化方法用于隨機化片段。在游戲中,用戶必須重新排列片段以形成完整的圖像。
gridSize表示圖像需要在每一側(水平和垂直)分割多少部分。硬編碼值400是盒子的大小。請注意,您可能想要擺脫這個硬編碼的值。我將在下一次更新中用一個變量來更新。基于gridSize,我將拼圖的級別分為3部分:容易,中等和難易。容易3x3格,中4x4和硬5x5。您可以通過更改相應的單選按鈕的值,以不同的方式實現相同的方式。
隨機斷開零件
在設置圖像損壞的部分后,如前面代碼塊的最后一行所示,隨機化方法用于隨機分割碎片。為此,創建一個小型通用隨機化函數來隨機化任何jquery元素集合。
隨機化方法的實現如下:
隨機斷開零件代碼
在這里,我們只是簡單地循環給定選擇器的每個子元素,并根據隨機數改變其位置。隨機數應在0和收集中的元素數之間。
拖放圖片碎片
為了使每個碎片拖動,使用了jquery draggable插件。請確保您的頁面中包含jquery-ui.js以實現可拖放/可拖放功能。
拖放圖片碎片代碼
正如您在上述代碼片段中可以看到的,每次下降之后,isSorted 都將被調用來檢查這些片段是否已被排序。正在根據包含li元素的data-value屬性檢查每個片段的排序。如果片段被排序,則表示圖片已完成。
設置樣式
已經使用了一個非常小的css來使其變得簡單易懂。所使用的css允許頁面響應,您可以在平板電腦或手機中玩游戲。沒有使用css的第三方庫,以便您可以輕松了解本機css樣式。
計數步驟
計數步驟或任何用戶操作是任何游戲中最常見的部分。這也是通過一個簡單的步驟實現的。在每次下降之后,它檢查圖像是否形成。如果是,游戲結束,如果沒有,則將stepCount 變量遞增1.然后,stepCount 使用jquery 更新UI。
計時器
計時器也是大多數游戲的重要組成部分之一。基于讀者提供的反饋,已經實現了一個基本的計時器來檢查完成拼圖所需的秒數。計時器正在游戲開始時啟動,tick 每秒鐘調用 方法來更新計時器。Tick方法一旦從start方法調用,然后每秒鐘調用自身(使用JavaScript SetTimeout)并使用JQuery更新UI中使用的時間。當圖片完成時,游戲結束,最后計時,并在使用JQuery的輸出中顯示。
下面是定時器方法的實現:
計時器代碼
請注意,getTime()方法給出自01/01/1970以來通過的毫秒數。如果您建議更好的方法來計算兩個DateTime 在javascript 之間的時間,我將不勝感激。我不想依靠1000毫秒的差距setTimeout()來增加1秒。
級別
根據用戶的反饋,游戲中添加了3個難度級別:1.輕松2.中等3.硬。在我們的例子中,選擇容易設置3x3矩陣中的難題,4x4矩陣中的中等,硬設置為5x5矩陣。
為了簡單起見,我避免使用Html 5或CSS 3,以便它可以在大多數瀏覽器中使用。由于使用了JQuery版本,此游戲可能不適用于較早的瀏覽器<IE8。如果您希望在舊版本的舊版本中使用此游戲,您可以將腳本引用替換為較舊的JQuery版本(1.9或更早版本)。最新的JQuery版本不支持舊版瀏覽器。
上文的示例網址應該在大多數最新的瀏覽器中運行。已經在IE 11和Google Chrome中測試過。
篇文章給大家帶來的內容是關于使用javascript中canvas實現拼圖小游戲 ,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。
如果您想要綜合使用javascript中canvas、原生拖拽、本地存儲等多種技術完成一個有趣的項目,那么這篇文章將非常適合您
該項目中的拼圖小游戲使用javascript原創,相比于網站上類似的功能,它使用到的技術點更先進豐富,功能更強大,還包含程序開發中更多先進的思想理念,從該項目中您將能學到:
項目源碼-github
下面是游戲界面的示例圖:
根據游戲界面圖我們可以將完成這么一個小游戲分為以下幾步來實現:
從以上對小游戲制作過程的分析來看,第4步是程序功能實現的重點和難點,在以上的每個步驟中都有很多小細節需要注意和探討,下面我就詳細分析一下每個步驟的實現細節,說的不好的地方,歡迎大家留言指正。
3.1 圖片內容讀取和加載
在游戲開發第1步中,我們將圖片拖拽到指定區域后,程序是怎樣得到圖片內容信息的呢?fileReader對象又是怎樣將圖片信息轉化為base64字符串內容的?Image對象拿到圖片的base64內容之后,又是怎樣初始化加載的?帶著這些疑問,我們來研究一下實現項目中實現了第一步的關鍵代碼。
var droptarget = document.getElementById("droptarget"),
output = document.getElementById("ul1"),
thumbImg = document.getElementById("thumbimg");
//此處省略相關代碼........
function handleEvent(event) {
var info = "",
reader = new FileReader(),
files, i, len;
EventUtil.preventDefault(event);
localStorage.clear();
if (event.type == "drop") {
files = event.dataTransfer.files;
len = files.length;
if (!/image/.test(files[0].type)) {
alert('請上傳圖片類型的文件');
}
if (len > 1) {
alert('上傳圖片數量不能大于1');
}
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var img = new Image(), //原圖
thumbimg = new Image(); //等比縮放后的縮略圖
reader.readAsDataURL(files[0]);
reader.onload = function (e) {
img.src = e.target.result;
}
//圖片對象加載完畢后,對圖片進行等比縮放處理。縮放后最大寬度為三百像素
img.onload = function () {
var targetWidth, targetHeight;
targetWidth = this.width > 300 ? 300 : this.width;
targetHeight = targetWidth / this.width * this.height;
canvas.width = targetWidth;
canvas.height = targetHeight;
context.clearRect(0, 0, targetWidth, targetHeight);
context.drawImage(img, 0, 0, targetWidth, targetHeight);
var tmpSrc = canvas.toDataURL("image/jpeg");
//在本地存儲完整的縮略圖源
localStorage.setItem('FullImage', tmpSrc);
thumbimg.src = tmpSrc;
}
//此處省略相關代碼......
EventUtil.addHandler(droptarget, "dragenter", handleEvent);
EventUtil.addHandler(droptarget, "dragover", handleEvent);
EventUtil.addHandler(droptarget, "drop", handleEvent);
}
這段代碼的思路就是首先獲得拖拽區域目標對象droptarget,為droptarget注冊拖拽監聽事件。代碼中用到的EventUtil是我封裝的一個對元素添加事件、事件對象的兼容處理等常用功能的簡單對象,下面是其添加注冊事件的簡單簡單代碼,其中還有很多其他的封裝,讀者可自行查閱,功能比較簡單。
var EventUtil = {
addHandler: function(element, type, handler){
if (element.addEventListener){
element.addEventListener(type, handler, false);
} else if (element.attachEvent){
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},
//此處省略代......
}
當用戶將圖片文件拖放到區域目標對象droptarget時,droptarget的事件對象通過event.dataTransfer.files獲取到文件信息,對文件進行過濾(限制只能為圖片內容,并且最多只能有一張圖片)。拿到文件內容以后,使用FileReader對象reader讀取文件內容,使用其readAsDataURL方法讀取到圖片的base64內容,賦值給Image對象img的src屬性,就可以等到img對象初始化加載完畢,使canvas對img進行下一步的處理了。這里有一個重點的地方需要說明:一定要等img加載完成后,再使用canvas進行下一步的處理,不然可能會出現圖片損壞的情況。原因是:當img的src屬性讀取圖片文件的base64內容時,可能還沒有將內容加載到內存中時,canvas就開始處理圖片(此時的圖片是不完整的)。所以我們可以看到canvas對圖片的處理是放在img.onload方法中進行的,程序后邊還會有這種情況,之后就不再贅述了。
3.2 圖片等比縮放和本地存儲
在第一步中我們完成了對拖拽文件的內容讀取,并將其成功加載到了Image對象img中。接下來我們使用canvas對圖片進行等比縮放,對圖片進行等比縮放,我們采取的策略是限制圖片的最大寬度為300像素,我們再來看一下這部分代碼吧:
img.onload = function () {
var targetWidth, targetHeight;
targetWidth = this.width > 300 ? 300 : this.width;
targetHeight = targetWidth / this.width * this.height;
canvas.width = targetWidth;
canvas.height = targetHeight;
context.clearRect(0, 0, targetWidth, targetHeight);
context.drawImage(img, 0, 0, targetWidth, targetHeight);
var tmpSrc = canvas.toDataURL("image/jpeg");
//在本地存儲完整的縮略圖源
localStorage.setItem('FullImage', tmpSrc);
thumbimg.src = tmpSrc;
}
確定了縮放后的寬度targetWidth和高度targetHeight之后,我們使用canvas的drawImage方法對圖像進行壓縮,在這之前我們最好先使用畫布的clearRect對畫布進行一次清理。對圖片等比縮放以后,使用canvas的toDataURL方法,獲取到縮放圖的base64內容,賦給新的縮放圖Image對象thumbimg的src屬性,待縮放圖加載完畢,進行下一步的切割處理。縮放圖的base64內容使用localStorage存儲,鍵名為"FullImage"。瀏覽器的本地存儲localStorage是硬存儲,在瀏覽器刷新之后內容不會丟失,這樣我們就可以在游戲過程中保持數據狀態,這點稍后再詳細講解,我們需要知道的是localStorage是有大小限制的,最大為5M。這也是為什么我們先對圖片進行壓縮,減少存儲數據大小,保存縮放圖base64內容的原因。關于開發過程中存儲哪些內容,下一小節會配有圖例詳細說明。
3.3 縮略圖切割
生成縮略圖之后要做的工作就是對縮略圖進行切割了,同樣的也是使用canvas的drawImage方法,而且相應的處理必須放在縮略圖加載完成之后(即thumbimg.onload)進行處理,原因前面我們已經說過。下面我們再來詳細分析一下源代碼吧:
thumbimg.onload = function () {
//每一個切片的寬高[切割成3*4格式]
var sliceWidth, sliceHeight, sliceBase64, n = 0, outputElement = '',
sliceWidth = this.width / 3,
sliceHeight = this.height / 4,
sliceElements = [];
canvas.width = sliceWidth;
canvas.height = sliceHeight;
for (var j = 0; j < 4; j++) {
for (var i = 0; i < 3; i++) {
context.clearRect(0, 0, sliceWidth, sliceHeight);
context.drawImage(thumbimg, sliceWidth * i, sliceHeight * j, sliceWidth, sliceHeight, 0, 0, sliceWidth, sliceHeight);
sliceBase64 = canvas.toDataURL("image/jpeg");
localStorage.setItem('slice' + n, sliceBase64);
//為了防止圖片三像素問題發生,請為圖片屬性添加 display:block
newElement = "<li name=\"" + n + "\" style=\"margin:3px;\"><img src=\"" + sliceBase64 + "\" style=\"display:block;\"></li>";
//根據隨機數打亂圖片順序
(Math.random() > 0.5) ? sliceElements.push(newElement) : sliceElements.unshift(newElement);
n++;
}
}
//拼接元素
for (var k = 0, len = sliceElements.length; k < len; k++) {
outputElement += sliceElements[k];
}
localStorage.setItem('imageWidth', this.width + 18);
localStorage.setItem('imageHeight', this.height + 18);
output.style.width = this.width + 18 + 'px';
output.style.height = this.height + 18 + 'px';
(output.innerHTML = outputElement) && beginGamesInit();
droptarget.remove();
}
上面的代碼對于大家來說不難理解,就是將縮略圖分割成12個切片,這里我給大家解釋一下幾個容易困惑的地方:
for (var j = 0; j < 4; j++) {
for (var i = 0; i < 3; i++) {
//此處省略邏輯代碼
}
}
這個問題大家仔細想一想就明白了,我們將圖片進行切割的時候,要記錄下來每一個圖片切片的原有順序。在程序中我們使用 n 來表示圖片切片的原有順序,而且這個n記錄在了每一個圖片切片的元素的name屬性中。在后續的游戲過程中我們可以使用元素的getAttribute('name')方法取出 n 的值,來判斷圖片切片是否都被拖動到了正確的位置,以此來判斷游戲是否結束,現在講起這個問題可能還會有些迷惑,我們后邊還會再詳細探討,我給出一張圖幫助大家理解圖片切片位置序號信息n:
序號n從零開始是為了和javascript中的getElementsByTagName()選擇的子元素坐標保持一致。
(Math.random() > 0.5) ? sliceElements.push(newElement) : sliceElements.unshift(newElement);
我們知道Math.random()生成一個[0, 1)之間的數,所以再canvas將縮略圖裁切成切片以后,根據這些切片生成的web節點順序是打亂的。打亂順序以后重新組裝節點:
//拼接元素
for (var k = 0, len = sliceElements.length; k < len; k++) {
outputElement += sliceElements[k];
}
然后再將節點添加到web頁面中,也就自然而然出現了圖片切片被打亂的樣子了。
下面的一行代碼,雖然簡單,但是用的非常巧妙:
(output.innerHTML = outputElement) && beginGamesInit();
有開發經驗的同學都知道 && 和 || 是短路運算符,代碼中的含義是:只有當切片元素節點都添加到
WEB頁面之后,才會初始化為這些節點綁定事件。
3.4 本地信息存儲
代碼中多次用到了本地存儲,下面我們來詳細解釋一下本游戲開發過程中都有哪些信息需要存儲,為什么要存儲?下面是我給出的需要存儲的信息圖示例(從瀏覽器控制臺獲取):
瀏覽器本地存儲localStorage使用key:value形式存儲,從圖中我們看到我們本次存儲的內容有:
保存FullImage縮略圖的信息是當游戲結束后顯示源縮略圖時,根據FullImage中的內容展示圖片。而imageWidth,imageHeight,slice*,nodePos是為了防止瀏覽器刷新導致數據丟失所做的存儲,當刷新頁面的時候,瀏覽器會根據本地存儲的數據加載沒有完成的游戲內容。其中nodePos是在為縮略圖切片發生拖動時存入本地存儲的,并且它隨著切片位置的變化而變化,也就是它追蹤著游戲的狀態,我們在接下來的代碼功能展示中會再次說到它。
3.5 拖拽事件注冊和監控
接下來我們要做的事才是游戲中最重要的部分,還是先來分析一下代碼,首先是事件注冊前的初始化工作:
//游戲開始初始化
function beginGamesInit() {
aLi = output.getElementsByTagName("li");
for (var i = 0; i < aLi.length; i++) {
var t = aLi[i].offsetTop;
var l = aLi[i].offsetLeft;
aLi[i].style.top = t + "px";
aLi[i].style.left = l + "px";
aPos[i] = {left: l, top: t};
aLi[i].index = i;
//將位置信息記錄下來
nodePos.push(aLi[i].getAttribute('name'));
}
for (var i = 0; i < aLi.length; i++) {
aLi[i].style.position = "absolute";
aLi[i].style.margin = 0;
setDrag(aLi[i]);
}
}
可以看到這部分初始化綁定事件代碼所做的事情是:記錄每一個圖片切片對象的位置坐標相關信息記錄到對象屬性中,并為每一個對象都注冊拖拽事件,對象的集合由aLi數組統一管理。這里值得一提的是圖片切片的位置信息index記錄的是切片現在所處的位置,而我們前邊所提到的圖片切片name屬性所保存的信息n則是圖片切片原本應該所處的位置,在游戲還沒有結束之前,它們不一定相等。待所有的圖片切片name屬性所保存的值和其屬性index都相等時,游戲才算結束(因為用戶已經正確完成了圖片的拼接),下面的代碼就是用來判斷游戲狀態是否結束的,看起來更直觀一些:
//判斷游戲是否結束
function gameIsEnd() {
for (var i = 0, len = aLi.length; i < len; i++) {
if (aLi[i].getAttribute('name') != aLi[i].index) {
return false;
}
}
//后續處理代碼省略......
}
下面我們還是詳細說一說拖拽交換代碼相關邏輯吧,拖拽交換的代碼如下圖所示:
//拖拽
function setDrag(obj) {
obj.onmouseover = function () {
obj.style.cursor = "move";
console.log(obj.index);
}
obj.onmousedown = function (event) {
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
obj.style.zIndex = minZindex++;
//當鼠標按下時計算鼠標與拖拽對象的距離
disX = event.clientX + scrollLeft - obj.offsetLeft;
disY = event.clientY + scrollTop - obj.offsetTop;
document.onmousemove = function (event) {
//當鼠標拖動時計算p的位置
var l = event.clientX - disX + scrollLeft;
var t = event.clientY - disY + scrollTop;
obj.style.left = l + "px";
obj.style.top = t + "px";
for (var i = 0; i < aLi.length; i++) {
aLi[i].className = "";
}
var oNear = findMin(obj);
if (oNear) {
oNear.className = "active";
}
}
document.onmouseup = function () {
document.onmousemove = null; //當鼠標彈起時移出移動事件
document.onmouseup = null; //移出up事件,清空內存
//檢測是否普碰上,在交換位置
var oNear = findMin(obj);
if (oNear) {
oNear.className = "";
oNear.style.zIndex = minZindex++;
obj.style.zIndex = minZindex++;
startMove(oNear, aPos[obj.index]);
startMove(obj, aPos[oNear.index], function () {
gameIsEnd();
});
//交換index
var t = oNear.index;
oNear.index = obj.index;
obj.index = t;
//交換本次存儲中的位置信息
var tmp = nodePos[oNear.index];
nodePos[oNear.index] = nodePos[obj.index];
nodePos[obj.index] = tmp;
localStorage.setItem('nodePos', nodePos);
} else {
startMove(obj, aPos[obj.index]);
}
}
clearInterval(obj.timer);
return false;//低版本出現禁止符號
}
}
這段代碼所實現的功能是這樣子的:拖動一個圖片切片,當它與其它的圖片切片有碰撞重疊的時候,就和與其左上角距離最近的一個圖片切片交換位置,并交換其位置信息index,更新本地存儲信息中的nodePos。移動完成之后判斷游戲是否結束,若沒有,則期待下一次用戶的拖拽交換。
下面我來解釋一下這段代碼中比較難理解的幾個點:
//碰撞檢測
function colTest(obj1, obj2) {
var t1 = obj1.offsetTop;
var r1 = obj1.offsetWidth + obj1.offsetLeft;
var b1 = obj1.offsetHeight + obj1.offsetTop;
var l1 = obj1.offsetLeft;
var t2 = obj2.offsetTop;
var r2 = obj2.offsetWidth + obj2.offsetLeft;
var b2 = obj2.offsetHeight + obj2.offsetTop;
var l2 = obj2.offsetLeft;
`if (t1 > b2 || r1 < l2 || b1 < t2 || l1 > r2)` {
return false;
} else {
return true;
}
}
這段代碼看似信息量很少,其實也很好理解,判斷兩個圖片切片是否發生碰撞,只要將它們沒有發生碰撞的情形排除掉就可以了。這有點類似與邏輯中的非是即否,兩個切片又確實只可能存在兩種情況:碰撞、不碰撞。圖中的這段代碼是判斷不碰撞的情況:if (t1 > b2 || r1 < l2 || b1 < t2 || l1 > r2),返回false, else 返回true。
2.碰撞檢測完成了之后,圖片切片之間又是怎樣尋找左上角定點距離最近的元素呢?
代碼是這個樣子的:
//勾股定理求距離(左上角的距離)
function getDis(obj1, obj2) {
var a = obj1.offsetLeft - obj2.offsetLeft;
var b = obj1.offsetTop - obj2.offsetTop;
return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}
//找到距離最近的
function findMin(obj) {
var minDis = 999999999;
var minIndex = -1;
for (var i = 0; i < aLi.length; i++) {
if (obj == aLi[i]) continue;
if (colTest(obj, aLi[i])) {
var dis = getDis(obj, aLi[i]);
if (dis < minDis) {
minDis = dis;
minIndex = i;
}
}
}
if (minIndex == -1) {
return null;
} else {
return aLi[minIndex];
}
}
因為都是矩形區塊,所以計算左上角的距離使用勾股定理,這點相信大家都能明白。查找距離最近的元素原理也很簡單,就是遍歷所有已經碰撞的元素,然后比較根據勾股定理計算出來的最小值,返回元素就可以了。代碼中也是使用了比較通用的方法,先聲明一個很大的值最為最小值,當有碰撞元素比其小時,再將更小的值最為最小值,遍歷完成后,返回最小值的元素就可以了。
答案是回調函數,圖片切片交換函數通過回調函數來判斷游戲是否已經結束,游戲是否結束的判斷函數前面我們已經說過。圖片切片交換函數就是通過添加gameIsEnd作為回調函數,這樣在每次圖片切片移動交換完成之后,就判斷一下游戲是否結束。圖片切片的交換函數還是比較復雜的,有興趣的同學可以研究一下,下面是其實現代碼,大家重點理解其中添加了回調函數監控游戲是否結束就好了。
//通過class獲取元素
function getClass(cls){
var ret = [];
var els = document.getElementsByTagName("*");
for (var i = 0; i < els.length; i++){
//判斷els[i]中是否存在cls這個className;.indexOf("cls")判斷cls存在的下標,如果下標>=0則存在;
if(els[i].className === cls || els[i].className.indexOf("cls")>=0 || els[i].className.indexOf(" cls")>=0 || els[i].className.indexOf(" cls ")>0){
ret.push(els[i]);
}
}
return ret;
}
function getStyle(obj,attr){//解決JS兼容問題獲取正確的屬性值
return obj.currentStyle?obj.currentStyle[attr]:getComputedStyle(obj,false)[attr];
}
function gameEnd() {
alert('游戲結束!');
}
function startMove(obj,json,fun){
clearInterval(obj.timer);
obj.timer = setInterval(function(){
var isStop = true;
for(var attr in json){
var iCur = 0;
//判斷運動的是不是透明度值
if(attr=="opacity"){
iCur = parseInt(parseFloat(getStyle(obj,attr))*100);
}else{
iCur = parseInt(getStyle(obj,attr));
}
var ispeed = (json[attr]-iCur)/8;
//運動速度如果大于0則向下取整,如果小于0想上取整;
ispeed = ispeed>0?Math.ceil(ispeed):Math.floor(ispeed);
//判斷所有運動是否全部完成
if(iCur!=json[attr]){
isStop = false;
}
//運動開始
if(attr=="opacity"){
obj.style.filter = "alpha:(opacity:"+(json[attr]+ispeed)+")";
obj.style.opacity = (json[attr]+ispeed)/100;
}else{
obj.style[attr] = iCur+ispeed+"px";
}
}
//判斷是否全部完成
if(isStop){
clearInterval(obj.timer);
if(fun){
fun();
}
}
},30);
}
4.1 游戲中值得完善的功能
我認為該游戲中值得優化的地方有兩個:
這些功能感興趣的小伙伴可以嘗試一下。
相關推薦:
用javascript實現web拼圖游戲
H5的canvas實現貪吃蛇小游戲
以上就是使用javascript中canvas實現拼圖小游戲的詳細內容,更多請關注其它相關文章!
更多技巧請《轉發 + 關注》哦!
學lufylegend.js之日,我用lufylegend.js開發了第一個HTML5小游戲——拼圖游戲,還寫了篇博文來炫耀一下:HTML5小游戲《智力大拼圖》發布,挑戰你的思維風暴。不過當時初學游戲開發,經驗淺薄,所以沒有好好專研游戲里的算法和代碼的缺陷,導致游戲出現了很多bug,甚至拼圖打亂后很可能無法復原。最近經常有朋友問起這個游戲,希望我能把代碼里的bug改一下方便初學者學習,順便我也打算測試一下自己寫這種小游戲的速度,所以就抽出了一些時間將這個游戲從頭到尾重新寫了一遍,計算了一下用時,從準備、修改素材到最后完成游戲,一共用了大約2h的時間。
以下是游戲地址:
由于頭條禁止在文章頁面加入鏈接,大家私信我“拼圖”即可獲取下載地址。
這是我的游戲記錄,歡迎各位挑戰:
接下來就來講講如何開發完成這款游戲的。(按“編年體”)
準備lufylegend游戲引擎,大家可以去官方網站下載:
由于頭條禁止在文章頁面加入鏈接,大家私信我“拼圖”即可獲取下載地址。
引擎文檔地址:
由于頭條禁止在文章頁面加入鏈接,大家私信我“拼圖”即可獲取下載地址。
可以說,如果沒有強大的lufylegend引擎,這種html5小游戲用原生canvas制作,少說要一天呢。
準備素材(10min) + 修改素材(20min)。由于在下實在手殘,不善于P圖,修改圖片用了大約20min,囧……
開發開始界面。游戲不能沒有開始界面所以我們首先實現這部分代碼。在此之前是index.html里的代碼,代碼如下:
<!DOCTYPE html>
<html>
<head>
<title>Puzzle</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<script type="text/javascript" src="./lib/lufylegend-1.10.1.simple.min.js"></script>
<script type="text/javascript" src="./js/Main.js"></script>
</head>
<body style="margin: 0px; font-size: 0px; background: #F2F2F2;">
<div id="mygame"></div>
</body>
</html>
主要是引入一些js文件,不多說。然后準備一個Main.js文件,在這個文件里添加初始化界面和加載資源的代碼:
/** 初始化游戲 */
LInit(60, "mygame", 390, 580, main);
var imgBmpd;
/** 游戲層 */
var stageLayer, gameLayer, overLayer;
/** 拼圖塊列表 */
var blockList;
/** 是否游戲結束 */
var isGameOver;
/** 用時 */
var startTime, time, timeTxt;
/** 步數 */
var steps, stepsTxt;
function main () {
/** 全屏設置 */
if (LGlobal.mobile) {
LGlobal.stageScale = LStageScaleMode.SHOW_ALL;
}
LGlobal.screen(LGlobal.FULL_SCREEN);
/** 添加加載提示 */
var loadingHint = new LTextField();
loadingHint.text = "資源加載中……";
loadingHint.size = 20;
loadingHint.x = (LGlobal.width - loadingHint.getWidth()) / 2;
loadingHint.y = (LGlobal.height - loadingHint.getHeight()) / 2;
addChild(loadingHint);
/** 加載圖片 */
LLoadManage.load(
[
{path : "./js/Block.js"},
{name : "img", path : "./images/img.jpg"}
],
null,
function (result) {
/** 移除加載提示 */
loadingHint.remove();
/** 保存位圖數據,方便后續使用 */
imgBmpd = new LBitmapData(result["img"]);
gameInit();
}
);
}
function gameInit (e) {
/** 初始化舞臺層 */
stageLayer = new LSprite();
stageLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "#EFEFEF");
addChild(stageLayer);
/** 初始化游戲層 */
gameLayer = new LSprite();
stageLayer.addChild(gameLayer);
/** 初始化最上層 */
overLayer = new LSprite();
stageLayer.addChild(overLayer);
/** 添加開始界面 */
addBeginningUI();
}
以上代碼有詳細注釋,大家可以對照引擎文檔和注釋進行閱讀。有些全局變量會在以后的代碼中使用,大家可以先忽略。接下來是addBeginningUI函數里的代碼,用于實現開始界面:
function addBeginningUI () {
var beginningLayer = new LSprite();
beginningLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "#EDEDED");
stageLayer.addChild(beginningLayer);
/** 游戲標題 */
var title = new LTextField();
title.text = "拼圖游戲";
title.size = 50;
title.weight = "bold";
title.x = (LGlobal.width - title.getWidth()) / 2;
title.y = 160;
title.color = "#FFFFFF";
title.lineWidth = 5;
title.lineColor = "#000000";
title.stroke = true;
beginningLayer.addChild(title);
/** 開始游戲提示 */
var hint = new LTextField();
hint.text = "- 點擊屏幕開始游戲 -";
hint.size = 25;
hint.x = (LGlobal.width - hint.getWidth()) / 2;
hint.y = 370;
beginningLayer.addChild(hint);
/** 開始游戲 */
beginningLayer.addEventListener(LMouseEvent.MOUSE_UP, function () {
beginningLayer.remove();
startGame();
});
}
到此,運行代碼,得到我們的開始界面:
看到這個畫面,其實我自己都想吐槽一下實在是太“樸素”了,囧……
不過我這次圖個制作速度,所以還望各位看官海量。
這40分鐘的時間,是最關鍵時期,期間我們要完成整個游戲的主體部分。首先,我們需要用代碼來實現以下過程:
初始化游戲界面數據(如游戲時間、所用步數)和顯示一些UI部件(如圖樣)
|
-> 獲取隨機的拼圖塊位置
|
-> 顯示打亂后的拼圖塊
我們將這些步驟做成一個個的函數方便我們統一調用:
function startGame () {
isGameOver = false;
/** 初始化時間和步數 */
startTime = (new Date()).getTime();
time = 0;
steps = 0;
/** 初始化拼圖塊列表 */
initBlockList();
/** 打亂拼圖 */
getRandomBlockList();
/** 顯示拼圖 */
showBlock();
/** 顯示縮略圖 */
showThumbnail();
/** 顯示時間 */
addTimeTxt();
/** 顯示步數 */
addStepsTxt();
stageLayer.addEventListener(LEvent.ENTER_FRAME, onFrame);
}
函數一開始,我們把isGameOver變量設定為false代表游戲未結束,在后期的代碼里,我們會看到這個變量的作用。接著我們初始化了用于表示時間和步數的time和steps這兩個全局變量,另外初始化變量startTime的值用于后面計算游戲時間。
接下來,我們就要開始初始化拼圖塊了。見initBlockList里的代碼:
function initBlockList () {
blockList = new Array();
for (var i = 0; i < 9; i++) {
/** 根據序號計算拼圖塊圖片顯示位置 */
var y = (i / 3) >>> 0, x = i % 3;
blockList.push(new Block(i, x, y));
}
}
這里我們使用了一個Block類,這個類用于顯示拼圖塊和儲存拼圖塊的數據,并提供了一些方法來操控拼圖塊,下面是其構造器的代碼:
function Block (index, x, y) {
LExtends(this, LSprite, []);
var bmpd = imgBmpd.clone();
bmpd.setProperties(x * 130, y * 130, 130, 130);
this.bmp = new LBitmap(bmpd);
this.addChild(this.bmp);
var border = new LShape();
border.graphics.drawRect(3, "#CCCCCC", [0, 0, 130, 130]);
this.addChild(border);
this.index = index;
this.addEventListener(LMouseEvent.MOUSE_UP, this.onClick);
}
Block類繼承自LSprite,屬于一個顯示對象,所以我們在這個類中添加了一個位圖對象用于顯示拼圖塊對應的圖片。除此之外,我們還為拼圖塊添加了一個邊框,在顯示時用于隔開周圍的拼圖塊。Block類有一個index屬性,代表拼圖塊在拼圖塊列表blockList中的正確位置。最后,我們為此類添加了一個鼠標按下事件,用于處理鼠標按下后移動圖塊操作。
接下來我們還要介紹這個類的一個方法setLocation:
Block.prototype.setLocation = function (x, y) {
this.locationX = x;
this.locationY = y;
this.x = x * 130;
this.y = y * 130;
};
這個方法用于設置拼圖塊對象的顯示位置以及保存拼圖塊的“數組位置”。什么是“數組位置”呢?各位看官可以通過下面的圖片加以了解:
可以看到,“數組位置”就類似于二維數組中的元素下標。儲存這個位置的作用在于可以很方便地從blockList中獲取到附近的其他拼圖塊。這個方法在我們顯示拼圖時有調用到,在顯示拼圖之前,我們得先打亂拼圖,見如下代碼:
function getRandomBlockList () {
/** 隨機打亂拼圖 */
blockList.sort(function () {
return 0.5 - Math.random();
});
/** 計算逆序和 */
var reverseAmount = 0;
for (var i = 0, l = blockList.length; i < l; i++) {
var currentBlock = blockList[i];
for (var j = i + 1; j < l; j++) {
var comparedBlock = blockList[j];
if (comparedBlock.index < currentBlock.index) {
reverseAmount++;
}
}
}
/** 檢測打亂后是否可還原 */
if (reverseAmount % 2 != 0) {
/** 不合格,重新打亂 */
getRandomBlockList();
}
}
打亂拼圖部分直接用數組的sort方法進行隨機打亂:
blockList.sort(function () {
return 0.5 - Math.random();
});
其實打亂算法有很多種,我這里采用最粗暴的方法,也就是隨機打亂。這種算法簡單是簡單,壞在可能出現無法復原的現象。針對這個問題,就有配套的檢測打亂后是否可還原的算法,具體的算法理論我借用lufy大神的評論:
此類游戲能否還原關鍵是看它打亂后的逆序次數之和是否為偶數
假設你打亂后的數組中的每一個小圖塊為obj0,obj1,obj2,…它們打亂之前的序號分別為obj0.num,obj1.num…
接下來循環數組,如果前面元素的序號比此元素后某個元素的序號大,如obj0.num > obj1.num或者obj2.num > obj4.num就表示一個逆序
當全部的逆序之和為奇數時表示不可還原,重新打亂即可,打亂后重新檢測,直到逆序之和為偶數為止
舉個例子,如果有一個數組為[3, 4, 2, 1],那么里面3 2, 3 1, 2 4, 4 1, 2 1是逆序的,所以逆序數是5。
上面我給出的getRandomBlockList里的代碼就是在實現打亂算法和檢測是否可還原算法。
還有一種打亂方式,大家可以嘗試嘗試:和復原拼圖一樣,將空白塊一步一步地與周圍的拼圖隨機交換順序。這個打亂算法較上一種而言,不會出現無法復原的現象,而且可以根據打亂的步數設定游戲難度。
在完成打亂拼圖塊后,如期而至的是顯示拼圖塊:
function showBlock() {
for (var i = 0, l = blockList.length; i < l; i++) {
var b = blockList[i];
/** 根據序號計算拼圖塊位置 */
var y = (i / 3) >>> 0, x = i % 3;
b.setLocation(x, y);
gameLayer.addChild(b);
}
}
顯示了拼圖塊后,我們要做的就是添加操作拼圖塊的功能。于是需要拓展Block類,為其添加事件監聽器onClick方法:
Block.prototype.onClick = function (e) {
var self = e.currentTarget;
if (isGameOver) {
return;
}
var checkList = new Array();
/** 判斷右側是否有方塊 */
if (self.locationX > 0) {
checkList.push(Block.getBlock(self.locationX - 1, self.locationY));
}
/** 判斷左側是否有方塊 */
if (self.locationX < 2) {
checkList.push(Block.getBlock(self.locationX + 1, self.locationY));
}
/** 判斷上方是否有方塊 */
if (self.locationY > 0) {
checkList.push(Block.getBlock(self.locationX, self.locationY - 1));
}
/** 判斷下方是否有方塊 */
if (self.locationY < 2) {
checkList.push(Block.getBlock(self.locationX, self.locationY + 1));
}
for (var i = 0, l = checkList.length; i < l; i++) {
var checkO = checkList[i];
/** 判斷是否是空白拼圖塊 */
if (checkO.index == 8) {
steps++;
updateStepsTxt();
Block.exchangePosition(self, checkO);
break;
}
}
};
首先,我們在這里看到了isGameOver全局變量的作用,即在游戲結束后,阻斷點擊拼圖塊后的操作。
在點擊了拼圖塊后,我們先獲取該拼圖塊周圍的拼圖塊,并將它們裝入checkList,再遍歷checkList,當判斷到周圍有空白拼圖塊后,即周圍有index屬性等于8的拼圖塊后,先更新操作步數,然后將這兩個拼圖塊交換位置。具體交換拼圖塊位置的方法詳見如下代碼:
Block.exchangePosition = function (b1, b2) {
var b1x = b1.locationX, b1y = b1.locationY,
b2x = b2.locationX, b2y = b2.locationY,
b1Index = b1y * 3 + b1x,
b2Index = b2y * 3 + b2x;
/** 在地圖塊數組中交換兩者位置 */
blockList.splice(b1Index, 1, b2);
blockList.splice(b2Index, 1, b1);
/** 交換兩者顯示位置 */
b1.setLocation(b2x, b2y);
b2.setLocation(b1x, b1y);
/** 判斷游戲是否結束 */
Block.isGameOver();
};
還有就是Block.getBlock靜態方法,用于獲取給定的“數組位置”下的拼圖塊:
Block.getBlock = function (x, y) {
return blockList[y * 3 + x];
};
在Block.exchangePosition中,我們通過Block.isGameOver判斷玩家是否已將拼圖復原:
Block.isGameOver = function () {
var reductionAmount = 0, l = blockList.length;
/** 計算還原度 */
for (var i = 0; i < l; i++) {
var b = blockList[i];
if (b.index == i) {
reductionAmount++;
}
}
/** 計算是否完全還原 */
if (reductionAmount == l) {
/** 游戲結束 */
gameOver();
}
};
到這里,我們就實現了打亂和操作拼圖塊部分。
最后30min用于細枝末節上的處理,如顯示拼圖縮略圖、顯示&更新時間和步數,以及添加游戲結束畫面,這些就交給如下冗長而簡單的代碼來完成吧:
function showThumbnail() {
var thumbnail = new LBitmap(imgBmpd);
thumbnail.scaleX = 130 / imgBmpd.width;
thumbnail.scaleY = 130 / imgBmpd.height;
thumbnail.x = (LGlobal.width - 100) /2;
thumbnail.y = 410;
overLayer.addChild(thumbnail);
}
function addTimeTxt () {
timeTxt = new LTextField();
timeTxt.stroke = true;
timeTxt.lineWidth = 3;
timeTxt.lineColor = "#54D9EF";
timeTxt.color = "#FFFFFF";
timeTxt.size = 18;
timeTxt.x = 20;
timeTxt.y = 450;
overLayer.addChild(timeTxt);
updateTimeTxt();
}
function updateTimeTxt () {
timeTxt.text = "時間:" + getTimeTxt(time);
}
function getTimeTxt () {
var d = new Date(time);
return d.getMinutes() + " : " + d.getSeconds();
};
function addStepsTxt () {
stepsTxt = new LTextField();
stepsTxt.stroke = true;
stepsTxt.lineWidth = 3;
stepsTxt.lineColor = "#54D9EF";
stepsTxt.color = "#FFFFFF";
stepsTxt.size = 18;
stepsTxt.y = 450;
overLayer.addChild(stepsTxt);
updateStepsTxt();
}
function updateStepsTxt () {
stepsTxt.text = "步數:" + steps;
stepsTxt.x = LGlobal.width - stepsTxt.getWidth() - 20;
}
function onFrame () {
if (isGameOver) {
return;
}
/** 獲取當前時間 */
var currentTime = (new Date()).getTime();
/** 計算使用的時間并更新時間顯示 */
time = currentTime - startTime;
updateTimeTxt();
}
function gameOver () {
isGameOver = true;
var resultLayer = new LSprite();
resultLayer.filters = [new LDropShadowFilter()];
resultLayer.graphics.drawRoundRect(3, "#BBBBBB", [0, 0, 350, 350, 5], true,"#DDDDDD");
resultLayer.x = (LGlobal.width - resultLayer.getWidth()) / 2;
resultLayer.y = LGlobal.height / 2;
resultLayer.alpha = 0;
overLayer.addChild(resultLayer);
var title = new LTextField();
title.text = "游戲通關"
title.weight = "bold";
title.stroke = true;
title.lineWidth = 3;
title.lineColor = "#555555";
title.size = 30;
title.color = "#FFFFFF";
title.x = (resultLayer.getWidth() - title.getWidth()) / 2;
title.y = 30;
resultLayer.addChild(title);
var usedTimeTxt = new LTextField();
usedTimeTxt.text = "游戲用時:" + getTimeTxt(time);
usedTimeTxt.size = 20;
usedTimeTxt.stroke = true;
usedTimeTxt.lineWidth = 2;
usedTimeTxt.lineColor = "#555555";
usedTimeTxt.color = "#FFFFFF";
usedTimeTxt.x = (resultLayer.getWidth() - usedTimeTxt.getWidth()) / 2;
usedTimeTxt.y = 130;
resultLayer.addChild(usedTimeTxt);
var usedStepsTxt = new LTextField();
usedStepsTxt.text = "所用步數:" + steps;
usedStepsTxt.size = 20;
usedStepsTxt.stroke = true;
usedStepsTxt.lineWidth = 2;
usedStepsTxt.lineColor = "#555555";
usedStepsTxt.color = "#FFFFFF";
usedStepsTxt.x = usedTimeTxt.x;
usedStepsTxt.y = 180;
resultLayer.addChild(usedStepsTxt);
var hintTxt = new LTextField();
hintTxt.text = "- 點擊屏幕重新開始 -";
hintTxt.size = 23;
hintTxt.stroke = true;
hintTxt.lineWidth = 2;
hintTxt.lineColor = "#888888";
hintTxt.color = "#FFFFFF";
hintTxt.x = (resultLayer.getWidth() - hintTxt.getWidth()) / 2;
hintTxt.y = 260;
resultLayer.addChild(hintTxt);
LTweenLite.to(resultLayer, 0.5, {
alpha : 0.7,
y : (LGlobal.height - resultLayer.getHeight()) / 2,
onComplete : function () {
/** 點擊界面重新開始游戲 */
stageLayer.addEventListener(LMouseEvent.MOUSE_UP, function () {
gameLayer.removeAllChild();
overLayer.removeAllChild();
stageLayer.removeAllEventListener();
startGame();
});
}
});
}
Ok,2h下來,整個游戲就搞定咯~不得不表揚一下lufylegend這個游戲引擎,實在是可以大幅提升開發效率。
最后奉上源代碼:
由于頭條禁止在文章頁面加入鏈接,大家私信我“拼圖”即可獲取下載地址。
這篇博文在最初寫成的時候,我沒有對逆序算法進行深入研究,再加上我的測試不仔細,我沒有發現算法的錯誤之處。因此,在博文發布后,不少讀者發現游戲無解現象并將此問題反饋給了我,經過網友熱心幫助,我才找到了問題所在,并更正了算法。在此對這些熱心的網友表示真心的感謝,也為我學習不深入,以及誤導了不少讀者而感到十分內疚自責。
如果大家對本文有任何意見或不解,歡迎留言~
*請認真填寫需求信息,我們會在24小時內與您取得聯系。