対応バージョン:Unity 6.3 LTS(6000.3系)/WebGL出力 この記事はシリーズ全11回の第3回です。第2回ではプロジェクト作成とシステム設計を行いました。今回からいよいよコードを書いていきます。 【PR】本記事にはアフィリエイトリンクを含みます。
前回の記事でフォルダ構成と設計図が固まりました。今回は実装の核心となる「会話システム」と「話題カードシステム」を作ります。
「▶ボタンを押すとセリフが進む」「話題カードを選ぶと対応する会話が始まる」という、このゲームで一番よく使われる部分です。
目次
- STEP1:DialogueSystem(会話テキスト送り)
- 日本語フォントの設定
- STEP2:TopicCardSystem(話題カード選択UI)
- 完成して振り返って
- 次回予告
- あわせて読みたいおすすめ書籍【PR】
- この記事を作成するにあたって参考にしたサイト
STEP1:DialogueSystem(会話テキスト送り)
データ構造を作る
まず1行分のセリフを表すデータ構造を作ります。Scripts/Dialogue/フォルダに「DialogueLine」という名前でMonoBehaviour Scriptを作成し、以下を書いて保存します。
using UnityEngine;
[System.Serializable]
public class DialogueLine
{
public string speakerName;
[TextArea(3, 6)]
public string dialogueText;
public bool isMonologue;
}
次に会話データ全体をまとめるScriptableObjectを作ります。同じフォルダに「DialogueData」という名前でMonoBehaviour Scriptを作成し、クラス宣言のMonoBehaviourをScriptableObjectに書き換えます。
using UnityEngine;
[CreateAssetMenu(fileName = "DialogueData", menuName = "MysteryADV/DialogueData")]
public class DialogueData : ScriptableObject
{
public string sceneTitle;
public DialogueLine[] lines;
[Header("フラグ設定")]
public string setFlagOnComplete;
[Header("証拠設定")]
public EvidenceData grantEvidenceOnComplete;
}
⚠️ 注意点 MonoBehaviour Scriptとして作成したあと、クラス定義の
MonoBehaviourをScriptableObjectに書き換えてください。保存後にUnityがスクリプトを認識し直すと、ProjectウィンドウでCreate→「MysteryADV」→「DialogueData」と選べるようになります。
会話UIを作る
🔧 詳しい手順
- Hierarchyで右クリック→「UI (Canvas)」→「Panel」を選択し、名前を「DialoguePanel」にする
- Canvas ScalerのUI Scale Modeを「Scale With Screen Size」・Reference Resolutionを1920×1080に設定する
- Rect TransformをShift+Altを押しながら下に横いっぱい(Bottom Stretch)に設定し、Heightを250にする
- DialoguePanelの中に「UI (Canvas)」→「Text – TextMeshPro」を2つ作成する
- 1つ目:名前「SpeakerNameText」(話者名・左上・Font Size:24)
- 2つ目:名前「DialogueText」(本文・中央・Font Size:22)
- 「UI (Canvas)」→「Button – TextMeshPro」を作成し名前を「NextButton」にする。ボタン内のText (TMP)を「次→」に変更し、右下に配置する
DialogueManagerを作る
Scripts/Dialogue/フォルダに「DialogueManager」という名前でMonoBehaviour Scriptを作成します。
using UnityEngine;
using TMPro;
public class DialogueManager : MonoBehaviour
{
public static DialogueManager Instance;
[Header("UI参照")]
public GameObject dialoguePanel;
public TMP_Text speakerNameText;
public TMP_Text dialogueText;
private DialogueLine[] lines;
private int currentIndex = 0;
private bool isPlaying = false;
private DialogueData currentData;
private CharacterData currentCharacter;
void Awake()
{
Instance = this;
dialoguePanel.SetActive(false);
}
void Start() { }
public void StartDialogue(DialogueData data, CharacterData character = null)
{
currentData = data;
currentCharacter = character;
lines = data.lines;
currentIndex = 0;
isPlaying = true;
dialoguePanel.SetActive(true);
ShowLine();
}
public void ShowNextLine()
{
if (!isPlaying) return;
currentIndex++;
if (currentIndex >= lines.Length)
{
EndDialogue();
return;
}
ShowLine();
}
void ShowLine()
{
DialogueLine line = lines[currentIndex];
speakerNameText.text = line.isMonologue ? "" : line.speakerName;
dialogueText.text = line.isMonologue
? "(" + line.dialogueText + ")"
: line.dialogueText;
}
void EndDialogue()
{
isPlaying = false;
dialoguePanel.SetActive(false);
if (currentData != null && currentData.setFlagOnComplete != "")
FlagManager.Instance.SetFlag(currentData.setFlagOnComplete);
if (currentData != null && currentData.grantEvidenceOnComplete != null)
EvidenceManager.Instance.CollectEvidence(currentData.grantEvidenceOnComplete);
if (currentCharacter != null)
TopicCardManager.Instance.OpenTopicPanel(currentCharacter);
}
}
設計のポイント
dialoguePanel.SetActive(false)をAwake()に書いているのは、Start()で呼ぶと別スクリプトのStart()と実行順序が競合するためです。Awake()はStart()より必ず先に実行されます。会話が終わったあとにTopicCardManager.Instance.OpenTopicPanel()を呼ぶことで、話題カードパネルが自動で再表示されます。
🔧 詳しい手順(GameManagerへの設置)
- Hierarchyで右クリック→「Create Empty」→名前を「GameManager」にする
- GameManagerにAdd ComponentでDialogueManagerを追加する
- Dialogue Panel・Speaker Name Text・Dialogue Textの各欄に対応するオブジェクトをドラッグ&ドロップして接続する
- NextButtonのOn Click ()にGameManagerをドラッグ&ドロップ→「DialogueManager」→「ShowNextLine」を設定する
日本語フォントの設定
ここが今回の実装で一番時間がかかった部分です。デフォルトの状態だと日本語が「□□□□」という豆腐になってしまいます。
Noto Sans JPをインポートする
- https://fonts.google.com/noto/specimen/Noto+Sans+JP から「Download family」でZIPをダウンロードして解凍する
- Assets/Sprites/Fontsフォルダに「NotoSansJP-Regular.ttf」をコピーしてインポートする
フォントアセットを生成する
- メニューの「Window」→「TextMeshPro」→「Font Asset Creator」を開く
- Source Font Fileに「NotoSansJP-Regular」を設定する
- Character Setを「Unicode Range (Hex)」に変更する
- Character Sequence欄に以下を入力する
0020-007E,2000-206F,3000-9FFF,F900-FAFF,FF00-FFEF
- Atlas Resolutionを8192×8192に設定する
- 「Generate Font Atlas」をクリックして完了を待つ(数分かかります)
- 完了したら「Save」でAssets/Sprites/Fontsフォルダに保存する
⚠️ ハマったポイント①:Character Setの形式エラー 最初「Custom Range」を選択して同じ文字列を入力したところ
FormatExceptionが発生しました。「Unicode Range (Hex)」を選択することで解決しました。
⚠️ ハマったポイント②:Atlasのサイズ不足 4096×4096では「Characters included: 5592/29631」となり日本語の文字が半分以上収まりませんでした。8192×8192に増やすことで解決しました。
⚠️ ハマったポイント③:文字コードSJIS問題 スクリプトをメモ帳で編集・保存したところ文字コードがSJISになり、
CS0101(クラスの二重定義)というコンパイルエラーが出続けました。メモ帳で保存する場合は「名前を付けて保存」→文字コードを「UTF-8」に変更してください。Visual Studioで編集する場合はデフォルトでUTF-8になります。
フォントアセットが生成できたら、SpeakerNameText・DialogueText・NextButton内のText (TMP)それぞれのFont Asset欄に作成したフォントアセットをドラッグ&ドロップして設定します。

