こんにちは、チロルです。
本記事は次の記事の続きです。

※【ぷよプロ】シリーズが初めての方はこちらの記事から

前回は、次の機能を中心に解説していきました。
- 「ぷよ」が落下する処理(落下先の判定・落下時のアニメーション)
- 「ぷよ」が4つ以上つながった場合の「ぷよ」の削除処理(連続する同色「ぷよ」の算出処理・ステージからの「ぷよ」の削除)
- 「ぷよ」が消えた場合の得点の処理(得点の算出処理(ボーナス加算))
この記事を読むと次の機能の処理がわかるようになります。
- 「ぷよ」の新規作成の処理(「ぷよ」を新たにステージに生み出す処理)
- ユーザからの操作受付処理(「→」を押したら右に「ぷよ」が移動する等、ユーザからの操作によって「ぷよ」の配置を変更する処理)
今回で「ぷよプロ」の開発編は最後になります。
ここまで学習してきた内容はどんな入門書で学ぶ内容よりも実践的で、具体的です。必ず皆さんのプログラミング力を高めてくれていると信じています。
それでは、どうぞ!
目次
本記事を読むとわかること
この「ぷよプロ」はゲーム開始後、ステータスを切り替え、ステータスごとに処理を高速で繰り返すことでゲームを進行させます。(第④回にて解説しています)
そして各ステータスごとの処理について前回と今回で解説しています。
「ぷよプロ」で利用しているステータスは次の通りでした。
ステータス | 説明 | 説明する記事(前回 or 今回) |
---|---|---|
start | ゲーム開始時 | 前回説明済 |
checkFall | 「ぷよ」が落下するか判定する | 前回説明済 |
checkErase | 「ぷよ」を消すか判定する | 前回説明済 |
erasing | 「ぷよ」を消して得点を算出する | 前回説明済 |
newPuyo | 「ぷよ」を新しく作成する | 今回説明 |
playing | 「ぷよ」を操作する | 今回説明 |
moving/rotating/fix | ユーザの操作に応じて「ぷよ」を動かす | 今回説明 |
gameOver | ゲームオーバー | 今回説明 |
batankyu | ゲーム終了 | 今回説明 |
上の表からわかる通り、本記事を読むと次の機能がわかるようになります。
- 「ぷよ」を新しく作成して、ユーザが操作できるようにする処理
- ユーザから「ぷよ」の操作を受け付ける処理
- ユーザの操作に応じて「ぷよ」を動かす処理
- ゲーム終了時のアニメーション
「ぷよ」を新しく作成してユーザが操作できるようにする処理
ステータスが「newPuyo」の場合は、ユーザが操作することができる「ぷよ」を新しくステージに作成します。
新しく作成された「ぷよ」は2つが上下に隣接した状態で作成されます。(色はそれぞれランダム)
上の「ぷよ」を「movablePuyo」、下の「ぷよ」を「centerPuyo」と呼び、ユーザが「↑」を押すと「movablePuyo」が反時計回りに90°回転します。

