2019年8月25日日曜日

[Enchant.js]テトリスもどきをつくってみたかった...~落ちて2つに破壊する~


こんにちは。

皆さん、「Enchant.js」ってご存知ですか?


「Enchant.js」とは


Enchant.jsとは、ゲームやアプリを簡単に作れるJavaScriptのライブラリで す。
HTML5+JavaScript+CSSのWebアプリを作る際に使えます。


オープンソース(MITライセン ス)なので、無料で使え、また、商用利用などもできます。


Webアプリなので、サーバーにアップロードしておけば、PCだけでなくiPhone・Androidなどのスマホでもアプリを実行できます。

Webアプリの状態でも全画面表示にしてアプリっぽくしたり、あるいはPWAにしてオフライン対応にすることもできます。
もしくは、Apache Cordovaでネイティブアプリにもできます。


要するにマルチプラットフォームなわけです。



こちらの公式サイトからダウンロードできます。

http://enchantjs.com/ja/


Enchant.js本体だけでなく、ゲームに使える画像素材やプラグインなども入っています。


テ■リスをつくりたかったのに...


今回は、このEnchant.jsを使って、テ■リスもどきをつくっていこうと思います。

あくまでも流れを紹介するだけなので、詳しいことは省きます。
チュートリアルではないので、ご了承ください。


ファイルなどの準備

まずは、ファイルなどの準備をしていきます。



最初に用意するのは、HTMLファイルと、JavaScriptファイルですね。


フォルダ構成はこんな感じです。



次に画像の作成を行いました。

ゲーム自体に使う画像と、アイコンにする画像の2つを用意しました。
フリーの画像編集ソフト「GIMP」を用いて作成しております。



ゲームに使う方の画像は、色違いでひとマスずつ作成し、プログラムの方で各プロックを作成する ようにしました。

また、アイコンの方は、スマホ(Android or iPhone)でホーム画面に追加したときに表示されるアプリアイコン画像です。


続きまして、HTMLの中身を用意します。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8" />
    <title>T□TRIS</title>

    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <link rel="apple-touch-icon" href="./img/icon.png">

    <script type="text/javascript" src="./js/lib/enchant.min.js"></script>
    <script type="text/javascript" src="./js/main.js"></script>

    <style>
        body {
            padding: 0;
            margin: 0;
            background: #000000;
        }
    </style>
</head>

<body>

</body>

</html>


こんな感じです。

メインのJavaScriptファイル及びEnchant.jsの読み込み、アイコンの設定、 スマホでホーム画面に追加したときの全画面設定などを行っております。


メインのJavaScriptを書いていく

ゲームを動作させるメインのプログラムをJavaScriptで書いていきます。

実装させる基本的な処理はこちらです。

  • スタート画面作成
  • 枠組みの表示
  • 各ブロック生成
  • テ■リス落下
  • 移動、回転処理
  • 着地処理
  • 一列揃ったら消す
  • スコアを付ける

では、早速書いていきましょう。


スタート画面の作成は、特に難しいことはないですね。

シーンを2つ作って、一つをスタート画面、もう一方をゲームメイン画面とします。
最初はスタートのシーンを表示して、それをタッチするとメイン画面に切り替えるようにしてあります。



枠組みは、Enchant.jsのマップ機能を使用して作成しました。
最初に用意した画像のうち、茶色のマスを使用しています。



次に、ブロックの生成ですね。


最初に、このような画像を用意したと思います。

一番左のマスは、先ほど枠組みとして使用しましたから、テ■リスのブロック生成には、残りの4 色を用います。


作るブロックの形は、以下の5種類です。
色は気にしないでください。
形ごとに4色分のブロックを作りますので、合計20種のブロックができます。(本家だと形ごとに色が決まってたりしましたっけ?そのへんあやふやです みません。)



形の種類の数が本家と違うと思いますが、著作権の問題のためです。
そのまま同じにはできないわけです。


ではこの部分のソースコードです。

