【ぷよプロ】ぷよぷよプログラミングを始めよう(開発編③)- initializeを実装して初期表示をできるようにしよう

みなさんこんにちは!

本記事は以下記事の続きです。

【ぷよプロ】ぷよぷよプログラミングを始めよう(開発編②)- index.htmlを用意しよう

本日は「ぷよプロ」シリーズの第5回、アプリ開発編③をやっていきましょう!

はじめに

確か前回まででindex.htmlを用意したんでしたよね?

いよいよJavaScriptですか。。難しそうな気がしますね。。

ともとも
ともとも

JavaScriptはきちんと学習すれば、アイデアを簡単に実現することができる強力な言語のひとつだよ!

多分大変とか難しいって感じるポイントは、JavaScriptだからではなくて、どの言語でも同じはずだから頑張っていこうね。

今回は、画面の初期表示ができるところまでを実装していきたいと思います。

初期表示ができるまでのステップ

以前次の図でアプリの大まかな構成を説明したと思います。

index.htmlがJavaScriptファイルを読み込むと、まずはgame.jsが起動します。そして、player.jsなどのほかのJavaScriptファイルがgame.jsから呼び出されます。

詳しくはこの後見ていきますが、初期表示時には次のような処理が走っています。

そして各ファイル内のinitialize()にて初期表示に必要な処理を実行していきます。

game.jsの初期表示時処理

まずはgame.jsの初期表示時に実行される処理を見ていきましょう。

game.js
// 起動されたときに呼ばれる関数を登録する
window.addEventListener("load", () => {
    // まずステージを整える
    initialize();

    // ゲームを開始する
    loop();
});

let mode; // ゲームの現在の状況
let frame; // ゲームの現在フレーム(1/60秒ごとに1追加される)
let combinationCount = 0; // 何連鎖かどうか

function initialize() {
    // 画像を準備する
    PuyoImage.initialize();
    // ステージを準備する
    Stage.initialize();
    // ユーザー操作の準備をする
    Player.initialize();
    // シーンを初期状態にセットする
    Score.initialize();
    // スコア表示の準備をする
    mode = 'start';
    // フレームを初期化する
    frame = 0;
}

イベントでユーザ等からの入力を受け付ける

通常は処理を実行する際にプログラムの上から順に実行されます。ただ、ユーザからの操作などを受けてから処理を実行したいことも多々あります。

例えば、ユーザが「会員登録ボタン」をクリックしたら会員登録処理を実施する時にもこのイベントの考え方が必要です。

簡単に言うとイベントとは、処理を実施する為のきっかけのことです。上の例で言うと、「ボタンをクリックすること」が「会員登録処理」を実行するイベントとなっています。

そして、イベントによっては、どんなイベントがあったのかを実施する処理の引数に自動で渡します。例えば、キー入力をする、というイベントの場合、「どんな内容が入力されたのか」を引数に渡します。これを使って「~と入力された場合は、xxxという処理を実行する」とさらに汎用的な実装をすることが可能になります。

JavaScriptで利用可能なイベントは沢山ありますが、よく使うイベントを表にしたので軽い気持ちで見てみましょう。

イベント説明
ロード:loadロードが完了したら実行するwindow.addEventListener(“load”, function() {
ロード時に実施する処理
});
クリック:clickボタンなどをクリックしたら実行するdocument.getElementById(“ボタンのid”).addEventListener(“click”, function() {
クリック時に実施する処理
});
キー入力:keydownキーボードを操作したら実行する
※キーを入力した瞬間に実行
document.addEventListener(“keydown”, function(event){
キーボード操作時に実施する処理
});
キー入力:keyupキーボードを操作したら実行する
※キーを入力して、キーを離した瞬間に実行
document.addEventListener(“keyup”, function(event){
キーボード操作時に実施する処理
});
画面にタッチする:touchstartタッチが開始された瞬間に実行するdocument.addEventListener(“touchstart”, function(event) {
タッチしたら実施する処理
});
画面にタッチして指を動かす:touchmoveタッチしてから指を動かすごとに実行するdocument.addEventListener(“touchmove”, function(event) {
タッチしたら実施する処理
});
画面から指を離すタッチが終了(指が画面から離れる)された瞬間に実行するdocument.addEventListener(“touchend”, function(event) {
タッチしたら実施する処理
});
JavaScriptのイベント

そしてわかるように、game.jsでまず利用されているイベントが、「ロード時に実行する」イベントです。次のコードの部分です。

