成果预览
先放上最后的成果吧~
完整的代码在文章结尾将会给出。这个小游戏一开始只是从互联网上随便找的一个代码。然后我玩起来各种不爽,就开始优(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】