Home > Article > Web Front-end > Use canvas in javascript to implement puzzle game
The content of this article is about using canvas in JavaScript to implement puzzle games. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.
If you want to use various technologies such as canvas, native drag and drop, and local storage in JavaScript to complete an interesting project, then this article will be very suitable for you
The puzzle game in this project is created using JavaScript. Compared with similar functions on the website, it uses more advanced and rich technical points, more powerful functions, and also contains more advanced ideas in program development. Concept, from this project you will be able to learn:
FileReader, Image object and canvas to compress and cut pictures.
Learn the most commonly used collision detection, status monitoring, and refreshing and maintaining status processing methods in mini game development.
Understand the details of drag-and-drop exchange elements, and learn how to handle dynamic element binding events and callback functions.
The following is an example of the game interface:
According to the game interface diagram, we can divide the completion of such a small game into the following steps:
1. Drag the image to the designated area, use the FileReader object to read the base64 content of the image, and then add it to the Image object
#2. When the Image object is loaded, use canvas to edit the image Scale proportionally, then get the base64 content of the thumbnail, add it to another thumbnail Image object, and save the base64 content of the thumbnail to local storage (localStorage)
3. After the thumbnail Image object is loaded, use canvas again to cut the thumbnail. In this game, the thumbnail is cut into 3*4 for a total of 12 equal parts. Use local storage to save the base64 content of each cut thumbnail. The order of the thumbnails is disrupted and displayed on the web page using the img tag
4. After the thumbnail slices are added to the web interface, add a registration drag for each thumbnail slice. The drag event allows the thumbnail slices to be exchanged with each other. In this process, the monitoring of the order status of the thumbnail slices is added. Once the puzzle is completed, the complete thumbnail will be displayed directly to complete the game
Judging from the above analysis of the mini-game production process, step 4 is the focus and difficulty of program function implementation. There are many small details that need to be paid attention to and discussed in each of the above steps. I will analyze each step in detail below. If the implementation details of each step are not good, you are welcome to leave a message and correct me.
In the first step of game development, what is the program like after we drag and drop the image to the designated area? What about getting image content information? How does the fileReader object convert image information into base64 string content? After the Image object gets the base64 content of the image, how does it initialize the loading? With these questions, let's study the key code that implements the first step in the project.
var droptarget = document.getElementById("droptarget"), output = document.getElementById("ul1"), thumbImg = document.getElementById("thumbimg"); //此处省略相关代码........ function handleEvent(event) { var info = "", reader = new FileReader(), files, i, len; EventUtil.preventDefault(event); localStorage.clear(); if (event.type == "drop") { files = event.dataTransfer.files; len = files.length; if (!/image/.test(files[0].type)) { alert('请上传图片类型的文件'); } if (len > 1) { alert('上传图片数量不能大于1'); } var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); var img = new Image(), //原图 thumbimg = new Image(); //等比缩放后的缩略图 reader.readAsDataURL(files[0]); reader.onload = function (e) { img.src = e.target.result; } //图片对象加载完毕后,对图片进行等比缩放处理。缩放后最大宽度为三百像素 img.onload = function () { var targetWidth, targetHeight; targetWidth = this.width > 300 ? 300 : this.width; targetHeight = targetWidth / this.width * this.height; canvas.width = targetWidth; canvas.height = targetHeight; context.clearRect(0, 0, targetWidth, targetHeight); context.drawImage(img, 0, 0, targetWidth, targetHeight); var tmpSrc = canvas.toDataURL("image/jpeg"); //在本地存储完整的缩略图源 localStorage.setItem('FullImage', tmpSrc); thumbimg.src = tmpSrc; } //此处省略相关代码...... EventUtil.addHandler(droptarget, "dragenter", handleEvent); EventUtil.addHandler(droptarget, "dragover", handleEvent); EventUtil.addHandler(droptarget, "drop", handleEvent); }
The idea of this code is to first obtain the drag area target object droptarget, and register the drag listening event for the droptarget. The EventUtil used in the code is a simple object that I encapsulated for common functions such as adding events to elements and compatible processing of event objects. The following is a simple and simple code for adding registration events. There are many other encapsulations. Readers can add them by themselves. Check, the function is relatively simple.
var EventUtil = { addHandler: function(element, type, handler){ if (element.addEventListener){ element.addEventListener(type, handler, false); } else if (element.attachEvent){ element.attachEvent("on" + type, handler); } else { element["on" + type] = handler; } }, //此处省略代...... }
When the user drags the image file to the area target object droptarget, the event object of the droptarget obtains the file information through event.dataTransfer.files and filters the file (limited to image content, and at most There can only be one picture). After getting the file content, use the FileReader object reader to read the file content, use its readAsDataURL method to read the base64 content of the image, and assign it to the src attribute of the Image object img. Then you can wait until the img object is initialized and loaded, so that the canvas can process the img. The next step is taken care of. There is an important point that needs to be explained here: Be sure to wait until the img is loaded before using canvas for the next step of processing, otherwise the image may be damaged. The reason is: When the src attribute of img reads the base64 content of the image file, the canvas may start processing the image (the image at this time is incomplete) before the content is loaded into the memory. . So we can see that canvas processes images in the img.onload method. This will happen later in the program, so we won’t go into details later.
In the first step, we completed reading the content of the dragged file and successfully loaded it into the Image object img. Next, we use canvas to scale the image proportionally. The strategy we adopt is to limit the maximum width of the image to 300 pixels. Let’s take a look at this part of the code again:
img.onload = function () { var targetWidth, targetHeight; targetWidth = this.width > 300 ? 300 : this.width; targetHeight = targetWidth / this.width * this.height; canvas.width = targetWidth; canvas.height = targetHeight; context.clearRect(0, 0, targetWidth, targetHeight); context.drawImage(img, 0, 0, targetWidth, targetHeight); var tmpSrc = canvas.toDataURL("image/jpeg"); //在本地存储完整的缩略图源 localStorage.setItem('FullImage', tmpSrc); thumbimg.src = tmpSrc; }
确定了缩放后的宽度targetWidth和高度targetHeight之后,我们使用canvas的drawImage方法对图像进行压缩,在这之前我们最好先使用画布的clearRect对画布进行一次清理。对图片等比缩放以后,使用canvas的toDataURL方法,获取到缩放图的base64内容,赋给新的缩放图Image对象thumbimg的src属性,待缩放图加载完毕,进行下一步的切割处理。缩放图的base64内容使用localStorage存储,键名为"FullImage"。浏览器的本地存储localStorage是硬存储,在浏览器刷新之后内容不会丢失,这样我们就可以在游戏过程中保持数据状态,这点稍后再详细讲解,我们需要知道的是localStorage是有大小限制的,最大为5M。这也是为什么我们先对图片进行压缩,减少存储数据大小,保存缩放图base64内容的原因。关于开发过程中存储哪些内容,下一小节会配有图例详细说明。
生成缩略图之后要做的工作就是对缩略图进行切割了,同样的也是使用canvas的drawImage方法,而且相应的处理必须放在缩略图加载完成之后(即thumbimg.onload)进行处理,原因前面我们已经说过。下面我们再来详细分析一下源代码吧:
thumbimg.onload = function () { //每一个切片的宽高[切割成3*4格式] var sliceWidth, sliceHeight, sliceBase64, n = 0, outputElement = '', sliceWidth = this.width / 3, sliceHeight = this.height / 4, sliceElements = []; canvas.width = sliceWidth; canvas.height = sliceHeight; for (var j = 0; j <img alt="Use canvas in javascript to implement puzzle game" >"; //根据随机数打乱图片顺序 (Math.random() > 0.5) ? sliceElements.push(newElement) : sliceElements.unshift(newElement); n++; } } //拼接元素 for (var k = 0, len = sliceElements.length; k <p>上面的代码对于大家来说不难理解,就是将缩略图分割成12个切片,这里我给大家解释一下几个容易困惑的地方:</p>
1.为什么我们再切割图片的时候,代码如下,先从列开始循环?
for (var j = 0; j <p>这个问题大家仔细想一想就明白了,我们将图片进行切割的时候,要记录下来每一个图片切片的原有顺序。在程序中我们使用 n 来表示图片切片的原有顺序,而且这个n记录在了每一个图片切片的元素的name属性中。在后续的游戏过程中我们可以使用元素的getAttribute('name')方法取出 n 的值,来判断图片切片是否都被拖动到了正确的位置,以此来判断游戏是否结束,现在讲起这个问题可能还会有些迷惑,我们后边还会再详细探讨,我给出一张图帮助大家理解图片切片位置序号信息n:</p><p><span class="img-wrap"><img src="https://img.php.cn//upload/image/549/348/526/1536570012356680.png" title="1536570012356680.png" alt="Use canvas in javascript to implement puzzle game"></span></p><p>序号n从零开始是为了和javascript中的getElementsByTagName()选择的子元素坐标保持一致。</p>
2 我们第3步实现的目的不仅是将缩略图切割成小切片,还要将这些图片切片打乱顺序,代码程序中这一点是怎样实现的?
阅读代码程序我们知道,我们每生成一个切片,就会构造一个元素节点: newElement = "<li name='\""' n style='\"margin:3px;\"'><img src="%5C%22%22" slicebase64 style="max-width:90%"display:block;\"' alt="Use canvas in javascript to implement puzzle game" ></li>";
。我们在是在外部先声明了一个放新节点的数组sliceElements,我们每生成一个新的元素节点,就会把它放到sliceElements数组中,但是我们向sliceElements头部还是尾部添加这个新节点则是随机的,代码是这样的:
(Math.random() > 0.5) ? sliceElements.push(newElement) : sliceElements.unshift(newElement);
我们知道Math.random()生成一个[0, 1)之间的数,所以再canvas将缩略图裁切成切片以后,根据这些切片生成的web节点顺序是打乱的。打乱顺序以后重新组装节点:
//拼接元素 for (var k = 0, len = sliceElements.length; k <p>然后再将节点添加到web页面中,也就自然而然出现了图片切片被打乱的样子了。</p>
3.我们根据缩略图切片生成的DOM节点是动态添加的元素,怎样给这样动态元素绑定事件呢?我们的项目中为每个缩略图切片DOM节点绑定的事件是“拖动交换”,和其他节点都有关系,我们要保证所有的节点都加载后再对事件进行绑定,我们又是怎样做到的呢?
下面的一行代码,虽然简单,但是用的非常巧妙:
(output.innerHTML = outputElement) && beginGamesInit();
有开发经验的同学都知道 && 和 || 是短路运算符,代码中的含义是:只有当切片元素节点都添加到
WEB页面之后,才会初始化为这些节点绑定事件。
代码中多次用到了本地存储,下面我们来详细解释一下本游戏开发过程中都有哪些信息需要存储,为什么要存储?下面是我给出的需要存储的信息图示例(从浏览器控制台获取):
浏览器本地存储localStorage使用key:value形式存储,从图中我们看到我们本次存储的内容有:
FullImage:图片缩略图base64编码。
imageWidth:拖拽区域图片的宽度。
imageHeight:拖拽区域图片的高度。
slice*:每一个缩略图切片的base64内容。
nodePos:保存的是当前缩略图的位置坐标信息。
保存FullImage缩略图的信息是当游戏结束后显示源缩略图时,根据FullImage中的内容展示图片。而imageWidth,imageHeight,slice*,nodePos是为了防止浏览器刷新导致数据丢失所做的存储,当刷新页面的时候,浏览器会根据本地存储的数据加载没有完成的游戏内容。其中nodePos是在为缩略图切片发生拖动时存入本地存储的,并且它随着切片位置的变化而变化,也就是它追踪着游戏的状态,我们在接下来的代码功能展示中会再次说到它。
接下来我们要做的事才是游戏中最重要的部分,还是先来分析一下代码,首先是事件注册前的初始化工作:
//游戏开始初始化 function beginGamesInit() { aLi = output.getElementsByTagName("li"); for (var i = 0; i <p>可以看到这部分初始化绑定事件代码所做的事情是:记录每一个图片切片对象的位置坐标相关信息记录到对象属性中,并为每一个对象都注册拖拽事件,对象的集合由aLi数组统一管理。这里值得一提的是图片切片的位置信息index记录的是切片现在所处的位置,而我们前边所提到的图片切片name属性所保存的信息n则是图片切片原本应该所处的位置,在游戏还没有结束之前,它们不一定相等。待所有的图片切片name属性所保存的值和其属性index都相等时,游戏才算结束(因为用户已经正确完成了图片的拼接),下面的代码就是用来判断游戏状态是否结束的,看起来更直观一些:</p><pre class="brush:php;toolbar:false">//判断游戏是否结束 function gameIsEnd() { for (var i = 0, len = aLi.length; i <p>下面我们还是详细说一说拖拽交换代码相关逻辑吧,拖拽交换的代码如下图所示:</p><pre class="brush:php;toolbar:false">//拖拽 function setDrag(obj) { obj.onmouseover = function () { obj.style.cursor = "move"; console.log(obj.index); } obj.onmousedown = function (event) { var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; obj.style.zIndex = minZindex++; //当鼠标按下时计算鼠标与拖拽对象的距离 disX = event.clientX + scrollLeft - obj.offsetLeft; disY = event.clientY + scrollTop - obj.offsetTop; document.onmousemove = function (event) { //当鼠标拖动时计算p的位置 var l = event.clientX - disX + scrollLeft; var t = event.clientY - disY + scrollTop; obj.style.left = l + "px"; obj.style.top = t + "px"; for (var i = 0; i <p>这段代码所实现的功能是这样子的:拖动一个图片切片,当它与其它的图片切片有碰撞重叠的时候,就和与其左上角距离最近的一个图片切片交换位置,并交换其位置信息index,更新本地存储信息中的nodePos。移动完成之后判断游戏是否结束,若没有,则期待下一次用户的拖拽交换。<br>下面我来解释一下这段代码中比较难理解的几个点:</p>
1.图片切片在被拖动的过程中是怎样判断是否和其它图片切片发生碰撞的?这就是典型的碰撞检测问题。
程序中实现碰撞检测的代码是这样的:
//碰撞检测 function colTest(obj1, obj2) { var t1 = obj1.offsetTop; var r1 = obj1.offsetWidth + obj1.offsetLeft; var b1 = obj1.offsetHeight + obj1.offsetTop; var l1 = obj1.offsetLeft; var t2 = obj2.offsetTop; var r2 = obj2.offsetWidth + obj2.offsetLeft; var b2 = obj2.offsetHeight + obj2.offsetTop; var l2 = obj2.offsetLeft; `if (t1 > b2 || r1 r2)` { return false; } else { return true; } }
这段代码看似信息量很少,其实也很好理解,判断两个图片切片是否发生碰撞,只要将它们没有发生碰撞的情形排除掉就可以了。这有点类似与逻辑中的非是即否,两个切片又确实只可能存在两种情况:碰撞、不碰撞。图中的这段代码是判断不碰撞的情况:if (t1 > b2 || r1 r2)
,返回false, else 返回true。
2.碰撞检测完成了之后,图片切片之间又是怎样寻找左上角定点距离最近的元素呢?
代码是这个样子的:
//勾股定理求距离(左上角的距离) function getDis(obj1, obj2) { var a = obj1.offsetLeft - obj2.offsetLeft; var b = obj1.offsetTop - obj2.offsetTop; return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)); } //找到距离最近的 function findMin(obj) { var minDis = 999999999; var minIndex = -1; for (var i = 0; i <p>因为都是矩形区块,所以计算左上角的距离使用勾股定理,这点相信大家都能明白。查找距离最近的元素原理也很简单,就是遍历所有已经碰撞的元素,然后比较根据勾股定理计算出来的最小值,返回元素就可以了。代码中也是使用了比较通用的方法,先声明一个很大的值最为最小值,当有碰撞元素比其小时,再将更小的值最为最小值,遍历完成后,返回最小值的元素就可以了。</p>
3.图片区块每次交换之后,是怎样监控判断游戏是否已经结束的呢?
答案是回调函数,图片切片交换函数通过回调函数来判断游戏是否已经结束,游戏是否结束的判断函数前面我们已经说过。图片切片交换函数就是通过添加gameIsEnd作为回调函数,这样在每次图片切片移动交换完成之后,就判断一下游戏是否结束。图片切片的交换函数还是比较复杂的,有兴趣的同学可以研究一下,下面是其实现代码,大家重点理解其中添加了回调函数监控游戏是否结束就好了。
//通过class获取元素 function getClass(cls){ var ret = []; var els = document.getElementsByTagName("*"); for (var i = 0; i =0则存在; if(els[i].className === cls || els[i].className.indexOf("cls")>=0 || els[i].className.indexOf(" cls")>=0 || els[i].className.indexOf(" cls ")>0){ ret.push(els[i]); } } return ret; } function getStyle(obj,attr){//解决JS兼容问题获取正确的属性值 return obj.currentStyle?obj.currentStyle[attr]:getComputedStyle(obj,false)[attr]; } function gameEnd() { alert('游戏结束!'); } function startMove(obj,json,fun){ clearInterval(obj.timer); obj.timer = setInterval(function(){ var isStop = true; for(var attr in json){ var iCur = 0; //判断运动的是不是透明度值 if(attr=="opacity"){ iCur = parseInt(parseFloat(getStyle(obj,attr))*100); }else{ iCur = parseInt(getStyle(obj,attr)); } var ispeed = (json[attr]-iCur)/8; //运动速度如果大于0则向下取整,如果小于0想上取整; ispeed = ispeed>0?Math.ceil(ispeed):Math.floor(ispeed); //判断所有运动是否全部完成 if(iCur!=json[attr]){ isStop = false; } //运动开始 if(attr=="opacity"){ obj.style.filter = "alpha:(opacity:"+(json[attr]+ispeed)+")"; obj.style.opacity = (json[attr]+ispeed)/100; }else{ obj.style[attr] = iCur+ispeed+"px"; } } //判断是否全部完成 if(isStop){ clearInterval(obj.timer); if(fun){ fun(); } } },30); }
我认为该游戏中值得优化的地方有两个:
1. Add thumbnails to the puzzle mini-game, because thumbnails are helpful in providing ideas for users who play the game. We also save the base64 content of the thumbnail in the browser's local storage, so it is easy to implement.
2. Caching is sometimes very painful. For example, some users want to start over in the game, but our mini-game is only cleared after the game is completed. Cache, refresh the page, and the game can be restarted. This gives the user a very bad experience. We can add a reset game button, clear the cache and optimize some logic after the game ends.
Friends who are interested in these functions can try it out.
Related recommendations:
Use javascript to implement web puzzle game
H5 canvas to implement the Snake game
The above is the detailed content of Use canvas in javascript to implement puzzle game. For more information, please follow other related articles on the PHP Chinese website!