拼图游戏逻辑分析和源码分享(复制黏贴到本地就能玩)

拼图游戏逻辑分析和源码分享(复制黏贴到本地就能玩)

前言

最近看到一篇博客,实现了拼图游戏并分享了源码。我感觉很好玩,就拿来改了改,实现了一个自己的版本。核心逻辑没有变,主要是优化了样式和交互,增加了些游戏的趣味性。

原文代码实现是用原生 js 面向对象的方式写的,这让习惯了使用 vue 语法糖的我,再次感到了用原生 js 好麻烦。好在,最近也是在重温原生js,打算再把基础学扎实一点,所以我遵循了原作者,也先用原生 js 做优化,后面还是打算改写为 vue 版本,是为了可以打包为移动端的 App,这样就可以在手机上玩了,哈哈。想拼什么图,就拼什么图,完美。

参考博文

拼图逻辑分析

1、一张图分割成几块,怎么分割?

定义构造函数,初始化数据。

function Jigsaw(row,boxWidth){this.row=row;this.itemWidth= boxWidth/this.row;this.fragment=[];//拼图碎片的dom数组this.originalKeys=[];//拼图碎片的下标,记录最初的正确顺序,方便后面对照拼图是否正确完成。this.keys=[];//拼图碎片的下标,游戏开始时会被打乱顺序。this.len=this.row*this.row;this.init();
}

拼图的尺寸是固定的,难度系数,确定了一张图分割为横纵的多少块,这样每块的宽高也就可以计算了。

分割使用背景图 background 属性,定位呈现整张图的固定部分区域。

background-position 属性设置背景图像的起始位置。

以 900px 的拼图大小分割为 3*3 的拼图碎片为例

(x , y)

x 随着每次换行 归零

y 随着每次换行 自增

游戏初始化

  //初始化init:function(){var fragment=dom.createDocumentFragment();var url = imgView.src;for(var i=0;i<this.len;i++){var div=dom.createElement('div');  div.style.cssText=`background:url(${url}) no-repeat -${(i%this.row)*this.itemWidth}px -${Math.floor(i/this.row)*this.itemWidth}px;height:${this.itemWidth}px;width:${this.itemWidth}px;`;this.fragment.push(div);//每个拼图碎片,都对应唯一的keys[i] 和 originalKeys[i]this.keys.push(i);this.originalKeys.push(i);fragment.appendChild(div);}box.innerHTML="";box.appendChild(fragment);},

完整的拼图就是多个应用了背景图属性的 div 组合而成的。

2、随机位置实现

随机位置实现,首先要打乱拼图碎片的下标数组。

arrayObject.sort(sortby)

排序函数的巧妙使用,用来打乱拼图碎片的下标数组。

sort 一般用于给乱序的数组排序,这里反其道用之,给正序的数组打乱顺序。参数 a b是数组中的元素。新的排序 依据 排序函数的返回值 来决定 a 和 b 在数组中的前后位置。

this.keys.sort(function(a,b){

      console.log('a = '+a+' , b = '+b)

      //return a-b

      //return Math.random()>0.5?1:-1;

})

乱序

this.keys.sort(function(a,b){

      console.log("a = "+a+' , b = '+b)

      return Math.random()>0.5?1:-1;

})

经测试,这里 sort()不传参数 a,b 也是可以的,应该是sort()方法在源码实现时,做了默认参数处理。

根据拼图碎片的随机下标,获取拼图碎片的随机 dom ,然后依次给拼图碎片应用绝对定位。

以 900px 的拼图大小分割为 3*3 的拼图碎片为例(原理同上使用背景图定位,所以后面代码实现也可以同上优化)

(x , y)

x 随着每次换行 归零

y 随着每次换行 自增

最外层相对定位,拼图碎片绝对定位。

this.item 是存储的拼图碎片 element 数组

也是在此时,给每个拼图碎片绑定鼠标事件。

