2小时完成HTML5拼图小游戏
当时初学游戏开发,经验浅薄,所以没有好好专研游戏里的算法和代码的缺陷,导致游戏出现了很多bug,甚至拼图打乱后很可能无法还原。最近经常有朋友问起这个游戏,希望我能把代码里的bug改一下方便初学者学习,顺便我也打算测试一下自己写这种小游戏的速度,所以就抽出了少量时间将这个游戏从头到尾重新写了一遍,计算了一下用时,从准备、修改素材到最后完成游戏,一共用了大约2h的时间。
这是我的游戏记录,欢迎各位挑战:

接下来就来讲讲如何开发完成这款游戏的。(按“编年体”)
这里推荐一下我的前台学习交流扣qun:784783012 ,里面都是学习前台的,从最基础的HTML+CSS+JS【炫酷特效,游戏,插件封装,设计模式】到手机端HTML5的项目实战的学习资料都有整理,送给每一位前台小伙伴。2019最新技术,从企业招聘人才需求 到怎样学习前台开发,和学习什么内容都有免费系统分享。好友都在里面学习交流,每天都会有大牛定时讲解前台技术!
点击:加入
准备阶段
准备lufylegend游戏引擎,大家可以去官方网站下载:
lufylegend.com/lufylegend
引擎文档地址:
lufylegend.com/lufylegend/api
可以说,假如没有强大的lufylegend引擎,这种html5小游戏用原生canvas制作,少说要一天呢。
0~30min
准备素材(10min) + 修改素材(20min)。因为在下实在手残,不善于P图,修改图片用了大约20min,囧……
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, 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(); } };到这里,我们就实现了打乱和操作拼图块部分。
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这个游戏引擎,实在是可以大幅提升开发效率。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 2小时完成HTML5拼图小游戏