はじめに
ゲームでは、その進行状況を「フラグ」と呼ばれるもので管理します。フラグとは「flag」、つまり「旗」のことで、ONまたはOFFどちらかの状態を表すものです。例えば「王様と会話したかどうか?」で発生するイベントを変えたいときなどは、王様と会話したときに、フラグがONになるよう(俗に「フラグを立てる」といいます)設定しておき、イベント発生時に、そのフラグをチェックして、ONならばイベントA、OFFならばイベントBのように処理を分岐します。主に、アドベンチャーゲームや、ロールプレイングゲームなどで用いられる手法ですね。今回はそのフラグ管理の方法を解説してみたいと思います。
サンプルプログラムは「ミステリールーム」。家に隠されたダイヤモンドを探し出す、ミステリーアドベンチャーです。さあ、あなたはこの家の謎を解いて、クリアすることが出来るでしょうか?
ちなみに元ネタは、パソコンゲームの名作「ミステリーハウス」(株式会社マイクロキャビン、1982年)。このサンプルを見て懐かしさを覚える人は、かなり古くからのパソコンユーザーだと思います。
対象ユーザー
ゲームプログラムに興味がある方を対象とします。また、グラフィックとシナリオデータを用意するだけで、別のゲームにすることも可能ですので、一度ゲームを作ってみたいという人にもオススメします。
必要な環境
アプレットの実行には、Javaを動作させることの出来るブラウザーが必要です。もし、お使いのブラウザーでアプレットが動かない場合は、Javaソフトウェアの無料ダウンロードから、最新のJava Runtime Environment(JRE)を入手してください。
また、コンパイルにはJ2SE Development Kit(JDK) 1.4以上が必要です。
画面構成と遊び方
![ミステリールーム ミステリールーム](http://cz-cdn.shoeisha.jp/static/images/article/164/screen.gif)
部屋の様子が表示されるのがメイン画面。ここをマウスでクリックするだけで進んでいきます。その下にはメッセージが表示され、取得したアイテムは右側に表示されます。
右下には現在の位置と方角が出ますので、部屋を移動する際の参考にしてください。
怪しいものがあればどんどんクリックして構いませんが、場合によってはゲームオーバーになるかもしれませんので、ご注意を! 見事ダイヤモンドを入手すれば、ゲームクリアです。
クラスの構造
このプログラムは以下の5つのクラスより構成されています。
クラス名 | 概要 |
MysteryGame | このゲームの土台であるアプレット。 |
GamePanel | メイン画面の描画、およびマウスイベントを担当。 |
Scenario | フラグを管理し、ゲームの進行を制御。 |
Message | クリックされたときに表示されるメッセージデータ。 |
Scene | メイン画面に表示される画像のデータ。 |
ポイントとなるのは、ゲームの流れを全て外部ファイル(「message.csv」と「scene.csv」)に記述しているということです。Message
クラスとScene
クラスは、それぞれ同じ名前の外部ファイルに対応しています。
マウスがクリックされると、Scenario
クラスが条件にマッチするMessage
オブジェクトとScene
オブジェクトを検索する、という流れでゲームが進行します。
では、ゲームデータの構成を中心に、全体の流れを見ていきましょう。
メッセージの構造
Message
クラスは、マウスをクリックしたときに表示されるメッセージとその情報を管理します。「message.csv」に記述されているメッセージデータ1件が、Message
オブジェクト1つに対応します。
「message.csv」は、カンマ区切りのCSV形式になっており、各要素は、左から順番に以下のような構成になってます。
順番 | 内容 | 詳細 |
1 | シーン名 | 後述のSceneクラスで定義されたシーン名。 |
2~5 | クリック範囲 | クリックで反応する範囲。 X座標、Y座標、幅、高さの順に指定。 |
6 | 表示条件 | このメッセージが表示されるための条件文。 |
7 | 後処理 | メッセージを表示した後の処理コマンド。 |
8 | 次のシーン名 | 次に移るシーンの名前。 |
9 | メッセージ | 表示されるメッセージ。 |
例えば、カギを発見するときのデータは以下のようになってます。
シーン名 | x | y | width | height | 条件 | 処理 | 次シーン | メッセージ |
ROOM1W | 52 | 95 | 11 | 23 | !key | KEY | カギを発見した!このカギはどこで使うのだろう? | |
KEY | 0 | 0 | 240 | 160 | key&+カギ | ROOM1W | カギを手に入れた。 |
これは、ROOM1W
(第1の部屋 西)のシーンで、座標(52, 95)
から高さ11
、幅23
の範囲(花瓶の花)をクリックしたとき、key
フラグが立ってなかったら、「カギを発見~」とのメッセージを出し、KEY
(カギ発見)のシーンに推移するという意味になります。
また、KEY
シーンでは、座標(0, 0)
から高さ240
、幅160
(つまり画面のどこでも)クリックした場合に、key
フラグをセットし、所持アイテムに「カギ」を追加して、ROOM1W
シーンに戻る、ということになります。
方向転換も、アイテムの取得も、次の部屋への移動も、すべてこの流れの中で行っていることを理解しておいてください。
「GamePanel.java」にdebug
というクラス変数がありますが、これをtrue
にすると、画面にクリック範囲が表示されます。シーンごとに用意されたメッセージの様子が分かりますので、一度試して欲しいと思います。
![debugモード画面 debugモード画面](http://cz-cdn.shoeisha.jp/static/images/article/164/debug.gif)
シーンの構造
Scene
クラスは各シーンごとの画像とその情報を管理します。「scene.csv」に記述されているシーンデータ1件が、Scene
オブジェクト1つに対応します。「scene.csv」もCSV形式で、以下のような構成なってます。
順番 | 内容 | 詳細 |
1 | シーン名 | 前述のMessageクラスにおける「シーン名」「次のシーン名」で使用する名前。 |
2 | 表示条件 | この画像が表示されるための条件文。 |
3 | ファイル名 | 画像のファイル名。 |
4 | 位置情報 | 現在地を表す部屋の名前と方向。 |
たとえば、第2の部屋で北を向いたときのデータは以下のようになってます。
シーン名 | 条件 | ファイル名 | 位置情報 |
ROOM2N | chair&trapdoor | room2n3.gif | 第2の部屋 北 |
ROOM2N | chair | room2n2.gif | 第2の部屋 北 |
ROOM2N | room2n1.gif | 第2の部屋 北 |
これは、ROOM2N
(第2の部屋 北)のシーンで、chair
フラグとtrapdoor
フラグがONの場合は「room2n3.gif」を表示し、char
フラグのみがONの場合は「room2n2.gif」、何もフラグが立ってないときは「room2n1.gif」を表示することなります。グラフィックの変化で、椅子を移動させた様子を表すのに利用しています。
URL scnUrl = new URL(applet.getDocumentBase(), SCENE_DATA); BufferedReader br = new BufferedReader( new InputStreamReader(scnUrl.openStream()));
Applet
のgetImage
メソッドで読み込むのが一般的ですが、このメソッドは非同期に画像を読み込むので、その後java.awt.MediaTracker
で読み込みの終了を監視する必要があります。しかし、Swingベースで作成している場合は、
ImageIcon
を利用することで、このMediaTracker
の処理を省略できます。ImageIcon
はコンストラクターの中で、MediaTracker
を使って、読み込みが終了するまで待ってくれるからです。URL imgUrl = new URL(applet.getDocumentBase(), "image/"+imageName); ImageIcon image = new ImageIcon(imgUrl); sceneImage = image.getImage();
画像ファイルをURLで指定する必要があります。
簡易インタプリタの実装
フラグの状況をチェックする条件文や、メッセージ表示後の処理は、いわばゲームの内容そのものであり、このデータをいかに組み上げるかで、ゲームの面白さが大きく左右されます。難易度、演出、ゲーム性に関わる部分であるため、ゲーム制作の最後まで修正を繰り返します。
ですから、プログラム本体とは切り離して管理する方が、メンテナンスの上でも望ましいことになります。
そこで、一般的には、ごく簡単なインタプリタ言語を実装し、フラグのチェックや命令を別ファイルに記述する方法が用いられます。今回のプログラムでは、Scenario
クラスがその言語の解析と実行を担っています。
では、そのインタプリタ言語を解析するメソッドを見ていきましょう。
条件文の解析
public boolean check(String condition) { // チェック条件がない場合はtrueを返す if (condition == null || condition.equals("")) { return true; } // 条件の解析 String[] cnd = condition.split("&"); for (int i = 0; i < cnd.length; i++) { char c = cnd[i].charAt(0); Boolean f; switch (c) { // フラグがOFFでtrue case '!': // 先頭の1文字除去 String flag = cnd[i].substring(1); // フラグを取り出してチェック f = (Boolean)(flagMap.get(flag)); if (f != null && f.booleanValue() == true) { return false; } break; // フラグがONでtrue default: // フラグを取り出してチェック f = (Boolean)(flagMap.get(cnd[i])); if (f == null || f.booleanValue() == false) { return false; } break; } } // 全ての条件を満たしていた場合 return true; }
check
メソッドは、ゲームデータに記述された条件文を解析し、フラグと照合した上でマッチするかどうかを判定するメソッドです。条件文は以下のように記述します。
[コマンド]フラグ名&[コマンド]フラグ名&...
区切り文字は「&
」で、コマンドには以下の記号が用意されています。
コマンド | 意味 |
! | フラグがOFFの場合True |
なし | フラグがONの場合True |
例えば、条件文が「clock&!chair
」の場合、clock
フラグがONになっていて、かつchair
フラグがOFFの場合に、True
を返します。
メッセージの条件文と、シーンの条件文は同じ文法になってます。複数の条件がマッチする場合は、最初にマッチしたデータを採用する仕様になってます。ゲームの進行において後に表示されるメッセージを先に記述することになりますので、データを用意する際に気をつけてください。
後処理コマンドの説明
public void exec(String command) { // 後処理がない場合はすぐに戻る if (command == null || command.equals("")) { return; } // 後処理の解析 String[] cmd = command.split("&"); for (int i = 0; i < cmd.length; i++) { char c = cmd[i].charAt(0); switch (c) { // アイテム取得 case '+': // 先頭の1文字除去 String item = cmd[i].substring(1); // アイテムを追加 applet.addItem(item); break; case '@': // フラグとアイテムをクリアしてゲームを初期化 flagMap = new HashMap(); applet.clearItem(); break; default: // フラグをONにする flagMap.put(cmd[i], new Boolean(true)); break; } } }
exec
メソッドは、記述されたコマンド文を解析し、指定された処理を行うメソッドです。これも条件文と同様に、「&
」を区切り文字としてコマンドを記述していきます。
記号 | コマンド |
+ | アイテムを追加する。 |
@ | ゲームを初期化する。 |
なし | フラグを立てる。 |
例えば、「key&+カギ
」というコマンドは、key
フラグをONにし、アイテムリストに「カギ」を追加するという意味を持ちます。
ちなみに、フラグはフラグ名をキーとするHashMap
で管理しています。ですので、基本型のboolean
ではなくて、参照型のBoolean
を利用してます(Java 5.0ならば、この辺の処理をもっと簡単に記述することが可能でしょう)。
最後に
このゲームは、データを外部ファイルに記述してますので、プログラムを変更することなく、ストーリーを変えることが可能です。ですので、可愛い女の子の画像を用意して、会話文を用意すれば、恋愛アドベンチャーにも変わります。
でも、実際にゲームのデータを作るのは、意外と難しいかもしれません。ちょっとフラグの管理を間違えただけで、ゲームがそれ以上進まなくなったりします。俗に言う「ハマリ」ですね。ただ、動いたときの感動はひとしおですので、ぜひ挑戦してもらいたいと思います。
また、より複雑なインタプリタ言語を実装するという方向性もあります。今回は、条件の論理演算にANDしか用意しませんでしたが、ORもあれば表現の幅がさらに広がることでしょう。また、後処理でも、音を鳴らすためのコマンドの実装などが考えられます。
このサンプルを元に、それぞれのアプローチでゲーム作りに取り組んでもらいたいと思います。