では、具体的なコードを見ていきましょう。
//ぷよ設置確認
static createNewPuyo () {
// ぷよぷよが置けるかどうか、1番上の段の左から3つ目を確認する
if(Stage.board[0][2]) {
// 空白でない場合は新しいぷよを置けない
return false;
}
ポイント1
// 新しいぷよの色を決める
const puyoColors = Math.max(1, Math.min(5, Config.puyoColors));
this.centerPuyo = Math.floor(Math.random() * puyoColors) + 1;
this.movablePuyo = Math.floor(Math.random() * puyoColors) + 1;
// 新しいぷよ画像を作成する
this.centerPuyoElement = PuyoImage.getPuyo(this.centerPuyo);
this.movablePuyoElement = PuyoImage.getPuyo(this.movablePuyo);
Stage.stageElement.appendChild(this.centerPuyoElement);
Stage.stageElement.appendChild(this.movablePuyoElement);
ポイント2
// ぷよの初期配置を定める
this.puyoStatus = {
x: 2, // 中心ぷよの位置: 左から2列目
y: -1, // 画面上部ギリギリから出てくる
left: 2 * Config.puyoImgWidth,
top: -1 * Config.puyoImgHeight,
dx: 0, // 動くぷよの相対位置: 動くぷよは上方向にある
dy: -1,
rotation: 90 // 動くぷよの角度は90度(上向き)
};
// 接地時間はゼロ
this.groundFrame = 0;
// ぷよを描画
this.setPuyoPosition();
return true;
}
今回「ぷよ」の色の数は4つです。それはConfig.jsに次の通り書かれています。
Config.puyoColors = 4; // 何色のぷよを使うか
また、JavaScriptの関数Math.max・Math.min・Math.floor・Math.randomは次のような使い方をします。
Math.max()
関数は、入力引数として与えられた0個以上の数値のうち最大の数を返します。
console.log(Math.max(1, 3, 2));
// expected output: 3
console.log(Math.max(-1, -3, -2));
// expected output: -1
const array1 = [1, 3, 2];
console.log(Math.max(...array1));
// expected output: 3
Math.min()
は静的関数で、引数で渡されたもののうち最小の値を返します。または引数のいずれかが数値以外で、数値に変換できない場合は NaN
を返します。
console.log(Math.min(2, 3, 1));
// expected output: 1
console.log(Math.min(-2, -3, -1));
// expected output: -3
const array1 = [2, 3, 1];
console.log(Math.min(...array1));
// expected output: 1
Math.floor()
関数は与えられた数値以下の最大の整数を返します。
console.log(Math.floor(5.95));
// expected output: 5
console.log(Math.floor(5.05));
// expected output: 5
console.log(Math.floor(5));
// expected output: 5
console.log(Math.floor(-5.05));
// expected output: -6
Math.random()
関数は、 0 以上 1 未満 (0 は含むが、 1 は含まない) の範囲で浮動小数点の擬似乱数を返します。その範囲ではほぼ均一な分布で、ユーザーは範囲の拡大をすることができます。実装側で乱数生成アルゴリズムの初期シードを選択します。ユーザーが初期シードを選択、またはリセットすることは出来ません。
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
console.log(getRandomInt(3));
// expected output: 0, 1 or 2
console.log(getRandomInt(1));
// expected output: 0
console.log(Math.random());
// expected output: a number from 0 to <1
Math.maxとMath.minの使い方が分かったところで、ポイント1の処理を見てみましょう。
// 新しいぷよの色を決める
const puyoColors = Math.max(1, Math.min(5, Config.puyoColors));
// puyoColorsは4になる
this.centerPuyo = Math.floor(Math.random() * puyoColors) + 1;
this.movablePuyo = Math.floor(Math.random() * puyoColors) + 1;
// Math.random() * 4 は0以上4未満の小数となる
// なので、Math.floor(Math.random() * puyoColors)は0以上3以下の整数になる。
// 「ぷよ」の色は1~4で管理されているので、1を足している。
ユーザが操作することができる「ぷよ」の情報はPlayer.jsのpuyoStatusで管理しています。
puyoStatusで管理している情報は次の通りです。
変数名 | 説明 | 初期値 |
---|---|---|
x | centerPuyoのX座標 | 2 |
y | centerPuyoのY座標 | -1 |
left | centerPuyoの画像の左端のX座標 | 2 * Config.puyoImgWidth |
top | centerPuyoの画像の上端のy座標 | -1 * Config.puyoImgHeight |
dx | movablePuyoのcenterPuyoとの相対位置(X座標) | 0 |
dy | movablePuyoのcenterPuyoとの相対位置(Y座標) | -1 |
rotation | movablePuyoの角度(90°で上向きを表す) | 90 |
上の表から、初期表示時に「ぷよ」は左から3番目で画面外から表示されるようになっています。
このpuyoStatusを操作することで操作中の「ぷよ」の動きを制御することができます。
ユーザから「ぷよ」の操作を受け付ける処理
ステータスが「playing」の場合は、ユーザの操作を受け付け、操作内容に応じて「ぷよ」を動かします。
ユーザができる操作は次の4つです。
- 急速落下:操作ぷよを早く落下させる
- 左に移動:操作ぷよを左に移動させる
- 右に移動:操作ぷよを右に移動させる
- 回転 :「ぷよ」を回転させる
コードは次の通りです。このユーザからの操作は最後の山場なので、気を引き締めて見ていきましょう。
※わかりづらい箇所があれば是非コメントください。
static playing(frame) {
ポイント1:落下する
// まず自由落下を確認する
// 下キーが押されていた場合、それ込みで自由落下させる
if(this.falling(this.keyStatus.down)) {
// 落下が終わっていたら、ぷよを固定する
this.setPuyoPosition();
return 'fix';
}
this.setPuyoPosition();
ポイント2:左右に移動する
if(this.keyStatus.right || this.keyStatus.left) {
// 左右のの確認をする
const cx = (this.keyStatus.right) ? 1 : -1;
const x = this.puyoStatus.x;
const y = this.puyoStatus.y;
const mx = x + this.puyoStatus.dx;
const my = y + this.puyoStatus.dy;
// その方向にブロックがないことを確認する
// まずは自分の左右を確認
let canMove = true;
if(y < 0 || x + cx < 0 || x + cx >= Config.stageCols || Stage.board[y][x + cx]) {
if(y >= 0) {
canMove = false;
}
}
if(my < 0 || mx + cx < 0 || mx + cx >= Config.stageCols || Stage.board[my][mx + cx]) {
if(my >= 0) {
canMove = false;
}
}
// 接地していない場合は、さらに1個下のブロックの左右も確認する
if(this.groundFrame === 0) {
if(y + 1 < 0 || x + cx < 0 || x + cx >= Config.stageCols || Stage.board[y + 1][x + cx]) {
if(y + 1 >= 0) {
canMove = false;
}
}
if(my + 1 < 0 || mx + cx < 0 || mx + cx >= Config.stageCols || Stage.board[my + 1][mx + cx]) {
if(my + 1 >= 0) {
canMove = false;
}
}
}
if(canMove) {
// 動かすことが出来るので、移動先情報をセットして移動状態にする
this.actionStartFrame = frame;
this.moveSource = x * Config.puyoImgWidth;
this.moveDestination = (x + cx) * Config.puyoImgWidth;
this.puyoStatus.x += cx;
return 'moving';
}
ポイント3:「ぷよ」を回転する
} else if(this.keyStatus.up) {
// 回転を確認する
// 回せるかどうかは後で確認。まわすぞ
const x = this.puyoStatus.x;
const y = this.puyoStatus.y;
const mx = x + this.puyoStatus.dx;
const my = y + this.puyoStatus.dy;
const rotation = this.puyoStatus.rotation;
let canRotate = true;
let cx = 0;
let cy = 0;
if(rotation === 0) {
// 右から上には100% 確実に回せる。何もしない
} else if(rotation === 90) {
// 上から左に回すときに、左にブロックがあれば右に移動する必要があるのでまず確認する
if(y + 1 < 0 || x - 1 < 0 || x - 1 >= Config.stageCols || Stage.board[y + 1][x - 1]) {
if(y + 1 >= 0) {
// ブロックがある。右に1個ずれる
cx = 1;
}
}
// 右にずれる必要がある時、右にもブロックがあれば回転出来ないので確認する
if(cx === 1) {
if(y + 1 < 0 || x + 1 < 0 || y + 1 >= Config.stageRows || x + 1 >= Config.stageCols || Stage.board[y + 1][x + 1]) {
if(y + 1 >= 0) {
// ブロックがある。回転出来なかった
canRotate = false;
}
}
}
} else if(rotation === 180) {
// 左から下に回す時には、自分の下か左下にブロックがあれば1個上に引き上げる。まず下を確認する
if(y + 2 < 0 || y + 2 >= Config.stageRows || Stage.board[y + 2][x]) {
if(y + 2 >= 0) {
// ブロックがある。上に引き上げる
cy = -1;
}
}
// 左下も確認する
if(y + 2 < 0 || y + 2 >= Config.stageRows || x - 1 < 0 || Stage.board[y + 2][x - 1]) {
if(y + 2 >= 0) {
// ブロックがある。上に引き上げる
cy = -1;
}
}
} else if(rotation === 270) {
// 下から右に回すときは、右にブロックがあれば左に移動する必要があるのでまず確認する
if(y + 1 < 0 || x + 1 < 0 || x + 1 >= Config.stageCols || Stage.board[y + 1][x + 1]) {
if(y + 1 >= 0) {
// ブロックがある。左に1個ずれる
cx = -1;
}
}
// 左にずれる必要がある時、左にもブロックがあれば回転出来ないので確認する
if(cx === -1) {
if(y + 1 < 0 || x - 1 < 0 || x - 1 >= Config.stageCols || Stage.board[y + 1][x - 1]) {
if(y + 1 >= 0) {
// ブロックがある。回転出来なかった
canRotate = false;
}
}
}
}
if(canRotate) {
// 上に移動する必要があるときは、一気にあげてしまう
if(cy === -1) {
if(this.groundFrame > 0) {
// 接地しているなら1段引き上げる
this.puyoStatus.y -= 1;
this.groundFrame = 0;
}
this.puyoStatus.top = this.puyoStatus.y * Config.puyoImgHeight;
}
// 回すことが出来るので、回転後の情報をセットして回転状態にする
this.actionStartFrame = frame;
this.rotateBeforeLeft = x * Config.puyoImgHeight;
this.rotateAfterLeft = (x + cx) * Config.puyoImgHeight;
this.rotateFromRotation = this.puyoStatus.rotation;
// 次の状態を先に設定しておく
this.puyoStatus.x += cx;
const distRotation = (this.puyoStatus.rotation + 90) % 360;
const dCombi = [[1, 0], [0, -1], [-1, 0], [0, 1]][distRotation / 90];
this.puyoStatus.dx = dCombi[0];
this.puyoStatus.dy = dCombi[1];
return 'rotating';
}
}
return 'playing';
}
まず、player.jsでは、keyStatusでユーザからどのような入力があったかを管理していました。
this.keyStatus = {
right: false,
left: false,
up: false,
down: false
};
キーボードの「→」を押すと、keyStatus.rightがtrueとなり、「←」がleft、「↑」がup、「↓」がdownがそれぞれtrueになります。
なので、「↓」が押されているとplayer.jsのfalling()にtrueがわたされます。
続いて、falling()の処理をみていきましょう。
落下処理はデータ的にはpuyoStatusのtopを書き替えることで実現できます。
topは「ぷよ」のY座標を表しているので、topを増加すると「ぷよ」の位置は下に移動して、落下処理を実現することができるのです。
ただし、落下可能かどうかの判定で少し工夫する必要があり、理解するには「ぷよ」の座標には次の2種類があることを知っておく必要があります。
- ステージ上の「ぷよ」の座標(横:6 × 縦:12の72マス)
- 画面に描画される「ぷよ」の画像の座標(横:240px × 縦:480px)
ステージ上の「ぷよ」の座標 とは、次の画像で示す全72マスのぷよが入る空間のことです。
データとしてはstage.jsのboardで管理しています。
ちなみに座標は左上が(0,0)、右下が(11, 5)となるように設定されています。

