テトリスをJavaScriptでまじめに作る。
連休に併せて、ちょいと作成したcanvasライブラリーの調整のためにテトリスを作ってみようと思います。
リアルタイムなゲームで簡単に作れる定番?といえばテトリスですよね。せっかくなのでOOでまじめに設計してみたいと思います。
2011/7/18 Wikiを元に記述を修正
ちなみにいつもblog用のダイアグラムはyUMLを活用していますが、今日は非常に重たいので後で変換します。
できあがり
1.分析
1.1 シナリオ
ゲームの内容をシナリオとして簡単に記述します。
1.1.1 ゲームを始める
ゲームの開始を促す表示がされていて、ボタンを押すとゲームが開始される。
最初は何も無いステージに様々な形状のブロックが上から落ちてくる。
ブロックは中央上部から落ちてくる。
ブロックが落下を始めると次のブロックが明示される
次のブロックは3つまで表示される。
1.1.2 操作する
プレイヤーは落ちていくブロックを回転、あるいは左右に移動させながらステージの下に隙間無く埋めていく。
また、ブロックの落下速度をプレイヤーの操作によって落下させることが出来る。
(加速中はスコアが増加する)
1.1.3 ラインを消す
ブロックが落ちた事で隙間無く埋まったラインは、消えてスコアーとなる。
同時に複数のラインを消すとスコアーは指数的に増加する。
ラインが消えると上のラインが下に落ちてくる
(※ライン単位でしか下に落ちないので、ブロックの粒の下に空洞があっても粒単独で下に落ちたりはしない(つまり連鎖は無い))。
1.1.4 レベルアップ
ゲームを進めてスコアーが一定以上になるとレベルが上がり、無操作状態でのブロックの落下速度が速くなる。
レベルアップと共に背景やBGMに変化がある。
1.1.5 ゲームオーバー
ブロックの落下口からブロックが落ちることが出来なければ、ゲームオーバーとなる。
ゲームオーバーになると、ゲームを終了するか、新しくゲームを始めるかの選択肢が表示される。
1.1.6 その他と非機能要件
-
ステージは縦20ライン、横10粒。
-
落下表示は、一粒よりも細かい粒度で表現されること。
-
横移動は粒単位で良い事とする。
-
言語はJavaScript。
-
プラットフォームは Webブラウザー 及び HTML5 canvasとする。
-
落下したブロックは見た目上ラインに接触していても回転や横滑り出来る猶予(遊び期間)を設ける。
「遊び中」に移動または回転すると、「遊び中」のカウントはクリアされる。
-
操作はキーボード(もしくはバーチャルパッド)
1.2 概念の抽出
前述のシナリオから名詞・動詞を拾う形で、思いつくままに概念を抽出してみます。
一つのテクニックとして、物、事、場の視点から抽出します。
1.2.1 物
プレイヤー
ブロックテトリミノ
赤(縦棒)、青(凸型)、黄(四角)、緑(残りのやつ)
次のブロックテトリミノ
粒(造語、ブロックを構成する一つ一つの四角を粒と呼ぶことにする)
ステージ
枠?
ライン
スコア
背景
BGM
ジングル
タイマー(暗黙の了解で追加)
※ブロックはプレイヤーが操作しなくても「時々刻々」と落下していく。時々刻々、即ちインターバルタイマーの登場(これを重力と表現するかは人の好き好き)
1.2.2 事
(ブロックが)落ちる
(ブロックが)加速する
(ブロックが)回転する
(ブロックが横に)移動する
(ブロックが)止まる(地面か、ラインにぶつかれば止まる)
(次のブロックを)決める
(レベルが)上がる
(スコアが)増える
(ラインが)消える
(ラインが)落ちる
(ゲームが)終わる
(ゲームが)始まる
(ステージを)クリアする
(BGMを)鳴らす
たまたまですが、どれもオブジェクトではなく振る舞いのようです。
1.2.3 場
ゲーム
1.3 概念モデル
抽出した概念から静的モデルを作成(知見を基にオブジェクトでは無く属性への落とし込みも行ってます)。
[ゲーム|スコア;レベル|開始();終了();BGM再生();次ブロック決定()]
[テトリミノ]+-[粒]
[テトリミノ|タイプ(赤,青...)|落下();加速();回転();移動()]
[ステージ]+-[ライン]
[ ステージ||初期化()]
[ライン]+-[粒]
[ライン|行|消す();落とす()]
1.4 シーケンス
※下記のシーケンスの文法は独自なのでがんばって解釈してください。
1.4.1 初期化
ゲーム->背景.作成()
ゲーム->スコア.初期化()
ゲーム->レベル.初期化()
ゲーム->ステージ.作成()->ライン.作成()
ゲーム->”Push Start”.表示()
ゲーム->BGM.再生(オープニング)
1.4.1 ゲーム開始
ゲーム->ステージ.クリア()->ライン.クリア()
ゲーム.次のテトリミノを決定()
ゲーム->テトリミノ:次.作成()
ゲーム.:次を落下するテトリミノへ移動()
1.4.2 プレイヤーなどによる操作
プレイヤー->テトリミノ.加速()
プレイヤー->テトリミノ.移動()
プレイヤー->テトリミノ.回転()
タイマー->テトリミノ.落下()
タイマー->底から順番に|ライン.消す()※但し、消せるなら
タイマー->ライン.落とす()※但し、落ちるなら
思いつくままに書くとこうなるが、自明なので以下に修正
タイマー->ゲーム.更新()->ブロック.落下(Level) ※落下出来なければ、ゲームオーバー
タイマー->ゲーム.更新()->スコア.更新(加速中?)
タイマー->ゲーム.更新()->底から順番に|ライン.消す()※但し、消せるなら
タイマー->ゲーム.更新()->スコア.更新(ライン数)
タイマー->ゲーム.更新()->底から順番に|ライン.落とす()※但し、落ちるなら
ここで気になるのがラインを消す演出です。ラインがそろうと、パッと光って、ポンと消えて、それからバスッと落ちます。
これを表現するためにはラインには状態管理が必要です。
通常->消せる(光ってる)->消した ※ライン自体が消えるのか、ライン上の粒だけがいなくなるのかは実装のお話。
1.4.3 ゲームオーバー
ゲーム->”Game Over”.表示()
ゲーム->選択肢.表示()
2. 設計
概念モデルでは当然、プラットフォームやアーキテクチャーには依存していないので、ここでモデルを少し具体化します。
2.1 アーキテクチャー
設計モデルに入る前に、アーキテクチャーの考察をしておきます。
まず、画面表示にはHTML5 canvasを使用します。canvasへの描画については、自前のライブラリーを使用します。
この時点で「背景」、「前景」、「スプライト」のオブジェクトが登場します。
背景には背景を、前景にはステージを表示します。スプライトは一粒、1スプライトとします。
描画は設定されたFPSに基づいて発生するインターバルタイマー毎に画面をクリア、更新します。
一粒は14px,14pxとします。
テトリミノの「遊び」の表現は状態遷移モデルで管理します。
[落下中]->[遊び中]
[遊び中](移動または、タイムアウト)->[落下中]
2.2 設計モデル
(ドメイン)モデルとビューの分離は難しく考え無い事にします。
全く分離しないなら、「自分のことは自分でする」という振る舞いの配置原則に従って、モデル自身が画面への描画責務を負います。まあ、それは現実解ではないでしょう(たぶん)。
(モデル)
[粒]->[スプライト]->[イメージ] ※このイメージはCSSスプライトとします。
[ステージ]->[イメージ] ※ステージのイメージ(枠)は一枚絵
[背景]->[イメージ] ※背景も一枚絵
[BGM]->[Audio] ※このAudioはHTML Audio を想定
プレイヤーは現実的にキー入力とします。
(シーケンス)
キー入力->ブロック.回転()
テトリミノは、回転や落下に伴い、スプライトの座標を更新します。
一般にモデルとビューは分離する物とされています。それを守るならコントローラーかスプライト側がモデルであるテトリミノを参照するべきです。
まあ、ここは適当に行きましょう(気になれば後で修正すればOKです)。
(モデル)
[キャンバス]->[スプライト]
[キャンバス]->[canvas]
キャンバスには全てのスプライトへの参照を保有させます。
と言うことは、次テトリミノの作成や次テトリミノを落下テトリミノへ移行した場合にスプライトをキャンバスへ登録したり、
逆にラインが消えると該当するスプライトをキャンバスから削除する必要があります。
(モデル)
[キャンバス||登録(スプライト);削除(スプライト)]
[ゲーム]->[テトリミノ:次]
[ゲーム]->[テトリミノ:落下中]
(シーケンス)
ゲーム->ブロック:次.作成()
ゲーム->キャンバス.スプライト登録(:次.スプライト群)
次のブロックから、落下ブロックへの昇格時はスプライトをそのまま引き継いで、座標だけを変更する事にします。
(シーケンス)
ゲーム->:次.昇格()->スプライト.移動()
描画は一連の更新作業が済んだ後に実施します。
(シーケンス)
タイマー->ゲーム.更新()
タイマー->キャンバス.更新()
このゲームの実装上のトピックとしては、粒の当たり判定と回転によるブロックの表示切り替えでしょうか。
これは、実装時にはっきりさせることにします。
3. イテレーション
最初のイテレーションでどこまで実装しましょうかね。
とりあえず、スコアとレベルの実装は無し。
BGMも無し。
ラインが消える際の演出も無し。
ブロックが落下できるまでとかけちくさいことは言わず、ゲームとして成立するレベルまでは仕上げることにします。