STEP2:TopicCardSystem(話題カード選択UI)
データ構造を作る
TopicData.cs(Scripts/Dialogue/フォルダに作成)
using UnityEngine;
[CreateAssetMenu(fileName = "TopicData", menuName = "MysteryADV/TopicData")]
public class TopicData : ScriptableObject
{
public string topicID;
public string topicTitle;
public DialogueData dialogue;
public bool isUnlocked = true;
[Header("フラグ設定")]
public string unlockFlagID;
public string setFlagOnComplete;
}
CharacterData.cs(同フォルダに作成)
using UnityEngine;
[CreateAssetMenu(fileName = "CharacterData", menuName = "MysteryADV/CharacterData")]
public class CharacterData : ScriptableObject
{
public string characterID;
public string characterName;
public TopicData[] topics;
}
話題カードのUIを作る
🔧 詳しい手順
- Hierarchyで右クリック→「UI (Canvas)」→「Panel」を選択し、名前を「TopicPanel」にする(Width:600・Height:500・画面中央)
- TopicPanel内に以下を作成する
- 「Text – TextMeshPro」→名前「TopicPanelTitle」(テキスト:「話題を選んでください」・上部配置)
- 「Panel」→名前「TopicButtonContainer」(Add Component→Vertical Layout Group・Spacing:10・Control Child Size Width:オン)
- 「Button – TextMeshPro」→名前「CloseButton」(テキスト:「閉じる」・下部配置)
- 話題カードのプレハブを作る:「Button – TextMeshPro」をHierarchyに作成し名前を「TopicButton」にする。PrefabsフォルダにドラッグしてプレハブにしたあとHierarchy上のものは削除する
- TopicButtonプレハブにAdd Component→「Layout Element」を追加し、Preferred Height:80・Min Height:60を設定する(Vertical Layout GroupがHeightを潰してしまうのを防ぐため)
- TopicPanelのチェックを外して最初は非表示にする
⚠️ レイアウトのハマりポイント Vertical Layout GroupのControl Child Size(Height)をオンにすると、TopicButtonのHeightが10px程度に潰されてボタンがほぼ見えなくなりました。Layout ElementでPreferred Heightを指定することで解決しました。
TopicCardManagerを作る
Scripts/UI/フォルダに「TopicCardManager」という名前でMonoBehaviour Scriptを作成します。
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class TopicCardManager : MonoBehaviour
{
public static TopicCardManager Instance;
[Header("UI参照")]
public GameObject topicPanel;
public Transform topicButtonContainer;
public GameObject topicButtonPrefab;
public GameObject closeButton;
private CharacterData currentCharacter;
void Awake()
{
Instance = this;
topicPanel.SetActive(false);
}
public void OpenTopicPanel(CharacterData characterData)
{
currentCharacter = characterData;
topicPanel.SetActive(true);
RefreshTopicButtons();
}
public void CloseTopicPanel()
{
topicPanel.SetActive(false);
ClearButtons();
}
void RefreshTopicButtons()
{
ClearButtons();
foreach (TopicData topic in currentCharacter.topics)
{
if (!topic.isUnlocked) continue;
GameObject btn = Instantiate(topicButtonPrefab, topicButtonContainer);
TMP_Text label = btn.GetComponentInChildren<TMP_Text>();
label.text = topic.topicTitle;
TopicData capturedTopic = topic;
btn.GetComponent<Button>().onClick.AddListener(() =>
{
OnTopicSelected(capturedTopic);
});
}
}
void OnTopicSelected(TopicData topic)
{
CloseTopicPanel();
if (topic.dialogue != null)
DialogueManager.Instance.StartDialogue(topic.dialogue, currentCharacter);
}
void ClearButtons()
{
foreach (Transform child in topicButtonContainer)
Destroy(child.gameObject);
}
}
設計のポイント
foreachの中でTopicData capturedTopic = topic;というローカル変数を挟んでいます。これを省くと、すべてのボタンがループの最後のtopicを参照してしまい「どのボタンを押しても同じ話題が開く」という不具合になります(クロージャの罠)。

