以下是游戏地址:
由于头条禁止在文章页面加入链接,大家私信我“拼图”即可获取下载地址。
这是我的游戏记录,欢迎各位寻衅:
接下来就来讲讲如何开拓完成这款游戏的。(按“编年体”)
准备阶段准备lufylegend游戏引擎,大家可以辞官方网站下载:
由于头条禁止在文章页面加入链接,大家私信我“拼图”即可获取下载地址。
引擎文档地址:
由于头条禁止在文章页面加入链接,大家私信我“拼图”即可获取下载地址。
可以说,如果没有强大的lufylegend引擎,这种html5小游戏用原生canvas制作,少说要一天呢。
0~30min准备素材(10min) + 修正素材(20min)。由于不才实在手残,不长于P图,修正图片用了大约20min,囧……
30~50min开拓开始界面。游戏不能没有开始界面以是我们首先实现这部分代码。在此之前是index.html里的代码,代码如下:
<!DOCTYPE html>
<html>
<head>
<title>Puzzle</title>
<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这个游戏引擎,实在是可以大幅提升开拓效率。
源代码下载末了奉上源代码:
由于头条禁止在文章页面加入链接,大家私信我“拼图”即可获取下载地址。
致谢与反思这篇博文在最初写成的时候,我没有对逆序算法进行深入研究,再加上我的测试不仔细,我没有创造算法的缺点之处。因此,在博文发布后,不少读者创造游戏无解征象并将此问题反馈给了我,经由网友热心帮助,我才找到了问题所在,并更正了算法。在此对这些热心的网友表示至心的感谢,也为我学习不深入,以及误导了不少读者而感到十分腼腆自责。
如果大家对本文有任何见地或不解,欢迎留言~