プログラマーの尾関です。
仕事の方が落ち着いたので、趣味のゲーム作りで脱出ゲームを作ってみました。
その過程で得られた、システム設計やゲームデザインの知見について書いてみたいと思います。
■作ったゲームの紹介
まずは作ったゲームの紹介です。Ver 1.00 はブログ内にうまく表示できなかったので、Ver 2.00 の紹介です。
・Ver 2.00
■脱出ゲームの基本システム
まず脱出ゲームを作るにあたって、既存のゲームをいくつか参考にしました。
参考になったゲームとしては、「脱出アドベンチャーシリーズ」や、「CRIMSON ROOM」などです。
典型的な脱出ゲームのシステムを模倣して、以下のシステムとしました。
- マウスクリックで画面内のオブジェクトを調べることができる
- 上下左右に移動カーソルがある場合は、それをクリックすると別の画面に移動できる
- アイテムを選んだ状態で画面をクリックするとアイテムを使うことができる
ゲームシステム
- シーン情報の管理
- クリックイベントの制御
- イベントスクリプト
この3つが今回の脱出ゲームにおけるシステムの核となっています。
シーンの情報
1つのシーンには以下の情報を配置できるようにしました。
- 背景画像
- クリック可能なオブジェクトと画像
- クリックしたときに発生するイベントの制御
- 移動カーソルの情報
これらのデータを管理するために、設定ファイルを XML を用意しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?xml version="1.0" encoding="utf-8" ?> <data> <!-- 背景 --> <bg id="1" image="bg.png" /> <!-- オブジェクト --> <obj id="1" image="sticker.png" x="268" y="188" click="001_sticker" on="0" off="50" /> <obj id="2" image="knob_yellow.png" x="224" y="376" click="001_knob" on="0" off="50" /> <obj id="4" image="knob_blue.png" x="224" y="288" click="001_knob" on="0" off="50" /> <obj id="5" image="knob_green.png" x="224" y="332" click="001_knob" on="0" off="50" /> <obj id="6" image="unlock2.png" x="201" y="126" click="001_unlock2" on="50" off="0" /> <obj id="7" image="unlock.png" x="460" y="126" on="50" off="0" /> <!-- 移動カーソル --> <move id="left" click="001_jump002" on="3" off="0" /> <move id="right" click="001_jump005" on="3" off="0" /> </data> |
データの詳細は以下の通りです。
- タグ:bg(背景)、obj(オブジェクト)、move(移動矢印)
- id: 識別用のID。”move” タグの表示方向の制御として使用 (left / up / right / down)
- image: 画像ファイル名
- x, y: 表示座標(左上)
- click: クリックしたときに発生するイベント。指定しない場合は発生しない
- on: 表示フラグ
- off: 非表示フラグ
“on” と “off” フラグの使い方が面白い仕組みになっていると思います。例えば先ほどの XML であれば、初期状態では、オブジェクト(obj)「1~5」が表示された状態です。
そしてフラグ「50」番が ON になると、それらは “off” が 50番に設定されているので、非表示となり、
オブジェクト「6~7」が表示されるようになります(onフラグが50番なので)。
なお、onフラグが 0 の場合は無条件で表示する項目です。
画面をクリックして調べる仕組み
背景とオブジェクト画像を合成して最終的な画面(シーン)を構成します。
画像の矩形そのものがあたり判定となっていて、クリックイベントを発生させるようにしました。
※それぞれのオブジェクトをクリックすると対応するイベントが発生
クリックイベントの制御
クリックイベントは “click” 属性に指定した名前に対応するスクリプトを実行するようにしています。
例えば先ほどの XML では、”001_sticker” / “001_knob” / “001_unlock” / “001_jump002” / “001_jump005″ が発生するイベント名です。
1 2 3 4 5 6 7 |
<!-- オブジェクト --> <obj id="1" image="sticker.png" x="268" y="188" click="001_sticker" on="0" off="50" /> <obj id="2" image="knob_yellow.png" x="224" y="376" click="001_knob" on="0" off="50" /> <obj id="4" image="knob_blue.png" x="224" y="288" click="001_knob" on="0" off="50" /> <obj id="5" image="knob_green.png" x="224" y="332" click="001_knob" on="0" off="50" /> <obj id="6" image="unlock2.png" x="201" y="126" click="001_unlock2" on="50" off="0" /> <obj id="7" image="unlock.png" x="460" y="126" on="50" off="0" /> |
それぞれ対応するスクリプトを呼び出しています。
■001_sticker.txt
1 2 3 |
":「カギを探せ」と書いてある……" // フラグ3をONにする %3 = true |
■001_knob.txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
$50 = 0 // 黄色のカギを持っているかどうか ITEM_HAS(ITEM_YELLOW_KEY) if($1) { $50 += 1 } // 青いカギを持っているかどうか ITEM_HAS(ITEM_BLUE_KEY) if($1) { $50 += 1 } // 緑色のカギを持っているかどうか ITEM_HAS(ITEM_GREEN_KEY) if($1) { $50 += 1 } if($50 == 3) { // 3つのカギがそろっている %50 = true ":ドアが開いた" } else if($50 > 0) { // カギが足りない $51 = 3 - $50 ":カギがあと$V51本必要だ" } else { ":カギがかかっていて扉は開かない" } |
■001_unlock.txt
1 2 |
// 脱出成功 COMPLETE() |
■001_jump002.txt
1 2 |
// シーン2に遷移 JUMP(2) |
■001_jump005.txt
1 2 |
// シーン5に遷移 JUMP(5) |
これらのスクリプトは事前にコンバートを行い、文法エラーや未定義の定数や関数の呼び出しをしていないかチェックします。
そして「命令コード」+「パラメータ」からなるアセンブラのような CSV ファイルを出力します。
(以下は、”001_knob.txt” をコンバートしたものです)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
INT,0 SET,0,50 INT,1 ITEM_HAS VAR,1 IF,00000010 INT,1 SET,1,50 GOTO,00000010 INT,3 ITEM_HAS VAR,1 IF,00000017 INT,1 SET,1,50 GOTO,00000017 INT,4 ITEM_HAS VAR,1 IF,00000024 INT,1 SET,1,50 GOTO,00000024 VAR,50 INT,3 EQ IF,00000032 BOOL,1 SET,16,50 MSG,9,ドアが開いた GOTO,00000043 VAR,50 INT,0 GE IF,00000042 INT,3 VAR,50 SUB SET,0,51 MSG,9,カギがあと$V51本必要だ GOTO,00000043 MSG,9,カギがかかっていて扉は開かない END |
このように変換しておくと、実行プログラム側で字句解析や構文解析する必要がなく、楽に実行エンジンを実装できます。
またここでは CSVファイルとしましたが、実行速度が求められたり、メモリサイズを節約する場合には、バイナリデータにした方が良いかもしれませんね。
独自スクリプトを作るメリット
今回の脱出ゲームを作るために、スクリプト言語は Python で自作しました。
Lua を使ったり、プログラムに直接書いてもよいですが、自作言語は自由にカスタマイズできるのがメリットです。
例えば、LuaでフラグをONにする処理を書く場合、
1 2 |
-- フラグ 12番をONにする BIT_ON(12) |
というのがよくある書き方ですが、自作すると特殊な記号をフラグに割り当てることができます。
1 2 |
// フラグ 12番をONにする %12 = true |
どちらが読みやすいのかは好みの問題ですが、個人的には後者の書き方がタイプ量が少なくて済むのがメリットと考えています。
(ただ % を剰余の演算子として扱うのが難しくなるデメリットがあるかもしれません)
なんにしても、自作することで自分がゲームを作るための最適な環境を選ぶことができるわけです。
■謎・パズルの作り方
謎・パズルは脱出ゲームを作るにあたって、とても重要な要素です。
当然ながら、ゲームの遊びのコア部分となるので、慎重に作る必要があります。
とはいえ、最初に脱出ゲームを作った際には、どの部分から作ればよいのかイメージができず苦労しました。
- 背景画像を先に作って、それに合う謎を用意するのか?
- 謎を作ってそれに合う画像を用意するのか?
- 得られるアイテムを基準に考えるのか?
- 他との謎との関連性を持たせるには?
考え始めると何も進みません…。
結局は、何でもいいから思いつくものを作って、ありものを組み合わせるくらいでよい、ということがわかりました。
(謎作成のプロではないし、趣味のゲームだからクオリティにこだわって完璧にする必要はない)
謎・パズル作成のために、よく使われる手法は「答えから逆算する」というものですね。
あと、ルールを決めてから問題を作るのも楽で良いです。
例えば「単語の穴あきパズル」というルールを決めてから、必要な情報を埋めていく、といったやり方も有効と思います。
1 2 3 4 5 6 |
■Q. 1~4を組み合わせた四字熟語は? 1. ??まき 2. ??やすみ 3. ??た 4. ??げしょう |
完璧な問題を作る必要はないですが、一般的でない難しい言葉は使わないほうがいいのかもしれません。
あと「順序のある言葉の並び」は、パズルにしやすく使い勝手がよいですね。あいうえお順とか、星座の並びとか、朝昼夜とか…。
デジタルゲームでのパズルの作り方
リアル脱出ゲームの場合、アナログなので、図形を使うなど有機的なパズルを作りやすいです。
しかしデジタルゲームでは、判定部分を作るために固有のプログラムを書く必要があります。
そのため、アナログな判定は止めておいて、「数値入力」「文字入力」など汎用的に使える仕組みを作るのが楽な実装です。
ただ、これだけだと単調になるので、特殊な場面では専用のパズルを用意するのも良いかもしれませんね。
謎・パズルのデバッグ方法
謎・パズルについては、実際に解いてもらって感想をもらうのがデバッグ方法としては最適です。簡単なものであれば、誰でも解くことができるので問題ありませんが、難しいものは要注意となります。
例えば、数字をキーワードにして以下の問題を作成したときのことです
■以下のものに共通する数字は?
- Q
- 死刑囚
- お坊さん
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
答えは「12」です。
1 2 3 |
・Q → トランプのクイーン → 12 ・死刑囚 → タロットカードの刑死者 → 12 ・お坊さん → 師走 → 12 |
ただ、何人かに解いてもらったところ誰もわからず、答えを聞いても “Q” 以外はピンとこなかったそうです。また、Qと死刑囚がカードつながりなのに、突然 “お坊さん” というのも意味が分からない、という感想もありました。
ということで、問題をひねりすぎて “連想が難しいもの” を選んでしまったようです。
このあたりの難易度をどのあたりに設定するかというのは、やはり誰かに解いてもらわないとわからない気がします。
それにより、「あ、これはやりすぎたな」という感覚が少しずつ身につくわけです。
■最後に
実際に脱出ゲームを作ってみてわかったのは、慣れないうちはとにかく「試行錯誤の連続」ということです。
謎を作ってみては、「解き方がわかりにくい」「解くための手順に必要な情報が不足している」「何か今一つ足りない」といった問題が発生しては作り直す(画像やスクリプトの作り直し)という作業が発生します。
…そもそも解答が間違っていて、データの作り直しが発生…、なんてこともありました。
そのため、完璧を目指すとなかなか完成することが難しく、理想としているものに届かなくても「次回作で対応する」というやり方もありなのかな、と思いました。
仕事で作る場合のゲームは一度きりの勝負なのでそういったことはできませんが、趣味で作る分には大きく妥協しても完成させることで次につなげることができるわけです。
ちゃんと完成させれば、誰かにプレイしてもらって感想をもらうとモチベーションにもつながります。今回の脱出ゲームも社内のwikiで公開して、いくつか反応をもらっていてモチベーションにつながっているので、やっぱり完成させるのは大事だなぁ…と改めて思うわけです。
何か新しいことを始める場合に「トップダウン」「ボトムアップ」のアプローチがあると思います。
「トップダウン」をゲーム制作に当てはめると、最初にコンセプトを決めておいて、それをもとにゲームの部品を作る考え方です。
この作り方をすると、コンセプトに忠実なゲームとなり、無駄な工数を減らすことができます。最適な形でゲームを作ることができるため、多くのゲームデザイナーが推奨している作り方です。
それに対して「ボトムアップ」は作りたいゲームの場面や部品をまずは作って、後からそれぞれを組み合わせる、という考え方です。
ただ、組み合わせがうまくいかない場合にその部品を捨てることになるため、無駄な工数が発生しやすいです。
私の場合、どちらのアプローチを取るかは、着想段階のフィーリングで決まることが多いですが、作り慣れないジャンルを作る場合は「ボトムアップ」のアプローチがおすすめです。
というのも、ゲームを作る場合「無駄な作業はしたくない!」と考えてしまい、作り慣れないジャンルでも「トップダウン」で始めてしまい、設計に時間をかけすぎてゲームが完成しない、などということが発生しがちだからです。
何もない状態(知識が足りていない状態)から何かを作るのはとても大変な労力を必要とします。その場合、とにかく手を動かして遊べるものを作り、それがどこかで見たことあるゲームになったとしても、あまり気にせずにひとまず完成させます。
そして、誰かにプレイしてもらいフィードバックをもらって、より洗練されたものにしていく…、というアプローチが改めて大切だなぁ…と思った次第です。もしくはボトムアップ開発で得た経験を生かして、コンセプトを先に決めてトップダウンで作り始めるのも良いかもしれません。
ということで、脱出ゲームを作ってみた話でした。