完成して振り返って
⚠️ ハマったポイント
SetActive(false)はStart()ではなくAwake()に書く。複数スクリプトのStart()が競合する問題を防げるforeachのラムダ式にループ変数を渡すときは必ずローカル変数にコピーしてから渡す- フォントアセットはAtlas Resolution 8192×8192・Unicode Range (Hex)が日本語ゲームの標準設定
- メモ帳でC#スクリプトを保存するときは必ずUTF-8を選ぶ。SJISで保存するとコンパイルエラーの原因になる
- 会話終了後に話題カードパネルを自動で開く設計にしておくとプレイヤーの操作がスムーズになる
次回予告
第4回では、FlagManager(フラグ管理・話題解放)とEvidenceSystem(証拠入手・通知)を実装します。「会話を聞いたら新しい話題が増える」「証拠を入手すると画面に通知が出る」という、ゲームの進行を管理する核心部分です。
あわせて読みたいおすすめ書籍【PR】
今回実装した会話システム・UIレイアウト・TextMeshProの設定についてさらに学びたい方には、以下の書籍もおすすめです。本セクションにはアフィリエイトリンクを含みます。
- 『Unityの教科書 Unity 6完全対応版』(北村愛実 著/SBクリエイティブ) ── Canvas設定・ScriptableObject・TextMeshProの基礎を体系的に学べる
- 『楽しく学ぶ Unity「2Dゲーム」作りのきほん』(森巧尚 著/マイナビ出版) ── UIレイアウト・TextMeshProの使い方を丁寧に解説
- 『作って学ぶゲームプログラミング Unity まるっと入門 Unity6完全対応』(トライタム 著/吉谷幹人 監修) ── ScriptableObjectとUI制御を組み合わせた実践的な実装例を含む
- 『ゲームシナリオの書き方 第2版』(佐々木智広 著/SBクリエイティブ) ── 今回実装した会話フローの設計思想をシナリオ側から学べる一冊
- 『作って学べる Unity本格入門 Unity6対応版』(賀好昭仁 著/技術評論社) ── 今回のシリーズを読み終えてさらにステップアップしたい人向けの中級者向け書籍
※このセクションのリンクはアフィリエイトリンクであり、商品が購入された場合、サイト運営者に紹介料が支払われることがあります【PR】。


コメント