0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jsonで複数キャラの表情制御が可能なダイアログを作る【Unityでゲーム制作①】

Last updated at Posted at 2025-04-28

*本記事はChatGPTを使って生成しています(表現が冗長・おかしいかもしれません)

やあ、こんにちわ。
ikigami.devチームです。
チームの期待の新星 22世紀最大の VibeCoderの私、山田 でよ。

今日からなんと 毎週ゲーム開発記事を出す企画 が始まりました 🥶
(この記事ちゃんと完走できるのか…?)

本稿では 「JSONで表情つきダイアログを回す仕組み」
最小3スクリプト + 1 JSON で実装していきます。まずは動作イメージをどうぞ👇

Screen Recording 2025-04-28 at 13.28.55.gif

🛠️ 環境 & 事前準備

項目 バージョン例
Unity 2022.3 LTS
TextMeshPro デフォルト
  1. 新規プロジェクト (URP/Built-in どちらでも可)
  2. フォルダを 2 つ作成
    • Assets/StreamingAssets/Dialogue/ … ここに JSON
    • Assets/Resources/Characters/ …… ここに表情スプライト
  3. Input SystemInteract アクション (例: 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);
        }
    }
}

解説

目的 仕組み
スキップ判定 skipPrintUpdate() で 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 (例) 驚き
  1. 画像を png で追加
  2. JSON の expression を新しい名前に変更
  3. コード変更なしで完了!

✨ 今後の拡張アイデア

機能 実装ヒント
選択肢付き会話 LineDatachoices[] を追加
立ち絵左右表示 speakerPosition を JSON に追加
セリフごとボイス voiceClipPath を追加 → AudioSource

📝 まとめ

  • JSON × Resources でシナリオ班だけでも会話をゴリゴリ更新可能
  • 必要ファイルは 3 スクリプト & 1 JSON のみ
  • 表情追加・セリフ変更ノーコード でサクサク

📡 次回予告

「超簡単なUnityロードスクリーンを解説してみた!」
毎週更新予定なので “LGTM 👍” と 🦆 フォローで応援おねがいします!

もし少しでも役にたったと感じたら、僕たちが開発しているゲームを見てみてください!
↓↓↓開発中のゲームの最新情報は以下から↓↓↓


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?