JavaScriptゲームの作り方

日付変更内容
2002-04-01プロジェクト開始
2002-04-06初版完成

目次

  1. この文書で書いたもの
  2. 何を作りたいのか
    1. ゲームの流れ
    2. 各画面説明
      1. フィールド画面
      2. 戦闘画面
      3. ゲームオーバー画面
    3. ルール
      1. 主人公移動時ルール
      2. 戦闘時ルール
      3. ゲームオーバー時ルール
  3. どうやって作るか
    1. 言語の選択
    2. プログラムの設計
  4. コーディング&テスト
    1. キャンバスとなるHTMLを作ろう
    2. クラスの書き方
    3. まずモデルクラスを作ろう
    4. ビュークラスを作ろう
      1. フィールドビュー
      2. 主人公状態ビュー
      3. ビュークラスのテスト
    5. 入力によってモデルが変化するようにしよう
    6. ルール適用しよう
    7. モードクラスを作ろう
      1. モードクラスとは
      2. キーマネージャーを作ろう
      3. ゲームモードクラスを作ろう
      4. 戦闘画面を作ろう
      5. ゲームオーバー画面を作ろう
      6. テスト
    8. 終了
    9. デバッグ手法
  5. あそぶ
  6. 評価
    1. 以前作ったランダムマップRPGをWebブラウザ上にベタ移植する
    2. webブラウザ上でのゲーム開発の基盤を作る
  7. 課題
    1. ゲーム内容に関して
    2. JavaScriptでの開発に関して
  8. 最後に

1 この文書で書いたもの

この文書では私が「ブラウザ上で動く簡単なミニゲームを作りたい」と思ってから、それを実現するまでの道のりを簡単に記録、解説した。

JavaScriptの参考だけでなく、ミニゲームの作り方の参考にもなるかも知れない。

JavaScriptの基礎や細かい言語仕様については説明していない。私はJavaScriptについて全体を把握していない。今回のプログラムに必要な部分だけを調べて知っているだけだ。

2 何を作りたいのか

達成すべき目標はなにか。ものを作るには必ずきっかけがある。理由がある。何を作りたいのか、なぜ作りたいのか。その部分を分析しておかないと、後々作る意義を見失うことになりやすい。

そもそもこのプロジェクトを立ち上げたのは「このごろゲーム作ってないな〜」という不満からだった。私は短時間で何度も遊べるミニゲームが好きだ。単純だがアイデア次第で面白くなるし、何より暇なときに何度も遊べる。それを人に見せたりするのも楽しい。しかしVC++でミニゲームを作ると発表の場に困るのだ。webに上げても「ミニゲーム程度」をわざわざダウンロードして展開してウィルススキャンして実行してくれる人は少ない。開発側から見てもVC++だと手間がかかる。細かい部分に時間をとられてゲームそのものにかけられる時間が少なくなってしまう。アイデアをもう少し短時間で実現して検討できる手段が欲しい。そんなことを思いながらも時間に余裕のない昨今、ゲームづくりはおろそかになっていった。

このような背景から今回私が作りたいのはwebブラウザ上で動作するミニゲームだ。ブラウザ上で動けば暇なときにいつでもどこでも遊べる。人に見せるのも簡単だ。ブラウザ上で動くと言えばJavaAppletかJavaScriptだが、どちらもC++よりも楽に作れそうだ。少なくとも画像の転送をアセンブラで書く必要はない。

さて、問題はどんなゲームを作るかだ。そう考えたとき、真っ先に思いついたのがランダムマップRPGである。

ランダムマップRPGは大昔(小学生?中学生?の頃)J氏との関連でN88日本語BASIC(86)で作ったミニゲームだ。RPGと言っても名ばかりでランダムで生成した陸と海だけの一画面フィールド上を主人公が歩き回り、これまたランダムで出てくる敵と戦い、何歩歩けるか、何ゴールド獲得できるかを競うゲームだ。一度歩いた場所は海になってしまうところが面白い。

実はこれ、MS-DOSからWin32(VC++)に移行したときにも移植したことがある。簡単なゲームなので手始めには打ってつけだろう。ゲームデザインに曖昧な点は全くない。何しろ昔のソースが残っているのだから。作るゲームはランダムマップRPGに決定だ。

達成したい目標をまとめてみよう

これらは十分に実現可能だと思える。1週間以内に。直感的に。

ゲームの仕様を以下におさらいしておく。

2.1 ゲームの流れ

         ゲーム開始
           ↓
     −−−−フィールド画面−−−−
      ↑↓         ↓
(戦闘終了)↑↓(敵と遭遇)   ↓(もう歩けない)
      ↑↓         ↓
    戦闘画面       ゲームオーバー画面