game.js
// 起動されたときに呼ばれる関数を登録する
window.addEventListener("load", () => {
    // まずステージを整える
    initialize();

    // ゲームを開始する
    loop();
});

まず知っておきたいのは、//で始まる行はコメントです。何を書いてもプログラムは無視します。

そして、イベントの登録です。window.addEventListener("load", ()=>{ 処理 }); でロード時に実施する処理を書いています。ただし実施する処理が、アロー関数で書かれています。

アロー関数とは?

アローとはズバリ=>のことです。=>が矢(アロー)のように見えることからアロー関数と名付けられています。そして、アロー関数とは、関数を簡単に書けるようにした方法のことで、中国の簡体字みたいなものです。

これまで、function(arg) { 処理 }と書いていたのを、(arg) => {}と表現できるようになりました。

そしてこの中では、2つの関数を呼んでいますね。initialize()loop()です。そして本記事で説明していくのがこのinitialize()です。(initiallizeは初期化するという意味)

各ファイルのinitializeを見ていこう

game.jsのinitialize

先ほどのコードの下に次のコードを追記してください。

game.js
let mode; // ゲームの現在の状況
let frame; // ゲームの現在フレーム(1/60秒ごとに1追加される)
let combinationCount = 0; // 何連鎖かどうか

function initialize() {
    // 画像を準備する
    PuyoImage.initialize();
    // ステージを準備する
    Stage.initialize();
    // ユーザー操作の準備をする
    Player.initialize();
    // シーンを初期状態にセットする
    Score.initialize();
    // スコア表示の準備をする
    mode = 'start';
    // フレームを初期化する
    frame = 0;
}

まず、変数を3つ(mode、frame、combination)定義しています。

そして次にinitialize関数を定義しています。initialize()の中では、ほかファイルのinitializeを呼び出して、その後、modeをstartに、frameを0に設定しています。なので、他ファイルのinitializeを見に行きましょう。

puyoimage.jsのinitialize

puyoimage.js
class PuyoImage {

    static initialize() {
        this.puyoImages = [];
        for(let i = 0; i < 5; i++) {
            const image = document.getElementById(`puyo_${i + 1}`);
            image.removeAttribute('id');
            image.width = Config.puyoImgWidth;
            image.height = Config.puyoImgHeight;
            image.style.position = 'absolute';
            this.puyoImages[i] = image;
        }
        this.batankyuImage = document.getElementById('batankyu');
        this.batankyuImage.width = Config.puyoImgWidth * 6;
        this.batankyuImage.style.position = 'absolute';
    }
}

classというのはオブジェクト指向でプログラミングする際に利用するものです。ただし、今回のアプリではあまりオブジェクト指向である意味がないため、細かい説明は割愛します。ただし、staticがつくとメソッド(関数のようなもの)が簡単に呼べるということだけ認識しておいてください。

それでは、static initialize()に移ると、まずは変数this.puyoImages空の配列[]を代入します。そして次にfor文でからの配列にぷよのデータを配列に詰めていきます。

for文の使い方

JavaScriptのfor文は次のように使います。

JavaScript
for(let i = 0; i < 繰り返す回数; i++) {
繰り返しする処理
}

上のコードでは、5回処理が繰り返されます。

for文の中でぷよのデータを取得して、必要な値をぷよにセットしていきます。

const image = document.getElementById(ぷよのid );でぷよを取得します。ここで指定しているぷよのid とは、前回作成したindex.html に書いてあるid のことです。

例えば、idがpuyo_1は緑色のぷよを表しています。次の画像の青枠で囲まれた画像が取得できます。

ぷよを取得後に幅や高さなどcssの属性値を変更しています。(positionをabsoluteに変更)各種の値を変更後、配列にぷよをつめています。

ぷよを詰め終わったら、次は同じ方法で「ばたんきゅー」のHTMLを取得し、CSSを設定します。

ぷよのidを削除している理由について

※コメントをいただき追記している内容です※

image.removeAttribute('id');でidを削除している理由ですが、これはHTMLのお約束によるものです。

HTMLのお約束とは「idは重複させてはいけない」というものです。

なぜ「idは重複させてはいけない」のかというと、idは要素を一意に特定するための要素で、idが重複することを想定していないからです。

例えば、javascriptのライブラリなどを使っているときに、idが重複することをそのライブラリが許容していない場合、不具合が発生してしまいます。

