読者です 読者をやめる 読者になる 読者になる

生涯未熟

プログラミングをちょこちょこと。

JavaScriptでテトリスっぽいもの#4

JavaScript 勉強連載 HTML

前回はブロックを操作するところまで作成しました。
今回はそのブロックを自動に動くところから始めましょう。

まずはサンプルプログラムです。

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
        <title>Sample</title>
        <script type="text/javascript">
            var ctx;
            var block = [
                [1,1],
                [0,1],
                [0,1]
            ];
            var posx = 0, posy = 0;

            function load() {
                var elmTarget = document.getElementById('target');
                ctx = elmTarget.getContext('2d');
                ctx.fillStyle = 'rgb(255, 0, 0)';

                setInterval(paint, 200);
            }

            function paint() {
                ctx.clearRect(0, 0, 200, 400);
                for (var y = 0; y < block.length; y ++) {
                    for (var x = 0; x < block[y].length; x ++) {
                        if (block[y][x]) {
                            ctx.fillRect((x + posx) * 20, (y + posy) * 20, 20, 20);
                        }
                    }
                }
                posy = posy + 1;
            }
        </script>
    </head>

    <body onload="load()">
        <canvas id="target" style="border: 5px solid gray" width="200" height="400"></canvas>
    </body>
</html>

ほぼ前回のプログラムと一緒ですが、ボタン操作が無くなっているのとsetIntervalという新たなメソッドが登場しました。

setIntervalは、第2引数で指定された時間(ms)ごとに第1引数の関数を実行するという命令です。
これを用いれば前回ではボタンをクリックするたびに移動していた図形が200msごとに移動するようになります。




次に、プログラムの記述をスマートにするために描画に関する部分を関数にします。

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
        <title>Sample</title>
        <script type="text/javascript">
            var ctx;
            var block = [
                [1,1],
                [0,1],
                [0,1]
            ];
            var posx = 0, posy = 0;

            function load() {
                var elmTarget = document.getElementById('target');
                ctx = elmTarget.getContext('2d');
                ctx.fillStyle = 'rgb(255, 0, 0)';

                setInterval(paint, 200);
            }

            function paintMatrix(matrix, offsetx, offsety) {
                for (var y = 0; y < matrix.length; y ++) {
                    for (var x = 0; x < matrix[y].length; x ++) {
                        if (matrix[y][x]) {
                            ctx.fillRect((x + offsetx) * 20, (y + offsety) * 20, 20, 20);
                        }
                    }
                }
            }

            function paint() {
                ctx.clearRect(0, 0, 200, 400);
                paintMatrix(block, posx, posy);
                posy = posy + 1;
            }
        </script>
    </head>

    <body onload="load()">
        <canvas id="target" style="border: 5px solid gray" width="200" height="400"></canvas>
    </body>
</html>

前回まではpaint内にあった一部がpaintMatrixとして新たに関数として定義されました。
これによって実際の実行する部分であるpaint関数がスマートになります。
特に実行する分には変化が無いのですが、プログラミングをするにおいてはこのように細かく分けておいた方が保守性に関しても向上します。



では、次はcanvasで示しているようなエリアをJavaScript内でも設定します。

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
        <title>Sample</title>
        <script type="text/javascript">
            var ctx;
            var block = [
                [1,1],
                [0,1],
                [0,1]
            ];
            var posx = 0, posy = 0;
            var map, mapWidth = 10, mapHeight = 20;

            function load() {
                var elmTarget = document.getElementById('target');
                ctx = elmTarget.getContext('2d');

                map = [];
                for (var y = 0; y < mapHeight; y++) {
                    map[y] = [];
                    for (var x = 0; x < mapWidth; x++) {
                        map[y][x] = 0;
                    }
                }

                setInterval(paint, 200);
            }

            function paintMatrix(matrix, offsetx, offsety, color) {
                ctx.fillStyle = color;
                for (var y = 0; y < matrix.length; y ++) {
                    for (var x = 0; x < matrix[y].length; x ++) {
                        if (matrix[y][x]) {
                            ctx.fillRect((x + offsetx) * 20, (y + offsety) * 20, 20, 20);
                        }
                    }
                }
            }

            function paint() {
                ctx.clearRect(0, 0, 200, 400);
                paintMatrix(map, 0, 0, 'rgb(128, 128, 128)');
                paintMatrix(block, posx, posy, 'rgb(255, 0, 0)');
                posy = posy + 1;
            }
        </script>
    </head>

    <body onload="load()">
        <canvas id="target" style="border: 5px solid gray" width="200" height="400"></canvas>
    </body>