2.2 各画面説明

2.2.1 フィールド画面

主人公がフィールドを歩くところを表現する画面。

フィールド画面には下図のように升目(セル)が2次元に並んでいる。

□■■□□□
■□■□■□
■■■●□■
■□■□■■
□■■■■□
■■■□□□
所持金:200G

■が陸地、□が海、●が主人公だ。下には所持金が表示される。

2.2.2 戦闘画面

 戦闘を表現する画面。

 敵が現れた!!
 敵の強さ : 58
 貴方の強さ: 65

 貴方の勝ち 192G獲得した!

2.2.3 ゲームオーバー画面

ゲームの結果を表示する画面。

 もう動けない!

 258G 獲得!
 64 歩移動!

 総合得点 : 1289

2.3 ルール

2.3.1 主人公移動時ルール

フィールド画面において主人公はプレイヤーの操作によってフィールド上を上下左右に移動できる。ただし移動時には以下のルールがチェックされる。

  1. フィールド外、海へは移動できない
  2. 移動元の地形属性は水になる
  3. 移動後、上下左右が全て移動不可能地形の場合ゲームオーバー
  4. 移動後、ある確率で敵が出現する

2.3.2 戦闘時ルール

主人公移動時ルールの4番目が適用されたとき戦闘が発生する。戦闘は以下の手順で行われる。

  1. 敵の強さを0から100の乱数で決める。
  2. 主人公の強さを0から100の乱数で決める。
  3. 敵の強さ主人公の強さを比べて大きい方が勝ち。強さが同じ場合は敵の勝ち。
  4. 金額を0から200までの乱数で決め、主人公が勝ったのなら所持金に追加し、負けたのなら所持金から引く。
  5. 戦闘終了

2.3.3 ゲームオーバー時ルール

主人公移動時ルールの3番目が適用されたときにゲームオーバーになる。ゲームオーバー時には以下のルールが適用される。

  1. ((獲得金額) + (移動歩数) * 20)が総合得点となる。
  2. ゲーム終了

3 どうやって作るか

何を作るか決まったら次はどうやって作るかを考える時だ。ちなみに、この「何を作るか」と「どうやって作るか」は分けた方がいい。目的と手段を混同してはいけないとはよく言われることだ。とはいっても完全に分離できるわけではない。近年ではウォーターフォールよりも繰り返し型の開発プロセスが注目されている。それでも十分「何を作るか」を考えてから「どうやって作るか」を考えるべきだと思う。詳しくはソフトウェア工学の本を参考にしよう。

3.1 言語の選択

本プロジェクトではJavaScriptを選択した。以下はその理由だ。

webブラウザ上で動くアプリケーションを作るには以下のような方法が存在する。

まずplug-in開発は論外。興味はあるけど。

flushやshock waveはマクロメディア製品を買わなければならない。あいにく私は持っていない。

CGIは言語ではないが動きのあるサイトを作る一つの手段だ。しかし今回の場合、主人公が一歩動くたびにアクセスされては迷惑だ。それにもっとリアルタイム性の高いゲームを作りたくなったときには応用が利かないし、それではこのプロジェクトの2番目の目標を十分達成したとは言えない。ただし、ランキングなどの部分で組み合わせて使うことは十分にあり得るだろう。

残るのはJavaAppletとJavaScriptで、これらは両方とも魅力的な選択肢だ。

まずJavaApplet。Javaは開発環境はフリーだしライブラリが充実している。JavaAppletはJavaScriptと比べた場合以下のような利点がある。

一方JavaScriptはJavaAppletと比べた場合、以下のような利点がある。

ここで重要なのは目的と合致するのはどちらかだ。私は手軽なゲームプレイ、手軽な開発を求めている。JavaAppletならかなり複雑なことができるが、コンパイルや圧縮の手間やjavaの起動時間の問題がある。どちらかといえばJavaScriptが適任だろう。

3.2 プログラムの設計

前述の仕様を見るとパッと下図のようなクラス図が浮かぶ。なぜ浮かぶかと言えば、これまで作ってきたものの類型と重ね合わせているのだろう。パターンというやつだろうか。ちなみにこうして図に起こしたのは開発が終わったあとだ。このくらいの規模なら頭の中だけで全体を把握できるだろうが、大規模な場合は事前に書いておいて整理した方が効率的だ。

class

一通り各クラスの役割について説明する。