仮にimage.removeAttribute('id');という処理をしなかった場合、同じ色のぷよが複数ステージにある場合、HTML上のidが重複することになります。次の画像はみどりぷよがステージにある場合のイメージです。(idが"puyo_1"のぷよが複数ステージにあることになります)

idが”puyo_1″のみどりぷよが複数ステージに存在することになる

stage.is のinitialize()

次はstage.jsinitialize()を見ていきましょう。

stage.js
class Stage {

    static initialize() {
        // HTML からステージの元となる要素を取得し、大きさを設定する
        const stageElement = document.getElementById("stage");
        stageElement.style.width = Config.puyoImgWidth * Config.stageCols + 'px';
        stageElement.style.height = Config.puyoImgHeight * Config.stageRows + 'px';
        stageElement.style.backgroundColor = Config.stageBackgroundColor;
        this.stageElement = stageElement;
        
        const zenkeshiImage = document.getElementById("zenkeshi");
        zenkeshiImage.width = Config.puyoImgWidth * 6;
        zenkeshiImage.style.position = 'absolute';
        zenkeshiImage.style.display = 'none';        
        this.zenkeshiImage = zenkeshiImage;
        stageElement.appendChild(zenkeshiImage);

        const scoreElement = document.getElementById("score");
        scoreElement.style.backgroundColor = Config.scoreBackgroundColor;
        scoreElement.style.top = Config.puyoImgHeight * Config.stageRows + 'px';
        scoreElement.style.width = Config.puyoImgWidth * Config.stageCols + 'px';
        scoreElement.style.height = Config.fontHeight + "px";
        this.scoreElement = scoreElement;

        // メモリを準備する
        this.board = [
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
        ];
        let puyoCount = 0;
        for(let y = 0; y < Config.stageRows; y++) {
            const line = this.board[y] || (this.board[y] = []);
            for(let x = 0; x < Config.stageCols; x++) {
                const puyo = line[x];
                if(puyo >= 1 && puyo <= 5) {
                    // line[x] = {puyo: puyo, element: this.setPuyo(x, y, puyo)};
                    this.setPuyo(x, y, puyo);
                    puyoCount++;
                } else {
                    line[x] = null;
                }
            }
        }
        this.puyoCount = puyoCount;
    }

はじめにpuyoimage.jsと同様に、ぷよぷよに必要な要素を取得しています。

stageElementは、次の画像で示される要素であり、背景に青のぷよが設定されているゲームのステージとなる領域です。index.htmlでは特に高さが設定されていないため、ここでステージの高さや幅などを設定しています。

あとは、背景と同様に、全消し(”zenkeshi”)とスコア(”score”)の要素を取得して、高さなどの属性値を設定しています。

そして次にゲームをプレイするステージを定義しています。

stage.js
// メモリを準備する
this.board = [
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
];

boardはゲームをプレイするステージ自体を表しています。

今回のステージは横に6列 × 縦に12行で、各マスに対して「ぷよが配置されていない」、もしくは、「ぷよが1つだけ配置されている」状態を持ちます。(0の場合何も配置されていないことを表します。)

そしてboardは2次元配列で表現されています。

ステージの状態を2次元配列で管理する

配列とは簡単に言うと、複数の値をセットで保持できる変数のようなものです。変数は値を1つのみ保持していますが、配列は変数を複数持てるいわば変数の拡張概念です。使い方は以下の通りです。

javascript
// arrayは配列
let array = [0, "あいうえお", true];
// 実際に値を使うときは、配列の何番目に入っているか指定する(0始まりなので注意)
console.log(array[0]); // 0を表示
console.log(array[1]); // あいうえおを表示
console.log(array[2]); // trueを表示

配列にはいろいろなものを入れることができます。例えば、数値・文字列・真偽値など様々な値を格納することが可能です。そして、配列の中に配列を入れることも可能です。そして配列の中に配列が入っているもののことを多次元配列といいます。

配列の中に配列を1つ入れているものは2次元配列と呼ばれ、今回のboardも2次元配列です。

boardはゲームステージのy座標とx座標を表しています。次のようなイメージで値を保持しています。

boardのイメージ

このboardでステージ上にどのぷよがどこに配置されているのかを管理しているのです。

boardを初期化しよう

次のコードを見てみましょう。

javascript
let puyoCount = 0;
        for(let y = 0; y < Config.stageRows; y++) {
            const line = this.board[y] || (this.board[y] = []);
            for(let x = 0; x < Config.stageCols; x++) {
                const puyo = line[x];
                if(puyo >= 1 && puyo <= 5) {
                    // line[x] = {puyo: puyo, element: this.setPuyo(x, y, puyo)};
                    this.setPuyo(x, y, puyo);
                    puyoCount++;
                } else {
                    line[x] = null;
                }
            }
        }
this.puyoCount = puyoCount;

puyoCountという変数は今ステージ上にいくつぷよがあるかを表す変数です。そして、forループの中でforループが使われていますが、これは”どの行の“(1つ目のforループ)、“どの列”(2つ目のforループ)かを特定することで、ステージ上の各boardの値に対して処理を実施しています。イメージは次の通りです。

1つ目のforループ

1つ目のforループでどの行の配列かを特定しています。見てわかるように例えば、0行目のboardは1次元配列となっていることにも注意しましょう。そして各行に対して2つ目のforループが実施されますが、例えばboard[0]に対するforループは次のように表されます。

2つ目のforループ

そのため、2つ目のforループの中では、ステージ上の各セルに対して処理を実施することができます。今回はboard[y][x]は必ず0なので、boardのすべてにnullが代入されます。

player.jsのinitialize()

次はplayer.jsinitialize()を見ていきましょう。

player.js
class Player {

