ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScriptでキャンバスを使用してパズルゲームを実装する

JavaScriptでキャンバスを使用してパズルゲームを実装する

不言
不言オリジナル
2018-09-10 17:02:459733ブラウズ

この記事の内容は、JavaScript でキャンバスを使用してパズル ゲームを実装する方法についてです。必要な方は参考にしていただければ幸いです。

キャンバス、ネイティブ ドラッグ アンド ドロップ、JavaScript のローカル ストレージなどのさまざまなテクノロジを使用して、興味深いプロジェクトを完成させたい場合は、この記事が最適です

1 概要とソース コード

小さなパズルこのプロジェクトでは、Web サイトの同様の機能と比較して、より高度で豊富な技術点、より強力な機能を使用し、プログラム開発にもより高度なアイデアと概念が含まれているオリジナルのゲームになります。学習可能:

  • FileReader、画像オブジェクト、キャンバスは画像の圧縮と切り取りに使用されます。

  • ミニゲーム開発で最も一般的に使用される衝突検出、ステータス監視、ステータス処理の更新および維持方法を学びます。

  • ドラッグ アンド ドロップ交換要素の詳細を理解し、動的要素バインディング イベントとコールバック関数を処理する方法を学びます。

プロジェクトのソースコード-github

以下はゲームインターフェイスのサンプル画像です:

JavaScriptでキャンバスを使用してパズルゲームを実装する

2 実装アイデア

ゲームインターフェイスの画像に従って、そのような完成を分割できます小さなゲームを次の手順で実行します。 実装:

  • 1. 画像を指定された領域にドラッグし、FileReader オブジェクトを使用して画像の Base64 コンテンツを読み取り、それを Image オブジェクトに追加します

  • 2. Image オブジェクトが読み込まれたら、キャンバスを使用して画像を編集し、比例的に拡大縮小してから、サムネイルの Base64 コンテンツを取得し、別のサムネイル Image オブジェクトに追加して、サムネイルの Base64 コンテンツをローカル ストレージ (localStorage) に保存します

  • 3. サムネイル画像オブジェクトの読み込みが完了したら、もう一度キャンバスを使用してサムネイルを切り取り、合計 12 個のサムネイルをローカル ストレージに保存します。各カットサムネイルの順序をバラバラにし、img を使用します。 ラベルが Web ページに表示されます

  • 4. サムネイルスライスが Web インターフェイスに追加された後、各サムネイルスライスに登録されたドラッグイベントを追加します。このプロセス中に、サムネイル スライスのシーケンス ステータスの監視を追加できます。パズルが完了すると、完全なサムネイルが直接表示され、ゲームが完了します

ミニゲーム制作プロセスのステップ 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を通じてファイル情報を取得し、ファイルをフィルタリングします(画像コンテンツに限定され、存在できるのは1つだけです)ほとんどの写真)。ファイルのコンテンツを取得したら、FileReader オブジェクト リーダーを使用してファイルのコンテンツを読み取り、その readAsDataURL メソッドを使用して画像の Base64 コンテンツを読み取り、それを Image オブジェクト img の src 属性に割り当てます。その後、img が取得されるまで待つことができます。オブジェクトが初期化されてロードされ、キャンバスが画像を処理できるようになります。次のステップが行われます。ここで説明する必要がある重要な点があります:

次の処理ステップでキャンバスを使用する前に、画像がロードされるまで必ず待ってください。そうしないと、画像が破損する可能性があります。その理由は、img の src 属性が画像ファイルの Base64 コンテンツを読み取るときに、コンテンツがメモリにロードされる前にキャンバスが画像の処理を開始する可能性があるためです (この時点の画像は不完全です)。したがって、canvas が img.onload メソッドで画像を処理することがわかります。これはプログラムの後半で行われるため、後で詳しく説明しません。

3.2 画像の比例スケーリングとローカル ストレージ

最初のステップでは、ドラッグされたファイルの内容の読み取りを完了し、それを Image オブジェクト img に正常にロードしました。次に、canvas を使用して画像を比例的に拡大縮小します。この戦略では、画像の最大幅を 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="JavaScriptでキャンバスを使用してパズルゲームを実装する" >";
                                //根据随机数打乱图片顺序
                                (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="JavaScriptでキャンバスを使用してパズルゲームを実装する"></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="JavaScriptでキャンバスを使用してパズルゲームを実装する" ></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 本地信息存储

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

JavaScriptでキャンバスを使用してパズルゲームを実装する

浏览器本地存储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. たとえば、ゲームの一部のユーザーは、ゲームが完了した後にキャッシュをクリアしてページを更新するだけです。再起動。これにより、ユーザーに非常に悪いエクスペリエンスが与えられます。ゲームのリセット ボタンを追加し、ゲーム終了後にキャッシュをクリアしてロジックを最適化することができます。

これらの機能に興味のある友達は試してみてください。

関連する推奨事項:

JavaScript を使用して Web パズル ゲームを実装する

H5 キャンバスを使用してスネーク ミニ ゲームを実装する

以上がJavaScriptでキャンバスを使用してパズルゲームを実装するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。