ゲームモード
ゲーム全体に対応するクラスだ。このクラスのインスタンスはゲーム中ずっと生き続け、ゲームに関連する全てのインスタンスは直接的、間接的にこのインスタンスが所有することになる。それら資源の管理と同時に、フィールド画面の制御も受け持つ。必要なときにサブモード(戦闘画面、ゲームオーバー画面)を起動して、ゲーム全体の流れを作る役目を持つ。
戦闘モード
戦闘画面を制御するためのクラスだ。戦闘結果を計算、表示し、結果を主人公モデルに反映させる。終了時にあらかじめ指定されているオブジェクトへ戦闘画面が終わった旨を通知するようにしておく。これによりゲームモードクラスに依存しなくて済む。戦闘モードクラスが依存するのは表示関係と主人公モデルだけだ。
ゲームオーバー
ゲームオーバー画面を制御するためのクラスだ。ゲームオーバーになったことやゲームの結果を表示する。戦闘モードと同じように、終了時にはあらかじめ指定されているオブジェクトへイベントを送る。これによりゲームモードクラスに依存しなくて済む。
フィールドモデル
フィールド(マップ)が持つデータを保持するためのクラスだ。フィールドのサイズと形状(属性配列)からなる。これらのパラメータを管理することに集中し、他のことは一切しない。他のクラスには依存しておらず、もっとも独立性が高いクラスである。
主人公モデル
主人公が持つデータを保持するためのクラスだ。主人公のフィールド上の位置と所持金からなる。これらのパラメータを管理することに集中し、他のことは一切しない。他のクラスには依存しておらず、もっとも独立性が高いクラスである。
フィールドビュー
フィールドモデルの内容を画面に表示する責任を持つクラスだ。フィールド画面にフィールドを表示するのに使う。フィールドモデルが更新されたら自動的に画面が更新されるように、Observerパターンを導入することも考えられるが、そこまで複雑にする必要もなさそうなので止めておく。
主人公状態ビュー
主人公モデルの内容を数値で画面に表示する責任を持つクラスだ。フィールド画面に所持金を表示するのに使う。

このようにクラスに分割するのは、それぞれを交換可能にするためである。例えば戦闘画面をビジュアル的でもっと格好良くしたくなったら、戦闘モードクラスだけ別なものと交換してしまえばよい。そのときの戦闘モードクラス以外への修正をほとんど0にするためには、極力依存関係を少なくする必要がある。クラスを分けるときは依存関係や変更され安いところ、ライフタイム、インタフェース等を考慮して分割するといいだろう。

ここではこの程度で十分だろう。JavaScriptのことをあまり知らないし、これ以上細かくしてもどの程度実現できるか分からない。JavaScriptを勉強しながら少しずつ作っていくことにする。

4 コーディング&テスト

さあ、いよいよ待ちに待ったコーディングの時間だ。

4.1 キャンバスとなるHTMLを作ろう

とりあえずJavaScriptで何か動かしてみよう。

<html>
<head>
<title>ランダムマップRPG</title>
<script type="text/javascript"><!--
/**
最初に実行される関数。
*/
function startup()
{
    document.write("hello world!!");
}
--></script>
</head>

<body>
<script type="text/javascript"><!--
startup();
-->
</script>

</body> </html>

おなじみのハローワールドだ。結果を確認したいならJavaScriptを有効にしてstep1.htmlを見て欲しい。

このhtmlをキャンバスにして、これからゲームを描いていくことになる。

4.2 クラスの書き方

本格的なコーディングをする前に、クラスの書き方を決めておこう。JavaScriptはクラスをちゃんとサポートしていない。しかし基本的にオブジェクト指向的な設計をしているので、JavaScriptでもクラスが書きたくなってくる。調べてみるとクラスと同じようなことができるみたいだ。

JavaScriptでのクラスの書き方はgoogleで「JavaScript クラス」をキーワードに検索して調査した。

基本的なクラスの書き方を以下にまとめる。

/**
クラス変数
*/
ClassName.classVariable = initialize;

/**
構築
*/
function ClassName()
{
    //メンバ変数の初期化
    this.memberVariable = initialize;
}

/**
メンバ関数
*/
ClassName.prototype.memberFunc = function()
{
    //メンバ変数の参照、変更
    this.memberVariable++;
    return this.memberVariable;
}

1つのクラスは1つの.jsファイルにまとめるものとする。

コーディングに戻ろう。

4.3 まずモデルクラスを作ろう

フィールドモデルクラス、主人公モデルクラスを作ろう。なぜモデルから作るかというと一番基本的な部分だからなのと一番他への依存が少ないからだ。このクラスはこのクラス単体で動ける。すなわち作った後すぐテストができる。例えばビュークラスの場合だとモデルクラスに依存しているので、先にビュークラスを作ってもモデルクラスを作らないとテストができない。作った物をすぐテストできることは重要だ。バグを少なく保てるし、頻繁に達成感が得られる。

