PR
スポンサーリンク

[Unity C#] シンプルな会話システムの作り方

Unity
スポンサーリンク

このブログで使用しているUnityHub/UnityのVer
・Unity 6000.0.33f1

えきふる
えきふる

こんちは!最近忙しくてゲーム作る時間が無い・・・えきふるです

RPGやアドベンチャーゲームみたいに会話がメインじゃなくて、
2DアクションのようなOP、EDやイベント時ににちょっとだけ会話があれば良いーー
例えばスーファミ時代のロックマンや魔界村みたいに、そんな時ってありますよね?

だけど本格的な会話(ダイアログ)の実装は重たいし、アセットストアの本格的なツールも学習コストが高い

そんな時に役に立つかもしれない、軽めの会話、セリフ送りの実装を今回は紹介してみたいと思います。
↓こんなんを作ります

スポンサーリンク

Unity会話用UIの準備

今回は自分の中でロックマンXをイメージして作ってみたいと思います。
ロックマンXでヴァヴァと話すシーンのイメージ・・・分かりますかね?(ね、年齢がバレる…)

具体的には会話イベントが始まったら
・会話用のUIが表示される
 →主人公の顔の画像が表示されて会話
 →敵側が会話したら敵の画像が表示されて会話
 →会話が終わったらUIが閉じる

みたいな感じにしようと思います。


それでは、Unityでまずは必要な素材をセットアップしていきましょう。
自分は今回Unityのプロジェクトは「Universal2Dテンプレート」で作成しました。

それではまずはテキストを表示するためのUIを作成していきましょう。
Hierarchyウインドウで
右クリック→UI→Text-TextMeshProでTMPtextを作成。

この時、TMPのImporterがポップアップしたらインポートしておきましょう。

TMPに関しては下記記事でも説明しています。(Unity6以前の記事なので、そのうちUnity6版も書こうと思います。)

次に、画面のセリフを出したい所に作成したTextUIを移動させましょう。

次にセリフを入れる枠を作ります。
Hierarchyウインドウで
右クリック→UI→Panelで作成。

作成したPanelのサイズを調整して、枠っぽくします。
私は下記のように配置しました。

次に会話している時に顔の表示をしたいので、左右にUIのimageを配置します。
Hierarchyウインドウで右クリック→UI→imageで作成。

主人公と敵側のイメージなので2つ作成して左右に配置します。
左側のimageを「Ch_image」右側を「En_image」という名前にしました。

そうしたら、配置したimageにそれぞれ顔の画像を設定しましょう。
imageUIのInspecterのSource Imageから選択して下さい。

※会話中に表情を変えたい場合は複数画像を用意しておいて下さいね。
↓こんな感じでセットアップしました。

左右で画像をセットし終わったら、次は会話を始めるボタンと会話の送るボタンを作ります。
今回はわかりやすく画面上にボタンを用意しますが、
スマホでタップしたらでも、特定のキーを入力したらでも、スクリプトで調整すれば自由に設定できると思います。

それでは毎度お馴染み、
Hierarchyウインドウで右クリック→UI→Button-TextMeshProで作成。

2つ作成してそれぞれの名前を「StartButton」「NextButton」と付け、次のように配置しました。

最後に、
今、UIの「Canvas」の中にそれぞれのUI素材が入っているかと思いますが、
Start」ボタンが押されたら会話が始まり各UIが表示されるようにしたいので、「Canvas」の中にさらに「Canvas」を作り、その中に「StartButton」以外のUI素材を入れたいと思います。

Hierarchyウインドウで右クリック→UI→Canvasで作成。
名前を「DialogueUI」としてその下の階層に「StartButton」以外のUI素材を入れます。

これで会話システムのUI素材のセットアップが終了しました!
だいぶシンプルだったのでは無いでしょうか?

えきふる
えきふる

次にシステムを作るためのスクリプトをセットアップしていくよ!

会話システムの実装

用意したスクリプトは下記↓
とっても長いよ!!

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using TMPro;

public class DialogueManager : MonoBehaviour
{
    [System.Serializable]
    public class DialogueEvent
    {
        public string[] sentences;  // セリフ
        public Sprite[] characterSprites; // 変更するキャラ画像
        public bool[] isChCharacter; // 主人公キャラかどうか (true:左, false:右)
    }

    public TMP_Text dialogueText;  // テキスト表示
    public Image ChCharacterImage;  // 主人公キャラの画像
    public Image EnCharacterImage; // 敵キャラの画像
    public Button startButton; // 開始ボタン
    public Button nextButton; // 次のセリフボタン
    public GameObject dialogueUI; // UI全体

    public DialogueEvent[] dialogueEvents; // 会話データ
    private int currentDialogueIndex = 0; // 現在の会話イベント
    private int currentSentenceIndex = 0; // 現在のセリフ
    private bool isTyping = false; // 文字表示中フラグ

    void Start()
    {
        dialogueUI.SetActive(false);
        startButton.onClick.AddListener(StartDialogue);
        nextButton.onClick.AddListener(NextSentence);
    }

    void StartDialogue()
    {
        dialogueUI.SetActive(true);
        startButton.gameObject.SetActive(false);
        currentDialogueIndex = 0;
        currentSentenceIndex = 0;
        nextButton.gameObject.SetActive(true);
        StartCoroutine(TypeSentence(dialogueEvents[currentDialogueIndex].sentences[currentSentenceIndex]));
        UpdateCharacterImage();
    }

    IEnumerator TypeSentence(string sentence)
    {
        isTyping = true;
        dialogueText.text = "";

        // **\n を正しく改行に変換**
        sentence = sentence.Replace("\\n", "\n");

        foreach (char letter in sentence.ToCharArray())
        {
            dialogueText.text += letter;
            yield return new WaitForSeconds(0.05f); // 文字送り速度
        }

        isTyping = false;
    }

    void NextSentence()
    {
        if (isTyping)
        {
            StopAllCoroutines();
            dialogueText.text = dialogueEvents[currentDialogueIndex].sentences[currentSentenceIndex].Replace("\\n", "\n");
            isTyping = false;
        }
        else
        {
            currentSentenceIndex++;

            if (currentSentenceIndex < dialogueEvents[currentDialogueIndex].sentences.Length)
            {
                StartCoroutine(TypeSentence(dialogueEvents[currentDialogueIndex].sentences[currentSentenceIndex]));
                UpdateCharacterImage();
            }
            else
            {
                currentSentenceIndex = 0;
                currentDialogueIndex++;

                if (currentDialogueIndex < dialogueEvents.Length)
                {
                    StartCoroutine(TypeSentence(dialogueEvents[currentDialogueIndex].sentences[currentSentenceIndex]));
                    UpdateCharacterImage();
                }
                else
                {
                    CloseDialogue();
                }
            }
        }
    }

    void UpdateCharacterImage()
    {
        // インデックスが範囲内かチェック
        if (currentDialogueIndex < dialogueEvents.Length && currentSentenceIndex < dialogueEvents[currentDialogueIndex].isChCharacter.Length)
        {
            bool isLeft = dialogueEvents[currentDialogueIndex].isChCharacter[currentSentenceIndex];
            Sprite charSprite = dialogueEvents[currentDialogueIndex].characterSprites[currentSentenceIndex];

            if (isLeft)
            {
                ChCharacterImage.sprite = charSprite;
                ChCharacterImage.color = new Color(1f, 1f, 1f, 1f); // 主人公を明るく
                EnCharacterImage.color = new Color(1f, 1f, 1f, 0.5f); // 敵を薄く
            }
            else
            {
                EnCharacterImage.sprite = charSprite;
                EnCharacterImage.color = new Color(1f, 1f, 1f, 1f); // 敵を明るく
                ChCharacterImage.color = new Color(1f, 1f, 1f, 0.5f); // 主人公を薄く
            }
        }
    }

    void CloseDialogue()
    {
        dialogueUI.SetActive(false);
        startButton.gameObject.SetActive(true);
    }
}

スクリプトの説明は後で行いますので、まずは先に実装をしてみたいと思います。

このスクリプトをUnityのAssetsフォルダの中に作成したら
(「DiarogueManager」名前のC#スクリプトにしました)

Hieraruchyウインドウで右クリック→「Create Empty」で空のゲームオブジェクトを作成しましょう。
このオブジェクトの名前を「GameManager」として先ほどのスクリプトをアタッチします。

そうしたら、スクリプトを開いて各UI素材を名前の通りにComponentに挿していって下さい。

これで準備ができました。
次に実際のセリフを書いていきましょう。

そのまま、下にある「DialogueEvents」を開いて下さい。

開かれたこのElementがセリフ一回分のセットになります。
・Sentences 
  →実際の喋るセリフを入れて下さい。表示するときに改行したい場合は「\n」を入れて下さい。
・CharacterSprites
  →このセリフを喋っている時のキャラの画像を設定。セリフによって画像を変えることができます。
・is Ch Character
  →主人公キャラ(左側)が喋っている場合はチェックを入れて下さい。

なお、セリフ1回分のセットの中でSentenceを2回入れたりも出来ます。

私はこんな感じ↓で作ってみました。

※喋っていない方の画像は半透明になるようになっています。
色々と触って試してみて下さいね。

なお、今回は英語でセリフを書いちゃいましたがTMPを日本語フォントで使えるようにすれば問題ありません。

えきふる
えきふる

それではスクリプトの説明にいきましょ〜

スクリプトの説明

スクリプトではポイントポイントになる部分に絞って説明していきたいと思います。

えきふる
えきふる

長いので必要な所だけ読むべし

 [System.Serializable]
    public class DialogueEvent
    {
        public string[] sentences;  // セリフ
        public Sprite[] characterSprites; // 変更するキャラ画像
        public bool[] isChCharacter; // 主人公キャラかどうか (true:左, false:右)
    }

[System.Serializable]は C#の属性の一つです。
この属性をつけたクラスはUnityのインスペクター上でクラスのインスタンスの編集ができるようになります
さらにこのクラスではそれぞれセリフを入れておく配列、キャラ画像を入れておく配列、さらにbool型でセリフを言っているのが誰かを判別できる配列を用意しています。
イメージ的には
sentences[0] → 1つ目のセリフ
characterSprites[0] → 1つ目のセリフ時のキャラ画像
isChCharacter[0] → 1つ目のセリフが 主人公側なら true、敵側なら false
となります。

配列に関しては過去に一度記事にしていますので不安な方は見てみて下さいね。


    void Start()
    {
        dialogueUI.SetActive(false);
        startButton.onClick.AddListener(StartDialogue);
        nextButton.onClick.AddListener(NextSentence);
    }

Startメソッドでは
dialogueUI.SetActive(false);でUIの表示をオフに、
onClick.AddListener(〇〇〇);では、
ボタンがクリックされたら〇〇〇メソッドを実行する、という内容になっています。
※要するに、Buttonの中の機能にあるOnClick()イベントをスクリプトで記載しているだけです。


   void StartDialogue()
    {
        dialogueUI.SetActive(true);
        startButton.gameObject.SetActive(false);
        currentDialogueIndex = 0;
        currentSentenceIndex = 0;
        nextButton.gameObject.SetActive(true);
        StartCoroutine(TypeSentence(dialogueEvents[currentDialogueIndex].sentences[currentSentenceIndex]));
        UpdateCharacterImage();
    }

StartDialogueメソッドでは各UIの表示/非表示の設定をした後に

StartCoroutine(TypeSentence〜の所でTypeSentenceという関数名のコルーチンを呼び出し、
引数として
dialogueEvents[currentDialogueIndex]→現在の会話イベントの番号

sentences[currentSentenceIndex]→現在のセリフの番号

を渡しています。

要するに
dialogueEvents配列の今の番号に入っているセリフを渡しているという事ですね。

コルーチンはサッと説明すると
StartCoroutine(関数名(引数)) で実行し、
実際の処理は IEnumerator 関数名(引数) の中に記述します。
IEnumerator は繰り返し処理や非同期処理を制御するための戻り値の型です。(後で出てきます。)

コルーチンとは、一度実行したメソッドを途中で一時停止してUnityに戻した後に一時停止したところから再度実行ができるメソッドです。※普通のメソッドは実行したら最後まで実行されます。


    IEnumerator TypeSentence(string sentence)
    {
        isTyping = true;
        dialogueText.text = "";

        // **\n を正しく改行に変換**
        sentence = sentence.Replace("\\n", "\n");

        foreach (char letter in sentence.ToCharArray())
        {
            dialogueText.text += letter;
            yield return new WaitForSeconds(0.05f); // 文字送り速度
        }

        isTyping = false;
    }

IEnumeratorは先ほど説明した通り、コルーチンを実行するためのメソッドにつける戻り値の型です。
yield return を使って処理を途中で停止、再開できるように動作を制御します。

このメソッドでは文字を一文字ずつ表示させるコルーチンを実装しています。

IEnumerator TypeSentence(string sentence)
で表示させるセリフを引数として入れています。

isTyping = true; → 文字送り中であることを示すフラグを立てる。
dialogueText.text = “”; → 一旦テキストを空にする。

sentence = sentence.Replace(“\\n”, “\n”);
ここでは\nを改行として認識させています。
これをしないと文字を一文字ずつ表示させるときに\が画面に表示されて\nで初めて改行、とされてしまう為です。

foreach (char letter in sentence.ToCharArray())
ここでsentence の文字列を1文字ずつ取り出し、表示します。
sentence.ToCharArray() で sentence の文字列を 1文字ずつの配列に変換 する。
foreach を使い、その配列から1文字ずつ取り出します。

dialogueText.text += letter; で 取り出した文字を1文字ずつUIに追加していきます。

yield return new WaitForSeconds(0.05f);
0.05秒待機し、次の文字を表示します。

isTyping = false;で
文字送りが完了したら、最後にisTyping を false に戻しています。

※コルーチンに関しては公式の説明も分かりやすいのでオススメです。↓

コルーチン - Unity マニュアル
コルーチンを使うと、複数のフレームにタスクを分散させることができます。Unity では、コルーチンは、実行を一時停止して制御を Unity に戻し、次のフレームで中断した所から続行することができるメソッドです。

   void NextSentence()

NextSentence() メソッドでは、次のセリフを表示する 処理を行います。
長いので以下にメソッド内部の説明を分けて説明していきます。

if (isTyping)
{
    StopAllCoroutines(); // 現在の文字送りを停止
    dialogueText.text = dialogueEvents[currentDialogueIndex].sentences[currentSentenceIndex].Replace("\\n", "\n"); 
    isTyping = false;
}

ここでは、文字送り(TypeSentence)が進行中(isTyping==true)なら、一旦コルーチンを止めます。
Replace("\\n", "\n") で改行を処理し、全文を即時表示。
isTyping = false;にして、次の入力ができるようにします。

else
{
    currentSentenceIndex++;

isTyping == falseの場合、次のセリフを表示するためcurrentSentenceIndexを1増やします。

if (currentSentenceIndex < dialogueEvents[currentDialogueIndex].sentences.Length)
{
    StartCoroutine(TypeSentence(dialogueEvents[currentDialogueIndex].sentences[currentSentenceIndex]));
    UpdateCharacterImage();
}

currentSentenceIndexが会話のセリフ数を超えていなければ、次のセリフを表示
StartCoroutine(TypeSentence(…)) で、文字送りを開始します。
UpdateCharacterImage(); で キャラの画像を更新します。

else
{
    currentSentenceIndex = 0;
    currentDialogueIndex++;

現在の会話イベント内のセリフが終わったらcurrentSentenceIndex をリセット
次の currentDialogueIndex に進む(次の会話イベントへ)

if (currentDialogueIndex < dialogueEvents.Length)
{
    StartCoroutine(TypeSentence(dialogueEvents[currentDialogueIndex].sentences[currentSentenceIndex]));
    UpdateCharacterImage();
}

currentDialogueIndex が dialogueEvents の範囲内なら次の会話イベントのセリフを表示します。
StartCoroutine(TypeSentence(…)) で新しいセリフの文字送り開始、
UpdateCharacterImage(); でキャラの画像を更新します。

else
{
    CloseDialogue();
}

currentDialogueIndex が dialogueEvents の範囲を超えたら、会話イベントが全て終了。
CloseDialogue(); でUIを閉じます。


   void UpdateCharacterImage()

この UpdateCharacterImage() メソッドは、
現在の会話の 話し手に応じてキャラクター画像と透明度を切り替える 役割を持っています。
こちらも長いので以下にメソッド内部の処理の説明を記述します。

if (currentDialogueIndex < dialogueEvents.Length && currentSentenceIndex < dialogueEvents[currentDialogueIndex].isChCharacter.Length)

配列の範囲外アクセスを防ぐ ためのチェックになります。
currentDialogueIndex と currentSentenceIndex が、それぞれ dialogueEvents と isChCharacter の範囲内であることを確認します。
※範囲外だとエラーが発生するため

dialogueEvents[currentDialogueIndex].isChCharacter[currentSentenceIndex];
Sprite charSprite = dialogueEvents[currentDialogueIndex].characterSprites[currentSentenceIndex];

isLeft会話中のキャラが左(主人公)か右(敵)かを判定
charSprite は、今のセリフに対応するキャラ画像を取得

if (isLeft)
{
ChCharacterImage.sprite = charSprite;
ChCharacterImage.color = new Color(1f, 1f, 1f, 1f); // 主人公を明るく
EnCharacterImage.color = new Color(1f, 1f, 1f, 0.5f); // 敵を薄く
}

会話中のキャラが左(主人公)の場合

ChCharacterImage.sprite = charSprite; → 主人公の画像を変更
ChCharacterImage.color = new Color(1f, 1f, 1f, 1f); → 主人公を 不透明(通常の明るさ)
EnCharacterImage.color = new Color(1f, 1f, 1f, 0.5f); → 敵を 半透明(薄くする)

else
{
    EnCharacterImage.sprite = charSprite;
    EnCharacterImage.color = new Color(1f, 1f, 1f, 1f); // 敵を明るく
    ChCharacterImage.color = new Color(1f, 1f, 1f, 0.5f); // 主人公を薄く
}

会話中のキャラが右(敵)の場合

EnCharacterImage.sprite = charSprite; → 敵の画像を変更
EnCharacterImage.color = new Color(1f, 1f, 1f, 1f); → 敵を 不透明(明るくする)
ChCharacterImage.color = new Color(1f, 1f, 1f, 0.5f); → 主人公を 半透明(薄くする)


    void CloseDialogue()
    {
        dialogueUI.SetActive(false);
        startButton.gameObject.SetActive(true);
    }

最後に、このメソッドでは会話終了後にダイアログUIを閉じて、会話を始めるボタンを表示させています。

えきふる
えきふる

以上で説明は終わりです!(長いねぇ・・・)

終わりに

今回はシンプルな会話システムを作ってみました。

この記事ではわかり易くボタンで会話を始める/送るを作りましたが、
スマホなら何かのイベントで会話が始まり、タップで次に進む、
最後の会話が終わったら次のシーンに進む、などちょっとしたアレンジで色々作れそうですね。

TMPの機能をうまく使えば色をつけた文字やフォントを変えた文字なんかも実装できそうです。

ゲームによってはちょっとしか会話が必要無いものや、短期間で作成するためにヘヴィなダイアログシステムを学習している暇がない、そんな時に役立つヒントになったら良いなと思います。

えきふる
えきふる

まあ私はアドベンチャーを作ってるから、有料のアセット使ってるんだけどね!ガハハ!

当ブログ「えきふるゲームラボ」では出来るだけ分かり易く読みやすい記事の作成を目指しています!
もし読んでくださった方の中で
「ここが良く分からなかった」「ここをもう少し掘り下げて欲しい」等ありましたら
ぜひコメントで教えて下さい!

えきふる
えきふる

コメント貰えると元気も出ますので、どうぞお気軽にお願いしますふる!

※このブログは、UnityTechnologiesまたはその関連会社が後援または提携しているものではありません。「Unity」は、UnityTechnologiesまたはその関連会社の米国およびその他の国における商標または登録商標です。

コメント

タイトルとURLをコピーしました