>  기사  >  웹 프론트엔드  >  자바스크립트에서 캔버스를 사용하여 퍼즐 게임 구현

자바스크립트에서 캔버스를 사용하여 퍼즐 게임 구현

不言
不言원래의
2018-09-10 17:02:459800검색

이 기사의 내용은 JavaScript로 캔버스를 사용하여 퍼즐 게임을 구현하는 것에 대한 내용입니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

JavaScript에서 캔버스, 기본 드래그 앤 드롭, 로컬 저장소 등 여러 기술을 사용하여 흥미로운 프로젝트를 완료하려는 경우 이 문서가 매우 적합할 것입니다

#🎜 🎜# 1 소개 및 소스 코드

이 프로젝트의 퍼즐 게임은 JavaScript를 사용하여 만들어졌습니다. 웹 사이트의 유사한 기능과 비교할 때 더 발전되고 풍부한 기술 포인트와 더 강력한 기능도 포함되어 있습니다. 프로그램 개발 이 프로젝트에서 더 고급 아이디어와 개념을 배울 수 있습니다:

  • FileReader, 이미지 개체 및 캔버스를 사용하여 사진을 압축하고 잘라냅니다.

  • 미니 게임 개발에서 가장 일반적으로 사용되는 충돌 감지, 상태 모니터링, 상태 처리 방법 새로 고침 및 유지 관리에 대해 알아보세요.

  • 드래그 앤 드롭 교환 요소에 대해 자세히 알아보고 동적 요소 바인딩 이벤트 및 콜백 함수를 처리하는 방법을 알아보세요.

프로젝트 소스 코드-github

다음은 게임 인터페이스의 예입니다. #🎜🎜 ##🎜 🎜#

자바스크립트에서 캔버스를 사용하여 퍼즐 게임 구현2 구현 아이디어

게임 인터페이스 다이어그램에 따르면 이러한 완성도를 나눌 수 있습니다. 다음 단계로 작은 게임을 수행합니다.

1. 이미지를 지정된 영역으로 드래그하고 FileReader 개체를 사용하여 이미지의 base64 콘텐츠를 읽습니다. 그런 다음 이미지 개체
    # 🎜🎜#
  • 2에 추가합니다. 이미지 개체가 로드된 후 캔버스를 사용하여 이미지 크기를 비례적으로 조정한 다음 축소판의 base64 콘텐츠를 가져오고 추가합니다. 이를 다른 썸네일 이미지 객체에 저장하고 썸네일의 base64 콘텐츠는 로컬 저장소(localStorage)

  • 3에 저장됩니다. 썸네일 이미지 객체가 로드된 후 캔버스를 사용합니다. 이 게임에서는 썸네일을 3*4로 총 12등분으로 자르고 각 컷 썸네일의 base64 콘텐츠를 저장하기 위해 로컬 스토리지를 사용합니다. img 태그는 웹 페이지

    #🎜🎜 #
  • 4에 표시하는 데 사용됩니다. 썸네일 슬라이스가 웹 인터페이스에 추가된 후 각 썸네일 슬라이스에 대해 등록된 드래그 이벤트를 추가합니다. 썸네일 조각을 서로 교환할 수 있도록 썸네일 조각 순서의 상태를 모니터링하세요. 퍼즐이 완료되면 전체 썸네일이 바로 표시되어 게임을 완료하는 데 중점을 둡니다. 위의 각 단계에는 주의하고 논의해야 할 작은 세부 사항이 많이 있습니다. 이제 각 단계의 구현 세부 사항을 자세히 분석하겠습니다. 의견과 수정 사항을 남겨주시면 감사하겠습니다.

    3 개발 세부사항
  • 3.1 이미지 콘텐츠 읽기 및 로딩

    게임 개발의 첫 번째 단계에서는 이미지를 지정된 위치로 드래그한 후 영역을 선택하면 프로그램은 어떻게 이미지 콘텐츠 정보를 얻나요? fileReader 객체는 어떻게 이미지 정보를 base64 문자열 콘텐츠로 변환합니까? Image 객체가 이미지의 base64 콘텐츠를 가져온 후 로드를 어떻게 초기화합니까? 이러한 질문을 가지고 프로젝트의 첫 번째 단계를 구현하는 핵심 코드를 연구해 보겠습니다.
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);            
}
이 코드의 아이디어는 먼저 드래그 영역 대상 개체 droptarget을 획득하고 droptarget에 대한 드래그 청취 이벤트를 등록하는 것입니다. 코드에 사용된 EventUtil은 요소에 이벤트 추가 및 이벤트 개체의 호환 처리와 같은 일반적인 기능을 위해 캡슐화한 간단한 개체입니다. 다음은 등록 이벤트를 추가하기 위한 간단한 코드입니다. 기능은 비교적 간단합니다.

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;
        }
    },
    
    //此处省略代......
 }
