*本記事はChatGPTを使って生成しています(表現が冗長・おかしいかもしれません)
やあ、こんにちわ。
ikigami.devチームです。
チームの期待の新星 22世紀最大の VibeCoderの私、山田 でよ。
今日からなんと 毎週ゲーム開発記事を出す企画 が始まりました 🥶
(この記事ちゃんと完走できるのか…?)
本稿では 「JSONで表情つきダイアログを回す仕組み」 を
最小3スクリプト + 1 JSON で実装していきます。まずは動作イメージをどうぞ👇
🛠️ 環境 & 事前準備
項目 | バージョン例 |
---|---|
Unity | 2022.3 LTS |
TextMeshPro | デフォルト |
- 新規プロジェクト (URP/Built-in どちらでも可)
- フォルダを 2 つ作成
-
Assets/StreamingAssets/Dialogue/
… ここに JSON -
Assets/Resources/Characters/
…… ここに表情スプライト
-
-
Input System
でInteract
アクション (例:E
キー) を用意
📂 フォルダ構成
Assets/
├- Resources/
│ └- Characters/
│ └- bio/
│ ├- default.png
│ └- angry.png
├- StreamingAssets/
│ └- Dialogue/
│ └- bio.json
└- Scripts/
├- Dialogue/
│ ├- DialogueManager.cs
│ └- TalkScript.cs
├- GameData.cs
└- NPC/
└- SampleNPC.cs
命名ルール
Resources/Characters/{speakerid}/{expression}.png
… この形でキャラのidと表情を管理してます。
System Overview
🔑 JSON で会話を記述する
bio.json
{
"npc": "BIO",
"situations": [
{
"key": "Sample",
"lines": [
{
"speakerid": "bio",
"displayname": "BIO",
"expression": "default",
"text": [
"なんだね君は...?",
"さっさとどこかに消えろ。",
"まったくやかましいな。",
"私はこのゲームの中ボスだ!",
"そうそう、またあとで出会うだろう"
]
}
]
}
]
}
フィールド | 意味 |
---|---|
npc |
登場キャラ (= JSON 1 枚につき 1 キャラ想定) |
situations[] |
シーンやクエストごとのまとまり |
lines[] |
1 発話単位で「表情 + 文章配列」を管理 |
speakerid |
どのキャラの立ち絵を使うかキメるよう |
expression |
speakeridのキャラの表情を指定するid |
🎤 TalkScript ― 文字を 1 字ずつ表示 + スキップ
フルコード
TalkScript.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
public class TalkScript : MonoBehaviour
{
TMP_Text textComp;
Image image;
public AudioClip sfx;
public float printRate = 7.5f;
[SerializeField] bool skipPrint = false;
public bool isActive;
public Coroutine currentTextSequence = null;
void Start()
{
textComp = transform.Find("TextBox").Find("Text").GetComponent<TMP_Text>();
image = transform.Find("IconBox").Find("Icon").GetComponent<Image>();
Hide();
}
void Update()
{
if (Input.GetButtonDown("Interact"))
{
skipPrint = true;
}
}
public void Show()
{
foreach (Transform t in transform)
t.gameObject.SetActive(true);
}
public void Hide()
{
foreach (Transform t in transform)
t.gameObject.SetActive(false);
textComp.text = null;
isActive = false;
}
// JSON 行ごとの処理
public IEnumerator ShowLine(LineData line)
{
if (image != null && !string.IsNullOrEmpty(line.speakerid) && !string.IsNullOrEmpty(line.expression))
{
var sprite = Resources.Load<Sprite>($"Characters/{line.speakerid}/{line.expression}");
if (sprite != null)
image.sprite = sprite;
else
Debug.LogError($"Sprite not found: Characters/{line.speakerid}/{line.expression}");
}
Show();
isActive = true;
foreach (string t in line.text)
{
skipPrint = false;
Coroutine c = StartCoroutine(PrintChar(t));
yield return c;
skipPrint = false;
yield return new WaitUntil(() => skipPrint); // Wait until Interacted.
}
Hide();
}
IEnumerator PrintChar(string text)
{
textComp.text = "";
foreach (char c in text)
{
if (skipPrint) { textComp.text = text; break; }
if (sfx != null)
GetComponent<AudioSource>().PlayOneShot(sfx);
textComp.text += c;
yield return new WaitForSeconds(1 / printRate);
}
}
}
解説
目的 | 仕組み |
---|---|
スキップ判定 |
skipPrint を Update() で E キー検知して立てる |
文字送り |
PrintChar() で WaitForSeconds(1/printRate)
|
立ち絵差し替え |
Resources.Load<Sprite>() で都度ロード(表情ごとフォルダ分け) |
UI の一括表示/非表示 |
Show()/Hide() で子オブジェクトをループ操作(レイアウトの自由度◎) |
外部判定フラグ |
isActive を GameData が監視し、会話中の入力ロックに利用 |
🧬 GameData ― シングルトンで全システムを仲介
ダイアログ関連部のみ抜粋 & 解説
GameData.cs (talk 部分)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System.IO;
[System.Serializable] // Required for JsonUtility
public class DialogueData
{
public string npc;
public SituationData[] situations;
}
[System.Serializable] // Required for JsonUtility
public class SituationData
{
public string key;
public LineData[] lines;
}
[System.Serializable] // Required for JsonUtility
public class LineData
{
public string speakerid;
public string displayname;
public string expression;
public string[] text;
}
public class GameData : MonoBehaviour
{
public static GameData Instance { get; private set; }
public GameObject canvasPrefab;
TalkScript _talkScript;
GameObject _canvas;
void Awake()
{
// If there is an instance, and it's not me, delete myself.
if (Instance != null && Instance != this)
{
Destroy(this.gameObject);
}
else
{
Instance = this;
}
DontDestroyOnLoad(gameObject);
// assignment
_canvas = Instantiate(canvasPrefab, transform);
_talkScript = _canvas.GetComponentInChildren<TalkScript>();
}
// From JSON
public void StartDialogue(string npcKey, string situationKey)
{
if (!IsTalking())
{
StartCoroutine(StartDialogueCoroutine(npcKey, situationKey));
}
}
public DialogueData LoadDialogue(string npcKey)
{
if (dialogueCache.ContainsKey(npcKey))
return dialogueCache[npcKey];
string path = Path.Combine(Application.streamingAssetsPath, "Dialogue", npcKey.ToLower() + ".json");
if (!File.Exists(path))
{
Debug.LogError($"Dialogue JSON not found: {path}");
return null;
}
string json = File.ReadAllText(path);
DialogueData data = JsonUtility.FromJson<DialogueData>(json);
dialogueCache[npcKey] = data;
return data;
}
public IEnumerator StartDialogueCoroutine(string npcKey, string situationKey)
{
DialogueData data = LoadDialogue(npcKey);
if (data == null)
yield break;
SituationData situation = null;
foreach (var s in data.situations)
{
if (s.key == situationKey)
{
situation = s;
break;
}
}
if (situation == null)
{
Debug.LogError($"Situation '{situationKey}' not found for NPC '{npcKey}'");
yield break;
}
foreach (var line in situation.lines)
{
yield return _talkScript.ShowLine(line);
}
}
ポイント
役割 | 説明 |
---|---|
中央ハブ | HP・ミッション等と同じ Singleton にまとめることで どこからでも 会話開始できる |
重複防止 |
IsTalking() で多重起動を防ぎ、入力ロック処理と連携しやすくする |
Canvas プレハブ | UI をシーンに置かず動的生成 → シーン遷移で TalkScript が消えない |
キャッシュ | 1 度読み込んだ JSON は Dictionary に保持して GC & IO 削減。 |
StreamingAssets | ビルド後も外部 JSON の差し替えが可能。 |
コルーチン |
TalkScript.ShowLine() を逐次実行し “セリフが終わるまで次へ進まない” 挙動を保証。 |
🤖 SampleNPC ― 会話のトリガーは一行で充分
SampleNPC.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SampleNPC : NPC
{
public override void Interact()
{
GameData.Instance.StartDialogue("bio", "Sample");
}
}
解説
-
目的別 JSON 呼び出し …
"bio"
は NPC、"Sample"
はシチュエーション。 -
汎用化 … テキストを喋らせたい場合は
dialogue
変数を直接Talk(text)
するだけで再利用可。
🎨 表情スプライトを増やすときは…
ファイルパス | 用途 |
---|---|
Resources/Characters/bio/default.png |
通常 |
Resources/Characters/bio/angry.png |
怒り |
Resources/Characters/bio/surprised.png (例) |
驚き |
- 画像を
png
で追加 - JSON の
expression
を新しい名前に変更 - コード変更なしで完了!
✨ 今後の拡張アイデア
機能 | 実装ヒント |
---|---|
選択肢付き会話 |
LineData に choices[] を追加 |
立ち絵左右表示 |
speakerPosition を JSON に追加 |
セリフごとボイス |
voiceClipPath を追加 → AudioSource
|
📝 まとめ
- JSON × Resources でシナリオ班だけでも会話をゴリゴリ更新可能
- 必要ファイルは 3 スクリプト & 1 JSON のみ
- 表情追加・セリフ変更 も ノーコード でサクサク
📡 次回予告
「超簡単なUnityロードスクリーンを解説してみた!」
毎週更新予定なので “LGTM 👍” と 🦆 フォローで応援おねがいします!
もし少しでも役にたったと感じたら、僕たちが開発しているゲームを見てみてください!
↓↓↓開発中のゲームの最新情報は以下から↓↓↓