start:function(){//随机位置this.keys.sort(function(a,b){return Math.random()>0.5?1:-1;})var keys = this.keys;//随机打乱的拼图碎片下标var colNum=0;var rowNum=0;box.innerHTML="";for(var i=0;i<keys.length;i++){if(i>0){if(i%this.row===0){rowNum++;colNum=0;}}var item = this.fragment[keys[i]];item.style.position='absolute';item.style.left=`${colNum++*this.itemWidth}px`;item.style.top=`${rowNum*this.itemWidth}px`;item.pos=i;//pos:item数组元素在 keys 数组中对应的下标 indexitem.key=keys[i];//key::item数组元素在 keys 数组中对应的值this.drag(item);box.appendChild(item);}},

start 方法 可以优化

start:function(){//随机位置this.keys.sort(function(a,b){return Math.random()>0.5?1:-1;})var keys = this.keys;//随机打乱的拼图碎片下标box.innerHTML="";//js % 模运算//余数指整数除法中被除数未被除尽部分,且余数的取值范围为0到除数之间(不包括除数)的整数。 [1]  例如:27除以6,商数为4,余数为3。//一个数除以另一个数,要是比另一个数小的话,商为0,余数就是它自己。 [1]  例如:1除以2,商数为0,余数为1;2除以3,商数为0,余数为2。for(var i=0;i<keys.length;i++){var item = this.fragment[keys[i]];item.style.position='absolute';item.style.left=`${(i%this.row)*this.itemWidth}px`;item.style.top=`${Math.floor(i/this.row)*this.itemWidth}px`;item.pos=i;//pos:item数组元素在 keys 数组中对应的下标 indexitem.key=keys[i];//key::item数组元素在 keys 数组中对应的值this.drag(item);box.appendChild(item);}
},

3、鼠标右键的点击问题

拼图交互,设置为鼠标左键选中,右键取消选中。

鼠标右键和系统事件冲突。解决方法是自定义鼠标右键 oncontextmenu 事件。

自定义鼠标右键 oncontextmenu 事件

//设置拖动drag:function(item){var me=this;//构造函数 Jigsawitem.onmousedown=function(e){var e = e||window.event;//this 当前点击的拼图碎片var target = me.findTarget(this);if(e.button===0){//左键(键值0)if(target){//前面已有一块拼图碎片,处于已点击 active 状态me.exchange(this,target);//单纯点击不增加步数,只有交换后才增加步数。//重点在于 在 findTarget 方法中,排除两次点击相同的情况steps++;stepMsg.innerHTML=steps;}else{this.moveFlag=true;this.className='active';}}e.preventDefault && e.preventDefault();}//右键(键值2)取消拼图选中//oncontextmenu 事件在元素中用户右击鼠标时触发并打开上下文菜单。//注意:所有浏览器都支持 oncontextmenu 事件, contextmenu 元素只有 Firefox 浏览器支持。item.oncontextmenu = function(e) {var e = e||window.event;//findTarget 传参 undefined 避开同次点击判断的限制//取消 active 一般都是同拼图碎片 点击var target = me.findTarget();if(target){target.className='';target.moveFlag=false;}e.preventDefault && e.preventDefault();}}

4、两块拼图位置交换

exchange:function(from,target){var fromLeft = from.style.left;var fromTop = from.style.top;var fromPos = from.pos;var fromKey = from.key;var targetLeft = target.style.left;var targetTop = target.style.top;var targetPos = target.pos;//pos:item数组元素在 keys 数组中对应的下标 indexvar targetKey = target.key;//key::item数组元素在 keys 数组中对应的值from.style.left=targetLeft;from.style.top=targetTop;from.pos=targetPos;target.style.left=fromLeft;target.style.top=fromTop;target.pos=fromPos;//交换过后取消 activetarget.className='';target.moveFlag=false;this.keys.splice(fromPos,1,targetKey);this.keys.splice(targetPos,1,fromKey);//如果相等表示已经找好了if(this.diff(this.originalKeys,this.keys)){this.tips();gameMsg.innerHTML="太棒了,成功了!";//关闭计时器clearInterval(timer);}else{//提示未完成,继续加油//随机数在范围从0到小于1,也就是说,从0(包括0)往上,但是不包括1(排除1)var i=Math.floor(Math.random()*10);//0~9gameMsg.innerHTML=gameMsgArr[i]}
},

exchange 方法可以优化

//交换两个div位置 既要交换视图上的位置,也要交换在 keys数组 中的位置。exchange:function(from,target){//解构语法交换两个变量的值[from.style.left,target.style.left]=[target.style.left,from.style.left];[from.style.top,target.style.top]=[target.style.top,from.style.top];[from.pos,target.pos]=[target.pos,from.pos];//交换过后取消 activetarget.className='';target.moveFlag=false;//交换它们在this.keys中的位置this.keys.splice(target.pos,1,target.key);this.keys.splice(from.pos,1,from.key);//如果相等表示已经找好了if(this.diff(this.originalKeys,this.keys)){this.tips();gameMsg.innerHTML="太棒了,成功了!";//关闭计时器clearInterval(timer);}else{//提示未完成,继续加油//随机数在范围从0到小于1,也就是说,从0(包括0)往上,但是不包括1(排除1)var i=Math.floor(Math.random()*10);//0~9gameMsg.innerHTML=gameMsgArr[i]}},

 5、是否完成拼图判断。

  //比较 this.originalKeys , this.keys 两个数组,如果一模一样 证明顺序对了		diff:function(a,b){var me=this;//Array.isArray() 用于确定传递的值是否是一个 Arrayvar isArrayA = Array.isArray(a);var isArrayB = Array.isArray(b);if (isArrayA && isArrayB) {//如果都是数组return a.every(function (item, index) {//用every和递归来比对a数组和b数组的每个元素,并返回return me.diff(item, b[index]);})}else{return String(a) === String(b)}},

6、总结

相较原文

1、增加了步数统计(有点问题待优化,单纯点击鼠标不应该增加步数)。

2、增加了文字提示(只要拼图没完成,就会随机文字提示,如果有音效就更棒了)。

3、增加了耗时统计,这样就知道拼图的快慢了。

4、顺序图片,改为随机图片,也是为了好玩,图库里的图片越多越好玩,有的简单,有的复杂。

5、难度设置由纵横两个维度,统一为一个维度。这样是为了保证方形的拼图,因为我觉得方形的图片好看。也是为了简化代码逻辑和视图呈现的好看。

6、原文拼图默认宽高是 600*600,我觉得有点小,改成了 900*900。所以,准备的拼图都是900*900的方图。如果是其他尺寸的图片,则要么拼不全,要么有空白。

7、增加了拼图的分割线。

8、部分代码优化。

我的实现

界面预览

源码分享

 
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;">
<style>
div.game{display: flex;flex-direction: row;justify-content: center;
}div.left{display: flex;flex-direction: column;align-items: flex-start;line-height: 50px;background-color: #f7f7f7;margin-right:10px;padding:20px;
}/* 预览图片 */
.imgView{width: 300px;box-sizing: content-box;border: 8px solid #bc6e42;
}.input{ -webkit-appearance: none;background-color: #fff;background-image: none;border-radius: 4px;border: 1px solid #dcdfe6;box-sizing: border-box;color: #606266;display: inline-block;font-size: inherit;height: 40px;line-height: 40px;outline: none;padding: 0 15px;transition: border-color .2s cubic-bezier(.645,.045,.355,1);width: 234px;
}div.btns{width: 100%;text-align: center;border-top:1px solid #999;margin-top:30px;padding:10px 0;
}button{display: inline-block;line-height: 1;white-space: nowrap;cursor: pointer;background: #fff;border: 1px solid #dcdfe6;color: #606266;-webkit-appearance: none;text-align: center;box-sizing: border-box;outline: none;margin: 0 10px;transition: .1s;font-weight: 500;padding: 12px 20px;font-size: 14px;border-radius: 4px;
}div.right{background-color: #f7f7f7;padding:20px;margin-left:10px;
}div.message{display: flex;flex-direction: row;justify-content: space-around;text-align: center;line-height: 60px;font-weight: bold;
}div.message div{margin:0 10px;
}div.message img{width:24px;
}div.message span{padding:0 16px;color:#F56200;
}/* 拼图盒子 */
#box{	width: 900px;height: 900px;box-sizing: content-box;position: relative;
}#box div{box-sizing: border-box;border: 1px dotted #fff;float:left;cursor:move;
}/* 消息盒子 */
#box p{box-sizing: border-box;line-height:900px;text-align:center;font-size:60px;color:#F56200;margin:0;
}.active{border:8px solid #bc6e42 !important;
}
</style>
</head><body><div class="container"><div class="game"><div class="left"><div>随机图片:<span id="imgNum">图1</span></div><div><img class="imgView" src='./images/1.jpg'/></div><div>难度系数:<input type='number' id='difficulty' class='input' min=3 max=9 value='3' onchange="gameInit()"></div><div class="btns"><button onclick='imgChange()'>换一张图</button><button onclick='startGame()'>开始游戏</button></div></div><div class="right"><div class="message"><div><img src="./images/step.png" alt="img"><span id='stepMsg'>0</span></div><div><img src="./images/msg.png" alt="img"><span id='gameMsg'>无</span></div><div><img src="./images/time.png" alt="img"><span id='timeMsg'>0</span></div></div><div id='box'></div></div></div></div>
</body><script>
//全局变量
var isFirst=true;//页面第一次加载
var dom=document;
var timer;
var timeMsg=dom.getElementById("timeMsg");
var stepMsg = dom.getElementById("stepMsg");
var gameMsg = dom.getElementById("gameMsg");
var gameMsgArr=['继续加油!','保持冷静!','马上就要完成了!','快成功了!','还差一点儿!','加油啊','继续保持','行不行啊','吁。。。','快点快点'
];
var box=dom.getElementById("box");
var imgView=dom.getElementsByClassName("imgView")[0]; 
var game;
var steps=0;function Jigsaw(row,boxWidth){this.row=row;this.itemWidth= boxWidth/this.row;this.fragment=[];//拼图碎片的dom数组this.originalKeys=[];//拼图碎片的下标,记录最初的正确顺序,方便后面对照拼图是否正确完成。this.keys=[];//拼图碎片的下标,游戏开始时会被打乱顺序。this.len=this.row*this.row;this.init();
}Jigsaw.prototype={//初始化init:function(){var fragment=dom.createDocumentFragment();var url = imgView.src;for(var i=0;i<this.len;i++){var div=dom.createElement('div');  div.style.cssText=`background:url(${url}) no-repeat -${(i%this.row)*this.itemWidth}px -${Math.floor(i/this.row)*this.itemWidth}px;height:${this.itemWidth}px;width:${this.itemWidth}px;`;this.fragment.push(div);//每个拼图碎片,都对应唯一的keys[i] 和 originalKeys[i]this.keys.push(i);this.originalKeys.push(i);fragment.appendChild(div);}box.innerHTML="";box.appendChild(fragment);},start:function(){//随机位置//sort() 方法用于对数组的元素进行排序。//返回值 对数组的引用。请注意,数组在原数组上进行排序,不生成副本。//如果调用该方法时没有使用参数,将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。//如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:// return a-b// 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。// 若 a 等于 b,则返回 0。// 若 a 大于 b,则返回一个大于 0 的值。// 这里没有用 return a-b ,用的是 return Math.random()>0.5?1:-1; 则 传入的 a,b 根据这个结果排序。this.keys.sort(function(a,b){return Math.random()>0.5?1:-1;})var keys = this.keys;//随机打乱的拼图碎片下标box.innerHTML="";//js % 模运算//余数指整数除法中被除数未被除尽部分,且余数的取值范围为0到除数之间(不包括除数)的整数。 [1]  例如:27除以6,商数为4,余数为3。//一个数除以另一个数,要是比另一个数小的话,商为0,余数就是它自己。 [1]  例如:1除以2,商数为0,余数为1;2除以3,商数为0,余数为2。for(var i=0;i<keys.length;i++){var item = this.fragment[keys[i]];item.style.position='absolute';item.style.left=`${(i%this.row)*this.itemWidth}px`;item.style.top=`${Math.floor(i/this.row)*this.itemWidth}px`;item.pos=i;//pos:item数组元素在 keys 数组中对应的下标 indexitem.key=keys[i];//key::item数组元素在 keys 数组中对应的值this.drag(item);box.appendChild(item);}},//寻找 active moveFlag = true findTarget:function(current){//从列表中查找是否已经有选择的目标了 for(var i=0;i<this.fragment.length;i++){//排除两次点击的一样if(current!==this.fragment[i]&&this.fragment[i].moveFlag){return this.fragment[i];}}// findTarget 使用 find 始终返回 undefined ,猜测可能是因为 this.item 数组元素为 element dom 对象,所以不能用 find 方法。// find() 方法返回通过测试(函数内判断)的数组的第一个元素的值。// find() 方法为数组中的每个元素都调用一次函数执行:// 当数组中的元素在测试条件时返回 true 时, find() 返回符合条件的元素,之后的值不会再调用执行函数。// 如果没有符合条件的元素返回 undefined// this.fragment.find(item=>{//   return current!==item&&item.moveFlag;// })},//交换两个div位置 既要交换视图上的位置,也要交换在 keys数组 中的位置。exchange:function(from,target){//解构语法交换两个变量的值[from.style.left,target.style.left]=[target.style.left,from.style.left];[from.style.top,target.style.top]=[target.style.top,from.style.top];[from.pos,target.pos]=[target.pos,from.pos];//交换过后取消 activetarget.className='';target.moveFlag=false;//交换它们在this.keys中的位置//arrayObject.splice(index,howmany,item1,.....,itemX)//返回值:由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。//index	必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。// howmany	必需。要删除的项目数量。如果设置为 0,则不会删除项目。// item1, ..., itemX	可选。向数组添加的新项目。this.keys.splice(target.pos,1,target.key);this.keys.splice(from.pos,1,from.key);//如果相等表示已经找好了if(this.diff(this.originalKeys,this.keys)){this.tips();gameMsg.innerHTML="太棒了,成功了!";//关闭计时器clearInterval(timer);}else{//提示未完成,继续加油//随机数在范围从0到小于1,也就是说,从0(包括0)往上,但是不包括1(排除1)var i=Math.floor(Math.random()*10);//0~9gameMsg.innerHTML=gameMsgArr[i]}},//完成后倒计时的提示tips:function(){setTimeout(function(){var suc=dom.createElement('p');suc.innerHTML='Bingo!';box.innerHTML="";box.appendChild(suc);},500)},//比较 this.originalKeys , this.keys 两个数组,如果一模一样 证明顺序对了		diff:function(a,b){var me=this;//Array.isArray() 用于确定传递的值是否是一个 Arrayvar isArrayA = Array.isArray(a);var isArrayB = Array.isArray(b);if (isArrayA && isArrayB) {//如果都是数组return a.every(function (item, index) {//用every和递归来比对a数组和b数组的每个元素,并返回return me.diff(item, b[index]);})}else{return String(a) === String(b)}},//设置拖动drag:function(item){var me=this;//构造函数 Jigsawitem.onmousedown=function(e){var e = e||window.event;//this 当前点击的拼图碎片var target = me.findTarget(this);if(e.button===0){//左键(键值0)if(target){//前面已有一块拼图碎片,处于已点击 active 状态me.exchange(this,target);//单纯点击不增加步数,只有交换后才增加步数。//重点在于 在 findTarget 方法中,排除两次点击相同的情况steps++;stepMsg.innerHTML=steps;}else{this.moveFlag=true;this.className='active';}}e.preventDefault && e.preventDefault();}//右键(键值2)取消拼图选中//oncontextmenu 事件在元素中用户右击鼠标时触发并打开上下文菜单。//注意:所有浏览器都支持 oncontextmenu 事件, contextmenu 元素只有 Firefox 浏览器支持。item.oncontextmenu = function(e) {var e = e||window.event;//findTarget 传参 undefined 避开同次点击判断的限制//取消 active 一般都是同拼图碎片 点击var target = me.findTarget();if(target){target.className='';target.moveFlag=false;}e.preventDefault && e.preventDefault();}}
}//页面初始化
function gameInit(){//关闭计时器if(!timer){timer=null;}else{//关闭计时器clearInterval(timer);}if(isFirst){//只执行一次 只在页面第一次初始化时执行isFirst=false;//随机的一张图var index=Math.floor(Math.random()*10)+1;imgView.src='./images/'+index+'.jpg';var imgNum=dom.getElementById("imgNum");imgNum.innerHTML="图"+index;}steps=0;stepMsg.innerHTML=0;timeMsg.innerHTML=0;gameMsg.innerHTML='无';var difficulty=dom.getElementById("difficulty").value;game = new Jigsaw(difficulty,900);
}//开始游戏
function startGame(){game.start();	timeStart();
}//随机的一张图
function imgChange(){isFirst=true;gameInit();
}function timeStart(){//防止多次点击开始游戏启动多个计时器//关闭计时器if(!timer){timer=null;}else{//关闭计时器clearInterval(timer);}var time=0;timer=setInterval(function(){timeMsg.innerHTML=time++;})
}//页面初始化
gameInit();	</script>
</html>

 

发布者:admin,转转请注明出处:http://www.yc00.com/news/1700976972a1041453.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信