search
LoginSignup
5
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

GameMaker Studio Advent Calendar 2020 Day 25

posted at

updated at

【GameMaker Studio2】会話イベントの作り方

この記事では GameMaker Studio2 での会話イベントの作り方を解説します。

ただ、会話イベントはゲームエンジンが本来得意とするものとは異なる実装になりがちなので、やや難しい内容となります。
そのため、この記事を読む前提としてGMLやプログラムについての基本知識があるものとしていますので、そのあたり注意が必要となります。

1. 会話テキスト表示をするイベントの実装

まずは会話テキストの表示を実装します。

会話テキスト管理オブジェクトの作成

会話テキストを管理するオブジェクトを作成します。ここでは obj_event とします。

Createイベント

Createイベントを作成して以下のように記述します。

Createイベント

// 状態定義.
enum eEventState {
  Init,         // 初期化.
  ParseCommand, // イベントコマンド解析.
  TextWait,     // テキスト表示中 (キー入力待ち).
};

// 状態を設定.
_state = eEventState.Init;

// イベントコマンド配列.
cmd_list = [];

// 実行中のコマンド配列の番号.
cmd_idx = -1;

// 表示中のテキスト.
_text = "";

Stepイベント

次に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イベントを作成します。

DrawGUIイベント
// 使いたいフォントをここで設定.
//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キーでテキストを進めることができます。

text.gif

改行の実装

GameMakerでは、「改行文字」でテキストを改行することができます。
改行文字というのは「¥n」で表現される特殊な文字となります。

なので、Stepイベントのテキストコマンドを以下のように修正すれば改行したテキストを表示することができます。

Stepイベント
  // テスト用のイベントテキストを設定.
  cmd_list = [
    "シーケンスは通常シーケンスエディターを\n使用してIDEで作成されますが、\nコードを使用して作成および編集することもできます。",
    "ただし、使用可能な関数を確認する前に、\nシーケンスがGameMakerで定義される方法と、\n使用されるさまざまな用語を理解することが重要です。"
  ];

text2.gif

改行のたびに、「¥n」と入力するのは手間なので、例えば「#」を改行の代わりに記述して……

Stepイベント
  // テスト用のイベントテキストを設定.
  cmd_list = [
    "シーケンスは通常シーケンスエディターを#使用してIDEで作成されますが、#コードを使用して作成および編集することもできます。",
    "ただし、使用可能な関数を確認する前に、#シーケンスがGameMakerで定義される方法と、#使用されるさまざまな用語を理解することが重要です。"
  ];

string_replace_all() でまとめて置き換える……

Stepイベント
  // コマンド解析処理
  _text = string_replace_all(cmd, "#", "\n"); // #を改行に置き換える
  _state = eEventState.TextWait; // テキスト表示処理へ.

という仕様にしてもよいと思います。

ちなみに macOS環境の場合、改行文字は「¥n」ではなく「\n」となるので注意です(option + ¥ で入力する)。それと macOSの場合はエディタ上で日本語が入力できないので、Included files (datafiles) にテキストファイルを配置して読み込む方法にすると入力が楽になります。(実装方法は少し難易度が上がりますが……)

プロジェクトファイルのダウンロード

ひとまずここまでを実装したプロジェクトファイルです。

もしうまく動かない場合はこちらを参考にしても良いかもしれません。

2. 背景・キャラクター表示

次に、背景とキャラクター表示コマンドを実装していきます。

背景スプライトの追加

まずは背景画像をプロジェクトに追加します。
スプライト名は spr_bg_classroom としておきます。

classroom.png

背景表示コマンドの実装

obj_eventのCreateイベントを開いて以下の定義を追加します。

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ブロックを修正します。

