オブジェクト指向についてQiitaの記事等を読んだりして学んだもののいまいち理解できていないといった方に楽しくオブジェクト指向のプログラミングを学べるよう、オブジェクト指向でゲームを作ってみようということでテトリスを作ってみました。
今回は手軽に作れるよう必要な環境はPCのブラウザだけでという感じでJavascriptを使い、表示はHTMLとCSSを使ってブラウザ上に表示するものとしました。
私自身、約7年前にも同じようなものを作ったことがあるのですが、今回はES2015で追加されたClass構文を使って実装しましょう。
設計
まず、テトリスのゲームを機能で分けられるクラス分けを考えてみましょう。
分け方はほかにもあるかもしれませんが、今回は落ちてくるブロックの塊テトリミノを扱うテトリミノクラス、フィールドを扱うフィールドクラス、ブラウザへの表示処理を扱うビュークラス、ゲームの操作、ルールを扱うゲームクラスに分けました。
テトリミノクラスは4×4の配列を使ってテトリミノの状態を扱います。
フィールドはフィールドの領域を配列で扱い、テトリミノをフィールド上でコントロールします。
ビュークラスはフィールドやテトリミノの状態を画面上に表示します。
ゲームクラスはスコアなどに応じて自動落下速度が速くなるなどの上記クラス以外のルールに関する部分の処理を行います。
次に、機能と設定をクラスごとに考えてみましょう。
オブジェクト指向でプログラミングを行うのは継承してコードを再利用、拡張することができるという利点がありますが、継承されることを想定して書かないと継承の恩恵が得られないことがあります。
また、javascriptではサブクラスでコンストラクタを定義するときは必ずスーパークラスのコンストラクタを先に呼ぶ必要があるので、特にコンストラクタの処理は考える必要があります。
そこで、オブジェクト指向で各クラス実装するにあったって、下記の3点を設計として順に考えていきます。
- クラスの汎用的な機能・設定
- 引数等で指定できる機能・設定
- 継承時に拡張できる機能・設定
クラスの汎用的な機能・設定は、クラスで決められた抽象的な振る舞いで、継承されても基本的に不変とされる機能です。
引数等で指定できる機能・設定は、コンストラクタやメソッドなどの引数等のインスタンスの外部から値を指定して内部の設定を変えることができ、値を変えれば柔軟に変更することができる機能や設定になります。
継承時に拡張できる機能・設定は、クラスとして実態は決まっているものの、継承先で変えることができる機能・設定になります。
詳しくは、各クラスの設計で見ていきましょう。
テトリミノクラスの設計
まず、クラスの汎用的な機能・設定を決めていきます。
- クラスの汎用的な機能・設定
- テトリミノの種類を定義
- インスタンス生成時にランダムに種類を選択
- テトリミノのブロックの状態を保持し、右回転、左回転が可能
テトリミノクラスはわかりやすいクラスだと思いますので説明しなくても理解していただけるかと思いますが、インスタンス生成時にランダムにテトリミノの種類を選択するという形を取りました。
- 引数等で指定できる機能・設定
- なし
コンストラクタの引数でテトリミノの種類を指定するという実装も考えられますが、上記の通りクラス内でランダムに種類を選択する形にしていますのでなしとなっております。
ただ、訳あって最終的に実装ではコンストラクタに引数を設けておりますが、外部から指定するために設けている訳ではないので省いております。
- 継承時に拡張できる機能・設定
- テトリミノの領域サイズ
- テトリミノの種類とブロック位置
- テトリミノのランダム選択
- テトリミノの回転処理
継承時に拡張できる機能・設定は、クラスの汎用的な機能・設定の中に内包されているものの中から機能や設定を抜き出す感じです。
テトリミノクラスでは継承してブロックが5つで18種類のペントミノクラスを作れたりするように上記の機能・設定を抜き出しています。
フィールドクラスの設計
- クラスの汎用的な機能・設定
- フィールドのブロックの状態を保持
- 現在フィールド上にあるテトリミノを保持
- 次以降に落下するテトリミノを保持
- テトリミノの回転命令を受け付け、テトリミノが回転できる場合に回転
- テトリミノの左右移動命令を受け付け、テトリミノが移動できる場合に移動
- テトリミノの下移動命令を受け付け、テトリミノが移動できる場合に移動させ、移動できない場合はフィールドに固定
- テトリミノの固定で行が全てブロックで埋まったら行を削除し、上にあるブロックを下に移動
- 次のテトリミノをフィールドに設定、次以降に落下するテトリミノを生成
- フィールドからはみ出したブロックがあるもしくは次に落下するテトリミノとフィールドのブロックが重なる場合はゲームオーバー
メインはフィールドのブロックと現在落下しているテトリミノですが、次のテトリミノ群もこのクラス内で扱うようにしています。
あとは、フィールドがゲームオーバーの状態かの判定も行います。
- 引数等で指定できる機能・設定
- フィールドの幅、高さ
- 次のテトリミノを表示する件数
- 使用するテトリミノクラス
上記は継承して変更できるというほどのものでもないかなと思ったのと、ビュークラスでも同じ設定が必要になるので引数で設定するようにしました。
- 継承時に拡張できる機能・設定
- テトリミノ開始位置
ここは悩んだのですが、例えばペントミノクラスを指定した場合に開始位置がテトリミノと同じだと不都合が起きそうなのでここに入れています。
ビュークラスの設計
-
クラスの汎用的な機能・設定
- フィールドの表示
- 次以降に落下するテトリミノの表示
- スコア、ライン、レベルの表示
-
引数等で指定できる機能・設定
- フィールドの幅、高さ
- 次のテトリミノを表示する件数
- 使用するテトリミノクラス
-
継承時に拡張できる機能・設定
- ブロックの種類ごとの色
ビュークラスについてはフィールド等の渡されてきたデータを表示するだけなのであまり説明は不要かと思います。
HTML、CSSとは全く違う方法で表示するのであればインターフェースがないので継承せずクラス総取っ替えでいい気がします。
ブロックの種類ごとの色については、最終的にCSSで色を指定していたりするのであまり意味はないかも……
ゲームクラスの設計
- クラスの汎用的な機能・設定
- 各操作受付制御
- 自動落下処理
- スコア、レベルの算出
- テトリミノの固定から次のテトリミノ表示の制御
ゲームクラスではフィールドクラスが扱う機能以外を扱う感じです。
特に、自動落下等の時間系の処理を行うようにしています。
- 継承時に拡張できる機能・設定
- 使用するテトリミノクラス
- 使用するフィールドクラス
- 使用するビュークラス
- フィールドの幅、高さ
- 次のテトリミノを表示する件数
- スコア設定
- レベル設定
- 自動落下時間
- テトリミノのフィールド固定後の時間
ゲームクラスは引数は受けずこのクラスで全てのルールを完結させています。
あと、上記以外にクラスではありませんが、ボタンの操作を受け付けるコントローラーもあります。
実装
それでは、上記で設計した各クラスを実装していきます。
Javascriptではインスタンスの情報隠蔽はできないので外部からインスタンス変数を参照できますが、外部から参照する必要のあるものはメソッドにしています。
コンストラクタの中だけで使用すれば情報隠蔽できるものもありますが、拡張性を重視していますので情報隠蔽に関しては特に気にしないでおきます。
テトリミノクラスの実装
// テトリミノクラス
class Tetrimino {
// テトリミノのサイズ取得
static getSize() {return 4;}
// テトリミノの種類ごとのブロック位置を指定
static getTypePointArray() {
return [
[[0,1],[1,1],[2,1],[3,1]], // 棒形
[[1,0],[1,1],[2,0],[2,1]], // 正方形
[[0,1],[1,0],[1,1],[2,0]], // S字
[[0,0],[1,0],[1,1],[2,1]], // Z字
[[0,0],[0,1],[1,1],[2,1]], // J字
[[0,1],[1,1],[2,0],[2,1]], // L字
[[0,1],[1,0],[1,1],[2,1]] // T字
];
}
// テトリミノの種類ごとのブロック位置を取得
static getTypePoint(type) {
return this.getTypePointArray()[type - 1];
}
// テトリミノの種類をランダムに取得
static getRandomType() {
return Math.floor(Math.random() * this.getTypePointArray().length) + 1;
}
// 回転時の回転範囲取得
static getRotationSize(type) {
if (type === 1) {
return 3;
} else if (type === 2) {
return -1;
}
return 2;
}
constructor(tetrimino) {
// クローン用
if (tetrimino) {
this.tbl = tetrimino.tbl;
this.rotationSize = tetrimino.rotationSize;
return;
}
// 配列初期化 0埋め
this.tbl = new Array(this.constructor.getSize());
for(let x = 0; x < this.constructor.getSize(); x++) {
this.tbl[x] = new Array(this.constructor.getSize()).fill(0);
}
const type = this.constructor.getRandomType();
// 配列に初期ブロック位置を指定
const typePoint = this.constructor.getTypePoint(type);
for(let i = 0; i < typePoint.length; i++) {
const point = typePoint[i];
this.tbl[point[0]][point[1]] = type;
}
// 回転時の回転範囲
this.rotationSize = this.constructor.getRotationSize(type);
}
// クローン
clone() {
return new this.constructor(this);
}
// テトリミノ右回転
rightRotation() {
// 正方形の場合は回転しない
if (this.rotationSize === -1) {
return;
}
// 新規配列初期化 0埋め
const newtbl = new Array(this.constructor.getSize());
for(let x = 0; x < this.constructor.getSize(); x++) {
newtbl[x] = new Array(this.constructor.getSize()).fill(0);
}
// 右回転
for (let i = 0; i <= this.rotationSize; i++) {
for (let j = 0; j <= this.rotationSize; j++) {
newtbl[i][j] = this.tbl[j][this.rotationSize-i];
}
}
this.tbl = newtbl;
}
// テトリミノ左回転
leftRotation() {
// 右回転と同様のため省略
}
// 該当ポイントのデータを返す
getPointBlock(x, y) {
return this.tbl[x][y];
}
}
このクラスでは静的メソッドになっているものが継承時に拡張できる機能・設定で出てきたもので、コンストラクタの処理の中から外に出したものとなっています。(getSizeについては外部からも参照するためのものでもあります。)
継承時に拡張できる機能・設定は静的メソッドである必要はありませんが、外に出した機能・設定についてはインスタンスの状態に依存しないので静的メソッドにしています。
メソッドにすることで、継承した際にそのメソッドだけをオーバーライドして変えることでその部分の機能・設定を変えることができます。
静的メソッドから静的メソッドを呼び出す場合はthis.メソッド名()でよいのですが、それ以外のメソッドはthis.constructor.メソッド名()で呼び出しています。
rotationSizeはテトリミノによって回転軸が違うので、回転する範囲を変えるために持たせており、getRotationSizeメソッドで種類ごとの回転範囲を取得できるようにしています。
あと、テトリミノを回転させる際にテトリミノのクローンが必要だったので、コンストラクタの引数にテトリミノがある場合にその引数のテトリミノと同じテトリミノを生成するようになっています。
フィールドクラスの実装
// フィールドクラス
class Field {
constructor(width, height, nextCount, tetriminoClass) {
this.width = width; // フィールドの幅
this.height = height; // フィールドの高さ
this.nextCount = nextCount; // 次のテトリミノの数
this.tetriminoClass = tetriminoClass; // テトリミノのクラス
// フィールド初期化
this.tbl = new Array(width);
for(let x = 0; x < width; x++) {
this.tbl[x] = new Array(height).fill(0);
}
// テトリミノ初期化
this.tetrimino = new this.tetriminoClass();
this.nextTetrimino = new Array(this.nextCount);
for(let i = 0; i < this.nextCount; i++) {
this.nextTetrimino[i] = new this.tetriminoClass();
}
// 開始位置設定
this.initStartPosition();
this.setStartPosition();
this.gameOverFlg = false;
}
// テトリミノ開始位置初期設定
initStartPosition() {
this.startPositionX = Math.floor(this.width / 2) - 2; // テトリミノ開始位置X座標
this.startPositionY = -1; // テトリミノ開始位置Y座標
}
// テトリミノ開始位置設定
setStartPosition() {
this.positionX = this.startPositionX;
this.positionY = this.startPositionY;
}
// 指定座標のブロックを取得
getPositionBlock(x, y) {
// 該当座標にテトリミノがあればテトリミノのブロックを返す
if (this.positionX <= x && x < this.positionX + this.tetriminoClass.getSize()
&& this.positionY <= y && y < this.positionY + this.tetriminoClass.getSize()
&& this.tetrimino.getPointBlock(x - this.positionX, y - this.positionY) > 0) {
return this.tetrimino.getPointBlock(x - this.positionX, y - this.positionY);
}
return this.tbl[x][y];
}
// 次のテトリミノを取得
getNextTetrimino() {
return this.nextTetrimino;
}
// テトリミノ右回転
rightRotation() {
const newTetrimino = this.tetrimino.clone();
newTetrimino.rightRotation();
if (this.fieldCheck (this.positionX, this.positionY, newTetrimino)) {
this.tetrimino = newTetrimino;
}
}
// テトリミノ左回転
leftRotation() {
// 右回転と同様のため省略
}
// テトリミノ右移動
rightMove() {
if (this.fieldCheck (this.positionX + 1, this.positionY, this.tetrimino)) {
this.positionX += 1;
}
}
// テトリミノ左移動
leftMove() {
if (this.fieldCheck (this.positionX - 1, this.positionY, this.tetrimino)) {
this.positionX -= 1;
}
}
// テトリミノ下移動
down() {
if (this.fieldCheck (this.positionX, this.positionY + 1, this.tetrimino)) {
this.positionY += 1;
return false;
} else {
// 移動できなければフィールドに固定
this.fixation();
return true;
}
}
// フィールドとテトリミノが被っていないか判定処理
fieldCheck (x, y, tetrimino) {
for (let i = 0; i < this.tetriminoClass.getSize(); i++) {
for (let j = 0; j < this.tetriminoClass.getSize(); j++) {
if (tetrimino.getPointBlock(i, j) > 0) {
if (x + i < 0 || x + i >= this.width || y + j >= this.height) {
return false;
} else if (this.tbl[x + i][y + j] > 0) {
return false;
}
}
}
}
return true;
}
// フィールド固定処理
fixation() {
// テトリミノをフィールドに固定
for (let i = 0; i < this.tetriminoClass.getSize(); i++) {
for (let j = 0; j < this.tetriminoClass.getSize(); j++) {
if (this.tetrimino.getPointBlock(i, j) > 0) {
if (this.positionY + j >= 0) {
// ブロックをフィールドに設定
this.tbl[this.positionX + i][this.positionY + j] = this.tetrimino.getPointBlock(i, j);
}
}
}
}
// 行チェック処理
this.lineCount = 0;
for (let y = 0; y < this.height; y++) {
let lineFlg = true;
for (let x = 0; x < this.width; x++) {
if (this.getPositionBlock(x, y) <= 0) {
lineFlg = false;
break;
}
}
if (lineFlg) {
// 消された行から上を下に移動
for (let i = y; i >= 0; i--) {
for (let j = 0; j < this.width; j++) {
this.tbl[j][i] = this.tbl[j][i-1];
}
}
for (let j = 0; j < this.width; j++) {
this.tbl[j][0] = 0;
}
this.lineCount++;
}
}
// フィールドからはみ出ているブロックを処理
if (this.positionY < 0) {
for (let i = 0; i < this.tetriminoClass.getSize(); i++) {
for (let j = 0; this.positionY + j < 0; j++) {
if (this.tetrimino.getPointBlock(i, j) > 0) {
if (this.positionY + j + this.lineCount < 0) {
// 最終的にはみ出ている場合はゲームオーバー
this.gameOverFlg = true;
} else {
// ブロックをフィールドに設定
this.tbl[this.positionX + i][this.positionY + j + this.lineCount] = this.tetrimino.getPointBlock(i, j);
}
}
}
}
}
// テトリミノを非表示
this.positionY = this.height + 1;
if (!this.gameOverFlg && !this.fieldCheck (this.startPositionX, this.startPositionY, this.nextTetrimino[0])) {
// 次に出てくるテトリミノと重なる場合はゲームオーバー
this.gameOverFlg = true;
}
}
// 直前に消された行数を取得
getLineCount() {
return this.lineCount;
}
// 次のテトリミノを表示
next() {
if (!this.gameOverFlg) {
// 次のテトリミノを設定
this.tetrimino = this.nextTetrimino[0];
for (let i = 0; i < this.nextTetrimino.length - 1; i++) {
this.nextTetrimino[i] = this.nextTetrimino[i + 1];
}
this.nextTetrimino[this.nextTetrimino.length - 1] = new this.tetriminoClass();
this.setStartPosition();
}
}
// ゲームオーバーを判定
checkGameOver() {
return this.gameOverFlg;
}
}
処理自体はソースをよく読んでいただければわかるかと思いますが、フィールドは左上を0として処理しています。
コンストラクタでは引数でテトリミノのクラスを渡しているので、引数の値を格納しているthis.tetriminoClassを使って「new this.tetriminoClass()」でテトリミノクラスのインスタンスを生成しています。
ビュークラスの実装
// ビュークラス
class View {
// ブロックの種類ごとの色を指定
static getTypeColorArray() {
// 省略
}
constructor(width, height, nextCount, tetriminoClass) {
this.width = width;
this.height = height;
this.nextCount = nextCount;
this.tetriminoClass = tetriminoClass;
// 画面表示初期化
this.initFieldView();
this.initNextTetriminoView();
}
// フィールド表示初期化
initFieldView() {
// 省略
}
// 次テトリミノ表示初期化
initNextTetriminoView() {
// 省略
}
// フィールド表示更新
viewField(field) {
// 省略
}
// 次テトリミノ表示更新
viewNextTetrimino(tetrimino) {
// 省略
}
// スコア、ライン、レベル表示更新
viewScore (score, line, level) {
// 省略
}
// メッセージ表示
viewMessage (type) {
let message = "";
switch (type) {
case 1:
message = "SINGLE!"
break;
case 2:
message = "DOUBLE!"
break;
case 3:
message = "TRIPLE!"
break;
case 4:
message = "TETRIS!"
break;
case -1:
message = "GAME OVER!"
break;
}
$('#message').text(message);
}
}
だいぶ省略しましたが、やっていることは幅×高さ分の数のdiv要素を最初に用意してそれぞれidを振ってブロックの種類に応じて背景色を設定しています。
メッセージ表示の処理を見ればわかりますが、JQueryを使って該当idのdivのクラスを変更して色を設定しています。
改行用のdiv要素も使用して後はCSSに任せてレイアウトしていますが、tableレイアウトを使用したほうが楽な気がします。
ビュークラスに表示処理をまとめているので、このクラスを変えるだけで表示を別の方法で作ることもできるはずです。
ゲームクラスの実装
// ゲームクラス
class Game {
// 使用するテトリミノクラスを取得
static getTetriminoClass() {
return Tetrimino;
}
// 使用するフィールドクラスを取得
static getFieldClass() {
return Field;
}
// 使用するビュークラスを取得
static getViewClass() {
return View;
}
static getWidth() {return 10;}
static getHeight() {return 20;}
static getNextCount() {return 3;}
static getScoreTable() {return [0, 10, 30, 60, 100];}
constructor() {
const viewClass = this.constructor.getViewClass();
this.view = new viewClass(this.constructor.getWidth(), this.constructor.getHeight(), this.constructor.getNextCount(), this.constructor.getTetriminoClass());
this.startSettings();
this.view.viewScore (this.score, this.line, this.level)
this.stopFlg = true;
}
startSettings() {
this.score = 0; // 初期スコア
this.line = 0;
this.level = 1;
this.downTime = 2000; // 初期落下時間 2秒
this.nextTime = 500; // 初期固定後時間 0.5秒
}
// startボタン操作
startButton() {
clearTimeout(this.timeout); // 落下時間キャンセル
clearTimeout(this.nextTimeout); // 固定時間キャンセル
const fieldClass = this.constructor.getFieldClass();
this.field = new fieldClass(this.constructor.getWidth(), this.constructor.getHeight(), this.constructor.getNextCount(), this.constructor.getTetriminoClass());
this.startSettings();
this.stopFlg = false; // 操作停止フラグ
// 表示更新
this.view.viewField(this.field)
this.view.viewNextTetrimino(this.field.getNextTetrimino());
this.view.viewScore (this.score, this.line, this.level)
this.view.viewMessage(0);
const self = this;
this.timeout = setTimeout(function(){self.down()},this.downTime); // 自動落下処理
}
// 落下時間更新
updateDownTime() {
this.downTime = 2000 - (this.level - 1) % 20 * 100 - Math.floor((this.level - 1) / 20) * 10;
}
// 固定後時間更新
updateNextTime() {
this.nextTime = 500 - Math.floor((this.level - 1) / 20) * 50;
}
updateScore() {
this.score += this.constructor.getScoreTable()[this.field.getLineCount()];
}
updateLevel() {
this.level = Math.floor(this.line / 10) + 1;
}
// 右回転操作
rightRotationButton() {
if (!this.stopFlg) {
this.field.rightRotation();
this.view.viewField(this.field);
}
}
// 左回転操作
leftRotationButton() {
// 右回転操作と同様のため省略
}
// 右移動操作
rightMoveButton() {
// 右回転操作と同様のため省略
}
// 左移動操作
leftMoveButton() {
// 右回転操作と同様のため省略
}
// 下移動操作
downButton() {
if (!this.stopFlg) {
this.down();
}
}
// 下移動処理
down() {
// 他の自動落下処理をキャンセル
clearTimeout(this.timeout);
const self = this;
// 下移動処理
if(this.field.down()) {
// ブロックが固定された場合
// 操作受付を停止
this.stopFlg = true;
// スコア更新
this.line += this.field.getLineCount();
this.updateScore();
this.updateLevel();
// 表示更新
this.view.viewField(this.field);
this.view.viewScore (this.score, this.line, this.level)
this.view.viewMessage(this.field.getLineCount());
// 落下時間、固定後時間更新
this.updateDownTime();
this.updateNextTime();
// ゲームオーバーの場合はゲームオーバー表示して終了
if (this.field.checkGameOver()) {
this.view.viewMessage(-1);
return;
}
// 指定時間後に次処理呼出
this.nextTimeout = setTimeout(function(){self.next()}, this.nextTime);
} else {
// 下移動できた場合
// 表示更新
this.view.viewField(this.field);
// 自動落下処理
this.timeout = setTimeout(function(){self.down()},this.downTime);
}
}
// 次処理
next() {
// 他の次処理をキャンセル
clearTimeout(this.nextTimeout);
// 操作受付を再開
this.stopFlg = false;
// フィールドの次処理呼出
this.field.next();
// 表示更新
this.view.viewField(this.field);
this.view.viewNextTetrimino(this.field.getNextTetrimino());
// 自動落下処理
const self = this;
this.timeout = setTimeout(function(){self.down()},this.downTime);
}
}
テトリミノの自動落下とフィールドへの固定から次のテトリミノが表示されるまでの間隔をsetTimeoutを使って設定しています。
その際、setTimeoutの第1引数のfunction内をself.down()としているのは、このfunctionが呼び出された時のthisがこのゲームオブジェクトを指していないので、一旦変数に自信を設定して呼び出すようにしています。
また、下移動でdown()が呼ばれた場合に自動落下処理でdown()が呼ばれないようclearTimeoutで自動落下処理をキャンセルしています。
コントローラーの実装
// コントローラー
$(function () {
var game = new Game();
$(document).on('keydown', function(e) {
// Xボタン
if(e.keyCode === 88) {
game.rightRotationButton();
}
// Zボタン
if(e.keyCode === 90) {
game.leftRotationButton();
}
// ←ボタン
if(e.keyCode === 37) {
game.leftMoveButton();
}
// →ボタン
if(e.keyCode === 39) {
game.rightMoveButton();
}
// ↓ボタン
if(e.keyCode === 40) {
game.downButton();
}
});
// スタートボタン
$("#start").on('click', function() {
game.startButton();
});
});
回転や移動の処理をキーボードのボタンを押して処理を呼び出せるようにし、スタートボタン押下のイベントも追加します。
これでtetris.js、game.jsをHTMLで呼び出してテトリスが遊べるようになりました!
ホールド機能を追加
今まで作ってきたもので遊べるものが1つできましたが、上で作ったものを変更せず、継承したクラスを使って別のゲームを作ることができますのでいろいろ試してみるとよいかと思います。
例えばゲームクラスのupdateDownTime()をオーバーライドすればテトリミノの落下速度が変わります。
今回は単純にメソッドをオーバーライドしない継承のサンプルとしてホールド機能を追加してみたいと思います。
ホールド機能とはテトリミノをキープしておける機能です。
この機能を上で作ったコードはそのままで継承して作ってみましょう。
フィールドクラスの拡張
// フィールド拡張クラス
class HoldableField extends Field {
constructor(width, height, nextCount, tetriminoClass) {
super(width, height, nextCount, tetriminoClass);
this.holdTetrimino = null;
this.holdFlg = false;
this.tmpTetrimino = this.tetrimino.clone(); // ホールド用の初期状態テトリミノ
}
// 保持テトリミノを取得
getHoldTetrimino() {
return this.holdTetrimino;
}
// テトリミノ保持
hold() {
if (this.holdFlg) {
return false;
}
this.holdFlg = true;
if (this.holdTetrimino !== null) {
// 保持テトリミノを設定
this.tetrimino = this.holdTetrimino;
this.positionX = this.startPositionX;
this.positionY = this.startPositionY;
} else {
super.next();
}
this.holdTetrimino = this.tmpTetrimino;
return true;
}
// 次のテトリミノを表示
next() {
super.next();
this.holdFlg = false;
this.tmpTetrimino = this.tetrimino.clone();
}
}
constructorの中のsuperでFieldクラスのコンストラクタが呼ばれています。
その後にホールド用の変数を追加しています。
hold()が呼び出されたらホールドしますが、使用した後に次のテトリミノが出てくるまで使えないようにしています。
最初ホールドしているテトリミノがない状態で次のテトリミノを表示し、なおかつ再ホールドできないようFieldクラスのnextメソッドを呼ぶようsuper.next()としています。
ビュークラスの拡張
ビュークラスはHTML上に表示領域を追加して次のテトリミノと同様に表示する処理を追加しました。
// ビュー拡張クラス
class HoldableView extends View {
constructor(width, height, nextCount, tetriminoClass) {
super(width, height, nextCount, tetriminoClass);
this.initHoldTetriminoView();
}
// 保持テトリミノ表示初期化
initHoldTetriminoView() {
// 省略
}
// 保持テトリミノ表示更新
viewHoldTetrimino(tetrimino) {
// 省略
}
}
ゲームクラスの実装
フィールドクラスとビュークラスの設定をオーバーライドして指定し、ホールドボタンの受付機能を追加します。
// ゲーム拡張クラス
class HoldableGame extends Game {
static getFieldClass() {
return HoldableField;
}
static getViewClass() {
return HoldableView;
}
// 保持操作
holdeButton() {
if (!this.stopFlg) {
if (this.field.hold()) {
this.view.viewField(this.field);
this.view.viewHoldTetrimino(this.field.getHoldTetrimino());
}
}
}
}
コントローラーの実装
あとはキープ用のボタンを追加します。
// コントローラー
$(function () {
var game = new HoldableGame(10, 20, 3);
$(document).on('keydown', function(e) {
// 省略
// Sボタン
if(e.keyCode === 83) {
game.holdeButton();
}
});
// 省略
});
これでtetris.js、tetris2.js、game2.jsをHTMLで指定すればホールド機能を追加して遊べます。
最後に
プログラミングに明確な正解というものはなく、上のプログラムについても継承の仕方次第では継承が難しかったりバグが発生したりします。
今回は「継承時に拡張できる機能・設定」という設計を行ったことで、この設計の範囲で拡張性を持たせられるオブジェクト指向のプログラムになったかと思います。
上でも述べましたが、上で作ったものに対して継承したクラスを使っていろいろ試してみてください。
いろいろ試すことでオブジェクト指向の利便性がわかったり、プログラムの改善点が見つかったりするかもしれません。