teriyaki note

好きなものはラーメンと将棋

Unityで脱出ゲームを作る 番外編 / テキストメッセージを表示する

| 一覧 | 次回 >

脱出ゲームを作ってみたい衝動に駆られたので、Unity で脱出ゲームを作ってみたいと思う。

と言っても、自分のUnity 歴は対して長いわけでもなく、友人と一緒に簡単なゲームを一度作ったことがある程度。 なので色々と調べながら、C#の勉強もしながら、という感じで進めていく感じになると思う。

なにぶん素人なので、色々とおかしな部分があるかもしれない。つっこんで貰えるとありがたいです。。

脱出ゲームでテキストメッセージが重要かどうかは分からないが、できそうなところから実装してみたい。

ということで、テキストを描画する機能を実装してみる。

成果物(Unity 2018.2.11f1)

f:id:teriyaki398:20181005135220g:plain

このように、何かのオブジェクトをクリックしたら対応するテキストを表示したい

今回の内容はGitHubに公開してるので、ご自由にどうぞ

github.com

参考にしたもの

Unityで脱出ゲームの作り方(5)「3Dオブジェクトをクリックで取得」 | 閃光絵巻ラボ

uGUIでノベルゲームのようなものを作る、その1 UIの表現と文字表現 - テラシュールブログ

オブジェクトの配置

とりあえず TextTestScene という名前で新しいシーンを作成する

次に、必要そうなUI やオブジェクトを適当な名前で生成していく
(カッコ内はオブジェクトの種類を表してる)

  • MessageCanvas (Canvas)
  • Panel (Panel)
  • MessageBox (Text)
  • Box (Cube)
  • Ball (Sphere)
  • Cylinder (Cylinder)
  • GameManager (Empty Object)

f:id:teriyaki398:20181005141824p:plain:w550

親子関係は上の画像のようにしておいた

パラメータを調節して、以下のようにそれっぽく配置する
Panel はメッセージを表示する背景部分(画像の灰色の部分)で、
MessageBox はテキストが実際に表示される場所。

MessageBoxPanel よりも一回り小さくしておくと見栄えがいい。

f:id:teriyaki398:20181005143305p:plain:w550

BoxBallCylinder は今回用意した触るとテキストが表示されるオブジェクト。
触れるオブジェクトのみ、以下のようにレイヤーを変更しておく。(今回は10番)

f:id:teriyaki398:20181005153127p:plain:w400

テキストファイルを作成

表示するテキストの内容を記述したファイルを作成する。

ソースコードに直接記述すると大変なことになるので、
別のファイルに記述して、必要になったら読み出すようにする。

そういう時のために、Unity には スクリプトから簡単にアクセスできる Resourcesフォルダが用意されている

Asset直下にResourcesフォルダを作成。
今回はさらにその中にTextフォルダを作成し、

  • Ball.txt
  • Box.txt
  • Cylinder.txt

という3つのファイルを配置し、表示したいテキストを記述しておく。

f:id:teriyaki398:20181005144314p:plain:w550

f:id:teriyaki398:20181005144630p:plain:w550

テキストメッセージを操作するクラス

TextMessage.csというクラスを作成します。

このクラスに持たせたい機能は以下の通り

  • 指定されたファイルを読み込む (Load)
  • 次のテキストを表示する (Next)
  • MessageCanvasが表示されているかどうかを取得できる (isMessageCanvasActive)

3つ目は無くてもいいかもしれないが、一応。

以下が具体的なコード。

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System;
using System.Text;
using UnityEngine;
using UnityEngine.UI;

public static class TextMessage{
    public static string[] texts;
    public static Text uiText;
    public static int lineNum;
    public static GameObject messageCanvas;

    // MessageCanvas を非表示に設定しておく
    public static void init(string canvas, string messageBox){
        messageCanvas = GameObject.Find(canvas);
        uiText = GameObject.Find(messageBox).GetComponent<Text>();
        messageCanvas.SetActive(false);
    }

    // ファイルをロードする
    public static void Load(string file_name){
        // 表示行数を初期化
        lineNum = 0;

        // messageCanvas が非表示なら表示する
        if(messageCanvas.activeSelf == false){
            messageCanvas.SetActive(true);
        }

        // ファイル読み込んで分割
        TextAsset textAsset = new TextAsset();
        textAsset = Resources.Load(file_name, typeof(TextAsset)) as TextAsset;
        texts = textAsset.text.Split('\n');

        // messageBox のテキストを更新
        uiText.text = texts[0];
        lineNum++;
    }


    // 次のメッセージを表示する
    public static void Next(){
        if(lineNum < texts.Length){
            // messageBox のテキストを更新
            uiText.text = texts[lineNum];
            lineNum++;
        } else {
            messageCanvas.SetActive(false);
        }
    }

    public static bool isMessageCanvasActive(){
        return messageCanvas.activeSelf;
    }
}

一つずつ読み取っていこう。

public static void init(string canvas, string messageBox){
    messageCanvas = GameObject.Find(canvas);
    uiText = GameObject.Find(messageBox).GetComponent<Text>();
    messageCanvas.SetActive(false);
}

この部分は初期化処理を記述してある。

  • 指定された名前のキャンバスメッセージボックスを取得する
  • キャンバスを非アクティブにする