var tetris_map = [
                [
                    [1, 1, 1, 1]
                ],
                [
                    [1, 1],
                    [1, 1]
                ],
                [
                    [0, 1, 0],
                    [1, 1, 1]
                ],
                [
                    [1, 0, 0],
                    [1, 1, 1]
                ],
                [
                    [1, 1, 0],
                    [0, 1, 1]
                ]
            ];

            var tetris_surface = new Array();


            for (var i = 0; i <= 3; i++) {
                for (var j = 0; j <= 4; j++) {

                    var tmp_surface = new Surface(tetris_map[j][0].length * 32, tetris_map[j].length * 32);

                    for (var k = 0; k < tetris_map[j].length; k++) {
                        for (var l = 0; l < tetris_map[j][k].length; l++) {

                            if (tetris_map[j][k][l] == 1) {
                                tmp_surface.draw(game.assets['./img/tetris.png'], (i + 1) * 32, 0, 32, 32, l * 32, k * 32, 32, 32);
                            }

                        }
                    }

                    tetris_surface.push(tmp_surface);

                }
            }


Surfaceというのは、Canvasをラップしたもので、Enchant.js上で Canvasを使用するのに使います
Spriteのimageに代入することで、画像として表示できます。


ここでは、あらかじめ配列にブロックの形を用意しておいて、4重ループで、それに応じて20のブロックを生成し、tetris_surfaceという 配列にプッシュしています。


次に行きます。

テトリスの落下は、特に難しいことはないですね。
ただy座標を足していくだけです。

個人的にはなめらかに動いてほしくないので、毎フレームちょっとずつ動かすのではなく、何フ レームかごとに1マス分足しています。

この何フレームかごとというのは、if分と変数を使って処理しています。


では次にいきましょう。


次は移動回転処理ですね。

これが地味に難しかったです。


まず操作方法ですが、PCとスマホのどちらでも動くようにしたいので、キー操作とタッチ操作の両方を実装します。


<PCの場合>
・方向キーの左右で移動
・方向キーの上で右回転、「/」キーで左回転
・方向キーの下で加速落下


<スマホの場合>

・画面のタップで移動
(左半分をタップすれば左、右半分なら右)


・左から右へドラッグで、右回転
右から左へドラッグで、左回転


・上から下へドラッグで、加速落下



操作の部分はそこまで難しくないです。
が、問題は実際の回転の部分なのです。


正方形のブロックは回転の中心がちょうどいいところにくるので大丈夫なのですが、そのほかのブロックは回転の中心が微妙なところにくるので、回転した 瞬間、枠組みの配列に対して、ズレが生じるのです。


ということで、回転後に計算した分のずれを、x軸、y軸に足して、調節することにしました。


tetrisという変数は、メインの動かしているスプライトです。

tetris_wとtetris_hは、それぞれ現在のブロックの幅と高さが入る変数です。
単純にtetris.widthとtetris.heightでは、回転した後も値が変わらず、現在表示されている状態での幅や高さにならないので、 わざわざこのような変数を用意して、回転に応じて値を調節しています。

tetris_center_xは、スプライトの中心のx座標です。
回転処理をしているうちにだんだん右に移動していったり、左にいったりなどということが起こってしまったので、この変数を使用して、それを防止してい ます。

これらの変数はあらかじめ宣言し、落下するブロックを生成した段階で、値を代入しています。


ソースコードを見てもらった方が早いですね。


var change_wh = function () {
    var tmp = tetris_w;
    tetris_w = tetris_h;
    tetris_h = tmp;
};

