成果预览
先放上最后的成果吧~

完整的代码在文章结尾将会给出。这个小游戏一开始只是从互联网上随便找的一个代码。然后我玩起来各种不爽,就开始优(bao)雅(li)的修改起代码来。
封装
一开始我看这个代码不太爽的原因,是代码基本上没有封装,各种逻辑都夹杂在一起,第一步就是把它封装一下。首先是把所有与游戏无关的逻辑剥离出去,比如关于获奖的代码。其次,则是将其改为原型模式,方便将其给其他项目使用
原型模式是什么意思?其实并不复杂,主体仍然是一个function,只不过我们将它的prototype替换为我们自己的代码,简单地说:
function App(config) {
//一些初始化代码
}
App.prototype = {
//游戏相关代码
};
App.prototype.constructor = App;
当然,我们的代码也需要减少对外部变量的依赖。因为JS灵活的闭包机制,我们可以方便的使用外部变量,但这并不利于我们的封装,最好的做法则是将其封装于其中:
function App(config) {
this.config = config;
}
App.prototype = {
config: null,
init: function() {
//使用this.config操作
}
//此处省略几万行代码(误)
};
当然我们不可避免的会在这里面遇到一些在回调中调用自身的情况,这时候this其实已经不是原来的this了,因此,我们利用JS的闭包写法,来实现变量的传递:
App.prototype = {
//……
countdown: function() {
//这里有个小细节
var _this = this;
//这里可以使用this来操作
this.runtime.timer = setInterval(function() {
//这里就不能用this了,我们用_this来代替
_this.runtime.pastTime += 0.5;
});
}
};
接下来则是将变量整理整理,例如,游戏运行的数据(如拼图打乱的顺序等)将其封装到runtime中,关于DOM的变量封装到dom中,诸如此类,各位可以按照自己的爱好自由发挥:
App.prototype = {
config: null,
runtime: {
//……
},
dom: {
box: null,
area: null,
//……
}
之后,我们可以再减少对原有HTML的依赖,例如,3×3的拼图需要9个img,之前是使用者自行创建,现在我们可以让JS来代替这个过程,这样一来的话,我们再将其改为4×4、5×5或者其他,就不需要用户再手动修改DOM了。
for (var i = 0; i < total; i++) {
this.dom.images[i] = document.createElement('li');
this.dom.images[i].appendChild(document.createElement('img'));
//……
}
当然,我们游戏归游戏,光玩游戏是没啥意思的,我们还需要和业务捆绑起来,比如,我们增加一个小功能,每拼四张图就可以获得一个小宝箱。因此,我们需要自己实现一个简单的事件回调,来减少游戏逻辑和业务逻辑的耦合度:
App.prototype = {
//我们把回调函数放到这里
eventHandler: {
complete: null,
timeout: null
},
//……
//这样设置回调比较简单
setEventHandler: function(name, callback) {
this.name = callback;
},
//……
//我们在需要回调的地方手动埋点,例如:
timeout: function() {
this.runtime.start = 0;
clearInterval(this.runtime.timer);
//触发
if (this.eventHandler.timeout !== null) {
this.eventHandler.timeout(this);
}
}
}
到这一步,其实已经基本上完成了整个封装过程,我们可以将其作为单独的模块放到其他需要用到拼图的页面中:
var Game = new App({
box: document.getElementById("game-box"),
startTime: 120
});
Game.setEventHandler('complete', function(g) {
console.log('拼图+1');
});
Game.setEventHandler('timeout', function(g) {
alert('游戏结束');
});
任意数量拼图
当然了,光是3×3可不够意思,我们还得让程序支持N×N
首先,是把创建的操作改改
this.dom.images = [];
//计算每个小方块的大小
this.blockSize = Math.floor(this.dom.box.offsetWidth / this.size);
var total = this.size * this.size;
for (var i = 0; i < total; i++) {
this.dom.images[i] = document.createElement('li');
this.dom.images[i].appendChild(document.createElement('img'));
//设置z-index
this.dom.images[i].style.zIndex = this.zIndex;
//设置大小
this.dom.images[i].style.width = this.blockSize + "px";
this.dom.images[i].style.height = this.blockSize + "px";
//设置边距
this.dom.images[i].style.top = (this.blockSize * Math.floor(i / this.size)) + "px";
this.dom.images[i].style.left = (this.blockSize * (i % this.size)) + "px";
this.dom.area.appendChild(this.dom.images[i]);
//这里是用于验证是否完成拼图的数组,当然可以改为其他方式实现
this.runtime.imgArr.push(i + 1);
this.runtime.oriArr.push(i + 1);
}
接下来改动一下打乱图片的代码
var imgWidth = Math.floor(this.imgSize / this.size);
var ctx = this.runtime.canvas.getContext('2d');
for (var i = 0; i < this.size; i++) {
for (var j = 0; j < this.size; j++) {
//获取image标签,分割后的图片将会放入这里
var im = this.dom.images[parseInt(this.runtime.imgArr[index - 1] - 1)].querySelector('img');
//将源图像的一部分绘制到canvas
ctx.drawImage(this.runtime.loadImage, imgWidth * j, imgWidth * i, imgWidth, imgWidth, 0, 0, 400, 400);
//标记正确的顺序,用于最后的答案校验
im.setAttribute('data-seq', index);
//将canvas里的图像数据写到image标签
im.src = this.runtime.canvas.toDataURL('image/jpeg');
index++;
}
}
至此,便完成了N×N的设计。当然,对于难度就是另一回事了……
拖动拼图
截止到目前为止,拼图仍然是往上划一下挪一格,这可得急死人,玩起来感觉太不友好了,我们得让拼图变成能拖动的!一开始,所有事件都是基于zepto的,zepto基于原生事件封装了swipeLeft、swipeDown等几个触摸事件,但是,原生只有touchstart、touchmove、touchend、touchcancel四个事件。其中,touchstart和touchmove事件响应中会携带坐标,touchend只会在部分浏览器中携带,很遗憾的是,据测试,大部分手机端的浏览器都没有。
现在,我们换成原生JS来实现:
//存一下开始移动的点
var startPoint = {x: 0, y: 0};
//存一下截止上次触发touchmove事件时所在的点
var lastPoint = {x: 0, y: 0};
var startIm = -1;
/**
* 获取元素的绝对定位
* touchstart和touchmove事件所传递的点,是相对于整个页面的定位
* 使用offsetLeft/offsetTop获取到的定位,有时候不是相对于整个页面的
* 例如我们的父元素是relative定位时,获取到的就是相对于父元素的定位
*/
function getOffset(el) {
var rect = el.getBoundingClientRect();
var win = el.ownerDocument.defaultView;
return {
top: rect.top + win.pageYOffset,
left: rect.left + win.pageXOffset
};
}
//依靠绝对定位,寻找到我们触摸点是位于哪张图片上
function findIm(x, y, exclude) {
for (var i in _this.dom.images) {
var elPos = getOffset(_this.dom.images[i]);
/**
* 示意图:
* ↓这一点是(elPos.left, elPos.top)
* +-----------+ ←这一点是(elPos.left + _this.blockSize, elPos.top)
* | |
* | |
* | |
* | |
* | |
* +-----------+ ←这一点是(elPos.left + _this.blockSize, elPos.top + _this.blockSize)
* ↑这一点是(elPos.left, elPos.top + _this.blockSize)
* 因此,我们判断触摸点是否位于一块拼图中
* 只需要比较坐标即可
*/
if (i != exclude && x > elPos.left && x < elPos.left + _this.blockSize && y > elPos.top && y < elPos.top + _this.blockSize) {
return i;
}
}
return -1;
}
//触摸事件开始
function onTouchStart(e) {
/**
* 分别检查开始标记、触摸标记和完成标记
* 触摸标记是用于只响应单指触摸,防止一些奇怪的问题
*/
if (!_this.runtime.start || startIm !== -1 || _this.runtime.isJustComplete) {
return;
}
startPoint.x = e.touches[0].pageX;
startPoint.y = e.touches[0].pageY;
lastPoint.x = e.touches[0].pageX;
lastPoint.y = e.touches[0].pageY;
startIm = findIm(startPoint.x, startPoint.y);
//拖动单个图像会使其“浮动”到其他图片上面,我们通过设置不同的z-index实现
_this.dom.images[startIm].style.zIndex = _this.zIndex + 1;
}
function onTouchMove(e) {
if (!_this.runtime.start) {
return;
}
lastPoint.x = e.touches[0].pageX;
lastPoint.y = e.touches[0].pageY;
//通过目前触摸所在点减去触摸开始点,分别获得x、y方向上的移动距离,通过设置margin实现实时移动
_this.dom.images[startIm].style.marginTop = (lastPoint.y - startPoint.y) + "px";
_this.dom.images[startIm].style.marginLeft = (lastPoint.x - startPoint.x) + "px";
}
function onTouchEnd(e) {
if (!_this.runtime.start) {
return;
}
//把元素移动回原来的位置
_this.dom.images[startIm].style.marginTop = 0;
_this.dom.images[startIm].style.marginLeft = 0;
_this.dom.images[startIm].style.zIndex = _this.zIndex;
var endIm = findIm(lastPoint.x, lastPoint.y, startIm);
if (endIm === -1) {
startIm = -1;
return;
}
//交换开始的图像和结束的图像
var t = _this.dom.images[startIm].innerHTML;
_this.dom.images[startIm].innerHTML = _this.dom.images[endIm].innerHTML;
_this.dom.images[endIm].innerHTML = t;
//初始化触摸标记
startIm = -1;
//检查是否完成了拼图
_this.check();
}
最后
有一些小细节我其实想单独说一下,首先,为什么大量使用var而不使用更简单、更“先进”的let、const等ES6、ES7的语法呢?主要是为了兼容性考虑。即使是在微信中运行,我们依然没有足够的时间确认是否所有的微信都能完美兼容,因此我们还是尽可能少的使用新的语法。
然后放上前端页面的代码,点击下载【密码:09tJ】