Stepイベント(eEventState.Init)
switch(_state) {
case eEventState.Init:
  // 初期化処理.

  // 背景表示テストコマンド.
  cmd_list = [
    "背景を表示します",
    [eCmd.BgDraw, spr_bg_classroom], // "spr_bg_classroom" を表示
    "背景を消します",
    [eCmd.BgErase], // 背景を消す
    "おしまい"
  ];

  // 解析処理に進む.
  _state = eEventState.ParseCommand;
  break;

テキスト以外のコマンドは配列で定義します。配列の中に配列がある……、という若干複雑なデータ構造となっています。

次に eEventState.ParseCommandのブロックを修正します。

Stepイベント(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イベントを作成して背景を描画します。

Drawイベント
if(sprite_exists(_bg)) {
  // 背景スプライトが存在していれば背景を描画する
  draw_sprite(_bg, 0, 0, 0);
}

実行

実行して背景が表示されることを確認します。
bg.gif

プロジェクトファイル

ここまでのプロジェクトファイルは以下からダウンロードできます。

キャラ表示コマンドの実装

次にキャラ表示です。
キャラ画像をスプライトとして登録します。
ここでは登録したスプライト名は spr_ch_ayumi とします。

キャラ表示コマンドの追加

Createイベントを開いて、キャラ表示コマンドの定義を追加します。

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にキャラ表示コマンドを追加します。

Stepイベント(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 にキャラ表示処理を追加します。

Stepイベント(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イベントにキャラ表示処理を追加します。

Drawイベント
if(sprite_exists(_bg)) {
  // 背景スプライトが存在していれば背景を描画する
  draw_sprite(_bg, 0, 0, 0);
}

if(sprite_exists(_ch)) {
  // キャラスプライトが存在していればキャラを描画する
  // 座標は表示させたい位置に調整します
  draw_sprite(_ch, 0, 700, 0);
}

実行

実行して動作を確認します。

ch.gif

プロジェクトファイルのダウンロード

ひとまずここまでを実装したプロジェクトファイルです。

3. コードを整理する

Stepイベントのコードが長くなってしまったので、これを整理します。
switch文が読みにくくなる原因の多くは caseブロックが長くなってしまうことです。
これを対処する一般的な方法として、caseブロックを関数化することで見通しを良くします。
これからやる方法は個人的な好みで関数化する方法なので、気に入らない場合は他の方法で記述した方が良いかもしれません。

ちなみにコードの動きを変えずにコードを読みやすく整理することは、「リファクタリング」と呼ばれます。

Createイベント

Createイベントに以下の定義を追加します。

Createイベント
// ユーザー定義イベント.
enum eEventUser {
  Init         = 0, // 初期化.
  ParseCommand = 1, // イベントコマンド解析.
  TextWait     = 2, // テキスト表示中.
};

私の好みでは「ユーザー定義イベント(User Event)」を使う方法でリファクタリングを行います。ユーザー定義イベントは番号指定の呼び出しなので、enum を使ってそれぞれの番号にわかりやすい名前を割り当てます。

ユーザー定義イベント "0" (初期化)

次にユーザー定義イベント "0" を作成して、eEventState.Init の処理をそのままカット&ペーストします。

user_event.gif

ユーザー定義イベント0
/// @description 初期化処理.

// テスト用のイベントテキストを設定.
cmd_list = [
  "背景を表示します",
  [eCmd.BgDraw, spr_bg_classroom],
  "キャラを表示します",
  [eCmd.ChDraw, spr_ch_ayumi],
  "背景を消します",
  [eCmd.BgErase],
  "キャラを消します",
  [eCmd.ChErase],
  "おしまい"
];

// 解析処理に進む.
_state = eEventState.ParseCommand;

eEventState.Init の記述をそのままコピーしたものです。
なお、ユーザー定義イベントに限りませんが、イベントの先頭のコメントに /// @description [イベントの説明] と記述するとイベントの処理がわかりやすい表示となるのでおすすめです。

TestEventRefactoring_-_GameMaker_Studio_2.png

ユーザー定義イベント "1" (コマンド解析)

eEventState.ParseCommand の部分をユーザー定義イベント "1" にカット&ペーストします

ユーザー定義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に移動させます。

ユーザー定義イベント2(eEventState.TextWait)
/// @description テキスト表示中.
if(keyboard_check_pressed(vk_space)) {
  // スペースキーを押したので次に進む.
  _state = eEventState.ParseCommand;
}

Stepイベント

最後に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イベントに選択肢の実装に必要な定義を追加します。

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; // 選択している番号.

先頭の eCmdLabel / Goto / Select コマンドを追加しています。
eEventState に、SelectText 状態を追加し、
eEventUserSelectText を定義し、
そして最後に _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;

このプログラムコードの流れは以下の図のようになります。

名称未設定.png

「はい」を選ぶまで選択肢を表示し続けるという流れとなります。

ユーザー定義イベント "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"]];

という多次元配列でデータを格納している処理となります。

名称未設定.png

図にすると、このように配列の中に配列が入れ子になっているデータ構造となります。

Stepイベント

これで選択肢状態に進める処理ができたので、実際に項目を選択する処理を作ります。
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" を作成して以下のように記述します。

ユーザー定義イベント3(eEventState.SelectText)
/// @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イベントを開いて以下のように修正します。

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;
}

実行

実行すると「はい」を選ぶまで選択肢が繰り返されることが確認できます。

select.gif

プロジェクトファイル

ここまでのプロジェクトファイルは以下からダウンロードできます。

5. コマンド解析の処理のもたつきを修正する

実際に動かしてみると気がついたかもしれませんが、選択肢を選んで次の文字が表示されるまでの若干の遅れが発生しています。

理由としては、ラベルジャンプの判定が1フレームにつき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;
  }
}