モデルクラスはとても単純だ。必要なデータを保持し、それに対する基本的な操作を用意するだけだ。

完成品を以下に示す。ここに示すものは完成品で幾つかのメソッドはこのステップ以降に追加している。いきなり全てを書いたわけではない。基本的に必要な機能は必要なときに追加すればよい。

このクラスが正常に動作するかどうか、テストスクリプトを書いてテストしてみよう。以前のスクリプトに追加した部分は赤く表示してある。

<html>
<head>
<title>ランダムマップRPG</title>
<script type="text/javascript" src="FieldModel.js"></script>
<script type="text/javascript" src="HeroModel.js"></script>
<script type="text/javascript"><!--

//定数
FIELD_MODEL_SIZE_X = 24;
FIELD_MODEL_SIZE_Y = 12;

/**
最初に実行される関数。
*/
function startup()
{
    //モデルの構築
    fieldModel = new FieldModel(FIELD_MODEL_SIZE_X
                              , FIELD_MODEL_SIZE_Y);
    fieldModel.generateMapRandom();
    heroModel = new HeroModel();
    heroModel.setPositionRandom(FIELD_MODEL_SIZE_X
                              , FIELD_MODEL_SIZE_Y);

    //テスト出力
    var x, y;
    for(y = 0; y < fieldModel.getSizeY(); y++){
        for(x = 0; x < fieldModel.getSizeX(); x++){
            var atr = fieldModel.getCellAtr(x, y);
            var cellStr;
            if(x == heroModel.getX() && y == heroModel.getY()){
                cellStr = "●";
            }
            else if(atr == FieldModel.CELL_ATR_GROUND){
                cellStr = "■";
            }
            else if(atr == FieldModel.CELL_ATR_WATER){
                cellStr = "□";
            }
            document.write(cellStr);
        }
        document.write("<br>");
    }
    document.write("所持金:" + heroModel.getMoney());
}
--></script>
</head>

<body>
<script type="text/javascript"><!--
startup();
-->
</script>

</body> </html>

結果を確認したいならJavaScriptを有効にしてstep3.htmlを見て欲しい。リロードするとランダムでフィールドが作られていることが分かるだろう。

このようなテキスト出力では動きのあるものが試せないので、次はしっかりしたビューを作ることにする。

4.4 ビュークラスを作ろう

ビューの役割はモデルを画面に表示することだ。それもただ1度表示するだけでなくモデルが変化したときにその変化を画面に反映する必要がある。

モデル、ビューとくるとObserverパターンを連想するが、ここでは更新を通知する機構は設けない。複雑になるしその割に必要性が薄いからだ。

4.4.1 フィールドビュー

まずフィールドビューを作ろう。その動作は至ってシンプルだ。インスタンス構築時にフィールドモデルと関連づけ、表示画像を準備する。updateメソッドでフィールドの状態と主人公の位置を読み出して表示してある画像を変更する。

問題はどうやって画像を表示し、変更するかだ。検索エンジンを使って調べてみると実に様々な方法があることが分かる。ブラウザによって違いがあり、多くのブラウザで動作させる(クロスブラウザ)には幾つかの方法を組み合わせなければならない。とはいってもいきなり多くのブラウザに対応するのは大変だ。

ここではW3C DOM(邦訳)を利用する。標準規格でありIE5以降、Netscape6(Mozilla)と複数のブラウザで対応しているからだ。これらの最新鋭のブラウザを持っていない人はいますぐゲットしよう。マシンパワー十分ならMozillaがお勧めだ。Windowsでzip版なら展開するだけですぐ使える。皆が標準に準拠したブラウザを使えば開発の負担が減り、より品質の高いアプリケーションが増えるだろう。

このようにブラウザに依存する画像制御部分はImageObjクラスとしてまとめよう。いわゆるスプライトクラスだ。こうしてまとめることによって対応ブラウザを変えたいときに修正する部分を局所化できる。さらに、このクラスは他のプロジェクトで再利用することを意識している。画像制御は他のプロジェクトでも頻繁に利用することが予想できるからだ。これによりプロジェクトの2番目の目標に近づくことになる。

さて、こうしてできたImageObjクラスを使ってFieldViewクラスを実装しよう。やることはフィールドモデルから地形データを読み出してImageObjクラスを使って表示するだけだ。

ここで忘れてはならないのが画像素材の作成だ。陸、海、主人公の画像を以下のように用意した。

4.4.2 主人公状態ビュー

次に主人公の状態を表示するビューを作成しよう。これも至って簡単。所持金をテキストで表示するだけなのだから。

ただテキスト内容を動的に変更するとなると、画像の場合と同じくブラウザによって様々な方法がある。ここでは画像の場合と同じようにW3C DOMを利用することにしよう。再利用とブラウザ依存部分の局所化のためにテキスト制御部をTextObjクラスとしてまとめた。

