Heim  >  Artikel  >  Web-Frontend  >  Verwenden Sie Canvas in Javascript, um Puzzlespiele zu implementieren

Verwenden Sie Canvas in Javascript, um Puzzlespiele zu implementieren

不言
不言Original
2018-09-10 17:02:459792Durchsuche

Der Inhalt dieses Artikels befasst sich mit der Verwendung von Canvas in JavaScript, um Puzzlespiele zu implementieren. Freunde in Not können darauf verweisen.

Wenn Sie verschiedene Technologien wie Canvas, natives Drag & Drop und lokale Speicherung in JavaScript verwenden möchten, um ein interessantes Projekt abzuschließen, dann ist dieser Artikel sehr gut für Sie geeignet

1 Einführung und Quellcode

Das Puzzlespiel in diesem Projekt wird mit JavaScript erstellt. Im Vergleich zu ähnlichen Funktionen auf der Website verwendet es fortgeschrittenere und umfangreichere technische Punkte, leistungsfähigere Funktionen und enthält auch fortgeschrittenere Ideen in der Programmentwicklung . Konzept, aus diesem Projekt können Sie lernen:

  • FileReader, Bildobjekt und Leinwand zum Komprimieren und Ausschneiden von Bildern.

  • Lernen Sie die am häufigsten verwendeten Kollisionserkennungs-, Statusüberwachungs- und Aktualisierungs- und Aufrechterhaltungsmethoden für die Statusverarbeitung in der Minispielentwicklung.

  • Verstehen Sie die Details von Drag-and-Drop-Austauschelementen und erfahren Sie, wie Sie mit dynamischen Elementbindungsereignissen und Rückruffunktionen umgehen.

Projektquellcode-github

Das Folgende ist ein Beispiel der Spieloberfläche:

Verwenden Sie Canvas in Javascript, um Puzzlespiele zu implementieren

2 Umsetzungsideen

Gemäß dem Spielschnittstellendiagramm können wir die Fertigstellung eines so kleinen Spiels in die folgenden Schritte unterteilen:

  • 1. Ziehen Sie das Bild in den angegebenen Bereich, lesen Sie den Base64-Inhalt des Bildes und fügen Sie ihn dann dem Bildobjekt hinzu

  • 2 Das Bildobjekt wird geladen. Verwenden Sie Canvas, um das Bild zu bearbeiten. Skalieren Sie es proportional, rufen Sie dann den Base64-Inhalt der Miniaturansicht ab, fügen Sie es zu einem anderen Miniaturansicht-Bildobjekt hinzu und speichern Sie den Base64-Inhalt der Miniaturansicht im lokalen Speicher (localStorage)

  • 3. Nachdem das Miniaturbildobjekt geladen wurde, schneiden Sie das Miniaturbild erneut in 3*4 Teile, um es in insgesamt 12 Teilen zu speichern Der Base64-Inhalt jedes ausgeschnittenen Miniaturbilds wird unterbrochen und auf der Webseite mithilfe des img-Tags angezeigt. 4. Nachdem die Miniaturbildausschnitte zur Weboberfläche hinzugefügt wurden, Fügen Sie für jedes Miniaturbild-Slice einen Registrierungs-Drag hinzu. Das Drag-Ereignis ermöglicht den Austausch der Miniaturbild-Slices untereinander. Bei diesem Vorgang wird die Überwachung des Bestellstatus der Miniaturbild-Slices hinzugefügt. Sobald das Puzzle abgeschlossen ist, wird das vollständige Miniaturbild hinzugefügt Wird direkt angezeigt, um das Spiel abzuschließen

  • Aus der obigen Analyse des Minispiel-Produktionsprozesses geht hervor, dass Schritt 4 den Schwerpunkt und die Schwierigkeit bei der Realisierung der Programmfunktion darstellt. Es sind viele kleine Details erforderlich Auf jeden der oben genannten Schritte muss geachtet und besprochen werden. Wenn die Implementierungsdetails der einzelnen Schritte nicht gut sind, können Sie mir gerne eine Nachricht hinterlassen.

  • 3 Detaillierte Erläuterung der Entwicklungsdetails

3.1 Lesen und Laden von Bildinhalten

Wie sieht das Programm im ersten Schritt der Spieleentwicklung aus, nachdem wir das Bild auf das Bild gezogen haben? Wie sieht es mit dem Abrufen von Bildinhaltsinformationen aus? Wie konvertiert das FileReader-Objekt Bildinformationen in Base64-String-Inhalte? Wie initialisiert das Image-Objekt den Ladevorgang, nachdem es den Base64-Inhalt des Bildes abgerufen hat? Lassen Sie uns anhand dieser Fragen den Schlüsselcode untersuchen, der den ersten Schritt im Projekt implementiert.

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);            
}