実行

実行して動作を確認します。
select2.gif
GIF動画だと違いがわかりにくいかもしれませんが、このように反応が良くなっています。

プロジェクトファイル

ここまでのプロジェクトファイルは以下からダウンロードできます。

6. 省略表記を実装する

ラベルジャンプ処理が毎回 eCmd.Label / eCmd.Goto と書かなければいけないのが冗長なので、省略表記で記述できるようにします。

ラベルの定義は "*" 、ラベルジャンプは ">" で記述可能なルールに修正します。

ユーザー定義イベント"0"(初期化処理)

ユーザー定義イベント"0"(初期化処理)を以下のように修正します。

ユーザー定義イベント0(初期化処理)
/// @description 初期化処理.

// テスト用のイベントコマンドを設定.
cmd_list = [
  "選択肢を開始します",

  "*start", // * 選択肢開始ラベル
  [eCmd.Select, "はい", "*yes", "いいえ", "*no"],

  "*yes", // * "はい" を選んだ場合のラベル
  "「はい」を選択しました",
  ">end", // 終了ラベルへジャンプ

  "*no", // * "いいえ" を選んだ場合のラベル
  "「はい」を選んでくださいね",
  ">start", // "*start" に戻る

  "*end", // * 終了ラベル
  "おしまい"
];

// 解析処理に進む.
_state = eEventState.ParseCommand;

記号だらけでより抽象的になってしまいましたが、以前よりも記述が短くなっているので、すっきりしていると思います。

ユーザー定義イベント"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)) {
    // 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. 通常のゲーム処理からイベント会話を呼び出す

最後に、キャラクターがマップを歩き回る処理から、会話イベントを呼び出せるようにします。

event_dialog.gif
※背景素材は「ぴぽや倉庫」様からお借りしました。

GameMakerで会話イベントを呼び出すときの問題点

ただ、会話イベントを呼び出すには GameMaker の通常の作り方だと不都合があります。
というのも、GameMaker の Object は常に Stepイベントが呼び出され、移動や衝突などの処理を行います。イベント中は 各Object が停止してほしいのですが、インスタンスを停止させるinstance_deactivate_*() という関数を呼び出すと描画まで無効になってしまう……、という問題があります。
これを回避する方法として、停止直前の画面をキャプチャする方法がありますが、これはこれでイベント中に Object を動かせなくなってしまいます。またキャプチャした画像(サーフェース)は WindowsやAndroidの環境ではいつでも破棄されてしまう問題があります。

結論として、各Objectを停止させるには、GameMakerが用意している移動処理やコリジョンイベント、入力イベント(キー入力、マウス入力のイベント)を使用せずにゲームを作り、停止フラグが立っていたらそれらの処理を行わない、という実装をすればGMLからの停止処理がやりやすくなります。

具体的には、is_pause という変数を各Objectに用意して、それが true だったら動かなくしてしまう……という方法です。

Createイベント
// 停止フラグ
is_pause = false;
Stepイベント
if(is_pause) {
  // 停止フラグが立っていたら何もしない
  return;
}

// 停止フラグが立っていなければ移動処理
// vspeed や hspeed は使用せずに直接座標を変更する
x += 10;
y += 5;

このような Object を継承して、各Object を実装していきます。

各Objectの実装

例えばCreateイベントで以下のように実装します。

Createイベント
// 継承元のCreateイベントを呼び出し
event_inherited();

// アクション関数を定義
action = function() {
  // コマンド定義
  var cmd_list = [
    "イベントテスト部屋にようこそ"
  ];

  // アクション関数が実行されたら会話イベントを実行  
  event_start(cmd_list);
};

timer = 0;

Stepイベントでは is_pause が有効である場合のみ、移動処理を行います

Stepイベント
if(is_pause) {
  // ポーズ中は移動しない
  return;
}

// 開始地点の半径64pxをぐるぐる回る
timer += 2;

x = xstart + 64 * dcos(timer);
y = ystart + 64 * dsin(timer);

会話イベント開始関数

会話イベント開始関数 event_start() は以下のように実装します。

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の開始関数は以下のように修正します

eEventState.Init
/// @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 にします。

Destroyイベント
// all objects un-pause.
with(all) {
  if(variable_instance_exists(self, "is_pause")) {
    self.is_pause = false;
  }
}

プレイヤーの処理

最後にプレイヤーの処理です。
プレイヤーの近くに会話イベントを発生できるオブジェクトが存在したら、会話イベントを開始します。
以下、実装例です。(プレイヤーのStepイベント)

obj_playerの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
  }
}

プロジェクトファイル

実装サンプルは以下からダウンロードできます.

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
5
Help us understand the problem. What are the problem?