実際に72マスでゲーム自体はできるのですが、そうするとガクガクなアニメーションになってしまいます。
※落下する際に「ぷよ」が12回の移動で一番下までついてしまう。
なので実際のアニメーションはもっと細かく1マスごとに40コマ設定して画像を描画するようにしています。
static falling (isDownPressed) {
// 現状の場所の下にブロックがあるかどうか確認する
let isBlocked = false;
let x = this.puyoStatus.x;
let y = this.puyoStatus.y;
let dx = this.puyoStatus.dx;
let dy = this.puyoStatus.dy;
A:落下可能かどうかを判定する
if(y + 1 >= Config.stageRows || Stage.board[y + 1][x] || (y + dy + 1 >= 0 && (y + dy + 1 >= Config.stageRows || Stage.board[y + dy + 1][x + dx]))) {
isBlocked = true;
}
if(!isBlocked) {
// 下にブロックがないなら自由落下してよい。プレイヤー操作中の自由落下処理をする
this.puyoStatus.top += Config.playerFallingSpeed;
if(isDownPressed) {
// 下キーが押されているならもっと加速する
this.puyoStatus.top += Config.playerDownSpeed;
}
B:落下しすぎていないか判定する
if(Math.floor(this.puyoStatus.top / Config.puyoImgHeight) != y) {
// ブロックの境を超えたので、再チェックする
// 下キーが押されていたら、得点を加算する
if(isDownPressed) {
Score.addScore(1);
}
y += 1;
this.puyoStatus.y = y;
if(y + 1 >= Config.stageRows || Stage.board[y + 1][x] || (y + dy + 1 >= 0 && (y + dy + 1 >= Config.stageRows || Stage.board[y + dy + 1][x + dx]))) {
isBlocked = true;
}
if(!isBlocked) {
// 境を超えたが特に問題はなかった。次回も自由落下を続ける
this.groundFrame = 0;
return;
} else {
// 境を超えたらブロックにぶつかった。位置を調節して、接地を開始する
this.puyoStatus.top = y * Config.puyoImgHeight;
this.groundFrame = 1;
return;
}
} else {
// 自由落下で特に問題がなかった。次回も自由落下を続ける
this.groundFrame = 0;
return;
}
}
if(this.groundFrame == 0) {
// 初接地である。接地を開始する
this.groundFrame = 1;
return;
} else {
this.groundFrame++;
if(this.groundFrame > Config.playerGroundFrame) {
return true;
}
}
}
操作中の「ぷよ」が落下可能かどうかを次の4つの条件のうちいずれかに該当するかどうかで判定しています。
- 「centerPuyo」がステージの一番下まで移動している
- 「centerPuyo」の下に「ぷよ」が配置されている
- 「movablePuyo」がステージの一番下まで移動している
- 「movablePuyo」の下に「ぷよ」が配置されている
実際に落下処理を行った後、1マス40コマの座標を超えていないか判定しています。
一回の処理で1/60秒ごとに0.9ずつ落下していくので、約44/60秒で1マス進みます。
ただ、45回目の落下で0.5だけ超過してしまうため、仮に下に「ぷよ」があった場合、0.5だけ「ぷよ」画像が被ってしまいます。そのため、落下しすぎていないか判定し、超過している場合は「ぷよ」の画像をマスの下端に合わせるようにしています。
この処理によって、 「ぷよ」の回転や「ぷよ」の移動時に、「ぷよ」の画像がかぶる場合、表示位置をずらして被らないように調整してくれています。
「ぷよ」を動かす場合は次の処理で左右に動かします。
- 左右に移動できるか判定する。
- (移動できる場合)移動先を決定して、移動アニメーションを開始する。
「ぷよ」の移動可否を次の通り、判定しています。
- 操作中の「ぷよ」がステージ上にあること
- 移動後の「ぷよ」がステージ上にあること
- 「ぷよ」の移動先に既に「ぷよ」がないこと
上記のうちいずれかを満たす場合、canMoveがFALSEとなり、移動ができないと判定します。
canMoveがTRUEの場合、移動情報を算出の上、ステータスを”moving”に更新します。
移動時には移動元の「ぷよ」の座標、移動先の「ぷよ」の座標を算出の上、移動先へ1フレームずつ動くように処理をしていきます。(そのためいきなり左右に動くのではなく、左右にすっと動くアニメーションが実現できます。)
「↑」を押すとkeyStatus.upがtrueとなり、「ぷよ」を回転します。
「ぷよ」を回転する処理は次の順で実施します。
- 回転先に「ぷよ」があるかどうか確認し、回転可能かどうかを判断する
- (回転可能な場合)回転先の座標を決定して、回転を開始する
移動中の「ぷよ」が今どれだけ回転しているかに応じて、回転方向を決定して「ぷよ」の回転可能かを判定します。
回転可否は次の表の通りに回転可否を判定しています。
rotate | 「ぷよ」の並び | 「↑」押下時の回転方向 | 回転可否 |
---|---|---|---|
0° | ![]() (回転する「ぷよ」が右) | ![]() (右の「ぷよ」が上に移動する) | 常に可能 |
90° | ![]() (回転する「ぷよ」が上) | ![]() (上の「ぷよ」が左に移動する) | 「ぷよ」の左右いずれかが空いている場合、可能 |
180° | ![]() (回転する「ぷよ」が左) | ![]() (左の「ぷよ」が下に移動する) | 常に可能 |
270° | ![]() (回転する「ぷよ」が下) | ![]() (下の「ぷよ」が右に移動する) | 「ぷよ」の左右いずれかが空いている場合、可能 |
回転前と回転後の「ぷよ」の座標を算出して、ステータス「rotating」を返すことで回転アニメーションを開始します。
回転前の「ぷよ」の回転に90°を加え、その値に応じて移動可能な「ぷよ」の移動先を決定しています。
ユーザの操作に応じて「ぷよ」を動かす処理
ユーザが「ぷよ」を操作するとステータスが次のいずれかで返されてきます。
- playing
- moving
- rotating
- fix
返ってきた値に応じてアニメーションを開始し、アニメーション完了後に次のステータスに更新します。
ここでは、「移動」を表す「moving」の場合のアニメーション処理について説明をしていきます。
case 'moving':
if(!Player.moving(frame)) {
// 移動が終わったので操作可能にする
mode = 'playing';
}
break;
Player.moving(frame)
を実施して、移動が完了するとステータスが「playing」に変更して、再度ユーザからの操作を受け付けるようにしています。
static moving(frame) {
// 移動中も自然落下はさせる
this.falling();
アニメーションが完了したかどうかを判定
const ratio = Math.min(1, (frame - this.actionStartFrame) / Config.playerMoveFrame);
this.puyoStatus.left = ratio * (this.moveDestination - this.moveSource) + this.moveSource;
this.setPuyoPosition();
if(ratio === 1) {
return false;
}
return true;
}
移動にかけるフレーム数をConfig.jsで指定しており、そのフレーム数が経過しているかどうかを判定しています。
デフォルトでは、次のように10フレームで指定されています。
Config.playerMoveFrame = 10; // 左右移動に消費するフレーム数
なので、初めてmovingの処理に来た場合、const ratio = Math.min(1, (1 - 0) / 10) = 0.1
となります。
puyoStatus.left
を目的の座標まで1割(0.1)進め、これを10回繰り返すので、結果として10フレームで移動が完了します。
そして、移動が完了するとratioが1となるので、flaseで値を返して移動を完了します。
ゲームオーバーでばたんきゅーを表示する
新しい「ぷよ」が生成できなくなると、ステータスが「gameOver」となります。
ぷよぷよではゲームオーバーとなると、次のような画像を表示します。
画像表示後は左右に移動しながら下にゆっくりと画像が移動していきます。