    static initialize () {
        // キーボードの入力を確認する
        this.keyStatus = {
            right: false,
            left: false,
            up: false,
            down: false
        };
        // ブラウザのキーボードの入力を取得するイベントリスナを登録する
        document.addEventListener('keydown', (e) => {
            // キーボードが押された場合
            switch(e.keyCode) {
                case 37: // 左向きキー
                    this.keyStatus.left = true;
                    e.preventDefault(); return false;
                case 38: // 上向きキー
                    this.keyStatus.up = true;
                    e.preventDefault(); return false;
                case 39: // 右向きキー
                    this.keyStatus.right = true;
                    e.preventDefault(); return false;
                case 40: // 下向きキー
                    this.keyStatus.down = true;
                    e.preventDefault(); return false;
            }
        });
        document.addEventListener('keyup', (e) => {
            // キーボードが離された場合
            switch(e.keyCode) {
                case 37: // 左向きキー
                    this.keyStatus.left = false;
                    e.preventDefault(); return false;
                case 38: // 上向きキー
                    this.keyStatus.up = false;
                    e.preventDefault(); return false;
                case 39: // 右向きキー
                    this.keyStatus.right = false;
                    e.preventDefault(); return false;
                case 40: // 下向きキー
                    this.keyStatus.down = false;
                    e.preventDefault(); return false;
            }
        });
    }
}

オブジェクト型でkeyStatusを保持する

変数にはがあります。型にはプリミティブ型オブジェクト型があり、このkeyStatusという変数はオブジェクト型と呼ばれる型です。

ここでは詳しくは説明しませんが、オブジェクト型の変数には、フィールドと呼ばれる値を保持する機能があり、ここではkeyStatusに以下のように値を保持しています。

// キーボードの入力を確認する
this.keyStatus = {
    right: false,
    left: false,
    up: false,
    down: false
};

keyStatusには、rightleftupdownの4つのフィールドがあり、各フィールドを別々の変数として定義してもよいのですが、「キーボードの入力を確認する時にはkeyStatusを見ればよいと考えたほうが、わかりやすい」ということでこのように定義しているのだと推察されます。

例えば、「右が押されているか」を確認するためには、keyStatus.rightを見ればわかる、ということになります。(trueの場合は押している、falseの場合は押していない)

キーボード入力を定義する

続いてイベントの登録を進めていきます。

まずは、"keydown"イベント(キーボードを入力したら実行する処理)を定義していきます。

該当するコードは以下です。

// ブラウザのキーボードの入力を取得するイベントリスナを登録する
document.addEventListener('keydown', (e) => {
    // キーボードが押された場合
    switch (e.keyCode) {
        case 37: // 左向きキー
            this.keyStatus.left = true;
            e.preventDefault(); return false;
        case 38: // 上向きキー
            this.keyStatus.up = true;
            e.preventDefault(); return false;
        case 39: // 右向きキー
            this.keyStatus.right = true;
            e.preventDefault(); return false;
        case 40: // 下向きキー
            this.keyStatus.down = true;
            e.preventDefault(); return false;
    }
});

押下したキー毎にキーコード(keycode)が定義されており、押されたキーに応じて処理を実施しています。

例えば、"↑"を押すとそのキーコードが"38"で返ってくるので、if(e.keyCode == 38)とすることで"↑"を押したときの処理を定義することができます。

左を押したときの処理を見てみましょう。

case 37: // 左向きキー
this.keyStatus.left = true;
e.preventDefault(); return false;

まず、keyStatus.lefttrueにして左を押している状態にステータスを変更します。

そして次にpreventDefault()を呼び出しています。これは、「(キー入力時の)デフォルトの処理を実行しない」ようにする処理です。ここでは「左を押したときに実施する処理を実行しない」ことを表します。通常「キーボードの左を押すと、画面が左にスライドする」処理が実行されることがありますが、この処理を実行しないようにしています。

次は"keyup"イベント(キーボードを入力後に離したら実行するイベント)を定義していきます。

該当するコードは以下です。

document.addEventListener('keyup', (e) => {
    // キーボードが離された場合
    switch (e.keyCode) {
        case 37: // 左向きキー
            this.keyStatus.left = false;
            e.preventDefault(); return false;
        case 38: // 上向きキー
            this.keyStatus.up = false;
            e.preventDefault(); return false;
        case 39: // 右向きキー
            this.keyStatus.right = false;
            e.preventDefault(); return false;
        case 40: // 下向きキー
            this.keyStatus.down = false;
            e.preventDefault(); return false;
    }
});

見てわかると思いますが、"keydown"trueにしたステータスをfalseに変更しています。

このkeyupkeydownの処理を組み合わせることで、keyStatusが次の意味を表す変数になることが分かったと思います。

keyStatus = "現在入力中の↑↓←→のキー操作"

score.jsのinitialize()

最後にscore.jsのinitializeを見ていきましょう。

score.js
class Score {