TextObjクラスを使ってHeroStateViewクラスを実装しよう。

4.4.3 ビュークラスのテスト

完成したクラスを以下に示す。

このクラスが正常に動作するかどうか、テストスクリプトを書いてテストしてみよう。前のテストスクリプトの表示部分をビュークラスに置き換える。

<html>
<head>
<title>ランダムマップRPG</title>
<script type="text/javascript" src="FieldModel.js"></script>
<script type="text/javascript" src="HeroModel.js"></script>
<script type="text/javascript" src="FieldView.js"></script>
<script type="text/javascript" src="HeroStateView.js"></script>
<script type="text/javascript" src="ImageObj_W3C.js"></script>
<script type="text/javascript" src="TextObj_W3C.js"></script>
<script type="text/javascript"><!--

//定数
FIELD_MODEL_SIZE_X = 24;
FIELD_MODEL_SIZE_Y = 12;

/**
最初に実行される関数。
*/
function startup()
{
    //モデルの構築
    fieldModel = new FieldModel(FIELD_MODEL_SIZE_X
                              , FIELD_MODEL_SIZE_Y);
    fieldModel.generateMapRandom();
    heroModel = new HeroModel();
    heroModel.setPositionRandom(FIELD_MODEL_SIZE_X
                              , FIELD_MODEL_SIZE_Y);

    //ビューの構築
    fieldView = new FieldView(fieldModel, heroModel);
    fieldView.update();
    heroStateView = new HeroStateView(heroModel);
    heroStateView.update();
}
--></script>
</head>

<body>
<script type="text/javascript"><!--
startup();
-->
</script>

</body> </html>

結果を確認したいならJavaScriptを有効にしてstep4.htmlを見て欲しい。ここからはW3C DOMに対応しているブラウザでしか動作しないので注意すること。現在動作を確認しているのはMozilla、Netscape6、IE5以降である。

4.5 入力によってモデルが変化するようにしよう

せっかくモデルの変化を表現できるビューが完成したのだから、動かしてみたくなるのが人情だろう。ここではキーボードによって主人公が動くようにしてみよう。

まず検索エンジンで「JavaScript キーボード」等としてキー入力方法を調査する。そして前のテストスクリプトにキー入力を追加したのが以下だ。

<html>
<head>
<title>ランダムマップRPG</title>
<script type="text/javascript" src="FieldModel.js"></script>
<script type="text/javascript" src="HeroModel.js"></script>
<script type="text/javascript" src="FieldView.js"></script>
<script type="text/javascript" src="HeroStateView.js"></script>
<script type="text/javascript" src="ImageObj_W3C.js"></script>
<script type="text/javascript" src="TextObj_W3C.js"></script>
<script type="text/javascript"><!--

//定数
FIELD_MODEL_SIZE_X = 24;
FIELD_MODEL_SIZE_Y = 12;

/**
最初に実行される関数。
*/
function startup()
{
    //モデルの構築
    fieldModel = new FieldModel(FIELD_MODEL_SIZE_X
                              , FIELD_MODEL_SIZE_Y);
    fieldModel.generateMapRandom();
    heroModel = new HeroModel();
    heroModel.setPositionRandom(FIELD_MODEL_SIZE_X
                              , FIELD_MODEL_SIZE_Y);

    //ビューの構築
    fieldView = new FieldView(fieldModel, heroModel);
    fieldView.update();
    heroStateView = new HeroStateView(heroModel);
    heroStateView.update();

    //キーイベント受け取り設定
    window.document.onkeydown = onKeyDown;
}

function onKeyDown(e)
{
    //キーコード取得
    var keyCode;
    if(e){ //Netscapeの場合
        keyCode = e.which;
    }
    else if(window && window.event){ //IEの場合
        keyCode = window.event.keyCode;
    }
    else{
        keyCode = 0;
    }
    //主人公移動
    if(keyCode == "6".charCodeAt(0) || keyCode == 39 || keyCode == 102){
        heroModel.walk(1, 0);
    }
    else if(keyCode == "4".charCodeAt(0) || keyCode == 37 || keyCode == 100){
        heroModel.walk(-1, 0);
    }
    else if(keyCode == "2".charCodeAt(0) || keyCode == 40 || keyCode == 98){
        heroModel.walk(0, 1);
    }
    else if(keyCode == "8".charCodeAt(0) || keyCode == 38 || keyCode == 104){
        heroModel.walk(0, -1);
    }
    //画面更新
    fieldView.update();
}
--></script>
</head>

<body>
<script type="text/javascript"><!--
startup();
-->
</script>