</html>


新たにmapという変数が増えました。
これは、実際に四角形の一片を20×20としているのでcanvasのwidthが200、heightが400と考えた際に横に10、縦に20個四角形が入るという考えでmapWidth=10、mapHeight=20としているのです。

さらに、その下のmapの操作は2次元配列を作成しています。
blockの時は直に書いているじゃないか!との意見もありそうですが、2次元配列の初期化(全てが0)にするには以下のように書かなければいけません。

map = [
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
]

と、とても長いものになってしまい、プログラム上よろしくありません。

しかし、上を見たら分かるようにmapで擬似的にゲーム上の配列を再現していることが分かります。
そして今回は同時にブロックの色についてもいじっています。

paintMatricsの引数に新たにcolorがという引数が加えられています。
これは関数paintから色を変えられるように設定するためなのですが、実際に使用するのは次のプログラムです。




次は、実際のテトリスの挙動に近づけるためエリア内にブロックが溜まるようにします。

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
        <title>Sample</title>
        <script type="text/javascript">
            var ctx;
            var block = [
                [1,1],
                [0,1],
                [0,1]
            ];
            var posx = 0, posy = 0;
            var map, mapWidth = 10, mapHeight = 20;

            function load() {
                var elmTarget = document.getElementById('target');
                ctx = elmTarget.getContext('2d');

                map = [];
                for (var y = 0; y < mapHeight; y++) {
                    map[y] = [];
                    for (var x = 0; x < mapWidth; x++) {
                        map[y][x] = 0;
                    }
                }
                setInterval(paint, 200);
            }

            function paintMatrix(matrix, offsetx, offsety, color) {
                ctx.fillStyle = color;
                for (var y = 0; y < matrix.length; y ++) {
                    for (var x = 0; x < matrix[y].length; x ++) {
                        if (matrix[y][x]) {
                            ctx.fillRect((x + offsetx) * 20, (y + offsety) * 20, 20, 20);
                        }
                    }
                }
            }

            function check(map, block, offsetx, offsety) {
                if (offsetx < 0 || offsety < 0 ||
                    mapHeight < offsety + block.length ||
                    mapWidth  < offsetx + block[0].length) {
                    return false;
                }
                for (var y = 0; y < block.length; y ++) {
                    for (var x = 0; x < block[y].length; x ++) {
                        if (block[y][x] && map[y + offsety][x + offsetx]) { 
                            return false;
                        }
                    }
                }
                return true;
            }

            function mergeMatrix(map, block, offsetx, offsety) {
                for (var y = 0; y < mapHeight; y ++) {
                    for (var x = 0; x < mapWidth; x ++) {
                        if (block[y - offsety] && block[y - offsety][x - offsetx]) {
                            map[y][x]++;
                        }
                    }
                }
            }

            function paint() {
                ctx.clearRect(0, 0, 200, 400);
                paintMatrix(map, 0, 0, 'rgb(128, 128, 128)');
                paintMatrix(block, posx, posy, 'rgb(255, 0, 0)');

                if (check(map, block, posx, posy + 1)) {
                    posy = posy + 1;
                }
                else {
                    mergeMatrix(map, block, posx, posy);
                    posx = 0; posy = 0;
                }
            }
        </script>
    </head>

    <body onload="load()">
        <canvas id="target" style="border: 5px solid gray" width="200" height="400"></canvas>
    </body>
</html>

今回の追記点は、関数checkと関数mergeMatrics、関数paint内のif文です。

関数checkは、次の移動先がエリア内に留まっているかどうかのチェックを行う関数です。
もし、x軸y軸ともにエリア外にブロックが出るようならposy、posxを+1しないことでエリア外へ出るのを防いでいます。


次に、関数mergeMatricsは関数paint内のif文でもし関数checkがfalseを返した場合に呼び出される関数です。
内容は、もし次の移動でエリア外に出るなら2次元配列mapにそのブロックが溜まったことを表すためエリア外に出る手前の場所を1にしています。
今回の場合の1つ目のブロックが積もる時、mapはこうなっています。

map = [
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [1,1,0,0,0,0,0,0,0,0],
  [0,1,0,0,0,0,0,0,0,0],
  [0,1,0,0,0,0,0,0,0,0],
]