public static void Load(string file_name){
    // 表示行数を初期化
    lineNum = 0;

    // messageCanvas が非表示なら表示する
    if(messageCanvas.activeSelf == false){
        messageCanvas.SetActive(true);
    }

    // ファイル読み込んで分割
    TextAsset textAsset = new TextAsset();
    textAsset = Resources.Load(file_name, typeof(TextAsset)) as TextAsset;
    texts = textAsset.text.Split('\n');

    // messageBox のテキストを更新
    uiText.text = texts[0];
    lineNum++;
}

このメソッドは引数としてファイル名が与えられ、
指定されたファイルを読み込む。

読み込まれたテキストは改行単位で分割されtexts配列に代入され、

MessageCanvas をアクティブにして、最初のテキストを表示する。
lineNumはテキストを何行表示したかを記録する変数。

public static void Next(){
    if(lineNum < texts.Length){
        // messageBox のテキストを更新
        uiText.text = texts[lineNum];
        lineNum++;
    } else {
        messageCanvas.SetActive(false);
    }
}

まだ表示するテキストがあるなら表示し、
最後まで表示していたらMessageCanvasを非表示にして終了する。

ゲーム側

テキストを操作する準備ができたので、ゲーム側の処理に移ろう。

プレイヤーの操作を監視したりするスクリプトTestManager.csを作成し

冒頭で生成した空のオブジェクトGameManagerにアタッチする。

(みんなどうやってるんだろう...)

f:id:teriyaki398:20181005152527p:plain:w550

さて、こいつの役割は次のような感じでしょう

  • プレイヤーのクリック操作を監視する
  • クリックされたら何のオブジェクトがクリックされたかを調べる
  • オブジェクトの種類に応じてテキストを表示させる

具体的なコードは以下の通り。

(参考:https://senkouemaki.com/lab/?p=98

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class TestManager : MonoBehaviour {

    public EventSystem eventSystem;

    // カメラ関係
    public Camera mainCamera;

    // ray関係
    public Ray ray;
    public RaycastHit hit;
    public GameObject selectedGameObject;
    
    // Use this for initialization
    void Start () {
        eventSystem = GameObject.Find("EventSystem").GetComponent<EventSystem>();
        mainCamera = GameObject.Find("Main Camera").GetComponent<Camera>();

        // テキストメッセージの初期化
        TextMessage.init("MessageCanvas", "MessageBox");
    }
    
    // Update is called once per frame
    void Update () {
        if(Input.GetMouseButtonUp(0)){
            if(TextMessage.isMessageCanvasActive()){
                TextMessage.Next();
            } else if (eventSystem.currentSelectedGameObject == null){
                searchRoom();
            }
        }
    }

    public void searchRoom(){
        selectedGameObject = null;
        ray = mainCamera.ScreenPointToRay(Input.mousePosition);
        if(Physics.Raycast(ray, out hit, 10000000, 1 << 10)){
            selectedGameObject = hit.collider.gameObject;

            switch(selectedGameObject.name){
                case "Box":
                    TextMessage.Load("Text/Box");
                    break;
                case "Ball":
                    TextMessage.Load("Text/Ball");
                    break;
                case "Cylinder":
                    TextMessage.Load("Text/Cylinder");
                    break;
            }
        }
    }

}

こちらも一つずつ読んでいこう。

void Start () {
    eventSystem = GameObject.Find("EventSystem").GetComponent<EventSystem>();
    mainCamera = GameObject.Find("Main Camera").GetComponent<Camera>();

    // テキストメッセージの初期化
    TextMessage.init("MessageCanvas", "MessageBox");
}

これはゲームが始まると同時に1度のみ実行される。

MessageCanvasをキャンバス、MessageBoxをメッセージボックスとして指定している。

void Update () {
    if(Input.GetMouseButtonUp(0)){
        if(TextMessage.isMessageCanvasActive()){
            TextMessage.Next();
        } else if (eventSystem.currentSelectedGameObject == null){
            searchRoom();
        }
    }
}

この部分は1フレームに一回実行される。
つまり、ゲームが動いている間繰り返し実行され続ける。

マウスがクリックされたとき、テキストメッセージが表示されているなら
次のテキストを表示する

表示されていないなら、何のオブジェクトがクリックされたかを調べる

という処理を行なっている。

public void searchRoom(){
    selectedGameObject = null;
    ray = mainCamera.ScreenPointToRay(Input.mousePosition);
    if(Physics.Raycast(ray, out hit, 10000000, 1 << 10)){
        selectedGameObject = hit.collider.gameObject;

        switch(selectedGameObject.name){
            case "Box":
                TextMessage.Load("Text/Box");
                break;
            case "Ball":
                TextMessage.Load("Text/Ball");
                break;
            case "Cylinder":
                TextMessage.Load("Text/Cylinder");
                break;
        }
    }
}

この部分では、クリックされたオブジェクトが何なのかを調べ、
オブジェクトの名前に応じて処理を分岐させている。

例えばオブジェクトがBoxだった場合は Text/Boxファイルを読み込ませている。

以上で、なんとかテキストメッセージを表示することができた。

Tips

今回はTextMessageクラスを静的クラスとして実装したが、これは簡潔さを求めたため。

もし他の場所にもテキストを描画させたりする予定があるなら、通常のクラスとして実装するべきで、initメソッドはコンストラクタとして実装するのが良いだろう。