この記事では GameMaker Studio2 での会話イベントの作り方を解説します。
ただ、会話イベントはゲームエンジンが本来得意とするものとは異なる実装になりがちなので、やや難しい内容となります。
そのため、この記事を読む前提としてGMLやプログラムについての基本知識があるものとしていますので、そのあたり注意が必要となります。
1. 会話テキスト表示をするイベントの実装
まずは会話テキストの表示を実装します。
会話テキスト管理オブジェクトの作成
会話テキストを管理するオブジェクトを作成します。ここでは obj_event
とします。
Createイベント
Createイベントを作成して以下のように記述します。
// 状態定義.
enum eEventState {
Init, // 初期化.
ParseCommand, // イベントコマンド解析.
TextWait, // テキスト表示中 (キー入力待ち).
};
// 状態を設定.
_state = eEventState.Init;
// イベントコマンド配列.
cmd_list = [];
// 実行中のコマンド配列の番号.
cmd_idx = -1;
// 表示中のテキスト.
_text = "";
Stepイベント
次にStepイベントを作成します。
switch(_state) {
case eEventState.Init:
// 初期化処理.
// テスト用のイベントテキストを設定.
cmd_list = [
"おはよう",
"こんにちは",
"おやすみ"
];
// 解析処理に進む.
_state = eEventState.ParseCommand;
break;
case eEventState.ParseCommand:
// コマンドを取り出す.
cmd_idx++;
if(cmd_idx >= array_length(cmd_list)) {
// コマンドがなくなったので終了.
instance_destroy();
exit;
}
var cmd = cmd_list[cmd_idx];
// コマンド解析処理
_text = cmd; // 今のところテキスト表示コマンドしかないのでそのまま代入
_state = eEventState.TextWait; // テキスト表示処理へ.
break;
case eEventState.TextWait:
// テキスト表示中.
if(keyboard_check_pressed(vk_space)) {
// スペースキーを押したので次に進む.
_state = eEventState.ParseCommand;
}
break;
}
Draw GUIイベント
最後にDraw GUIイベントを作成します。
// 使いたいフォントをここで設定.
//draw_set_font(fnt_jp);
var px = 64; // 左は 64px とします
var py = 600; // 600px の高さに表示する
var width = display_get_gui_width(); // 画面の幅を取得
// テキストウィンドウの描画
draw_set_color(c_blue);
draw_set_alpha(0.2);
draw_roundrect(16, py-32, room_width-16, py+104, false);
// テキストウィンドウの枠の描画
draw_set_color(c_white);
draw_set_alpha(1);
draw_roundrect(16, py-32, room_width-16, py+104, true);
switch(_state) {
case eEventState.TextWait:
// テキスト表示時のみ描画する
draw_text(px, py, _text);
break;
}
日本語を表示するため、日本語フォントが存在する前提となっています。
日本語フォントの設定をしない状態でひとまず動作を確認したい場合は、テキストを英語にしてみるとよいです。
また、ルームサイズ(解像度)は 1280x720としています。
実行
実行すると Spaceキーでテキストを進めることができます。
改行の実装
GameMakerでは、「改行文字」でテキストを改行することができます。
改行文字というのは「¥n」で表現される特殊な文字となります。
なので、Stepイベントのテキストコマンドを以下のように修正すれば改行したテキストを表示することができます。
// テスト用のイベントテキストを設定.
cmd_list = [
"シーケンスは通常シーケンスエディターを\n使用してIDEで作成されますが、\nコードを使用して作成および編集することもできます。",
"ただし、使用可能な関数を確認する前に、\nシーケンスがGameMakerで定義される方法と、\n使用されるさまざまな用語を理解することが重要です。"
];
改行のたびに、「¥n」と入力するのは手間なので、例えば「#」を改行の代わりに記述して……
// テスト用のイベントテキストを設定.
cmd_list = [
"シーケンスは通常シーケンスエディターを#使用してIDEで作成されますが、#コードを使用して作成および編集することもできます。",
"ただし、使用可能な関数を確認する前に、#シーケンスがGameMakerで定義される方法と、#使用されるさまざまな用語を理解することが重要です。"
];
string_replace_all()
でまとめて置き換える……
// コマンド解析処理
_text = string_replace_all(cmd, "#", "\n"); // #を改行に置き換える
_state = eEventState.TextWait; // テキスト表示処理へ.
という仕様にしてもよいと思います。
ちなみに macOS環境の場合、改行文字は「¥n」ではなく「\n」となるので注意です(option + ¥
で入力する)。それと macOSの場合はエディタ上で日本語が入力できないので、Included files (datafiles)
にテキストファイルを配置して読み込む方法にすると入力が楽になります。(実装方法は少し難易度が上がりますが……)
プロジェクトファイルのダウンロード
ひとまずここまでを実装したプロジェクトファイルです。
もしうまく動かない場合はこちらを参考にしても良いかもしれません。
2. 背景・キャラクター表示
次に、背景とキャラクター表示コマンドを実装していきます。
背景スプライトの追加
まずは背景画像をプロジェクトに追加します。
スプライト名は spr_bg_classroom
としておきます。
背景表示コマンドの実装
obj_eventのCreateイベントを開いて以下の定義を追加します。
// -------------------
// ※ここを追加
// イベントコマンド定義
enum eCmd {
Message, // テキスト表示.
BgDraw, // 背景の表示.
BgErase, // 背景の消去.
};
// -------------------
// 状態定義.
enum eEventState {
Init, // 初期化.
ParseCommand, // イベントコマンド解析.
TextWait, // テキスト表示中 (キー入力待ち).
};
// 状態を設定.
_state = eEventState.Init;
// イベントコマンド配列.
cmd_list = [];
// 実行中のコマンド配列の番号.
cmd_idx = -1;
// 表示中のテキスト.
_text = "";
// -------------------
// ※ここを追加
// 表示中の背景スプライト
_bg = noone;
追加したのは、最初の eCmd
の部分と、最後の _bg
です。
Stepイベントの修正
Stepイベントを修正して背景コマンドを呼び出すようにします。
switch文の eEventState.Init
の caseブロックを修正します。
switch(_state) {
case eEventState.Init:
// 初期化処理.
// 背景表示テストコマンド.
cmd_list = [
"背景を表示します",
[eCmd.BgDraw, spr_bg_classroom], // "spr_bg_classroom" を表示
"背景を消します",
[eCmd.BgErase], // 背景を消す
"おしまい"
];
// 解析処理に進む.
_state = eEventState.ParseCommand;
break;
テキスト以外のコマンドは配列で定義します。配列の中に配列がある……、という若干複雑なデータ構造となっています。
次に eEventState.ParseCommand
のブロックを修正します。
// コマンドを取り出す.
cmd_idx++;
if(cmd_idx >= array_length(cmd_list)) {
// コマンドがなくなったので終了.
instance_destroy();
exit;
}
var cmd = cmd_list[cmd_idx];
if(is_string(cmd)) {
// テキストをコマンド形式の配列に変換.
cmd = [eCmd.Message, cmd];
}
if(is_array(cmd) == false) {
// 間違ったデータを指定したのでエラー
show_message(string(cmd_idx) + "番目のコマンドが正しくありません");
break;
}
// コマンド解析処理
var cmd_id = cmd[0];
switch(cmd_id) {
case eCmd.Message: // テキスト表示.
_text = string_replace_all(cmd[1], "#", "\n"); // #を改行に置き換える
_state = eEventState.TextWait; // テキスト表示処理へ.
break;
case eCmd.BgDraw: // 背景表示.
_bg = cmd[1];
break;
case eCmd.BgErase: // 背景消去.
_bg = noone;
break;
default:
show_message("未実装のコマンドです: " + string(cmd_id));
break;
}
break;
最後にDrawイベントを作成して背景を描画します。
if(sprite_exists(_bg)) {
// 背景スプライトが存在していれば背景を描画する
draw_sprite(_bg, 0, 0, 0);
}
実行
プロジェクトファイル
ここまでのプロジェクトファイルは以下からダウンロードできます。
キャラ表示コマンドの実装
次にキャラ表示です。
キャラ画像をスプライトとして登録します。
ここでは登録したスプライト名は spr_ch_ayumi
とします。
キャラ表示コマンドの追加
Createイベントを開いて、キャラ表示コマンドの定義を追加します。
// イベントコマンド定義
enum eCmd {
Message, // テキスト表示.
BgDraw, // 背景の表示.
BgErase, // 背景の消去.
ChDraw, // キャラの表示.
ChErase, // キャラの消去.
};
// 状態定義.
enum eEventState {
Init, // 初期化.
ParseCommand, // イベントコマンド解析.
TextWait, // テキスト表示中 (キー入力待ち).
};
// 状態を設定.
_state = eEventState.Init;
// イベントコマンド配列.
cmd_list = [];
// 実行中のコマンド配列の番号.
cmd_idx = -1;
// 表示中のテキスト.
_text = "";
// 表示中の背景スプライト
_bg = noone;
// 表示中のキャラスプライト
_ch = noone;
eCmd
のところに ChDraw
ChErase
、最後のところに _ch
変数を追加しています。
Stepイベント
caseブロックeEventState.Init
にキャラ表示コマンドを追加します。
switch(_state) {
case eEventState.Init:
// 初期化処理.
// テスト用のイベントテキストを設定.
cmd_list = [
"背景を表示します",
[eCmd.BgDraw, spr_bg_classroom],
"キャラを表示します",
[eCmd.ChDraw, spr_ch_ayumi],
"背景を消します",
[eCmd.BgErase],
"キャラを消します",
[eCmd.ChErase],
"おしまい"
];
// 解析処理に進む.
_state = eEventState.ParseCommand;
break;
次に eEventState.ParseCommand
にキャラ表示処理を追加します。
case eEventState.ParseCommand:
// コマンドを取り出す.
cmd_idx++;
if(cmd_idx >= array_length(cmd_list)) {
// コマンドがなくなったので終了.
instance_destroy();
exit;
}
var cmd = cmd_list[cmd_idx];
if(is_string(cmd)) {
// テキストをコマンド形式の配列に変換.
cmd = [eCmd.Message, cmd];
}
if(is_array(cmd) == false) {
// 間違ったデータを指定したのでエラー
show_message(string(cmd_idx) + "番目のコマンドが正しくありません");
break;
}
// コマンド解析処理
var cmd_id = cmd[0];
switch(cmd_id) {
case eCmd.Message: // テキスト表示.
_text = string_replace_all(cmd[1], "#", "\n"); // #を改行に置き換える
_state = eEventState.TextWait; // テキスト表示処理へ.
break;
case eCmd.BgDraw: // 背景表示.
_bg = cmd[1];
break;
case eCmd.BgErase: // 背景消去.
_bg = noone;
break;
case eCmd.ChDraw: // キャラ表示.
_ch = cmd[1];
break;
case eCmd.ChErase: // キャラ消去.
_ch = noone;
break;
default:
show_message("未実装のコマンドです: " + string(cmd_id));
break;
}
break;
最後に Drawイベントにキャラ表示処理を追加します。
if(sprite_exists(_bg)) {
// 背景スプライトが存在していれば背景を描画する
draw_sprite(_bg, 0, 0, 0);
}
if(sprite_exists(_ch)) {
// キャラスプライトが存在していればキャラを描画する
// 座標は表示させたい位置に調整します
draw_sprite(_ch, 0, 700, 0);
}
実行
実行して動作を確認します。
プロジェクトファイルのダウンロード
ひとまずここまでを実装したプロジェクトファイルです。
3. コードを整理する
Stepイベントのコードが長くなってしまったので、これを整理します。
switch文が読みにくくなる原因の多くは caseブロックが長くなってしまうことです。
これを対処する一般的な方法として、caseブロックを関数化することで見通しを良くします。
これからやる方法は個人的な好みで関数化する方法なので、気に入らない場合は他の方法で記述した方が良いかもしれません。
ちなみにコードの動きを変えずにコードを読みやすく整理することは、「リファクタリング」と呼ばれます。
Createイベント
Createイベントに以下の定義を追加します。
// ユーザー定義イベント.
enum eEventUser {
Init = 0, // 初期化.
ParseCommand = 1, // イベントコマンド解析.
TextWait = 2, // テキスト表示中.
};
私の好みでは「ユーザー定義イベント(User Event)」を使う方法でリファクタリングを行います。ユーザー定義イベントは番号指定の呼び出しなので、enum を使ってそれぞれの番号にわかりやすい名前を割り当てます。
##ユーザー定義イベント "0" (初期化)
次にユーザー定義イベント "0" を作成して、eEventState.Init
の処理をそのままカット&ペーストします。
/// @description 初期化処理.
// テスト用のイベントテキストを設定.
cmd_list = [
"背景を表示します",
[eCmd.BgDraw, spr_bg_classroom],
"キャラを表示します",
[eCmd.ChDraw, spr_ch_ayumi],
"背景を消します",
[eCmd.BgErase],
"キャラを消します",
[eCmd.ChErase],
"おしまい"
];
// 解析処理に進む.
_state = eEventState.ParseCommand;
eEventState.Init
の記述をそのままコピーしたものです。
なお、ユーザー定義イベントに限りませんが、イベントの先頭のコメントに /// @description [イベントの説明]
と記述するとイベントの処理がわかりやすい表示となるのでおすすめです。
##ユーザー定義イベント "1" (コマンド解析)
eEventState.ParseCommand
の部分をユーザー定義イベント "1" にカット&ペーストします
/// @description コマンド解析処理
// コマンドを取り出す.
cmd_idx++;
if(cmd_idx >= array_length(cmd_list)) {
// コマンドがなくなったので終了.
instance_destroy();
exit;
}
var cmd = cmd_list[cmd_idx];
if(is_string(cmd)) {
// テキストをコマンド形式の配列に変換.
cmd = [eCmd.Message, cmd];
}
if(is_array(cmd) == false) {
// 間違ったデータを指定したのでエラー
show_message(string(cmd_idx) + "番目のコマンドが正しくありません");
exit;
}
// コマンド解析処理
var cmd_id = cmd[0];
switch(cmd_id) {
case eCmd.Message: // テキスト表示.
_text = string_replace_all(cmd[1], "#", "\n"); // #を改行に置き換える
_state = eEventState.TextWait; // テキスト表示処理へ.
break;
case eCmd.BgDraw: // 背景表示.
_bg = cmd[1];
break;
case eCmd.BgErase: // 背景消去.
_bg = noone;
break;
case eCmd.ChDraw: // キャラ表示.
_ch = cmd[1];
break;
case eCmd.ChErase: // キャラ消去.
_ch = noone;
break;
default:
show_message("未実装のコマンドです: " + string(cmd_id));
break;
}
is_array()
の判定の後にある break
キーワードが caseブロックの中でのみ使える記述なので、ここを exit
に変更した以外は同じコードとなっています。
if(is_array(cmd) == false) {
// 間違ったデータを指定したのでエラー
show_message(string(cmd_idx) + "番目のコマンドが正しくありません");
exit; // ※ここを break から exit に変更
}
ユーザー定義イベント "2" (テキストの表示)
テキスト表示処理(eEventState.TextWait)を、ユーザー定義イベント2に移動させます。
/// @description テキスト表示中.
if(keyboard_check_pressed(vk_space)) {
// スペースキーを押したので次に進む.
_state = eEventState.ParseCommand;
}
Stepイベント
最後にStepイベントにそれぞれのユーザー定義イベントの呼び出しを記述します。
switch(_state) {
case eEventState.Init:
// 初期化処理.
event_user(eEventUser.Init);
break;
case eEventState.ParseCommand:
// コマンド解析処理.
event_user(eEventUser.ParseCommand);
break;
case eEventState.TextWait:
// テキスト表示中.
event_user(eEventUser.TextWait);
break;
}
だいぶスッキリして見通しが良くなったのではないかと思います。
実行
実行して動作を確認します。
以前と同じ動きになっていれば問題ありません
プロジェクトファイル
ここまでのプロジェクトファイルは以下からダウンロードできます。
4.選択肢を実装する
最後に選択肢を実装します。
ただ選択肢は他と比べて複雑な処理となります。
Createイベント
Createイベントに選択肢の実装に必要な定義を追加します。
// イベントコマンド定義
enum eCmd {
Message, // テキスト表示.
BgDraw, // 背景の表示.
BgErase, // 背景の消去.
ChDraw, // キャラの表示.
ChErase, // キャラの消去.
Label, // ラベル.
Goto, // ラベルジャンプ.
Select, // 選択肢.
};
// 状態定義.
enum eEventState {
Init, // 初期化.
ParseCommand, // イベントコマンド解析.
TextWait, // テキスト表示中 (キー入力待ち).
SelectText, // 選択肢表示中.
};
// ユーザー定義イベント.
enum eEventUser {
Init = 0, // 初期化.
ParseCommand = 1, // イベントコマンド解析.
TextWait = 2, // テキスト表示中.
SelectText = 3, // 選択肢表示中.
};
// 状態を設定.
_state = eEventState.Init;
// イベントコマンド配列.
cmd_list = [];
// 実行中のコマンド配列の番号.
cmd_idx = -1;
// 表示中のテキスト.
_text = "";
// 表示中の背景スプライト
_bg = noone;
// 表示中のキャラスプライト
_ch = noone;
// 選択肢の実装に必要な変数.
_jump_label = noone; // ジャンプするラベル名.
_sel_list = []; // 選択肢テキスト.
_sel_idx = 0; // 選択している番号.
先頭の eCmd
に Label
/ Goto
/ Select
コマンドを追加しています。
eEventState
に、SelectText
状態を追加し、
eEventUser
に SelectText
を定義し、
そして最後に _jump_label
/ _sel_list
/ _sel_idx
変数を追加しています。
ユーザー定義イベント "0" (eEventState.Init)
初期化処理で選択肢を呼び出すコマンドを記述します。
/// @description 初期化処理.
// テスト用のイベントコマンドを設定.
cmd_list = [
"選択肢を開始します",
[eCmd.Label, "label_start"], // * 選択肢開始ラベル
[eCmd.Select, "はい", "label_yes", "いいえ", "label_no"],
[eCmd.Label, "label_yes"], // * "はい" を選んだ場合のラベル
"「はい」を選択しました",
[eCmd.Goto, "label_end"], // 終了ラベルへジャンプ
[eCmd.Label, "label_no"], // * "いいえ" を選んだ場合のラベル
"「はい」を選んでくださいね",
[eCmd.Goto, "label_start"], // "label_start" に戻る
[eCmd.Label, "label_end"], // * 終了ラベル
"おしまい"
];
// 解析処理に進む.
_state = eEventState.ParseCommand;
このプログラムコードの流れは以下の図のようになります。
「はい」を選ぶまで選択肢を表示し続けるという流れとなります。
ユーザー定義イベント "1" (eEventState.ParseCommand)
イベントコマンド解析処理に選択肢の処理を実装します。
/// @description コマンド解析処理
// コマンドを取り出す.
cmd_idx++;
if(cmd_idx >= array_length(cmd_list)) {
// コマンドがなくなったので終了.
instance_destroy();
exit;
}
var cmd = cmd_list[cmd_idx];
if(is_string(cmd)) {
// テキストをコマンド形式の配列に変換.
cmd = [eCmd.Message, cmd];
}
if(is_array(cmd) == false) {
// 間違ったデータを指定したのでエラー
show_message(string(cmd_idx) + "番目のコマンドが正しくありません");
exit;
}
// コマンド解析処理
var cmd_id = cmd[0];
// ②ラベル判定処理.
if(_jump_label != noone) {
// ラベルが指定されていたらチェックが必要
if(cmd_id != eCmd.Label) {
exit; // ラベルでない行は無視する.
}
if(_jump_label == cmd[1]) {
_jump_label = noone; // ラベルが一致したのでラベルジャンプ終了.
}
exit;
}
switch(cmd_id) {
case eCmd.Message: // テキスト表示.
_text = string_replace_all(cmd[1], "#", "\n"); // #を改行に置き換える
_state = eEventState.TextWait; // テキスト表示処理へ.
break;
case eCmd.BgDraw: // 背景表示.
_bg = cmd[1];
break;
case eCmd.BgErase: // 背景消去.
_bg = noone;
break;
case eCmd.ChDraw: // キャラ表示.
_ch = cmd[1];
break;
case eCmd.ChErase: // キャラ消去.
_ch = noone;
break;
case eCmd.Label:
break; // Labelコマンドは特に何もしない
case eCmd.Goto:
// ①ラベルジャンプ開始
_jump_label = cmd[1];
cmd_idx = -1; // 最初から解析し直し
break;
case eCmd.Select:
// ③選択肢開始
_sel_idx = 0;
_sel_list = [];
// ④選択肢パラメータを _sel_list に格納.
for(var i = 1; i < array_length(cmd); i += 2) {
var idx = int64((i-1)/2);
_sel_list[idx] = [
cmd[i], // (i)番目を表示するテキストに設定.
cmd[i+1], // (i+1)番目をジャンプするラベルに設定.
];
}
// ⑤選択肢開始
_state = eEventState.SelectText;
break;
default:
show_message("未実装のコマンドです: " + string(cmd_id));
break;
}
修正箇所が多いので解説します。
まずはラベルジャンプについてですが、①でジャンプ先のラベル名を _jump_label
変数に設定しています。
ジャンプ先のラベルは実行中のコマンド位置よりも前にある可能性があるので、最初から解析をやり直します。
②がラベルの判定処理です。eCmd.Label
コマンドのパラメータ文字列が一致するまでコマンド実行行を進めていく処理となります。
③〜⑤が選択肢の開始処理となります。
[eCmd.Select, "はい", "label_yes", "いいえ", "label_no"],
という記述が選択肢コマンドですが、このコマンドは、
[eCmd.Select, // cmd[0] 選択肢開始コマンド
"はい" // cmd[1] "選択肢1のテキスト"
"label_yes", // cmd[2] "選択肢1のジャンプ先のラベル名"
"いいえ", // cmd[3] "選択肢2のテキスト"
"label_no" // cmd[4] "選択肢2のジャンプ先のラベル名"
]
というデータ構造になっています。
そして先程の選択肢コマンド解析処理は変数 _sel_list
に
_sel_list = [["はい", "label_yes"], ["いいえ", "label_no"]];
という多次元配列でデータを格納している処理となります。
図にすると、このように配列の中に配列が入れ子になっているデータ構造となります。
Stepイベント
これで選択肢状態に進める処理ができたので、実際に項目を選択する処理を作ります。
Stepイベントを以下のように修正します。
switch(_state) {
case eEventState.Init:
// 初期化処理.
event_user(eEventUser.Init);
break;
case eEventState.ParseCommand:
// コマンド解析処理.
event_user(eEventUser.ParseCommand);
break;
case eEventState.TextWait:
// テキスト表示中.
event_user(eEventUser.TextWait);
break;
case eEventState.SelectText:
// 選択肢表示中.
event_user(eEventUser.SelectText);
break;
}
選択肢表示中の呼び出しを追加しました。
ユーザー定義イベント "3" (eEventState.SelectText)
ユーザー定義イベント "3" を作成して以下のように記述します。
/// @description 選択肢表示中.
// 選択肢の項目数を取得する
var sel_max = array_length(_sel_list);
if(keyboard_check_pressed(vk_up)) {
// 上キーで項目を上に移動
_sel_idx--;
if(_sel_idx < 0) {
_sel_idx = sel_max - 1; // 最小値を下回ったので一番下に移動する
}
}
if(keyboard_check_pressed(vk_down)) {
// 下キーで項目を下に移動
_sel_idx++;
if(_sel_idx >= sel_max) {
_sel_idx = 0; // 最大値を上回ったので一番上に移動する
}
}
if(keyboard_check_pressed(vk_space)) {
// Spaceキーで決定
// 選択している項目の情報を取り出す
var sel = _sel_list[_sel_idx];
_jump_label = sel[1]; // [1]にラベル名が入っているのでジャンプ先ラベルに設定
// 最初から解析し直し.
cmd_idx = -1;
// コマンド解析に戻る
_state = eEventState.ParseCommand;
}
上下キーで項目番号 _sel_idx
を移動させ、Spaceキーで決定処理を行います。
ジャンプ処理は、選択した項目の情報が _sel_list
に入っているので、選択している項目番号から取り出した後、sel[1]
でラベル名を取り出して、飛び先のラベルを _jump_label
に設定しています。
###DrawGUIイベント
最後に選択肢の表示です。
DrawGUIイベントを開いて以下のように修正します。
// 使いたいフォントをここで設定.
//draw_set_font(fnt_jp);
var px = 64; // 左は 64px とします
var py = 600; // 600px の高さに表示する
var dy = string_height("a"); // ①文字の高さを取得
var width = display_get_gui_width(); // 画面の幅を取得
// テキストウィンドウの描画
draw_set_color(c_blue);
draw_set_alpha(0.2);
draw_roundrect(16, py-32, width-16, py+104, false);
// テキストウィンドウの枠の描画
draw_set_color(c_white);
draw_set_alpha(1);
draw_roundrect(16, py-32, width-16, py+104, true);
switch(_state) {
case eEventState.TextWait:
// テキスト表示時のみ描画する
draw_text(px, py, _text);
break;
case eEventState.SelectText:
// ②選択肢の描画
for(var i = 0; i < array_length(_sel_list); i++) {
if(i == _sel_idx) {
// 選択している項目なのでカーソルを描画する
draw_set_color(c_blue);
draw_roundrect(px, py, px+width-128, py+dy, false);
draw_set_color(c_white);
}
// 選択肢のテキストを描画
var sel = _sel_list[i];
draw_text(px, py, sel[0]);
// 文字の高さだけ位置を下に移動させる
py += dy;
}
break;
}
実行
実行すると「はい」を選ぶまで選択肢が繰り返されることが確認できます。
プロジェクトファイル
ここまでのプロジェクトファイルは以下からダウンロードできます。
5. コマンド解析の処理のもたつきを修正する
実際に動かしてみると気がついたかもしれませんが、選択肢を選んで次の文字が表示されるまでの若干の遅れが発生しています。
理由としては、ラベルジャンプの判定が1フレームにつき1度しか行われないことが原因です。
なので、そのもたつきをなくすように修正します。
ユーザー定義イベント "1" (コマンド解析処理)
ユーザー定義イベント "1" を開き、以下のように修正します。
/// @description コマンド解析処理
// ①exitするまでループし続ける.
while(true) {
// コマンドを取り出す.
cmd_idx++;
if(cmd_idx >= array_length(cmd_list)) {
// コマンドがなくなったので終了.
instance_destroy();
exit;
}
var cmd = cmd_list[cmd_idx];
if(is_string(cmd)) {
// テキストをコマンド形式の配列に変換.
cmd = [eCmd.Message, cmd];
}
if(is_array(cmd) == false) {
// 間違ったデータを指定したのでエラー
show_message(string(cmd_idx) + "番目のコマンドが正しくありません");
continue; // ※ exitではなく continue
}
// コマンド解析処理
var cmd_id = cmd[0];
// ラベル判定処理.
if(_jump_label != noone) {
// ラベルが指定されていたらチェックが必要
if(cmd_id != eCmd.Label) {
continue; // ラベルでない行は無視する. // ※ exitではなく continue
}
if(_jump_label == cmd[1]) {
_jump_label = noone; // ラベルが一致したのでラベルジャンプ終了.
}
continue; // ※ exitではなく continue
}
switch(cmd_id) {
case eCmd.Message: // テキスト表示.
_text = string_replace_all(cmd[1], "#", "\n"); // #を改行に置き換える
_state = eEventState.TextWait; // テキスト表示処理へ.
break;
case eCmd.BgDraw: // 背景表示.
_bg = cmd[1];
break;
case eCmd.BgErase: // 背景消去.
_bg = noone;
break;
case eCmd.ChDraw: // キャラ表示.
_ch = cmd[1];
break;
case eCmd.ChErase: // キャラ消去.
_ch = noone;
break;
case eCmd.Label:
break; // Labelコマンドは特に何もしない
case eCmd.Goto:
// ラベルジャンプ開始
_jump_label = cmd[1];
cmd_idx = -1; // 最初から解析し直し
break;
case eCmd.Select:
// 選択肢開始
_sel_idx = 0;
_sel_list = [];
// 選択肢パラメータを _sel_list に格納.
for(var i = 1; i < array_length(cmd); i += 2) {
var idx = int64((i-1)/2);
_sel_list[idx] = [
cmd[i], // (i)番目を表示するテキストに設定.
cmd[i+1], // (i+1)番目をジャンプするラベルに設定.
];
}
// 選択肢開始
_state = eEventState.SelectText;
break;
default:
show_message("未実装のコマンドです: " + string(cmd_id));
break;
}
if(_state != eEventState.ParseCommand) {
// 状態が変化したら終了
exit;
}
}
実行
実行して動作を確認します。
GIF動画だと違いがわかりにくいかもしれませんが、このように反応が良くなっています。
プロジェクトファイル
ここまでのプロジェクトファイルは以下からダウンロードできます。
6. 省略表記を実装する
ラベルジャンプ処理が毎回 eCmd.Label
/ eCmd.Goto
と書かなければいけないのが冗長なので、省略表記で記述できるようにします。
ラベルの定義は "*" 、ラベルジャンプは ">" で記述可能なルールに修正します。
ユーザー定義イベント"0"(初期化処理)
ユーザー定義イベント"0"(初期化処理)を以下のように修正します。
/// @description 初期化処理.
// テスト用のイベントコマンドを設定.
cmd_list = [
"選択肢を開始します",
"*start", // * 選択肢開始ラベル
[eCmd.Select, "はい", "*yes", "いいえ", "*no"],
"*yes", // * "はい" を選んだ場合のラベル
"「はい」を選択しました",
">end", // 終了ラベルへジャンプ
"*no", // * "いいえ" を選んだ場合のラベル
"「はい」を選んでくださいね",
">start", // "*start" に戻る
"*end", // * 終了ラベル
"おしまい"
];
// 解析処理に進む.
_state = eEventState.ParseCommand;
記号だらけでより抽象的になってしまいましたが、以前よりも記述が短くなっているので、すっきりしていると思います。
ユーザー定義イベント"1"(コマンド解析)
ユーザー定義イベント"1"(コマンド解析)を修正します。
/// @description コマンド解析処理
// exitするまでループし続ける.
while(true) {
// コマンドを取り出す.
cmd_idx++;
if(cmd_idx >= array_length(cmd_list)) {
// コマンドがなくなったので終了.
instance_destroy();
exit;
}
// ---------------------------------
// ※ここの部分を修正
var cmd = cmd_list[cmd_idx];
if(is_string(cmd)) {
// 1文字目を取得する
var c = string_char_at(cmd, 1);
switch(c) {
case "*": // ラベル定義
cmd = [eCmd.Label, cmd];
break;
case ">": // ラベルジャンプ
// ">" を "*" に置き換える
var label = string_replace(cmd, ">", "*");
cmd = [eCmd.Goto, label];
break;
default:
// テキストをコマンド形式の配列に変換.
cmd = [eCmd.Message, cmd];
break;
}
}
// ここまで修正
// ----------------------------
if(is_array(cmd) == false) {
// 間違ったデータを指定したのでエラー
show_message(string(cmd_idx) + "番目のコマンドが正しくありません");
continue;
}
// コマンド解析処理
var cmd_id = cmd[0];
// ラベル判定処理.
if(_jump_label != noone) {
// ラベルが指定されていたらチェックが必要
if(cmd_id != eCmd.Label) {
continue; // ラベルでない行は無視する.
}
if(_jump_label == cmd[1]) {
_jump_label = noone; // ラベルが一致したのでラベルジャンプ終了.
}
continue;
}
switch(cmd_id) {
case eCmd.Message: // テキスト表示.
_text = string_replace_all(cmd[1], "#", "\n"); // #を改行に置き換える
_state = eEventState.TextWait; // テキスト表示処理へ.
break;
case eCmd.BgDraw: // 背景表示.
_bg = cmd[1];
break;
case eCmd.BgErase: // 背景消去.
_bg = noone;
break;
case eCmd.ChDraw: // キャラ表示.
_ch = cmd[1];
break;
case eCmd.ChErase: // キャラ消去.
_ch = noone;
break;
case eCmd.Label:
break; // Labelコマンドは特に何もしない
case eCmd.Goto:
// ラベルジャンプ開始
_jump_label = cmd[1];
cmd_idx = -1; // 最初から解析し直し
break;
case eCmd.Select:
// 選択肢開始
_sel_idx = 0;
_sel_list = [];
// 選択肢パラメータを _sel_list に格納.
for(var i = 1; i < array_length(cmd); i += 2) {
var idx = int64((i-1)/2);
_sel_list[idx] = [
cmd[i], // (i)番目を表示するテキストに設定.
cmd[i+1], // (i+1)番目をジャンプするラベルに設定.
];
}
// 選択肢開始
_state = eEventState.SelectText;
break;
default:
show_message("未実装のコマンドです: " + string(cmd_id));
break;
}
if(_state != eEventState.ParseCommand) {
// 状態が変化したら終了
exit;
}
}
修正箇所は is_string()
でコマンドが文字列かどうかを判定している部分で、文字列であれば string_char_at(cmd, 1)
で1文字目を取り出す処理を行っています。
そして、switch〜case文で 1文字目が "*" か ">" であればラベルまたはラベルジャンプのコマンドに置き換える処理をしています。
では、実行して以前と同じ動きであることを確認します。
プロジェクトファイル
ここまでのプロジェクトファイルは以下からダウンロードできます。
7. 通常のゲーム処理からイベント会話を呼び出す
最後に、キャラクターがマップを歩き回る処理から、会話イベントを呼び出せるようにします。
※背景素材は「ぴぽや倉庫」様からお借りしました。
GameMakerで会話イベントを呼び出すときの問題点
ただ、会話イベントを呼び出すには GameMaker の通常の作り方だと不都合があります。
というのも、GameMaker の Object は常に Stepイベントが呼び出され、移動や衝突などの処理を行います。イベント中は 各Object が停止してほしいのですが、インスタンスを停止させるinstance_deactivate_*()
という関数を呼び出すと描画まで無効になってしまう……、という問題があります。
これを回避する方法として、停止直前の画面をキャプチャする方法がありますが、これはこれでイベント中に Object を動かせなくなってしまいます。またキャプチャした画像(サーフェース)は WindowsやAndroidの環境ではいつでも破棄されてしまう問題があります。
結論として、各Objectを停止させるには、GameMakerが用意している移動処理やコリジョンイベント、入力イベント(キー入力、マウス入力のイベント)を使用せずにゲームを作り、停止フラグが立っていたらそれらの処理を行わない、という実装をすればGMLからの停止処理がやりやすくなります。
具体的には、is_pause
という変数を各Objectに用意して、それが true
だったら動かなくしてしまう……という方法です。
// 停止フラグ
is_pause = false;
if(is_pause) {
// 停止フラグが立っていたら何もしない
return;
}
// 停止フラグが立っていなければ移動処理
// vspeed や hspeed は使用せずに直接座標を変更する
x += 10;
y += 5;
このような Object を継承して、各Object を実装していきます。
各Objectの実装
例えばCreateイベントで以下のように実装します。
// 継承元のCreateイベントを呼び出し
event_inherited();
// アクション関数を定義
action = function() {
// コマンド定義
var cmd_list = [
"イベントテスト部屋にようこそ"
];
// アクション関数が実行されたら会話イベントを実行
event_start(cmd_list);
};
timer = 0;
Stepイベントでは is_pause
が有効である場合のみ、移動処理を行います
if(is_pause) {
// ポーズ中は移動しない
return;
}
// 開始地点の半径64pxをぐるぐる回る
timer += 2;
x = xstart + 64 * dcos(timer);
y = ystart + 64 * dsin(timer);
会話イベント開始関数
会話イベント開始関数 event_start()
は以下のように実装します。
function event_start(cmd_list) {
// 会話イベントオブジェクトを生成
var evt = instance_create_depth(0, 0, 0, obj_event);
// イベントコマンドリストを登録
evt.cmd_list = cmd_list;
}
obj_eventの修正(開始処理)
obj_eventの開始関数は以下のように修正します
/// @description Init
// all objects pause.
with(all) {
// ひとまずすべてのオブジェクトを処理する
if(variable_instance_exists(self, "is_pause")) {
// "is_pause" 変数があればすべて true にする
self.is_pause = true;
}
}
_state = eEventState.ParseCommand;
ゲームに関係あるオブジェクト(プレイヤー、敵、敵弾、アイテムなど)だけ停止すれば良いかもしれませんが、ひとまずここではすべてのインスタンスを処理しています。
イベントコマンドのリストは外部から渡されるので、コマンドリストの定義はここには書きません。
obj_eventの終了処理
obj_event の Destroyイベントで、停止したオブジェクトの "is_pause" をすべて false にします。
// all objects un-pause.
with(all) {
if(variable_instance_exists(self, "is_pause")) {
self.is_pause = false;
}
}
プレイヤーの処理
最後にプレイヤーの処理です。
プレイヤーの近くに会話イベントを発生できるオブジェクトが存在したら、会話イベントを開始します。
以下、実装例です。(プレイヤーのStepイベント)
// フラグ初期化
_can_action = false; // アクション開始可能かどうか
_action_target = noone; // アクション対象のオブジェクト
with(obj_npc) { // アクション対象のオブジェクトはすべて "obj_npc" を継承している
// 対象のオブジェクトの "hit_size" 範囲内にプレイヤーが存在していたらアクションチェック.
if(point_in_circle(px, py, x, y, hit_size)) {
if(variable_instance_exists(self, "action")) {
// "action" 関数を持っていたらアクション開始可能.
other._can_action = true;
other._action_target = self;
}
}
}
if(keyboard_check_pressed(vk_space)) {
// Spaceキーで会話イベント開始
if(_can_action) {
// "action"関数を実行できるので実行.
_action_target.action();
return
}
}
プロジェクトファイル
実装サンプルは以下からダウンロードできます.