UnityでRPGを作るpart3 接触してるときにボタンを押したらメッセージを表示する

# はじめに
[前回 : 2Dトップビューのマップと衝突判定](https://qiita.com/_tybt/items/0e9605b7acc955078c64)の投稿から1年以上立ってるというのがまず驚きでした :innocent: 
前回の投稿時期を見るに突発案件などで社畜みが上がり始めた頃ですね。

ということで、久々にゲーム制作お勉強の気力が湧いたので今回はRPGの定番である「宝箱からアイテムをゲットする」をやります。
が、説明をしっかり書きたいので**今回は、話しかけたら会話メッセージを表示する**ところまでです。
(ウインドウ作成のところだけ、だいぶ前にやってたので若干記憶がうろ覚えです）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/daf53e8a-5ffb-bcbc-874c-96220154e6d6.png" width="600px" />

### 直近の投稿内容の予定

1. **オブジェクトに接触したときにEnterキーを押したら、イベント発生させる(メッセージを表示する)** <----- :eyes:ｲﾏｺｺ!!
2. アイテム管理のためにScriptableObject + アイテム取得のイベント(オブジェクト間のイベント受け渡しまで)
3. アイテムを取得したら、宝箱のグラフィックを開けた状態にする（フラグ管理）

をやっていきます。今回はスクリプト成分多めです。

フリー素材は前回と同様に下記のサイトのものを利用させてもらっております。
[ぴぽや倉庫](https://pipoya.net/sozai/)

# 接触時にボタンを押したらメッセージを表示する
2D RPGといえばキャラが動いたら次は戦闘なのか街での会話なのかというぐらい大事（たぶん）な処理ですね。

とりあえず、前回マップチップやキャラをおいたのと同様に「話しかけたら会話を始めるキャラ」のオブジェクトを置きましょう。(前回参考)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/dadcdacc-a9d4-b5f6-d192-e4b4b82b8cde.png" width="600px" />
貫通しないように、Box Collider 2Dで衝突判定。画面にも見えていますがこのあと「MessageCharactor」というScriptを作っていきます。
（ちなみに、これだけだと実際に動かした際に思うように動かないハズですがそれは後ほど。try & errorで学習していく流れです :ok_hand: )

## 文字を表示するメッセージウインドウを準備する

まずは文字を表示するためのメッセージウインドウを作っていきましょう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/64875164-da19-0106-aef5-c806ed12d51c.png" width="600px" />
…明らかにこれまでのウインドウと大きさに差がありますが、とりあえず無視してください。
実行時によしなに大きさが変わる設定もあります。

まずHierarchy上で、ウインドウ用のオブジェクトを生成していくのですが右のmessage_window以下はそれぞれ↓のような役割を担います。

CanvavsやImage・Textは、 `右クリック -> UI` から一覧が表示されます。

* message_window(Canvas) : メッセージを表示する領域の設定  
  * window(Image) : ウインドウの枠を描画に関する設定
      * window_text(Text) : ウインドウに表示するテキストの設定

### ウインドウの領域を設定する
まずは、Canvaｓのオブジェクトを作成して、下記のような設定をしていきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/3e7038bf-8dec-96cb-7d65-859a8f504088.png" width="300px" />

主にCanvas Scalerの設定を変更していきます。

* UI Scale Mode -> Scale With Screen Size にする。これで、表示時にウインドウの大きさに合わせて子要素などの大きさがよしなに調整されます。

他の値は、とりあえず今の所はそのまま(上の画像と同じ)でOKです。

### ウインドウの色や画像を設定する
上記通りにImageのオブジェクトが表示されるので、ImageのComponentの設定を変更していきます

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/dba964e4-856e-1b1d-795e-f4fd2c4a8636.png" width="300px" />

* Source Image -> デフォのAssetであるBackgroundを選択
* Color -> 好きな色で設定してください。
* ImageType -> Source Imageを選択すると表示される。SlicedのままでOK

ここまでいくと、message_windowが表示できる枠いっぱいに設定した色の枠が表示されるので、上のRect Transformで調整をしていきましょう。ここでの設定は**message_windowに対しての座標**で設定していきます。

### テキストのオブジェクトを設定する
最後に、メッセージを表示するためのテキストオブジェクトを設定していきます。
参考までに画像は貼りますがやることはかんたん。作ったウインドウの上に乗るように位置や幅の調整。お好みにあわせて文字サイズやフォントなどを設定するぐらいです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/085352ac-b6c5-b9bb-7f7b-71907ea574b4.png" width="300px" />

※独自フォントの設定や、場面によって使い分ける。といったことはまたいずれ。
これでウインドウの設定は完了しました。実行すればウインドウが画面したにいい感じに出せているかと思います。
ただこのままだとなにもないのにウインドウが表示されたままになるので、インスペクタ左上のチェックボックスのチェックを外しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/659dfb52-156b-de2d-22d5-cdd0f3826d05.png)

次項のスクリプトでウインドウの表示 / 非表示を制御していきます。


## 接触時にボタンを押したら反応するためのScript
ここからC#成分がゴリゴリ増えます。
とりあえずC#学んで見るのもいいですが、とりあえず動かしてから後で学んで見るでもいいと思います。
（by モチベーションドリブンで勉強するマン）

最初に学ぶときはあまり継承関係意識しないほうが理解しやすいとは思いますが、後ほど作ることが見えていて
同じコード複数書くのは大変なのでここでは下記の様に作っています・

* 親 : 「キャラが接触しているときにボタンを押したら特定のメソッドを呼び出す。フィールド全般でオブジェクトに適応できる共通クラス」
* 子 : 「親で特定のメソッドが呼ばれたとき、何か処理を行う(ここではメッセージを表示)。セリフはUnityのインスペクタ上から設定できるようにする。」

次回、宝箱用スクリプトに対してもこのクラスを親として使います。

#### 親クラス

```C#:FieldObjectBase.cs
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

/**
 * フィールドオブジェクトの基本処理
 */
public abstract class FieldObjectBase : MonoBehaviour
{

    // Unityのインスペクタ(UI上)で、前項でつくったオブジェクトをバインドする。
　　// （次項 : インスペクタでscriptを追加して、設定をする で説明）
    public Canvas window;
    public Text target;

    // 接触判定
    private bool isContacted = false;
    private IEnumerator coroutine;


    // colliderをもつオブジェクトの領域に入ったとき(下記で説明1)
    private void OnTriggerEnter2D(Collider2D collider) {
        isContacted = collider.gameObject.tag.Equals("Player");
    }

    // colliderをもつオブジェクトの領域外にでたとき(下記で説明1)
    private void OnTriggerExit2D(Collider2D collider) {
        isContacted = !collider.gameObject.tag.Equals("Player");
    }

    private void FixedUpdate() {
        if (isContacted && coroutine == null && Input.GetButton("Submit") && Input.anyKeyDown) {
            coroutine = CreateCoroutine();
            // コルーチンの起動(下記説明2)
            StartCoroutine(coroutine);
        }
    }

    /**
     * リアクション用コルーチン(下記で説明2)
     */
    private IEnumerator CreateCoroutine() {
        // window起動
        window.gameObject.SetActive(true);

        // 抽象メソッド呼び出し 詳細は子クラスで実装
        yield return OnAction();

        // window終了
        this.target.text = "";
        this.window.gameObject.SetActive(false);

        StopCoroutine(coroutine);
        coroutine = null;
    }

    protected abstract IEnumerator OnAction();

    /**
     * メッセージを表示する
     */
    protected void showMessage(string message) {
        this.target.text = message;
    }
}
```

<details><summary>余談</summary>ReActionのwindowの表示/非表示のタイミングも、これから作っていくオブジェクトによって要/不要やタイミングが異なるかもしれないので、共通化するかどうかは後々検討していきましょう :thumbsup: </details>

##### 説明1 Colliderの領域接触時の判定
つまりこんなイメージです。枠はそれぞれ(少女・魔王)のオブジェクトに設定しているColliderの範囲を可視化したものです
(グラフィックソフトで雰囲気伝わりやすくしただけのもの）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/2a583d96-f7ef-2a74-1a94-ea8484f955f9.png" width="600px" />

青Colliderに対して、赤Colliderの領域が接触した瞬間がOnTriggerEnter2D
青Colliderから赤Colliderが離れた瞬間がOnTriggerExit2D
が呼ばれるタイミングになります。
(OnTriggerStay2Dという領域の中で呼ばれるコールバックメソッドもありますが、今回は不要なので省略しています)

で、それぞれのメソッドでフラグをON/OFFする際に「領域に入ってきたオブジェクトがプレイヤーかどうか」を↓のような呼び出しでチェックしている。

```
        isContacted = collider.gameObject.tag.Equals("Player");
```
ということです。
これでまず、「接触しているかどうか」の判定を解決します。

##### 説明2 コルーチン

いきなりStartCoroutineという処理が呼び出されていて「コルーチンとはなんぞや？」と思われるかと思います。
プログラミングよくやる人なら聞いたことあると思いますが、多分今後もなにかとお世話になると思うのでざっくりと説明を書いておきます。
[コルーチン - Unity マニュアル](https://docs.unity3d.com/jp/540/Manual/Coroutines.html) など調べて出てくるものがたくさんあるので参考まで。

**コルーチン**とは、擬似的な処理の並列化です。マルチスレッドぽく見えるけど、シングルスレッドでやってます。
細かい話は他の皆々様が色々投稿してくれてるので参考にしましょう。

なぜ今回の会話処理をコルーチンから実行するか？ というと、よくあるRPGにおける会話は↓のイメージがあるかと思います。

* メッセージが表示される
* ボタンを押すと次のメッセージ or 終了
* 背景のキャラは動くし、BGMは流れてる

Unity的には、常にフレームを更新したい。会話などの一時的な処理も入力を待ちたい。のに
例えばコルーチンなしでボタンの入力まちをする。ということになると、その待ちのためだけに他の処理ができなくなってしまうのです。
…厳密にはアレですが、処理をとめたくないので待ちが発生する(キー入力など)ようなイベントはコルーチンで実行することでフレーム更新を通常通り行いつつ、待ちが発生するようなイベント処理を実行させているのです。


コルーチンの中で `yield` みたいなものを今後色々書くことになりますが、返すもので挙動が変わるので子クラスの説明にあわせて記載します

#### 子クラス

```C#:MessageCharactor.cs
public class MessageCharactor : FieldObjectBase {

    // セリフ : Unityのインスペクタ(UI上)で会話文を定義する 
　　// （次項 : インスペクタでscriptを追加して、設定をする で説明）
    [SerializeField]
    private List<string> messages;

    // 親クラスから呼ばれるコールバックメソッド (接触 & ボタン押したときに実行)
    protected override IEnumerator OnAction() {

        for (int i = 0; i < messages.Count; ++i) {
            // 1フレーム分 処理を待機(下記説明1)
            yield;

            // 会話をwindowのtextフィールドに表示
            showMessage(messages[i]);
            
            // キー入力を待機 (下記説明1)
            yield return new WaitUntil(() => Input.anyKeyDown);
        }

        yield break;
    }
}

```

##### 説明1  yield return xxxで
前項の説明に続き、コルーチンの中での処理なので、yield return xxxx で処理を待機します。
記述による挙動の違いは下記。

|記述|説明|
|---|---|
|yield|一旦処理を待機させる。(Unity的にいうなら1フレーム分待機)　メッセージを一つずつ表示するときにこれがないと、一回エンターを押した瞬間に一気にメッセージが送られてしまう事が発生するので、それを防止している|
|yield return new WaitUntil|再開条件を返す関数でtrueを返すまで処理を待機。ここでは何かキーを押されるまで処理を待機させている。|
|yield break|処理を中断する。ここでは親クラスに処理が戻る。|

※他にも色々ありますが、コルーチンについてはぜひ色々調べてみてください。

## インスペクタでscriptを追加して、設定をする
最後に、ここまで書いてきたコードに対してオブジェクトの設定をインスペクタで行っていきます。

- publicのメンバ変数
- SerializeFieldをつけたメンバ変数(型は限られる)

はUnityのインスペクタ（オブジェクトで紐づくコンポーネントが表示されている部分）で値を設定することができます。
ここで、最初につくったウインドウとテキストのオブジェクトの設定 + 話しかけたときにメッセージウインドウに表示する文章をそれぞれ追加していきます。

今回みたいな一つのオブジェクトで試すだけであれば、コード上に会話文をベタで書いてしまってもいいですが
後々いろんなオブジェクトに設定することを考えた場合、当然会話内容は変えていきたいので会話文はUI上でかんたんに設定できるようにしておくと良いでしょう。

上記のMessageCharactorクラスのように書くと、ObjectにAdd ComponentをしてこのScriptを追加した場合
Listのサイズが0で表示されているので、Sizeに会話の分だけSizeを指定(↓では3)すると
テキストを設定する枠が追加されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/03e6faae-00d2-36f4-1d1d-f604c8663d10.png" width="600px" />


## イベントトリガのためのBox Colliderを追加
ここまで設定して起動すると、「あれ？何も反応しないぞ」となります。
そうです。上のほうで話してたこれが原因です。

> （ちなみに、これだけだと実際に動かした際に思うように動かないハズですがそれは後ほど。try & errorで学習していく流れです :ok_hand: )

前回の衝突判定などをしたときにColliderを設定して、衝突判定(自動)をするところは実施しましたが
これだけでは接触したときにトリガーにまつわる機能が働かないのです。

そこで Box Colliderにある 「is Trigger」にチェックをつけて実行すると今度は
キャラへの衝突判定がなくなります。魔王様に乗り放題です。
Colliderの領域に入ったことは検出できるので、エンターを押すとメッセージはでます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/517bc17e-6098-a77e-47d3-12b4fecf6870.png" width="600px" />

Box Colliderの特性としてis TriggerをONにした場合、接触など物理的な機能がなくなり
領域に物体が入ってきたか検出する機能に絞られます。

これを解決するための方法はかんたんで、一つのオブジェクト（ここでは魔王様）に2つのColliderを設定して
片方はis TriggerをON。もう一つはOFFとすればOKです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/ffedff83-9d3b-47d4-8470-812939346e50.png" width="300px" />




# 動作確認とまとめ
ここまで来ると、歩いて魔王様にエンターキーで話しかけると↓のように会話ができるようになります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/10fbc8f0-329e-c1d7-79e2-d5111a874c22.png" width="600px" />

ちょっとずつRPGが作れる気がしてきましたね :tada: 
…戦闘までの道のりはまだ長いですが :innocent: 

次回は、アイテムの管理と取得と保存のためのScriptableObjectについて解説していければと思います！