</body> </html>

動作を確認したいならstep5.htmlを見て欲しい。キーボードで主人公(赤い箱)が動くのが確認できるはずだ。

4.6 ルール適用しよう

大分ゲームらしくなってきたがまだまだだ。ゲームにはルールが存在する。

このゲームのルールは2.3で述べたとおりだ。これを素直にコードにしたものが以下である。

<html>
<head>
<title>ランダムマップRPG</title>
<script type="text/javascript" src="FieldModel.js"></script>
<script type="text/javascript" src="HeroModel.js"></script>
<script type="text/javascript" src="FieldView.js"></script>
<script type="text/javascript" src="HeroStateView.js"></script>
<script type="text/javascript" src="ImageObj_W3C.js"></script>
<script type="text/javascript" src="TextObj_W3C.js"></script>
<script type="text/javascript"><!--

//定数
FIELD_MODEL_SIZE_X = 24;
FIELD_MODEL_SIZE_Y = 12;

/**
最初に実行される関数。
*/
function startup()
{
    //モデルの構築
    fieldModel = new FieldModel(FIELD_MODEL_SIZE_X
                              , FIELD_MODEL_SIZE_Y);
    fieldModel.generateMapRandom();
    heroModel = new HeroModel();
    heroModel.setPositionRandom(FIELD_MODEL_SIZE_X
                              , FIELD_MODEL_SIZE_Y);

    //ビューの構築
    fieldView = new FieldView(fieldModel, heroModel);
    fieldView.update();
    heroStateView = new HeroStateView(heroModel);
    heroStateView.update();

    //キーイベント受け取り設定
    window.document.onkeydown = onKeyDown;
}

function onKeyDown(e)
{
    //キーコード取得
    var keyCode;
    if(e){ //Netscapeの場合
        keyCode = e.which;
    }
    else if(window && window.event){ //IEの場合
        keyCode = window.event.keyCode;
    }
    else{
        keyCode = 0;
    }

    //移動後座標算出
    var oldX = heroModel.getX();
    var oldY = heroModel.getY();
    var newX = oldX;
    var newY = oldY;

    if(keyCode == "6".charCodeAt(0) || keyCode == 39 || keyCode == 102){
        newX++;
    }
    else if(keyCode == "4".charCodeAt(0) || keyCode == 37 || keyCode == 100){
        newX--;
    }
    else if(keyCode == "2".charCodeAt(0) || keyCode == 40 || keyCode == 98){
        newY++;
    }
    else if(keyCode == "8".charCodeAt(0) || keyCode == 38 || keyCode == 104){
        newY--;
    }

    //移動時ルール処理
    if(newX != oldX || newY != oldY){
        //rule1.移動不可能地形制約(画面外、水へは移動できない)。移動可能なら移動する
        if(newX < 0 || newX >= fieldModel.getSizeX()
        || newY < 0 || newY >= fieldModel.getSizeY()){
            return;//画面範囲外につき移動不可能
        }
        if(fieldModel.getCellAtr(newX, newY) != FieldModel.CELL_ATR_GROUND){
            return;//地面属性以外につき移動不可能
        }
        heroModel.walk(newX - oldX, newY - oldY);

        //rule2.移動元の地形属性は水になる
        fieldModel.setCellAtr(oldX, oldY, FieldModel.CELL_ATR_WATER);

        //rule3.移動後、上下左右が全て移動不可能地形の場合ゲームオーバー
        if(fieldModel.getCellAtr(newX + 1, newY) != FieldModel.CELL_ATR_GROUND
        && fieldModel.getCellAtr(newX - 1, newY) != FieldModel.CELL_ATR_GROUND
        && fieldModel.getCellAtr(newX, newY + 1) != FieldModel.CELL_ATR_GROUND
        && fieldModel.getCellAtr(newX, newY - 1) != FieldModel.CELL_ATR_GROUND){
            var score = (heroModel.getGain() + heroModel.getWalkCount() * 20) * 5;
            alert("Game Over\n"+
                  "歩行回数:"+heroModel.getWalkCount()+"\n"+
                  "所持金:"+heroModel.getMoney()+"\n"+
                  "総合得点:"+score);
        }
        //rule4.移動後、ある確率で敵が出現する
        else if(Math.random() < 0.15){
            //戦闘ルール
            alert("敵が現れた!!");
            var enemyPower = Math.floor(Math.random() * 100);
            var heroPower  = Math.floor(Math.random() * 100);
            var money = Math.floor(Math.random() * 200);
            if(heroPower > enemyPower){
                alert("貴方の勝ち " + money + "G 手に入れた");
                heroModel.addMoney(money);
            }
            else{
                alert("貴方の負け " + money + "G 盗られた");
                heroModel.addMoney(-money);
            }
        }
    }
    //画面更新
    fieldView.update();
    heroStateView.update();
}
--></script>
</head>