var tetris_left_rote = function () {
    tetris.rotation -= 90;
    change_wh();
    tetris.x = Math.floor((tetris.x + (tetris.width - tetris_w) / 2 - 4) / 32) * 32 + 4 - (tetris.width - tetris_w) / 2;
    tetris.y = Math.round((tetris.y + (tetris.height - tetris_h) / 2 - 80) / 32) * 32 + 80 - (tetris.height - tetris_h) / 2;

    if (tetris_center_x - 32 >= tetris.x + tetris.width / 2) {
        tetris.x += 32;
        tetris_center_x = tetris.x + tetris.width / 2
    }

    if (tetris.x + ((tetris.width - tetris_w) / 2) < 36) {
        tetris.x = 36 - (tetris.width - tetris_w) / 2;
    }
    if (tetris.x + tetris.width - ((tetris.width - tetris_w) / 2) > 324) {
        tetris.x = 324 - (tetris.width - ((tetris.width - tetris_w) / 2));
    }
};
var tetris_right_rote = function () {
    tetris.rotation += 90;
    change_wh();
    tetris.x = Math.ceil((tetris.x + (tetris.width - tetris_w) / 2 - 4) / 32) * 32 + 4 - (tetris.width - tetris_w) / 2;
    tetris.y = Math.round((tetris.y + (tetris.height - tetris_h) / 2 - 80) / 32) * 32 + 80 - (tetris.height - tetris_h) / 2;

    if (tetris_center_x + 32 <= tetris.x + tetris.width / 2) {
        tetris.x -= 32;
        tetris_center_x = tetris.x + tetris.width / 2
    }

    if (tetris.x + ((tetris.width - tetris_w) / 2) < 36) {
        tetris.x = 36 - (tetris.width - tetris_w) / 2;
    }
    if (tetris.x + tetris.width - ((tetris.width - tetris_w) / 2) > 324) {
        tetris.x = 324 - (tetris.width - ((tetris.width - tetris_w) / 2));
    }
};


回転した後に左右の枠をはみ出た際の処理も書いています。

移動でも同様な感じでズレの判定をしています。



では、次に行きましょう。

次は着地処理及び、消去処理ですが、ここで問題発生です。

ここら辺の書き方がよくわからなかった。
僕には少し早かったみたいです。


ということで、この部分の処理をやめて、違う処理を入れました。

落下し、着地した瞬間、こんな感じにします。



地面についた瞬間、2つに破壊されるんですね。



しょうもな...


ということで、やっていきましょう。


テ■リスを2つに破壊する



では、どうやって先ほどのように割るのかというと、JavaScriptでCanvasのピク セル操作を行うことで、破壊されたように見せます。


CanvasのコンテキストのgetImageDataで指定した範囲のピクセルデータを取得し、putImageDataで逆に書き出します。


実際に、プログラムを見てもらいましょう。

var mod = function (i, j) {
    return (i % j) < 0 ? (i % j) + 0 + (j < 0 ? -j : j) : (i % j + 0);
};