사용자가 이미지 파일을 영역 대상 개체 droptarget으로 드래그하면 droptarget의 이벤트 개체가 event.dataTransfer.files를 통해 파일 정보를 얻고 파일을 필터링합니다(이미지 콘텐츠에 한함). 그리고 최대 하나의 사진만 있을 수 있습니다.) 파일 내용을 가져온 후 FileReader 객체 판독기를 사용하여 파일 내용을 읽고 readAsDataURL 메서드를 사용하여 이미지의 base64 내용을 읽은 다음 이를 Image 객체 img의 src 속성에 할당합니다. 객체가 초기화되고 로드되므로 캔버스가 img를 처리할 수 있습니다. 여기서 설명해야 할 중요한 사항이 있습니다:

다음 처리 단계에서 캔버스를 사용하기 전에 img가 로드될 때까지 기다리십시오. 그렇지 않으면 이미지가 손상될 수 있습니다. 그 이유는 img의 src 속성이 이미지 파일의 base64 콘텐츠를 읽을 때 콘텐츠가 메모리에 로드되기 전에 캔버스가 이미지 처리를 시작할 수 있기 때문입니다(현재 이미지는 불완전합니다)

. 따라서 img.onload 메서드에서 캔버스가 이미지를 처리하는 것을 볼 수 있습니다. 이는 프로그램 후반부에 발생하므로 나중에 자세히 설명하지 않겠습니다.

3.2 이미지 비례 스케일링 및 로컬 저장

첫 번째 단계에서는 드래그된 파일의 내용 읽기를 완료하고 이미지 개체 img 중간에 성공적으로 로드했습니다. 다음으로, 우리가 채택한 전략은 이미지의 최대 너비를 300픽셀로 제한하는 것입니다. 코드의 이 부분을 다시 살펴보겠습니다.

 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内容的原因。关于开发过程中存储哪些内容,下一小节会配有图例详细说明。

3.3 缩略图切割

生成缩略图之后要做的工作就是对缩略图进行切割了,同样的也是使用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="자바스크립트에서 캔버스를 사용하여 퍼즐 게임 구현" >";
                                //根据随机数打乱图片顺序
                                (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="자바스크립트에서 캔버스를 사용하여 퍼즐 게임 구현"></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="자바스크립트에서 캔버스를 사용하여 퍼즐 게임 구현" ></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页面之后,才会初始化为这些节点绑定事件。

3.4 本地信息存储

代码中多次用到了本地存储,下面我们来详细解释一下本游戏开发过程中都有哪些信息需要存储,为什么要存储?下面是我给出的需要存储的信息图示例(从浏览器控制台获取):

자바스크립트에서 캔버스를 사용하여 퍼즐 게임 구현

浏览器本地存储localStorage使用key:value形式存储,从图中我们看到我们本次存储的内容有:

  • FullImage:图片缩略图base64编码。

  • imageWidth:拖拽区域图片的宽度。

  • imageHeight:拖拽区域图片的高度。

  • slice*:每一个缩略图切片的base64内容。

  • nodePos:保存的是当前缩略图的位置坐标信息。

保存FullImage缩略图的信息是当游戏结束后显示源缩略图时,根据FullImage中的内容展示图片。而imageWidth,imageHeight,slice*,nodePos是为了防止浏览器刷新导致数据丢失所做的存储,当刷新页面的时候,浏览器会根据本地存储的数据加载没有完成的游戏内容。其中nodePos是在为缩略图切片发生拖动时存入本地存储的,并且它随着切片位置的变化而变化,也就是它追踪着游戏的状态,我们在接下来的代码功能展示中会再次说到它。

3.5 拖拽事件注册和监控

接下来我们要做的事才是游戏中最重要的部分,还是先来分析一下代码,首先是事件注册前的初始化工作:

//游戏开始初始化
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);
}

4 补充和总结

4.1 游戏中值得完善的功能

我认为该游戏中值得优化的地方有两个:

  • 1. 퍼즐 미니 게임에 썸네일을 추가하세요. 썸네일은 게임을 플레이하는 사용자에게 아이디어를 제공하는 데 도움이 되기 때문입니다. 또한 썸네일의 base64 콘텐츠를 브라우저의 로컬 저장소에 저장하므로 구현이 쉽습니다.

  • 2. 캐싱은 때때로 매우 고통스럽습니다. 예를 들어, 게임의 일부 사용자는 게임을 다시 시작하고 싶어하지만, 우리의 미니 게임은 게임이 완료된 후에만 캐시를 지우고 페이지를 새로 고칩니다. 다시 시작합니다. 이는 사용자에게 매우 나쁜 경험을 제공합니다. 게임 재설정 버튼을 추가하고, 캐시를 지우고, 게임이 끝난 후 일부 로직을 최적화할 수 있습니다.

이러한 기능에 관심이 있는 친구들은 사용해 볼 수 있습니다.

관련 추천:

자바스크립트를 사용하여 웹 퍼즐 게임 구현

H5 캔버스에 스네이크 미니 게임 구현

위 내용은 자바스크립트에서 캔버스를 사용하여 퍼즐 게임 구현의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.