左下にが1になっているのが分かりますね。
そして、この1になっている部分がどうして灰色で表示されるかというと、プログラムの関数paintを見てください。
この関数内でpaintMatrics(map〜という部分がエリア内に積もったブロックを表示しているのです。


今回から色々と複雑になってきたので、まとめていきましょう。

  1. 関数checkでエリア内かエリア外の確認をしている。
  2. 関数mergeMatricsmapに積もったブロックの座標を示す。
  3. それらをload内のpaintMatricsで常に表示している。

次は、左右にブロックを動かしてみるプログラムを作成します。

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
        <title>Sample</title>
        <script type="text/javascript">
            var ctx;
            var block = [
                [1,1],
                [0,1],
                [0,1]
            ];
            var posx = 0, posy = 0;
            var map, mapWidth = 10, mapHeight = 20;

            function load() {
                var elmTarget = document.getElementById('target');
                ctx = elmTarget.getContext('2d');

                map = [];
                for (var y = 0; y < mapHeight; y++) {
                    map[y] = [];
                    for (var x = 0; x < mapWidth; x++) {
                        map[y][x] = 0;
                    }
                }
                setInterval(paint, 200);
            }

            function paintMatrix(matrix, offsetx, offsety, color) {
                ctx.fillStyle = color;
                for (var y = 0; y < matrix.length; y ++) {
                    for (var x = 0; x < matrix[y].length; x ++) {
                        if (matrix[y][x]) {
                            ctx.fillRect((x + offsetx) * 20, (y + offsety) * 20, 20, 20);
                        }
                    }
                }
            }

            function check(map, block, offsetx, offsety) {
                if (offsetx < 0 || offsety < 0 ||
                    mapHeight < offsety + block.length ||
                    mapWidth < offsetx + block[0].length) {
                    return false;;
                }
                for (var y = 0; y < block.length; y ++) {
                    for (var x = 0; x < block[y].length; x ++) {
                        if (block[y][x] && map[y + offsety][x + offsetx]) { 
                            return false;
                        }
                    }
                }
                return true;
            }

            function mergeMatrix(map, block, offsetx, offsety) {
                for (var y = 0; y < mapHeight; y ++) {
                    for (var x = 0; x < mapWidth; x ++) {
                        if (block[y - offsety] && block[y - offsety][x - offsetx]) {
                            map[y][x]++;
                        }
                    }
                }
            }

            function paint() {
                ctx.clearRect(0, 0, 200, 400);
                paintMatrix(block, posx, posy, 'rgb(255, 0, 0)');
                paintMatrix(map, 0, 0, 'rgb(128, 128, 128)');

                if (check(map, block, posx, posy + 1)) {
                    posy = posy + 1;
                }
                else {
                    mergeMatrix(map, block, posx, posy);
                    posx = 0; posy = 0;
                }
            }

            function key(keyCode) {
                switch (keyCode) {
                    case 39:
                        if (!check(map, block, posx + 1, posy)) {
                            return;
                        }
                        posx = posx + 1;
                        break;
                    case 37:
                        if (!check(map, block, posx - 1, posy)) {
                            return;
                        }
                        posx = posx - 1;
                        break;
                    case 40:
                        var y = posy;
                        while (check(map, block, posx, y)) { y++; }
                        posy = y - 1;
                        break;
                    default:
                        return;
                }
                ctx.clearRect(0, 0, 200, 400);
                paintMatrix(block, posx, posy, 'rgb(255, 0, 0)');
                paintMatrix(map, 0, 0, 'rgb(128, 128, 128)');
            }

        </script>
    </head>

    <body onload="load()" onkeydown="key(event.keyCode)">
        <canvas id="target" style="border: 5px solid gray" width="200" height="400"></canvas>
    </body>
</html>

関数にkeyというのが追加されました。
これはキーボードのキーに振り分けられたキーコードごとにブロックの挙動を決めている関数です。
ここで使われているキーコード39、37、40はそれぞれ→、←、↓のキーを表しています。
もし挙動が何もなければ何も起こらずそのままブロックが自然落下します。

また、キーを押したことによりエリア外に出ないように関数checkも行われます。


そして、キーを押したときにkeyが作動するようにはbodyタグ内のonkeydownで行われています。
キー操作に関するものはonkeydownの他にもonkeyuponkeypressなどがあります。
更に余談ですが、onkeydownの中のeventオブジェクトでは他にも様々な動作を取得出来ます。
詳しくはこちらを参照のこと。


さて、今回はここまで。
次回は、ブロックの回転から始めたいと思います。

JavaScriptでテトリスっぽいもの#5