みなさんこんにちは!
本記事は以下記事の続きです。

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

確か前回まででindex.htmlを用意したんでしたよね?
いよいよJavaScriptですか。。難しそうな気がしますね。。

JavaScriptはきちんと学習すれば、アイデアを簡単に実現することができる強力な言語のひとつだよ!
多分大変とか難しいって感じるポイントは、JavaScriptだからではなくて、どの言語でも同じはずだから頑張っていこうね。
今回は、画面の初期表示ができるところまでを実装していきたいと思います。
初期表示ができるまでのステップ
以前次の図でアプリの大まかな構成を説明したと思います。

index.htmlがJavaScriptファイルを読み込むと、まずはgame.jsが起動します。そして、player.jsなどのほかのJavaScriptファイルがgame.jsから呼び出されます。
詳しくはこの後見ていきますが、初期表示時には次のような処理が走っています。

そして各ファイル内のinitialize()
にて初期表示に必要な処理を実行していきます。
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) { タッチしたら実施する処理 }); |
そしてわかるように、game.js
でまず利用されているイベントが、「ロード時に実行する」イベントです。次のコードの部分です。
// 起動されたときに呼ばれる関数を登録する
window.addEventListener("load", () => {
// まずステージを整える
initialize();
// ゲームを開始する
loop();
});
まず知っておきたいのは、//
で始まる行はコメントです。何を書いてもプログラムは無視します。
そして、イベントの登録です。window.addEventListener("load", ()=>{ 処理 });
でロード時に実施する処理を書いています。ただし実施する処理が、アロー関数
で書かれています。
そしてこの中では、2つの関数を呼んでいますね。initialize()
とloop()
です。そして本記事で説明していくのがこのinitialize()
です。(initiallizeは初期化するという意味)
各ファイルのinitializeを見ていこう
game.jsのinitialize
先ほどのコードの下に次のコードを追記してください。
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
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文の中でぷよのデータを取得して、必要な値をぷよにセットしていきます。
const image = document.getElementById(ぷよのid );
でぷよを取得します。ここで指定しているぷよのid とは、前回作成したindex.html に書いてあるid のことです。
例えば、idがpuyo_1
は緑色のぷよを表しています。次の画像の青枠で囲まれた画像が取得できます。

ぷよを取得後に幅や高さなどcssの属性値を変更しています。(positionをabsoluteに変更)各種の値を変更後、配列にぷよをつめています。
ぷよを詰め終わったら、次は同じ方法で「ばたんきゅー」のHTMLを取得し、CSSを設定します。
stage.is のinitialize()
次はstage.js
のinitialize()
を見ていきましょう。
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”)の要素を取得して、高さなどの属性値を設定しています。
そして次にゲームをプレイするステージを定義しています。
// メモリを準備する
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次元配列で表現されています。
配列とは簡単に言うと、複数の値をセットで保持できる変数のようなものです。変数は値を1つのみ保持していますが、配列は変数を複数持てるいわば変数の拡張概念です。使い方は以下の通りです。
// 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でステージ上にどのぷよがどこに配置されているのかを管理しているのです。
次のコードを見てみましょう。
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ループでどの行の配列かを特定しています。見てわかるように例えば、0行目のboardは1次元配列となっていることにも注意しましょう。そして各行に対して2つ目のforループが実施されますが、例えばboard[0]に対するforループは次のように表されます。

そのため、2つ目のforループの中では、ステージ上の各セルに対して処理を実施することができます。今回はboard[y][x]は必ず0なので、boardのすべてにnullが代入されます。
player.jsのinitialize()
次はplayer.js
のinitialize()
を見ていきましょう。
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に以下のように値を保持しています。
// キーボードの入力を確認する
this.keyStatus = {
right: false,
left: false,
up: false,
down: false
};
keyStatus
には、right
、left
、up
、down
の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.left
をtrue
にして左を押している状態にステータスを変更します。
そして次に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
に変更しています。
このkeyup
とkeydown
の処理を組み合わせることで、keyStatus
が次の意味を表す変数になることが分かったと思います。
keyStatus = "現在入力中の↑↓←→のキー操作"
score.jsのinitialize()
最後にscore.js
のinitializeを見ていきましょう。
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にしてゲームを開始するようにしています。
さいごに
いかがでしたでしょうか?
ここまで書いたコードを起動すると、次のような画面が表示されると思います。

まだ、ぷよは落ちてきませんが、ステージの表示や下準備は整いました!
不明点などあれば、詳しく追記するので、コメントください!
では、次回から実際にぷよを落としてゲームを完成させましょう。
詳しい解説で勉強になりました。質問なんですが、puyoimage.jsのinitializeでIDを削除していると思うのですが、これを削除する意味ってあるんでしょうか?プログラム的には有っても変わらないように思えるのですが、何か別の意図が考えられるのでしょうか?
ケケさん
読んでくださりありがとうございます!
※コメント初めていただいたので、うれしいです(笑)
>puyoimage.jsのinitializeでIDを削除していると思うのですが、これを削除する意味ってあるんでしょうか?
そうですね、削除しなくてもアプリは問題なく動作します。
なので、必須かというと「必須ではない」という回答になります。
ただ、一般に同じDOM内でid属性は重複しないようにするのが普通です。
(idは一意に要素を特定するための属性なので)
そして、idを削除している次の処理ですが、
image.removeAttribute('id');
この処理がない場合、「ぷよ」がid属性を持ったままのため、例えば「みどりぷよ」が複数ステージにある場合、みどりぷよのidである「puyo_1」が複数同じDOMに存在することになります。
参考にidが重複してはいけない理由について解説している記事を載せておきます。
Quita:HTMLのID属性値が重複している場合の動き
また、今回いただいたコメントを記事に反映しています!
よろしければ参考にしてみてください。
ありがとうございました。
丁寧な返信ありがとうございます!
とてもわかりやすい解説で納得できました。
他の記事もとても良い勉強になっていますのでこれからも応援しています!