初學lufylegend.js之日,我用lufylegend.js開發了第一個HTML5小遊戲-拼圖遊戲,也寫了篇文章來炫耀一下:HTML5小遊戲《智力大拼圖》發布,挑戰你的思維風暴。不過當時初學遊戲開發,經驗淺薄,所以沒有好好專研遊戲裡的演算法和程式碼的缺陷,導致遊戲出現了很多bug,甚至拼圖打亂後很可能無法復原。最近常有朋友問起這個遊戲,希望我能把程式碼裡的bug改一下方便初學者學習,順便我也打算測試一下自己寫這種小遊戲的速度,所以就抽出了一些時間將這個遊戲從頭到尾重新寫了一遍,計算了一下用時,從準備、修改素材到最後完成遊戲,一共用了大約2h的時間。
以下是遊戲網址:
http://yuehaowang.github.io/games/puzzle/
這是我的遊戲紀錄,歡迎各位挑戰:
接下來就來講講如何開發完成這款遊戲的。 (按「編年史」)
準備lufylegend遊戲引擎,大家可以去官方網站下載:
lufylegend.com/lufylegend
引擎文件地址:
lufylegend.com/lufylegend/api
可以說,如果沒有強大的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;"> <p id="mygame"></p> </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); /** 添加2小時完成HTML5拼圖小遊戲代碼圖文介紹 */ 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, preBlock = null; i < l; i++) { if (!preBlock) { preBlock = blockList[0]; continue; } var currentBlock = blockList[i]; if (currentBlock.index < preBlock.index) { reverseAmount++; } preBlock = currentBlock; } /** 检测打乱后是否可还原 */ 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
,这表示一个逆序
当全部的逆序之和为奇数时表示不可还原,重新打乱即可,打乱后重新检测,直到逆序之和为偶数为止
上面我给出的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这个游戏引擎,实在是可以大幅提升开发效率。
以上就是2小时完成HTML5拼图小游戏代码图文介绍的内容,更多相关内容请关注PHP中文网(m.sbmmt.com)!