Die Idee dieses Codes besteht darin, zunächst das Drag-Area-Zielobjekt Droptarget abzurufen und das Drag-Listening-Ereignis für das Droptarget zu registrieren. Das im Code verwendete EventUtil ist ein einfaches Objekt, das ich für allgemeine Funktionen wie das Hinzufügen von Ereignissen zu Elementen und die kompatible Verarbeitung von Ereignisobjekten gekapselt habe. Es gibt viele andere Kapselungen, die der Leser durchführen kann. Die Funktion ist relativ einfach.

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;
        }
    },
    
    //此处省略代......
 }

Wenn der Benutzer die Bilddatei in den Bereich Zielobjekt Droptarget zieht, ruft das Ereignisobjekt des Droptargets die Dateiinformationen über event.dataTransfer.files ab und filtert die Datei (beschränkt auf Bildinhalte und höchstens Es kann nur ein Bild vorhanden sein. Nachdem Sie den Dateiinhalt abgerufen haben, verwenden Sie den FileReader-Objektleser, um den Dateiinhalt zu lesen, verwenden Sie seine readAsDataURL-Methode, um den Base64-Inhalt des Bildes zu lesen, und weisen Sie ihn dem src-Attribut des Bildobjekts img zu. Anschließend können Sie warten, bis das img Das Objekt wird initialisiert und geladen, damit die Leinwand das Bild verarbeiten kann. Der nächste Schritt ist erledigt. Hier gibt es einen wichtigen Punkt, der erklärt werden muss:

Warten Sie unbedingt, bis das Bild geladen ist, bevor Sie Canvas für den nächsten Verarbeitungsschritt verwenden, da das Bild sonst beschädigt werden kann. Der Grund ist: Wenn das src-Attribut von img den Base64-Inhalt der Bilddatei liest, beginnt Canvas möglicherweise mit der Verarbeitung des Bildes, bevor der Inhalt in den Speicher geladen wird (das Bild ist zu diesem Zeitpunkt unvollständig)

. Wir können also sehen, dass Canvas Bilder in der img.onload-Methode verarbeitet. Dies wird später im Programm geschehen, daher gehen wir später nicht auf Details ein.

3.2 Bildproportionale Skalierung und lokale Speicherung Im ersten Schritt haben wir den Inhalt der gezogenen Datei vollständig gelesen und ihn erfolgreich in das Bildobjekt img geladen. Als nächstes verwenden wir Canvas, um das Bild proportional zu skalieren. Die Strategie, die wir anwenden, besteht darin, die maximale Breite des Bildes auf 300 Pixel zu begrenzen. Schauen wir uns diesen Teil des Codes noch einmal an:

 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="Verwenden Sie Canvas in Javascript, um Puzzlespiele zu implementieren" >";
                                //根据随机数打乱图片顺序
                                (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="Verwenden Sie Canvas in Javascript, um Puzzlespiele zu implementieren"></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="Verwenden Sie Canvas in Javascript, um Puzzlespiele zu implementieren" ></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 本地信息存储

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

Verwenden Sie Canvas in Javascript, um Puzzlespiele zu implementieren

浏览器本地存储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. Fügen Sie Miniaturansichten zum Puzzle-Minispiel hinzu, da Miniaturansichten hilfreich sind, um Benutzern, die das Spiel spielen, Ideen zu liefern. Außerdem speichern wir den Base64-Inhalt der Miniaturansicht im lokalen Speicher des Browsers, sodass er einfach zu implementieren ist.

  • 2. Das Caching ist manchmal sehr schmerzhaft. Einige Benutzer möchten zum Beispiel noch einmal im Spiel beginnen, aber unser Minispiel wird erst gelöscht, nachdem das Spiel abgeschlossen ist Die Seite wird angezeigt und das Spiel kann neu gestartet werden. Dies gibt dem Benutzer ein sehr schlechtes Erlebnis. Wir können eine Schaltfläche zum Zurücksetzen des Spiels hinzufügen, den Cache leeren und einige Logik optimieren, nachdem das Spiel beendet ist.

Freunde, die sich für diese Funktionen interessieren, können es ausprobieren.

Verwandte Empfehlungen:

Verwenden Sie Javascript, um ein Web-Puzzlespiel zu implementieren

H5-Canvas, um das Snake-Spiel zu implementieren

Das obige ist der detaillierte Inhalt vonVerwenden Sie Canvas in Javascript, um Puzzlespiele zu implementieren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn

In Verbindung stehende Artikel

Mehr sehen