ひとりでのアプリ開発 - fineの備忘録 -

ひとりでアプリ開発をするなかで起こったことや学んだことを書き溜めていきます

Unity - 会話のシステムを自作する

初めに

 本記事では、会話システムを自作してみます。テキストの入力や表示、話しているキャラクターの取得、表示をさせます。



会話のシステムの作り方

使用の確認

 今回は、次のような仕様にします。

  • CSVファイルに記入されているキャラクターの名前とテキストを読み込み、取得する
  • Resources フォルダ内に表示させるキャラクターの Image を入れておき、その画像を取得し、表示させる
  • 会話をおくる用のボタンを用意し、押すと次のテキストを表示させる
  • テキストは1文字ごと表示させる

(今回作るスクリプト

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;

public class TextController : MonoBehaviour
{
  //ストーリー番号
  [Header("1-1のように入力")]
  public string storynum;

  private string[] words;
  public Text textLabel;

  private GameObject[] CharaImages;
  public GameObject CharaImageBack;
  private GameObject charaimage = null;

  public GameObject talkButton;
  private int talkNum = 0;

  //csvファイル用
  StoryTalkData[] storytalks;

  void Awake()
  {
      // テキストファイルの読み込みを行ってくれるクラス
      TextAsset textasset = new TextAsset();
      // 先ほど用意したcsvファイルを読み込ませる。
      // ファイルは「Resources」フォルダを作り、そこに入れておくこと。
      // Resources.Load 内はcsvファイルの名前。今回は Story1-1 や Story2-5 のようにステージ番号によって読み込むファイルが変えられるようにしている。
      textasset = Resources.Load("Story" + storynum, typeof(TextAsset)) as TextAsset;
      // CSVSerializerを用いてcsvファイルを配列に流し込む。
      storytalks = CSVSerializer.Deserialize<StoryTalkData>(textasset.text);

      CharaImages = new GameObject[storytalks.Length];

      for(int i = 0; i < storytalks.Length; i++)
      {
        CharaImages[i] = (GameObject)Resources.Load("TalkCharaImage/" + storytalks[i].talkingChara + "Talk");
      }
  }

  public void Start()
  {
    talkButton.SetActive(false);
    OnTalkButtonClicked();
  }

  // ボタンを押すと会話スタート
  public void OnTalkButtonClicked()
  {
      // 会話フィールドをリセットする。
      textLabel.text = "";

      //キャラクター画像を生成
      if(charaimage != null)
      {
        Destroy(charaimage);
      }

      charaimage = Instantiate(CharaImages[talkNum], CharaImageBack.transform);

      StartCoroutine(Dialogue());

      // トークボタンを非表示にする。
      talkButton.SetActive(false);
  }

  // コルーチンを使って、1文字ごと表示する。
  IEnumerator Dialogue()
  {
      // 半角スペースで文字を分割する。
      words = storytalks[talkNum].talks.Split(' ');

      foreach (var word in words)
      {
          // 0.1秒刻みで1文字ずつ表示する。
          textLabel.text = textLabel.text + word;
          yield return new WaitForSeconds(0.1f);
      }

      // 次のセリフがある場合には、トークボタンを表示する。
      if(talkNum + 1 < storytalks.Length)
      {
          talkButton.SetActive(true);
      }

      // 次のセリフをセットする。
      talkNum = talkNum + 1;
  }
}

[System.Serializable]
public class StoryTalkData
{
    public string talkingChara;
    public string talks;
}
CSV ファイルを作る

 次のように csv ファイルでテキストデータを作っていきます。talkingChara には話しているキャラクター名を、talks にはテキストを入力します

talkingChara talks
キャラ1 テキスト1
キャラ2 テキスト2
キャラ3 テキスト3

 保存するときは、csv ファイルにしておきます。

 また、文字コードUTF-8 にしておかないと、文字化けをします。メモ帳などを使って、文字コードUTF-8 に変換しましょう。詳しくは下のリンク先をご覧ください。
fineworks-fine.hatenablog.com

CSV ファイルを読み込む

 次に CSV ファイルを読み込みます。今回は csv ファイルの名前を Story1-1 としておきます。

 csv ファイルを Resources 内に入れておきましょう。

 csv ファイルを読み込むためのスクリプトを用意します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;

public class TextController : MonoBehaviour
{
  //ストーリー番号
  [Header("1-1のように入力")]
  public string storynum;

  //csvファイル用
  StoryTalkData[] storytalks;

  void Awake()
  {
      // テキストファイルの読み込みを行ってくれるクラス
      TextAsset textasset = new TextAsset();
      // 先ほど用意したcsvファイルを読み込ませる。
      // ファイルは「Resources」フォルダを作り、そこに入れておくこと。
      // Resources.Load 内はcsvファイルの名前。今回は Story1-1 や Story2-5 のようにステージ番号によって読み込むファイルが変えられるようにしている。
      textasset = Resources.Load("Story" + storynum, typeof(TextAsset)) as TextAsset;
      // CSVSerializerを用いてcsvファイルを配列に流し込む。
      storytalks = CSVSerializer.Deserialize<StoryTalkData>(textasset.text);
  }

[System.Serializable]
public class StoryTalkData
{
    public string talkingChara;
    public string talks;
}

 [System.Serializable]属性をつけたクラス(StoryTalkData)が流し込みたい csv ファイルのデータ一覧です。それを Resources.Load で読み込み、 CSVSerializer.Deserialize を使って読み込んだデータを流し込んでいます。

 ゲームオブジェクトにこのスクリプトをつけ、実行すると csv ファイルが読み込めます。storynum は Inspector から入力しておきましょう(今回は csv ファイルを Story1-1 としているので、storynum も 1-1 にする)。

 CSV ファイルの読み込みに関する詳しい説明は下の記事をご覧ください。(先ほどの文字コード変換の記事と同じ記事です)
fineworks-fine.hatenablog.com

テキスト、画像表示用のオブジェクトを作る

 必要なオブジェクトは次の通りです。

  • テキストを表示させる背景(TalkBack)
  • テキスト(TalkText)
  • キャラクター画像を表示させる場所の背景(CharaImageBack)
  • 会話を次に進めるためのボタン(TalkNextButton)
スクリプトからオブジェクトを取得する

 スクリプトに追記して、上記で作成したオブジェクトを取得していきます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;

public class TextController : MonoBehaviour
{
  //ストーリー番号
  [Header("1-1のように入力")]
  public string storynum;

  public Text textLabel;  //追記

  public GameObject CharaImageBack;  //追記

  public GameObject talkButton;  //追記
  private int talkNum = 0;

  //csvファイル用
  StoryTalkData[] storytalks;

  void Awake()
  {
      // テキストファイルの読み込みを行ってくれるクラス
      TextAsset textasset = new TextAsset();
      // 先ほど用意したcsvファイルを読み込ませる。
      // ファイルは「Resources」フォルダを作り、そこに入れておくこと。
      // Resources.Load 内はcsvファイルの名前。今回は Story1-1 や Story2-5 のようにステージ番号によって読み込むファイルが変えられるようにしている。
      textasset = Resources.Load("Story" + storynum, typeof(TextAsset)) as TextAsset;
      // CSVSerializerを用いてcsvファイルを配列に流し込む。
      storytalks = CSVSerializer.Deserialize<StoryTalkData>(textasset.text);
  }

[System.Serializable]
public class StoryTalkData
{
    public string talkingChara;
    public string talks;
}

 Inspector から取得します。

話しているキャラクターの画像を取得し、表示させる

 まず、スクリプトからキャラクターの画像を取得できるようにします。

 Resources フォルダ内に TalkCharaImage というフォルダを作り、そこにキャラクターの Image を入れておきます。作成する Image の名前は (キャラクター名)Talk という名前にしておきます。
(例)PikachuTalk、MarioTalk

 作成するキャラクターの Image は事前に先ほど作ったキャラクター画像を表示させる場所の背景(CharaImageBack)とサイズや位置を合わせて、Prehab 化しておきましょう。このキャラクターの画像は CharaImageBack の子オブジェクトとして生成するため、位置は CharaImageBack の子オブジェクトにした状態で調整しておきます。

 キャラクターの画像を表示する際、Mask 機能を使うことも選択肢に入るかと思います。その場合は CharaImageBack に Mask コンポーネントを追加しましょう。
fineworks-fine.hatenablog.com

スクリプトからキャラクターの画像を取得する

 スクリプトからキャラクターの画像を取得します。csv ファイルから話しているキャラクター名を取得しているので、そのキャラクターの画像を配列で取得します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;

public class TextController : MonoBehaviour
{
  //ストーリー番号
  [Header("1-1のように入力")]
  public string storynum;

  public Text textLabel;

  private GameObject[] CharaImages;  //追記
  public GameObject CharaImageBack;

  public GameObject talkButton;
  private int talkNum = 0;

  //csvファイル用
  StoryTalkData[] storytalks;

  void Awake()
  {
      // テキストファイルの読み込みを行ってくれるクラス
      TextAsset textasset = new TextAsset();
      // 先ほど用意したcsvファイルを読み込ませる。
      // ファイルは「Resources」フォルダを作り、そこに入れておくこと。
      // Resources.Load 内はcsvファイルの名前。今回は Story1-1 や Story2-5 のようにステージ番号によって読み込むファイルが変えられるようにしている。
      textasset = Resources.Load("Story" + storynum, typeof(TextAsset)) as TextAsset;
      // CSVSerializerを用いてcsvファイルを配列に流し込む。
      storytalks = CSVSerializer.Deserialize<StoryTalkData>(textasset.text);

    /* 追記(画像を取得) */
      CharaImages = new GameObject[storytalks.Length];

      for(int i = 0; i < storytalks.Length; i++)
      {
        CharaImages[i] = (GameObject)Resources.Load("TalkCharaImage/" + storytalks[i].talkingChara + "Talk");
      }
  }

[System.Serializable]
public class StoryTalkData
{
    public string talkingChara;
    public string talks;
}
画像の表示、テキストの表示

 最後に、テキストと話しているキャラクターの画像を表示できるようにします。

 具体的にやる必要がある作業は次の通りです。

  • キャラクターの画像を CharaImageBack の子オブジェクトとして生成する
  • 会話をおくる用のボタンを用意し、押すと次のテキストを表示させる
  • テキストは1文字ごと表示させる
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;

public class TextController : MonoBehaviour
{
  //ストーリー番号
  [Header("1-1のように入力")]
  public string storynum;

  private string[] words;  //追記
  public Text textLabel;

  private GameObject[] CharaImages;
  public GameObject CharaImageBack;
  private GameObject charaimage = null;  //追記

  public GameObject talkButton;
  private int talkNum = 0;

  //csvファイル用
  StoryTalkData[] storytalks;

  void Awake()
  {
      // テキストファイルの読み込みを行ってくれるクラス
      TextAsset textasset = new TextAsset();
      // 先ほど用意したcsvファイルを読み込ませる。
      // ファイルは「Resources」フォルダを作り、そこに入れておくこと。
      // Resources.Load 内はcsvファイルの名前。今回は Story1-1 や Story2-5 のようにステージ番号によって読み込むファイルが変えられるようにしている。
      textasset = Resources.Load("Story" + storynum, typeof(TextAsset)) as TextAsset;
      // CSVSerializerを用いてcsvファイルを配列に流し込む。
      storytalks = CSVSerializer.Deserialize<StoryTalkData>(textasset.text);

      CharaImages = new GameObject[storytalks.Length];

      for(int i = 0; i < storytalks.Length; i++)
      {
        CharaImages[i] = (GameObject)Resources.Load("TalkCharaImage/" + storytalks[i].talkingChara + "Talk");
      }
  }

  /* ここから追記 */
  public void Start()
  {
    talkButton.SetActive(false);
    OnTalkButtonClicked();
  }

  // ボタンを押すと会話スタート
  public void OnTalkButtonClicked()
  {
      // 会話フィールドをリセットする。
      textLabel.text = "";

      //キャラクター画像を生成
      if(charaimage != null)
      {
        Destroy(charaimage);
      }

      charaimage = Instantiate(CharaImages[talkNum], CharaImageBack.transform);

      StartCoroutine(Dialogue());

      // トークボタンを非表示にする。
      talkButton.SetActive(false);
  }

  // コルーチンを使って、1文字ごと表示する。
  IEnumerator Dialogue()
  {
      // 半角スペースで文字を分割する。
      words = storytalks[talkNum].talks.Split(' ');

      foreach (var word in words)
      {
          // 0.1秒刻みで1文字ずつ表示する。
          textLabel.text = textLabel.text + word;
          yield return new WaitForSeconds(0.1f);
      }

      // 次のセリフがある場合には、トークボタンを表示する。
      if(talkNum + 1 < storytalks.Length)
      {
          talkButton.SetActive(true);
      }

      // 次のセリフをセットする。
      talkNum = talkNum + 1;
  }
}
/* 追記終わり */

[System.Serializable]
public class StoryTalkData
{
    public string talkingChara;
    public string talks;
}

 OnTalkButtonClicked は次の会話をスタートするための関数です。具体的な内容は次のようになっています。

  • テキストのリセット
  • 話すキャラクターの画像を CharaImageBack の子オブジェクトとして生成
  • テキスト表示用のコルーチン(Dialogue)の起動
  • 会話をおくる用のボタンを非表示にする

 テキスト表示用のコルーチン(Dialogue)は次のような内容になっています。

  • テキストの取得、一文字ずつに分割
  • 一文字ずつ表示
  • 次のセリフがあれば、ボタンを表示
  • talkNum を1増加させ、次のテキストをセットする

 今回は、起動させたときに会話がスタートしてほしいので、Start から OnTalkButtonClicked 関数を呼び出しています。また、いきなり2つめの会話が表示されることのないように、 Start で会話をおくる用のボタンを非表示にしています。

完成


最後に

 会話のシステムを自作してみました。以前 Fungus という会話画面を導入するためのパッケージを紹介しましたが、必要最低限の機能でよければ自作することも選択肢の一つかと思います。また、自分で作ってみることで様々な Unity の機能を使うため学んだことの復習になったように感じました。