【GameMaker Studio2】脱出ゲームの作り方

GameMaker Studio2 で脱出ゲームを作ったので作り方の概要を説明します。

# サンプル
HTML5で出力したサンプルゲームです

![esc.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/4b8e8e5b-089c-aec6-07b4-cdc24c2024b4.gif)

* http://syun777.sakura.ne.jp/tmp/gms2/html5/test_escape

プロジェクトファイルは以下からダウンロードできます
[TestEscape.yyz](http://syun777.sakura.ne.jp/tmp/gms2/TestEscape.yyz)

# 脱出ゲームとは

脱出ゲームとは、たいていの場合、主人公が閉鎖空間に閉じ込められてアイテムを探して使ったりその空間に仕掛けられた謎を解き明かして、その空間から脱出することを目的としたゲームです。

物語のジャンルで言うと、「エイリアン」「エクソシスト」のような『狭い空間にモンスターと一緒に閉じ込められる』ホラー的なパターン（青鬼など）や、デスゲーム系が相性が良いようです。特に物語がなく脱出するだけ、といったものも多いです。

脱出ゲームには色々と派生があります。

* コマンド選択
* ポイント＆クリック
* 3人称視点の探索・マップ移動
* 議論パートあり

|分類  |概要  |例  |補足  |
|---|---|---|---|
|コマンド選択  |コマンド選択、選択肢で決まった場所のみ調べられる  |古典的アドベンチャーゲームやノベル系ゲーム。ミステリーハウス(※文字入力によるアクション)、弟切草、かまいたちの夜、Ever17など  |推理ゲームではポイント＆クリックと組み合わされることが多い。明確には脱出ゲームとは違うかも  |
|ポイント＆クリック  |画面の気になるポイントをクリックする  |MYST、慟哭、CRIMSON ROOM、極限脱出、密室のサクリファイス、脱出アドベンチャーシリーズ、やばたにえんなど|CRIMSON ROOMの大ヒットで脱出ゲームというジャンルが定着した。一般的にはこのイメージが強そう  |
|3人称視点の探索・マップ移動  |3人称視点でマップ内を自由に歩き回れる  |クロックタワー、コープスパーティー、Ib、寄生ジョーカー、マヨヒガ、青鬼、Mad Fatherなど|自由操作できるのでアクション要素と相性が良い  |
|議論パートあり  |逆転裁判のような議論パートがある  |キミガシネ、異世界勇者の殺人遊戯（デスゲーム）、死神探偵少女、アリスの精神裁判など|他のシステムとの複合であることも多い。デスゲーム系と相性良さそう|

上記の表は、脱出ゲームの分類というよりは、脱出ゲームと相性の良いゲームシステムの分類、と言えるかもしれません。
それぞれ実装方法が若干異なるのですが、ここでは「ポイント＆クリック」式の脱出ゲームを作るものとします。

## ポイント＆クリックとは

「ポイント＆クリック」というのは、**画面の特定のポイントをマウスなどでクリックすることでゲームが進行する**タイプのものです。
このタイプは操作がシンプルで直感的なため、わかりやすいのが特徴です。

![point_and_click.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/e06d4582-e846-8fdb-8fa6-7183c62f7817.png)

画面の特定のポイントをクリックすると、おおよそ上記の画像のアクションが発生します。

* 別の画像が表示される（引き出しの開閉など）
* 説明テキストが表示される
* 暗号の入力モードに切り替わる
* 別の部屋に移動する

このレイアウトを GameMaker Studioで実装するのはとても簡単で、標準のルームの機能で簡単に配置できます。

![Escape001_-_GameMaker_Studio_2_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/a198d78a-ae43-1fae-f326-924fe0db6903.png)

まずは背景スプライトを設定して……

![objects.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/04d86d81-5f2a-a2fa-0446-ed438cb523cd.png)

クリック可能なオブジェクトをルーム内に配置します。
クリック判定の範囲はスプライトのサイズで変更できます。

![Escape001_-_GameMaker_Studio_2_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/3f3504d6-14a4-f489-3cc2-666e38e287e3.png)

基本的に自動で設定される当たり判定で問題ありませんが、Sprite Editorで自由にサイズを決められるのが素晴らしいですね！

あと、脱出ゲームにありがちな「目に見えないクリック範囲」も、Room Editor上でオブジェクトの透過値（Alpha）を変更することで簡単に実装できます。

![alpha.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/4377f939-1a02-0f97-716a-047fc3cac73f.gif)

あと、判定するサイズを調整したい場合には、オブジェクトの拡大・縮小値を変更することで対応できます（ただし画像に補完がかかって、ぼやっとした見た目になるので注意）

## 当たり判定の実装

~~配置したオブジェクトの Mouse > Left Down イベントでクリックしたときの判定ができます。~~
（***※左クリックイベントでの判定はあまり良くありません。理由は後述***）

もしくは、以下の GMLで行います。

```js
if(mouse_check_button_pressed(mb_left)) {
  // 左クリックした
  var _inst = place_meeting(mouse_x, mouse_y, [判定したいオブジェクト]);
  if(_inst != noone) {
    // 何かしらのクリックしたときのアクションを発生
    // 例えばクリックしたオブジェクトを消すなど
    instance_destroy(_inst);
  }
}
```

## テキスト表示

説明テキストの表示は、テキスト表示用のオブジェクトを作って、Alarmイベントで一定時間経過後に破棄するようにすれば実装できます

## 暗号入力

暗号入力は、ダイヤル式であれば実装が楽です。

![code.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/183962cc-6787-578f-85b3-6bbb4435e2dc.gif)

このように上下の矢印をクリックする入力方式にすれば、左クリックイベントで判定するだけなので、実装は難しくないと思います。

# フラグ管理

アイテムを入手した、暗号を解いた、特定のポイントをクリックした、などといった判定をするのにフラグがあると複雑なイベントを作りやすいです。

私の実装では、ゲーム開始時に global に bits 変数の配列を用意するようにしました

```js
// フラグの初期化.
global.bits[MAX_BIT] = false;
for(var i = 0; i < MAX_BIT; i++) {
  global.bits[i] = false;
}
```

そして、以下のような function を用意して必要に応じて、On/Off、フラグが立っているかどうかのチェックをしました。

```js
/// @description check bit
/// @param idx
/// @return is on?
function bit_chk(idx) {
	if(idx < 0 || MAX_BIT <= idx) {
		show_debug_message("bit_chk(): Invalid idx = " + string(idx));
		return false;
	}

	return global.bits[idx];
}

/// @description off bit
/// @param idx
function bit_off(idx) {
	if(idx < 0 || MAX_BIT <= idx) {
		show_debug_message("bit_off(): Invalid idx = " + string(idx));
		return;
	}

	global.bits[idx] = false;
}

/// @description on bit
/// @param idx
function bit_on(idx) {
	if(idx < 0 || MAX_BIT <= idx) {
		show_debug_message("bit_on(): Invalid idx = " + string(idx));
		return;
	}

	global.bits[idx] = true;
}

```

# アイテムメニューの実装

アイテムメニューの実装はかなり大変なのですが、たぶんアイテムごとにオブジェクトを用意して、画面下や画面右側に並べていくのが簡単かもしれません。

私の場合は、右上にアイテムボタンを置いて、それをクリックするとアイテムメニューが表示されて、選べるようにしました。

![item.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/5f7ffae0-6d9a-a580-ce63-522d814bfee6.gif)

このアイテムメニューはアイテムを装備して使う形式ですが、この方式よりも画面の下や右側にならべて、ドラッグ＆ドロップしてアイテムを使う形式の方が、メニュー画面遷移が必要ないので実装は楽なのではないかと思います。

# 最後に

クリック時に発生するイベントやルーム情報を管理するツールを作ると効率的に脱出ゲームを作ることができます。ただ、この実装はそれなりのプログラムの知識が必要となります。

参考に私の実装では、フラグ管理やルーム管理に [CastleDB](http://castledb.org) というデータベースを使い、イベントには自作のスクリプト言語(Pythonで実装)を組み込んだので、簡単に紹介します。

・CastleDBによるルーム管理
![CastleDB.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/33e372a0-3248-fa15-81ad-067631771bef.png)


・CastleDBによるフラグ管理
![CastleDB.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/191af98a-4b79-2900-db9c-ee110f0129ca.png)

・自作のスクリプト言語

```js
def eraser
{
    // 黒板消しを獲得
    ITEM_ADD(ITEM_BLACK_BOARD_ERASER)
}
def sticker
{
    ":「仲間はずれを探せ」と書かれている……"
}

def check_item
{
    if(%LF_13) {
        // 正解したら消せないようにする
        $RET = false
        return
    }

    ITEM_CHK(ITEM_BLACK_BOARD_ERASER)
    if($RET == false) {
        "文字を指で擦ってみるが消えない/"
        "[$hero:301]……？"
        "指では消えないのかな……/"
    }
}
```

# 試しに作ってみて検証した結果

理論上はこれで動くはず……！
と思って試しに実装してみたのですが、「左クリックイベント」で判定する方法はあまりよくないようでした……。
理由としては、「数値入力」などで、数値入力以外の左クリックイベントを無効にしたい場合に、instance_deactive_*() を呼び出すしかなくて（ひょっとしたら他に方法があるかもしれませんが）、数値入力の実装につまづきました。

結論としては、左クリックイベントで入力判定を受け取るのではなく、各オブジェクトに "click" のような関数を定義します。

```js:各オブジェクトのCreateイベント
// クリック時の関数を定義
click = function() {
	esc_message("「い○か○ろ○ま○あ○も○」と書かれている");
};
```

そして、ゲームを管理しているオブジェクトから判定処理を呼び出します

```js:obj_escapeのStepイベント
switch(state) {
case eEscState.Main:
	if(mouse_check_button_pressed(mb_left) == false) {
		// 左クリックしていないときは何もしない
		return;
	}

	// "Instances" レイヤーからオブジェクトをすべて取得
	var _layer = layer_get_id("Instances");
	var _elements = layer_get_all_elements(_layer);
	for(var i = 0; i < array_length(_elements); i++) {
		var elem = _elements[i];
		if(layer_get_element_type(elem) != layerelementtype_instance) {
			// not instance.
			continue;
		}
		// get instance id.
		var _inst = layer_instance_get_instance(elem);
		//_inst.image_blend = c_white;
		if(place_meeting(mouse_x, mouse_y, _inst) == false) {
			// don't hit
			continue;
		}
		
		//_inst.image_blend = c_red;
		
		if(variable_instance_exists(_inst, "click")) {
			// "click" イベントを呼び出す
			_inst.click();
		}
	}
	break;
```

これで判定すると、インスタンスの制御もやりやすいと思います。
ひょっとしたら左クリックイベントで楽に実装できるかなー、と思ったのですがやはりやってみないと問題点は見えませんね……（過去に作成した脱出ゲームでも左クリックイベントは使っていません）

あと、サンプルコードのわかりにくいところとして、非表示オブジェクトは CreateCode イベントで、visible に false を設定しています。

# サンプルプロジェクトの簡単な解説
サンプルを作った結果、処理が先程の説明と若干変わってしまったので、改めて解説します。
（※違う部分は左クリックイベントの判定部分くらいです）

## リソースについて

* オブジェクト
    * Objects
        * obj_esc_input_number: 数値入力
        * obj_escape: 脱出ゲーム管理
        * obj_notice: 通知テキスト表示
    * Rooms/obj
        * obj_door_close: ドアが閉じた状態
        * obj_door_exit: 開いたドアのクリア判定用
        * obj_door_hit: ドアが閉じた状態の「開かない」メッセージ判定用
        * obj_door_open: 開いたドア
        * obj_input: 数値入力
        * obj_photo_frame_hit: 写真立ての判定
        * obj_poster: ポスター
        * obj_rack_hit: ラックの当たり判定
        * obj_sticker1: 暗号1
        * obj_sticker2: 暗号2
* スプライト
    * Sprites
        * spr_escape: 脱出ゲーム管理のクリック有効範囲
    * Rooms/spr
        * spr_bg: 背景
        * spr_door_close: 閉じたトア
        * spr_door_open: 開いたドア
        * spr_hit: 当たり判定の汎用スプライト
        * spr_input: 数値入力
        * spr_poster: ポスター
        * spr_sticker1: 暗号1
        * spr_sticker2: 暗号2
* スクリプト
    * Scripts
        * scr_escape
            * flag_on: フラグをONにする
            * flag_off: フラグをOFFにする
            * flag_chk: フラグが立っているかどうか
            * flag_reset: フラグを初期化する
            * esc_message: 画面下部に表示される通知テキスト
            * esc_start_input: 数値入力開始

## obj_escapeについて

obj_escapeは脱出ゲーム全体を管理するオブジェクトです。

* オブジェクトのクリック判定
* 数値入力オブジェクトの制御と判定

を行っています。
ここの Stepイベントで数値入力の正解判定を行っていますが、もし汎用的な作りにするなら、呼び出し時のパラメータに正解判定用の値を渡すなどの工夫が必要となります。

### Createイベント

```js:obj_escapeのCreateイベント
// 状態
enum eEscState {
	Main,        // メイン(クリック判定)
	InputNumber, // 数値入力
};

// フラグを初期化
flag_reset();

// メイン状態に設定
state = eEscState.Main;

// 実行オブジェクト初期化
exec_obj = noone;

// 数値入力開始関数
start_input_number = function() {
	global.input_number = 0;
	if(instance_exists(exec_obj)) {
		instance_destroy(exec_obj);
	}
	exec_obj = instance_create_depth(0, 0, -100, obj_esc_input_number);
	state = eEscState.InputNumber;
};

```

Createイベントでは状態やフラグの初期化、数値入力開始関数の定義を行っています。
ルームを複数に分ける場合は、フラグの初期化は起動直後のみにする必要があります。

### Stepイベント

```js:obj_escapeのStepイベント
switch(state) {
case eEscState.Main:
	// クリック判定
	if(mouse_check_button_pressed(mb_left) == false) {
		// 左クリックしていなければ何もしない
		return;
	}

	// "Instances" レイヤーを取得
	var _layer = layer_get_id("Instances");
	// 要素をすべて取得
	var _elements = layer_get_all_elements(_layer);
	for(var i = 0; i < array_length(_elements); i++) {
		var elem = _elements[i];
		if(layer_get_element_type(elem) != layerelementtype_instance) {
			// instanceでないので何もしない
			continue;
		}

		// instance idを取得
		var _inst = layer_instance_get_instance(elem);
		//_inst.image_blend = c_white;
		// obj_escapeと当たり判定(クリック判定)を行う
		if(place_meeting(mouse_x, mouse_y, _inst) == false) {
			// don't hit
			continue;
		}
		
		//_inst.image_blend = c_red;
		
		if(variable_instance_exists(_inst, "click")) {
			// click関数が存在すれば実行する
			_inst.click();
		}
	}
	break;

case eEscState.InputNumber:
	// 数値入力判定
	if(instance_exists(exec_obj) == false) {
		// 数値入力が終わったら正解判定をする
		if(global.input_number == 3156) {
			// 正解なのでunlockする
			instance_destroy(obj_input);
			instance_destroy(obj_door_close);
			instance_destroy(obj_sticker1);
			instance_destroy(obj_door_hit);
			obj_door_open.visible = true;

			// flag on (unlock number).
			flag_on(eFlag.UNLOCK_NUMBER);
			
			esc_message("ドアが開いた");
		}
		else {
			esc_message("正しくないようだ");
		}
		
		// メインに戻る
		state = eEscState.Main;
	}
	break;
}
```

eEscState.Mainの場合はクリック判定です。
現状、クリック判定の場所が重なっている場合に一番上のオブジェクトだけ判定するなどの処理がないので、そのあたりが問題になる場合は何らかの対策が必要となります。

また、数値入力正解後の処理を直接ここに書いています。数値入力判定が複数ある場合は、正解時の判定をパラメータ指定やコールバック（functionを渡す) で行う必要があります

## obj_esc_input_number

obj_esc_input_numberは数値入力判定を行うオブジェクトです

### Createイベント

```js:obj_esc_input_numberのCreateイベント
// 入力数値を格納する配列
number_list = [];
// 入力位置
number_index = 0;
// 最大桁数
number_max = 4;
```

1文字ずつ入力する方式としたので、格納する変数は配列であるほうが都合が良かったので、number_list という配列を用意しました。
また桁数がここで指定されているので、この値は外部から指定できるようにした方が汎用的な作りになると思います。

### Stepイベント

```js:obj_esc_input_numberのStepイベント
if(keyboard_check_pressed(vk_enter)) {
	// Enterキーで決定。配列を数値に変換する
	global.input_number = 0;
	for(var i = 0; i < number_index; i++) {
		// 10^n をかけて各桁数を計算
		var digit = number_max - i - 1;
		var val = number_list[i] * power(10, digit);
		global.input_number += val;
	}

	// 終了
	instance_destroy();
	return;
}

if(keyboard_check_pressed(vk_backspace)) {
	// 1文字削除
	if(number_index > 0) {
		number_index--;
		number_list[number_index] = 0;
	}
}

if(number_index >= number_max) {
	// すべての桁を入力したら以下の処理は行わない
	return;
}

// キーボードの0〜9の入力判定
for(var i = 0; i < 9 + 1; i++) {
	if(keyboard_check_pressed(ord(string(i)))) {
		number_list[number_index] = i;
		number_index++;
	}
}
```

### Draw GUIイベント

```js:obj_esc_input_numberのDrawGUIイベント
// 背景を描画
draw_set_alpha(0.5);
draw_set_color(c_black);
draw_rectangle(0, 0, room_width, room_height, false);

draw_set_alpha(1);
draw_set_color(c_white);

// 入力中の数値を描画
var px = 512;
var py = 256;
var size = 32;
for(var i = 0; i < number_max; i++) {
	draw_set_color(c_black);
	draw_rectangle(px, py, px+size, py+size, false);
	draw_set_color(c_white);
	draw_rectangle(px, py, px+size, py+size, true);
	if(i < number_index) {
		draw_text(px+8, py, string(number_list[i]));
	}
	px += size;
}

// 説明文を描画
px = 512;
draw_set_font(fnt_jp);
draw_text(px, py+128, "数字キーで入力");
draw_text(px, py+128+32, "* Enterキーで決定 *");
draw_text(px, py+128+64, "* Backspaceキーで1文字戻る *");

```

## obj_notice
obj_noticeはクリック時の説明テキスト表示用オブジェクトです。

### Createイベント

```js:obj_noticeのCreateイベント
enum eNoticeAlarm {
	LifeTime = 0, // Alarm[0]を表示時間とする
};

text = "";
depth = -10;
```

### Alarm 0イベント

```js:obj_noticeのAlarm[0]イベント
// 削除
instance_destroy();
```

### Draw GUIイベント

```js:obj_noticeのDrawGUIイベント
var height = 48;

var px = room_width/2;
var py = room_height - height;

// 背景の描画
draw_set_alpha(0.3);
draw_set_color(c_black);
draw_rectangle(0, py-16, room_width, room_height, false);

// テキストの描画
draw_set_alpha(1);
draw_set_color(c_white);
draw_set_font(fnt_jp);

// 中央揃えにする
draw_set_halign(fa_center);
draw_text(px, py, text);
draw_set_halign(fa_left);
```

## scr_escape
scr_escapeはこのアプリケーションで使用する定数や関数を定義しています。

```js:scr_escape
// 使用するフラグを定義
enum eFlag {
	UNLOCK_NUMBER, // ドアを開いた
	FLAG_01,
	FLAG_02,
	FLAG_03,
	FLAG_04,
	FLAG_05,
	FLAG_06,
	FLAG_07,
	
	Max,
};
```

これはフラグの定義で、このサンプルでは、0番目のみ使用しています。
より汎用的な作りにするには、ポスターを剥がしたフラグでポスターの表示のON/OFF を設定できるようにするなど、できるだけ インスタンスの制御で表示のON/OFFを処理しない方が良いかもしれません。

flag_*() の関数はフラグ操作の関数です。

```js:scr_escapeのesc_message()
/// @description 通知テキスト表示
/// @param text 表示するテキスト
function esc_message(text) {
	if(instance_exists(obj_notice)) {
		// テキストがすでに表示されていたら消す
		instance_destroy(obj_notice);
	}
	
	// 通知テキスト表示オブジェクトを生成
	var _inst = instance_create_depth(0, 0, 0, obj_notice);
	_inst.text = text; // テキストを設定
	_inst.alarm[eNoticeAlarm.LifeTime] = 180; // 表示時間は3秒
	
	return _inst;
}
```

esc_message() は通知テキスト表示関数となります