    static initialize() {
        this.fontTemplateList = [];
        let fontWidth = 0;
        for (let i = 0; i < 10; i++) {
            const fontImage = document.getElementById(`font${i}`);
            if (fontWidth === 0) {
                fontWidth = fontImage.width / fontImage.height * Config.fontHeight;
            }
            fontImage.height = Config.fontHeight;
            fontImage.width = fontWidth;
            this.fontTemplateList.push(fontImage);
        }

        this.fontLength = Math.floor(Config.stageCols * Config.puyoImgWidth / this.fontTemplateList[0].width);
        this.score = 0;
        this.showScore();
    }
}

このscore.jsではfontTemplateListに、スコアのテンプレートを順番に詰めています。

スコアのテンプレートとは次の画像で示される内容です。

0・1・2・3・4・5・6・7・8・9が1文字ずつ画像で定義されており、それらの幅や高さを定義してfontTemplateListに詰めています。

そして最後にscoreを0にしてゲームを開始するようにしています。

さいごに

いかがでしたでしょうか?

ここまで書いたコードを起動すると、次のような画面が表示されると思います。

initializeを実装後に初期表示で表示される画面

まだ、ぷよは落ちてきませんが、ステージの表示や下準備は整いました!

不明点などあれば、詳しく追記するので、コメントください!

では、次回から実際にぷよを落としてゲームを完成させましょう。

3 COMMENTS

ケケ

詳しい解説で勉強になりました。質問なんですが、puyoimage.jsのinitializeでIDを削除していると思うのですが、これを削除する意味ってあるんでしょうか?プログラム的には有っても変わらないように思えるのですが、何か別の意図が考えられるのでしょうか?

返信する
チロル

ケケさん

読んでくださりありがとうございます!
※コメント初めていただいたので、うれしいです(笑)

>puyoimage.jsのinitializeでIDを削除していると思うのですが、これを削除する意味ってあるんでしょうか?
そうですね、削除しなくてもアプリは問題なく動作します。
なので、必須かというと「必須ではない」という回答になります。

ただ、一般に同じDOM内でid属性は重複しないようにするのが普通です。
(idは一意に要素を特定するための属性なので)

そして、idを削除している次の処理ですが、
image.removeAttribute('id');

この処理がない場合、「ぷよ」がid属性を持ったままのため、例えば「みどりぷよ」が複数ステージにある場合、みどりぷよのidである「puyo_1」が複数同じDOMに存在することになります。

参考にidが重複してはいけない理由について解説している記事を載せておきます。
Quita:HTMLのID属性値が重複している場合の動き

また、今回いただいたコメントを記事に反映しています!
よろしければ参考にしてみてください。

ありがとうございました。

返信する
ケケ

丁寧な返信ありがとうございます!
とてもわかりやすい解説で納得できました。
他の記事もとても良い勉強になっていますのでこれからも応援しています!

返信する

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です