<body>
<script type="text/javascript"><!--
startup();
-->
</script>

</body> </html>

是非step6.htmlを見て欲しい。見た目はどうあれ、ゲームとしてはほとんど完成だ。alertを使っている関係でフォーカスが外れることがあるので注意しよう。

ふう……。これで一段落。思ったより面白くないかも。と、ここで止まってしまいがちだが、あともう一踏ん張りしよう。やはりalertを使うのは格好悪い。戦闘画面、ゲームオーバー画面を作ろう。

4.7 モードクラスを作ろう

4.7.1 モードクラスとは

この段階で残された部分は戦闘画面とゲームオーバー画面だ。これらはモードクラスによって実現する。

ここでいうモードクラスとは戦闘画面モード、ゲームオーバー画面モードといったゲームの各状態を担当するクラスのことだ。シーンクラス等とも呼ぶ。

モードクラスをどのように分割すればよいかは2.1 ゲームの流れを見れば分かる。この部分のクラス図を以下に示す。

mode class

ゲームモードはゲーム全体に対応するクラスだ。オブジェクトの寿命は「ゲーム中ずっと」であり、ゲーム中の全てのオブジェクトはこのクラスが管理することになる。このゲームではゲーム中ずっと主人公はフィールドを移動していることになっているので、フィールド移動に関する処理も任せることにした。他にもフィールドモデルオブジェクトや主人公モデルオブジェクトはゲーム中ずっと継続して生きているので、ゲームモードに管理させる。

戦闘モード、ゲームオーバーモードはゲームモードのサブモードだ。ゲームモードオブジェクトが必要なときにサブモードオブジェクトを生成し、一定時間処理を任せ、終わったら破棄する(ただしこれは理想であり効率のためにあらかじめ生成しておくこともある)。

戦闘モードオブジェクトの寿命は「戦闘中」であり、戦闘中のみで使用するオブジェクトはこのクラスが管理することになる。戦闘モードオブジェクトは生成され、スタートメッセージが呼ばれると画面に戦闘の状況を表示する。プレイヤーの入力を受け付け、戦闘を進行し、終了したらスタートメッセージを送った相手に終了を通知する。以下はその流れをシーケンス図で示したものだ。

battle sequence

ちなみに終了通知は専用のインタフェースを用意する。これによってモードを起動したクライアントが誰なのかに影響を受けなくて済む。

ゲームオーバーモードオブジェクトも同様にゲームオーバー画面中の時間を管理する。

このようにモードクラスは特定の時間帯そのものを抽象化したクラスと考えることができる。あるオブジェクトの所有権を誰が握るか迷ったときはそのオブジェクトの寿命を考えてみよう。その時間に対応するモードクラスに管理を任せられないか考えてみよう。

4.7.2 キーマネージャーを作ろう

モードクラスを作る前に、上のシーケンス図内にはキーマネージャーというオブジェクトがある。これはキーボードイベントを一元的に管理するオブジェクトである。モードの切り替えに対して適切なオブジェクトにキーイベントを振り分けるために必要だ。まずこのキーマネージャーを作ってしまおう。KeyManager.js

あるオブジェクトがキーマネージャーからキーイベントを受け取りたい場合は、KeyManager_addListener()メソッドを呼び出す。そのオブジェクトはonKeyDown(keyCode)とonKeyUp(keyCode)メソッドを持っていなければならない。キーが押されたとき、そのキーコードを引数にしてonKeyDown()が呼ばれる。それ以上フックチェーンをたどりたくない場合はonKeyDownはfalseを返すこと。trueを返すと以前フックしていたオブジェクトのonKeyDownメソッドが呼ばれる。キーフックを解除したいならKeyManager_removeListener()メソッドを呼び出す。

4.7.3 ゲームモードクラスを作ろう

ゲームモードクラスの役割は以下の二点だ。

実はこれ、既にテストスクリプトでやってきたことだ。今までテストスクリプトに書き殴っていた部分を整理してクラス化したものがGameMode.jsである。このファイルは完成品で、戦闘モード画面やゲームオーバー画面を呼び出すコードも書かれているが、実際に書いたのはそれらのクラスを作ってからだ。

4.7.4 戦闘画面を作ろう

まず戦闘画面内での状態遷移を考える。遷移のきっかけとなるイベントも考える。それさえできればあとは簡単だ。BattleMode.js

