【Unity】Unityで会話(ダイアログ)システムを作る:ScriptableObject・タイプライター・選択肢分岐

作成: 2026-07-12最終更新: 2026-07-13

RPGやADVの会話ウィンドウを、最小構成で作ります。セリフをScriptableObjectで定義し、TextMeshProで1文字ずつ表示するタイプライター演出、クリックで送る仕組み、選択肢による分岐まで、データとUIを分離した会話エンジンの作り方を解説します。

RPGの村人との会話、ADVのキャラクターの語り、チュートリアルの説明——ゲームで「文章を見せる」場面は驚くほど多いものです。ところがUnityで会話ウィンドウを作ろうとすると、テキスト表示・送り・演出・分岐がバラバラに絡まって、意外と手が止まります。実は、 データとUIを分離 して考えると、会話システムはとてもすっきり作れます。

この記事では、最小構成の会話エンジンを1本組み立てます。セリフを ScriptableObject で定義し、TextMeshPro1文字ずつ表示するタイプライター演出クリックで送る 仕組み、そして 選択肢による分岐 まで。ADVの会話パート、RPGのイベントシーン、ノベルゲームの本文——どれもこの土台から作れます。

会話システムのイメージ。画面下部の会話ウィンドウに、話者名とセリフが1文字ずつタイプライター風に表示され、下に選択肢ボタンが並んでいる様子

この記事でわかること

  • セリフを ScriptableObject で定義する(データとUIの分離)
  • TextMeshPro で1文字ずつ出す タイプライター演出
  • クリックで送る(演出中は全文表示 → 次のセリフ)
  • 選択肢による分岐(ボタン生成 → 次の会話へ)
  • 実践:村人との会話+選択肢で反応が変わる

動作確認環境: Unity 2022.3 LTS / Unity 6

Sponsored

データとUIを分離する

会話システムでいちばん大事な考え方は、 「何を喋るか(データ)」と「どう見せるか(UI)」を分ける ことです。ここがごちゃ混ぜになると、セリフを1つ直すたびにコードをいじる羽目になります。

会話システムの構成図。左に会話データ(ScriptableObject:話者名とセリフのリスト)、右�に会話UI(ウィンドウ・話者名テキスト・本文テキスト・選択肢ボタン)。中央のDialogueManagerがデータを読んでUIに流し込む3層構造を示す

構成は3つの部品に分かれます。

部品役割
会話データ(ScriptableObject)話者名とセリフのリスト。「何を喋るか」だけを持つ
会話UI(Canvas)ウィンドウ・話者名・本文・選択肢ボタン。「どう見せるか」だけを持つ
DialogueManager(スクリプト)データを読み、UIに1行ずつ流し込む「進行役」

こう分けておくと、シナリオを書く人は ScriptableObjectのInspectorでセリフを編集するだけ で済み、コードを触る必要がありません。UIの見た目を変えたいときも、データはそのまま。この分離が、あとで会話を増やすときに効いてきます。

セリフをScriptableObjectで定義する

まず「何を喋るか」を ScriptableObject で定義します。1つの会話(一連のやり取り)を1つのアセットにするイメージです。

ScriptableObjectの会話データの図。Dialoguedata(会話アセット)の中に、複数のLine(話者名+セリフ本文)が順番に並んでいる。Inspectorでセリフを追加・編集できる様子を示す
using UnityEngine;

// 1行のセリフ
[System.Serializable]
public class DialogueLine
{
    public string speaker;           // 話者名
    [TextArea] public string text;   // セリフ本文
    public DialogueChoice[] choices; // 選択肢(無ければ空のまま。後述の分岐で使う)
}

// 会話1本ぶんのデータ(Create > Dialogue で作れる)
[CreateAssetMenu(fileName = "NewDialogue", menuName = "Dialogue/Dialogue Data")]
public class DialogueData : ScriptableObject
{
    public DialogueLine[] lines;   // セリフを順に並べる
}

[CreateAssetMenu] を付けると、Project ビューの Create > Dialogue > Dialogue Data から会話アセットを作れます。あとはInspectorで lines にセリフを追加していくだけ。「村人A」「こんにちは、旅人さん。」といった内容を、コードを触らずに並べられます。会話を増やしたいときは、このアセットを複製するだけで済みます。

タイプライター演出とクリック送り

セリフを一気にドンと出すより、 1文字ずつ「カタカタ」と表示 されると、ぐっとゲームらしくなります。これはTextMeshProの maxVisibleCharacters で実現します。

タイプライター演出とクリック送りの図。全文をあらかじめセットしておき、表示文字数(maxVisibleCharacters)をコルーチンで少しずつ増やす。演出中にクリックすると全文即表示、完了後にクリックすると次のセリフへ進む2段�階の流れを示す