case 'gameOver':
// ばたんきゅーの準備をする
PuyoImage.prepareBatankyu(frame);
mode = 'batankyu';
break;
static prepareBatankyu(frame) {
this.gameOverFrame = frame;
Stage.stageElement.appendChild(this.batankyuImage);
this.batankyuImage.style.top = -this.batankyuImage.height + 'px';
}
ステータスが「gameOver」となると、ばたんきゅーの画像を表示します。
そして次にステータスを「batankyu」に変更します。
ばたんきゅーが表示されると、時間経過とともにばたんきゅーをアニメーションさせながら移動させます。
そしてユーザから「↑」が入力されると再度ゲームをスタートします。
case 'batankyu':
PuyoImage.batankyu(frame);
Player.batankyu();
break;
static batankyu(frame) {
const ratio = (frame - this.gameOverFrame) / Config.gameOverFrame;
const x = Math.cos(Math.PI / 2 + ratio * Math.PI * 2 * 10) * Config.puyoImgWidth;
const y = Math.cos(Math.PI + ratio * Math.PI * 2) * Config.puyoImgHeight * Config.stageRows / 4 + Config.puyoImgHeight * Config.stageRows / 2;
this.batankyuImage.style.left = x + 'px';
this.batankyuImage.style.top = y + 'px';
}
移動時のアニメーションと同じく、ratioを利用してConfig.gameOverFrameで指定されたフレーム数をかけてばたんきゅーの画像を動かしていきます。
また、ばたんきゅーの移動の仕方ですが、Math.cos()を利用しており、次の画像のような軌跡を描きながら移動していきます。(※説明には、三角関数の説明が必要であるため、詳細は割愛します。。)

そして、「↑」を押した場合、リロードをして再度ゲームをスタートします。
static batankyu() {
if (this.keyStatus.up) {
location.reload()
}
}
おわりに
お疲れさまでした。今回で「ぷよプロ」シリーズの説明は最後となります。
次回はこの「ぷよプロ」を学んだあとで次に学ぶべき内容を紹介していきます。
不明点などあれば、説明を追加するので是非コメントをください!では!