var destroy = function (s) {

    var speed = s;

    tetris.visible = false;

    var destroy_imagedata = tetris.image.context.getImageData(0, 0, tetris.width, tetris.height);

    var destroy_surface_left = new Surface(tetris.width, tetris.height);
    var destroy_surface_right = new Surface(tetris.width, tetris.height);

    var destroy_left = new Sprite(tetris.width, tetris.height);
    var destroy_right = new Sprite(tetris.width, tetris.height);


    main.addChild(destroy_left);
    main.addChild(destroy_right);



    destroy_left.x = tetris.x;
    destroy_left.y = tetris.y;
    destroy_left.rotation = tetris.rotation;
    destroy_right.x = tetris.x;
    destroy_right.y = tetris.y;
    destroy_right.rotation = tetris.rotation;


    switch (mod(tetris.rotation / 90, 4)) {
        case 0:
            for (var h = 0; h < tetris.height; h++) {
                var r = Math.floor(Math.random() * 20) - 9;

                for (var w = 0; w < tetris.width; w++) {

                    if (w == (tetris.width / 2) + r - 1) {
                        destroy_surface_left.context.putImageData(destroy_imagedata, 0, 0, 0, h, w + 1, 1);
                    }
                    if (w == tetris.width - 1) {
                        destroy_surface_right.context.putImageData(destroy_imagedata, 0, 0, (tetris.width / 2) + r, h, tetris.width - ((tetris.width / 2) + r), 1);
                    }
                }
            }
            break;
        case 1:
            for (var w = 0; w < tetris.width; w++) {
                var r = Math.floor(Math.random() * 20) - 9;

                for (var h = 0; h < tetris.height; h++) {

                    if (h == (tetris.height / 2) + r - 1) {
                        destroy_surface_right.context.putImageData(destroy_imagedata, 0, 0, w, 0, 1, h + 1);
                    }
                    if (h == tetris.height - 1) {
                        destroy_surface_left.context.putImageData(destroy_imagedata, 0, 0, w, (tetris.height / 2) + r, 1, tetris.height - ((tetris.height / 2) + r));
                    }

                }

            }
            break;
        case 2:
            for (var h = 0; h < tetris.height; h++) {
                var r = Math.floor(Math.random() * 20) - 9;

                for (var w = 0; w < tetris.width; w++) {

                    if (w == (tetris.width / 2) + r - 1) {
                        destroy_surface_right.context.putImageData(destroy_imagedata, 0, 0, 0, h, w + 1, 1);
                    }
                    if (w == tetris.width - 1) {
                        destroy_surface_left.context.putImageData(destroy_imagedata, 0, 0, (tetris.width / 2) + r, h, tetris.width - ((tetris.width / 2) + r), 1);
                    }
                }
            }
            break;
        case 3:
            for (var w = 0; w < tetris.width; w++) {
                var r = Math.floor(Math.random() * 20) - 9;

                for (var h = 0; h < tetris.height; h++) {

                    if (h == (tetris.height / 2) + r - 1) {
                        destroy_surface_left.context.putImageData(destroy_imagedata, 0, 0, w, 0, 1, h + 1);
                    }
                    if (h == tetris.height - 1) {
                        destroy_surface_right.context.putImageData(destroy_imagedata, 0, 0, w, (tetris.height / 2) + r, 1, tetris.height - ((tetris.height / 2) + r));
                    }

                }

            }
            break;

    }



    destroy_left.image = destroy_surface_left;
    destroy_right.image = destroy_surface_right;


    destroy_left.tl.rotateBy(-270, speed);
    destroy_right.tl.rotateBy(270, speed)
    destroy_left.tl.and();
    destroy_right.tl.and();
    destroy_left.tl.moveBy(-200, 300, speed);
    destroy_right.tl.moveBy(200, 300, speed);

    destroy_right.tl.then(function () {
        main.removeChild(destroy_left);
        main.removeChild(destroy_right);

        createTetris();
        frame_cnt = 0;
        frame_max = 30;
        touch_down = false;

        down_fg = true;
    });

};


まず、破壊された後、左側に落ちるスプライトと、右側に落ちるスプライトの2つを用意します。


ブロックの回転具合によって、分割する箇所が異なるので、そこのところを場合分けで書いていきます。



いよいよピクセル処理の出番です。


Enchant.jsのSurfaceの方にも、getPixelとsetPixelというピクセル処理の機能が備わっているのですが、こちらは、1 ピクセルごとに処理するため、動作がかなり重いです。


なので、今回はラップされていないもともとの「getImageData」と「putImageData」を使用します。



回転による場合分けをした後、2重ループを使い、ブロックの幅をランダムな長さにしながら、左右のスプライトに分けています。



分けた後は、もとのメインスプライトを非表示にし、2つのスプライトをうまく動かして、落下させます。

落下スピードに応じて、破壊後の動きにも変化をつけています。


破壊処理は以上ですね。


スコアは適当につけています。


かなり、はしょったわかりにくい説明だったと思います。
ソースコードもこれだけではよくわからないでしょう。


ということで、ソースコードの全体は、こちらから見れます。

http://kumakuma.life.coocan.jp/DestroyTetris/js/main.js


完成



プレイ動画(?)みたいなのは、この動画の後半にあります。


また、こちらのURLから遊ぶ(?)ことができます。

http://kumakuma.life.coocan.jp/DestroyTetris/


スマホで開いて、ホーム画面に追加するとアプリっぽくなります。
オフラインには対応してませんが。


では、これで終わります。
Enchant.js面白いので、ぜひ触ってみてください。


人気の記事