仕組みはこうです。 本文を全部 text に入れておき、maxVisibleCharacters を0から少しずつ増やす と、表示される文字数だけが増えていき、タイプライター風になります。

using System.Collections;
using TMPro;
using UnityEngine;

public class TypewriterText : MonoBehaviour
{
    [SerializeField] private TMP_Text bodyText;
    [SerializeField] private float charsPerSecond = 30f;

    private Coroutine typeRoutine; // 自分が開始したコルーチンを覚えておく
    private bool isTyping;

    // 外から呼ぶのはこれ。開始も停止も、このコンポーネントの中で完結させる
    public void Play(string line)
    {
        if (typeRoutine != null) StopCoroutine(typeRoutine); // 前の演出が残っていたら止める
        typeRoutine = StartCoroutine(TypeLine(line));
    }

    private IEnumerator TypeLine(string line)
    {
        bodyText.text = line;                 // 全文をセット(まだ見えない)
        bodyText.maxVisibleCharacters = 0;
        isTyping = true;

        int total = line.Length;
        for (int i = 0; i <= total; i++)
        {
            bodyText.maxVisibleCharacters = i; // 表示文字数を1ずつ増やす
            yield return new WaitForSeconds(1f / charsPerSecond);
        }
        isTyping = false;
    }

    // クリック時:演出中なら全文表示、完了後なら次へ進めたい合図を返す
    public bool SkipOrAdvance()
    {
        if (isTyping)
        {
            StopCoroutine(typeRoutine); // 自分で開始したコルーチンだから確実に止められる
            bodyText.maxVisibleCharacters = bodyText.text.Length; // 全文即表示
            isTyping = false;
            return false;  // まだ次へは行かない
        }
        return true;       // 次のセリフへ進んでOK
    }
}

注意(定番の落とし穴): StopCoroutineStopAllCoroutinesは、 そのコンポーネント自身がStartCoroutineしたコルーチンにしか効きません 。もし進行役のDialogueManager側でStartCoroutine(typewriter.TypeLine(...))と書いてしまうと、コルーチンはDialogueManagerの上で動くため、Typewriter側からは止められず、 スキップしたのに文字送りが続く バグになります。上のコードのように「開始(Play)も停止も同じコンポーネントの中で完結させる」のがコツです。

ポイントは クリックの2段階 です。 演出の途中でクリックされたら、まず全文を即座に表示 する(せっかちなプレイヤーへの配慮)。 表示が終わった後にクリックされたら、次のセリフへ進む。この2段階にすると、「待たされる」ストレスがなくなり、会話のテンポが一気に良くなります。

Sponsored

選択肢による分岐

会話の醍醐味は 選択肢 です。「はい/いいえ」でストーリーが変わる、あの仕組みを作ります。

選択肢による分岐の図。あるセリフの後に選択肢が現れ、「受け��る」を選ぶとクエスト開始の会話へ、「断る」を選ぶと別れの会話へと、それぞれ次の会話データが分岐していく様子を示す

考え方はシンプルです。 セリフデータに「選択肢」と「各選択肢が進む先の会話」を持たせる。選択肢が来たらボタンを生成し、押されたボタンに対応する次の会話へ切り替えます。

[System.Serializable]
public class DialogueChoice
{
    public string choiceText;       // 「クエストを受ける」など
    public DialogueData nextDialogue; // 選んだら進む先の会話
}

DialogueLineDialogueChoice[] choices を持たせておき、選択肢のあるセリフに来たら、uGUIボタン を選択肢の数だけ生成します。ボタンを生成して分岐させる部分は、これだけのコードで動きます。

using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class ChoicePanel : MonoBehaviour
{
    [SerializeField] private DialogueManager manager;
    [SerializeField] private Transform buttonParent; // Vertical Layout Group付きの親
    [SerializeField] private Button buttonPrefab;    // TMPテキスト付きのボタンPrefab

    public void Show(DialogueChoice[] choices)
    {
        // 前回の選択肢ボタンを消す
        foreach (Transform child in buttonParent) Destroy(child.gameObject);

        foreach (var choice in choices)
        {
            Button button = Instantiate(buttonPrefab, buttonParent);
            button.GetComponentInChildren<TMP_Text>().text = choice.choiceText;
            button.onClick.AddListener(() =>
            {
                gameObject.SetActive(false);
                manager.StartDialogue(choice.nextDialogue); // 選んだ先の会話へ
            });
        }
        gameObject.SetActive(true);
    }
}

押されたボタンが「対応する nextDialogue の会話を開始する」——会話データ同士を 参照でつないでいく イメージです。複雑な分岐も、この「会話→選択肢→次の会話」の連鎖で表現できます。