一つだけ問題になったのはタイマー処理。組み込みのsetTimeout関数を使ったのだが、この関数では特定のオブジェクトのメソッドは呼べないようだ。どうせこのクラスのインスタンスは一時期に最大一つまでしか存在しないのだからと安易な方法で解決した。このあたりは検討課題だ。

4.7.5 ゲームオーバー画面を作ろう

まずゲームオーバー画面内での状態遷移を考える。遷移のきっかけとなるイベントも考える。それさえできればあとは簡単だ。GameOverMode.js

こちらもタイマー関連の問題があったが、戦闘画面と同じ方法で対処した。

4.7.6 テスト

それではこれらをテストしよう。この文書ではまとめてテストを行っているが、実際にはもっと小刻みにテスト・修正をしている。

<html>
<head>
<title>ランダムマップRPG</title>
<script type="text/javascript" src="FieldModel.js"></script>
<script type="text/javascript" src="HeroModel.js"></script>
<script type="text/javascript" src="FieldView.js"></script>
<script type="text/javascript" src="HeroStateView.js"></script>
<script type="text/javascript" src="ImageObj_W3C.js"></script>
<script type="text/javascript" src="TextObj_W3C.js"></script>
<script type="text/javascript" src="KeyManager.js"></script>
<script type="text/javascript" src="GameMode.js"></script>
<script type="text/javascript" src="BattleMode.js"></script>
<script type="text/javascript" src="GameOverMode.js"></script>
<script type="text/javascript"><!--

/**
最初に実行される関数。
*/
function startup()
{
    gameMode = new GameMode();
}

--></script>
</head>
<body>

<script type="text/javascript"><!--
startup();
-->
</script>
</body> </html>

ゲームモードクラスに全てを追いやったので随分シンプルになった。やっているのはゲームモードクラスを生成するだけだ。あとはゲーム中のことはゲームモードクラスがやってくれる。

実行はstep7.htmlを見て欲しい。

4.8 終了

全てのクラスのコーディングとテストが終わった。これで終了だ。自分にご褒美をあげよう。

ドキュメントの整備やプロジェクトの評価もお忘れなく。

4.9 デバッグ手法

最後にこれまであまり触れてなかったデバッグの方法について少し記録しておく。

デバッグで大事なのはバグの場所を特定することだ。JavaScriptは何も言わずに止まってしまうことが多いので大変だ。document.writeやalertを使って手動でトレースするのが簡単だろうか。

やっかいなのは構文の間違いやファイル名の間違いなどだ。一つのミスで全体が全く動かなくなってしまうこともある。インタプリタという特長を生かし、頻繁にテストすることである程度場所を特定できる。

5 あそぶ

自分が作ったゲームをプレイして悦に浸るのは楽しいものだ。存分にプレイしようっと。

6 評価

最初に述べた本プロジェクトの目標をどの程度達成しているかを検討した。

6.1 以前作ったランダムマップRPGをWebブラウザ上にベタ移植する

全く同じものができた。長方形の地形画像がBASIC時代を彷彿させる見事な再現だ。

6.2 webブラウザ上でのゲーム開発の基盤を作る

今回のプロジェクトでJavaScriptの一部の言語仕様については理解した。しかし全体を体系的に把握したとは言えない。全体を把握していないと問題のある書き方をしても気づかない恐れがある。

以下の再利用可能なコンポーネントが完成した。これらは簡単なインタフェースを提供すると共にブラウザ間の差異を吸収し、次回以降、開発の効率化に貢献するものと期待できる。

タイマー処理について再利用可能なコンポーネントを製作する余地があった。しかし今回は必要性が薄かったのと、タイマー処理はブラウザ間の差異が少ないため見送った。

7 課題

7.1 ゲーム内容に関して

今回の目的はベタ移植なのでゲーム内容について不満を言っても仕方がないが、以下の事項については検討する余地があるだろう。

7.2 JavaScriptでの開発に関して

JavaScriptでの開発に関する課題には以下のようなものがある。

8 最後に

こうして開発過程を一通り書いてみると、たかがこれだけのゲームでも実に様々な事を考えていることがわかる。これでも書き足りないくらいだ。特に後半のコーディング部分では1行1行、1トークンごと考えて書いている。さすがにそこまでは解説しきれなかった。

バージョンの多さ、ブラウザ間の違い、公式文書の邦訳が少ない等JavaScriptには作りづらいところがある。JavaAppletの方が良かったか?と思うこともしばしば。標準化が進みもっと作りやすくなることを願う。

このプロジェクトで示したコードについては自由に取り扱っていただいて構わない。もちろん各自の責任で取り扱うこと。私は何の保証もできないが、この文書を正しく維持する責任はある。間違っている部分があれば是非ご一報いただきたい。


上へ

Last modified: Sat Apr 06 18:59:57 2002