初學lufylegend.js之日,我用lufylegend.js開發了第一個HTML5小遊戲——拼圖遊戲,還寫了篇博文來炫耀一下:HTML5小遊戲《智力大拼圖》發佈,挑戰你的思惟風暴。不過當時初學遊戲開發,經驗淺薄,因此沒有好好專研遊戲裏的算法和代碼的缺陷,致使遊戲出現了不少bug,甚至拼圖打亂後極可能沒法復原。最近常常有朋友問起這個遊戲,但願我能把代碼裏的bug改一下方便初學者學習,順便我也打算測試一下本身寫這種小遊戲的速度,因此就抽出了一些時間將這個遊戲從頭至尾從新寫了一遍,計算了一下用時,從準備、修改素材到最後完成遊戲,一共用了大約2h的時間。javascript
這是個人遊戲記錄,歡迎各位挑戰:css
接下來就來說講如何開發完成這款遊戲的。(按「編年體」)html
準備lufylegend遊戲引擎,你們能夠去官方網站下載:html5
引擎文檔地址:算法
lufylegend.com/lufylegend/apicanvas
能夠說,若是沒有強大的lufylegend引擎,這種html5小遊戲用原生canvas製做,少說要一天呢。api
0~30min數組
準備素材(10min) + 修改素材(20min)。因爲在下實在手殘,不善於P圖,修改圖片用了大約20min,囧……dom
30~50min
開發開始界面。遊戲不能沒有開始界面因此咱們首先實現這部分代碼。在此以前是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(); }); }
到此,運行代碼,獲得咱們的開始界面:
看到這個畫面,其實我本身都想吐槽一下實在是太「樸素」了,囧……
不過我此次圖個製做速度,因此還望各位看官海量。
50~90min
這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(); } };
到這裏,咱們就實現了打亂和操做拼圖塊部分。
90~120min
最後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這個遊戲引擎,實在是能夠大幅提高開發效率。
一、文件截圖
二、雙擊index.html便可運行
三、運行時的截圖
這篇博文在最初寫成的時候,我沒有對逆序算法進行深刻研究,再加上個人測試不仔細,我沒有發現算法的錯誤之處。所以,在博文發佈後,很多讀者發現遊戲無解現象並將此問題反饋給了我,通過網友熱心幫助,我才找到了問題所在,並更正了算法。在此對這些熱心的網友表示真心的感謝,也爲我學習不深刻,以及誤導了很多讀者而感到十份內疚自責。