分岐が複雑になったら: 会話が数十本に増え、分岐が入り組んできたら、専用のダイアログ管理アセット(Yarn Spinner、Ink、Dialogue Systemなど)の導入を検討しましょう。まずは自作で仕組みを理解し、規模が大きくなったらツールに移るのが王道です。

実践:反応が変わる村人の会話

ここまでを組み合わせて、 話しかけると喋り、選択肢で反応が変わる村人 を作りましょう。RPGのクエスト受注NPC、ADVの攻略キャラ、ノベルゲームの分岐——どれも同じ仕組みです。

実践の完成イメージ。村人に話しかけると会話ウィンドウが開き、タイプライターでセリフが表示される。「クエストを受ける/断る」の選択肢が出て、選んだ方で次のセリフが変わる完成した会話シーンを示す

進行役の DialogueManager が全体を制御します。流れはこうです。

  1. プレイヤーが村人に近づいて決定キー → DialogueManager.StartDialogue(villagerDialogue) を呼ぶ
  2. 会話ウィンドウを開き、lines を先頭から1行ずつ、話者名をセットしてタイプライターで表示
  3. クリックで送り、選択肢のあるセリフに来たらボタンを生成
  4. 選ばれたボタンの nextDialogue に切り替えて続行
  5. 最後のセリフまで来たらウィンドウを閉じる
public class DialogueManager : MonoBehaviour
{
    [SerializeField] private GameObject window;
    [SerializeField] private TMP_Text speakerText;
    [SerializeField] private TypewriterText typewriter;
    [SerializeField] private ChoicePanel choicePanel;

    private DialogueData current;
    private int index;

    public void StartDialogue(DialogueData data)
    {
        current = data;
        index = 0;
        window.SetActive(true);
        ShowLine();
    }

    void ShowLine()
    {
        var line = current.lines[index];
        speakerText.text = line.speaker; // 話者名
        typewriter.Play(line.text);      // タイプ表示はTypewriter側で開始・管理する
    }

    // クリックで呼ぶ
    public void OnClick()
    {
        if (!typewriter.SkipOrAdvance()) return;  // 演出中なら全文表示だけ

        // 表示し終えたセリフに選択肢があれば、送らずに分岐へ
        var line = current.lines[index];
        if (line.choices != null && line.choices.Length > 0)
        {
            choicePanel.Show(line.choices);
            return;
        }

        index++;
        if (index < current.lines.Length) ShowLine();
        else window.SetActive(false);             // 会話終了
    }
}

ポイントは2つです。 「進行はインデックスで管理する」(今何行目かを index で持ち、クリックで進める。この単純さが安定の秘訣)と、 「演出とデータを切り離す」(タイプライターは TypewriterText、データは DialogueData、進行は DialogueManager と役割を分ける。1つのクラスに詰め込まない)。会話中はBGMを少し下げると聞き取りやすくなります——AudioMixerで動的にBGMを制御 のダッキングと組み合わせると、プロっぽい演出になります。

おまけ:先に知っておくと良いこと

  • テキストは必ずTextMeshProで: 通常のTextコンポーネントは日本語や装飾に弱いです。会話は文章量が多いので、TextMeshPro 一択。日本語フォントのアセット作成を忘れずに。
  • リッチテキストで表現力アップ: TextMeshProは <color> <b> <size> などのタグが使えます。「重要な単語だけ色を変える」といった演出が、セリフ本文にタグを埋め込むだけでできます。
  • ボイスやSEと同期: セリフの1文字ごとに小さな「ピッ」というSEを鳴らすと、往年のADV風になります。タイプライターのループ内でSEを鳴らすだけです。
  • オートセーブとの相性: どの会話まで進んだかを セーブ&ロード で記録すれば、「一度聞いた会話はスキップ」なども実装できます。

まとめ

  • 会話システムは データ(ScriptableObject)・UI(Canvas)・進行役(DialogueManager) の3層に分ける
  • セリフは ScriptableObject で定義。シナリオはInspectorで編集でき、コード不要
  • タイプライター演出maxVisibleCharacters をコルーチンで増やす。クリックは「全文表示 → 次へ」の2段階
  • 選択肢分岐 は、セリフに「選択肢→次の会話」を持たせ、ボタンでつなぐ
  • 進行は インデックスで管理。演出・データ・進行を切り離すのが安定のコツ

まずは3〜4行の短い会話を1本、ScriptableObjectで作って表示するところから始めてください。タイプライターで文字が流れ、クリックで送れた瞬間、もう立派な会話エンジンです。あなたのゲームのキャラクターは、プレイヤーに何を語りかけますか?

さらに学ぶために