バーチャル3Dクリエイター神部まゆみです(*^_^*)
この記事はUnityで会話システムを作ってみた記事の続きです。
前回の記事はこちら。
動作確認した最新のバージョンはUnity6000.0.32です。
やりたいこと:ScriptableObjectでセリフデータと一緒に画像も指定できるようにする
現状だと↓みたいにテキストを指定するだけ、画像は初期設定のまま変わらないので別の村人に話しかけても画像が同じになってしまう。


ここでプレイヤーと村人の画像を指定できるようにしたい。
喜怒哀楽の表情を登録しておいて切り替えられるようにしようかと思ったが…
事前に登録しておく方式のほうがラクかと思ったけど、登録や削除する機能も作らなければならないので面倒だからやめておきました(^_^;)
面倒ではあるけどセリフごとに画像を設定する方式にした。
まぁそんなに長文にならなければこの方式でも大丈夫だろう…。面倒なぶん汎用性は高いし。
実際に実装してみる
ChatGPT氏に聞きながらやって実装できたので以下に手順を書いておきます。
DialogueEntry.csを書き換える
これはオブジェクトにアタッチせず作っておくだけで良いやつ。
using UnityEngine;
[System.Serializable]
public class DialogueEntry
{
public string sentence; // セリフの文章
public bool isVillagerSpeaking; // 村人が話すなら true、プレイヤーなら false
public Sprite villagerImage; // 村人の画像
public Sprite playerImage; // プレイヤーの画像
public DialogueEntry(string sentence, bool isVillagerSpeaking, Sprite villagerImage = null, Sprite playerImage = null)
{
this.sentence = sentence;
this.isVillagerSpeaking = isVillagerSpeaking;
this.villagerImage = villagerImage;
this.playerImage = playerImage;
}
}
DialogueData.cs を書き換える
これもオブジェクトにアタッチせず作っておくだけで良いやつ。
using UnityEngine;
using System.Collections.Generic;
[CreateAssetMenu(fileName = "NewDialogueData", menuName = "Dialogue/DialogueData")]
public class DialogueData : ScriptableObject
{
public List<DialogueEntry> sentences; // セリフデータ
// 村人とプレイヤーの画像を設定する配列
public Sprite[] villagerImages; // 村人の画像
public Sprite[] playerImages; // プレイヤーの画像
}
DialogueManager.csを書き換える
これは村人にアタッチするやつ。
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
public class DialogueManager : MonoBehaviour
{
public Canvas dialogueCanvas; // 会話用のキャンバス
public Image villagerImage; // 村人の画像フィールド
public Image playerImage; // プレイヤーの画像フィールド
public Text villagerText; // 村人のテキストフィールド
public Text playerText; // プレイヤーのテキストフィールド
private Queue<DialogueEntry> sentences; // セリフをキューで管理
private bool isTalking = false;
void Start()
{
sentences = new Queue<DialogueEntry>();
dialogueCanvas.gameObject.SetActive(false); // 最初は非表示
}
public void StartDialogue(DialogueData dialogueData)
{
if (isTalking) return;
isTalking = true;
Time.timeScale = 0; // ゲームを一時停止
sentences.Clear();
foreach (var entry in dialogueData.sentences)
{
sentences.Enqueue(entry);
}
dialogueCanvas.gameObject.SetActive(true);
DisplayNextSentence();
}
public void DisplayNextSentence()
{
if (sentences.Count == 0)
{
EndDialogue();
return;
}
DialogueEntry entry = sentences.Dequeue();
if (entry.isVillagerSpeaking)
{
villagerText.text = entry.sentence;
playerText.text = ""; // プレイヤーのテキストを非表示
villagerImage.sprite = entry.villagerImage; // 村人の画像を更新
// ★修正★ 村人が話している間でもプレイヤーの画像を更新する
if (entry.playerImage != null)
{
playerImage.sprite = entry.playerImage;
}
}
else
{
playerText.text = entry.sentence;
villagerText.text = ""; // 村人のテキストを非表示
playerImage.sprite = entry.playerImage; // プレイヤーの画像を更新
// ★修正★ プレイヤーが話している間でも村人の画像を更新する
if (entry.villagerImage != null)
{
villagerImage.sprite = entry.villagerImage;
}
}
}
public void EndDialogue()
{
dialogueCanvas.gameObject.SetActive(false);
Time.timeScale = 1; // ゲーム再開
isTalking = false;
}
void Update()
{
if (isTalking && Input.GetKeyDown(KeyCode.Return)) // Enterキーで次の文章へ
{
DisplayNextSentence();
}
}
}
NPCInteraction.CSはそのまま
これは前回と変わってないけど、一応コードを載せておく。
村人にアタッチします。
using UnityEngine;
using UnityEngine.UI;
public class NPCInteraction : MonoBehaviour
{
public GameObject talkUI; // 「話す(T)」のUIオブジェクト
public DialogueData dialogueData; // そのNPC専用のセリフデータ
private DialogueManager dialogueManager;
private bool canTalk = false; // 会話可能状態のフラグ
void Start()
{
dialogueManager = FindObjectOfType<DialogueManager>(); // DialogueManager を見つけて取得
}
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player")) // プレイヤーか確認
{
talkUI.SetActive(true);
canTalk = true;
}
}
void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player"))
{
talkUI.SetActive(false);
canTalk = false;
}
}
void Update()
{
if (canTalk && Input.GetKeyDown(KeyCode.T))
{
if (dialogueManager != null && dialogueData != null)
{
dialogueManager.StartDialogue(dialogueData); // ここでエラーが発生しないように修正!
}
else
{
Debug.LogWarning("DialogueManager または DialogueData が設定されていません!");
}
}
}
}
スクリプトができたら村人にアタッチする
DialogueManager.csとNPCInteraction.csを村人にアタッチして、必要なUIなどオブジェクトを指定します。
↓こんな感じで指定する。


↓UIはデフォルトではインスペクターオフで非表示にしておく。


↓UIはこんな感じね。親のキャンバスがあって、画像二つ、テキスト二つ、パネルが二つ。
プレイヤーが下、村人を上に表示することを想定してあります。


これで動いた!
画像は適当に指定したやつですがちゃんと動きました!
画像を用意してみる
ちゃんと画像を用意してやってみます。
VeryAnimationで適当にポーズ取らせて作りました。




ポーズとらせてスクショ撮ってGIMPで透過、スプライトに設定しただけです。
そのあたりについては前回の記事を参照。
これ画像指定する時に探すのめんどくさいから、ファイル名の先頭はわかりやすくしておいたほうが良さそう。
これで良い感じになった!
これでちゃんと動いた!
別の村人を増やしてみる
人口一名の村というのも寂しいので村人を増やしてみます。
これまでのおさらいみたいな感じなので、最初の記事から見た方が良いかも。↓のあたりね。
別のHumanoidを配置して、
最初の村人と同じアニメーターコントローラーを設定、


Navmesh AgentとNPCWander.cs、NPCAnimation.csを追加、この時点でウロウロはできるようになる。




TalkUIをコピペして位置合わせ、コライダーもコピペする。
NPCInteraction.csとDialogueManager.csもコピペする。
↓ではやってないけど、NPCInteraction.csのTalkUIの参照も書き換えてください。コピペしただけだと前の村人のやつになってるので。
DialogueManagerで指定するUIは使いまわせるのでそのままでOK。
あとは新しいダイアログを作って設定すればOK。
作ったダイアログはNPCInteraction.csに割り当ててください。TalkUIの参照も新しいやつにする。


これで動いた!
おお、村っぽくなってきたかな?( *´艸`)
毎回手動でコンポーネントを追加するのはめんどくさいから、ChatGPTに聞いて一つスクリプトを追加すれば自動で追加できるような試みをやってみるか…。
他のアセットだと、一つコンポーネントを追加すると自動でいくつか追加される形式のがあるから多分できるような気がする。
必須コンポーネントを自動で追加するスクリプトを書いてみた VillagerStarter.cs
既にスクリプトを用意しておく必要があるけど、今回作った会話システムに必要なコンポーネントを自動で追加してくれるスクリプトをChatGPTに書いてもらった。


VillagerStarterをコンポーネントに追加するだけで、
Animator
NavMeshAgent
NPCWander ※以前の記事を参照
NPCAnimation ※以前の記事を参照
CapsuleCollider
NPCInteraction
DialogueManager
の7つのコンポーネントを勝手に追加してくれる。重複チェックもしているので大丈夫なはず。
太字はこのブログの記事で書いたスクリプトなので別途コピペして用意する必要アリ。たぶんスクリプト名が同じなら中身が違っても追加されてしまうけど、まぁそんなことはめったにないかな(^_^;)
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NPCInteraction))] // 基本的に必要なものはここで確保
[RequireComponent(typeof(DialogueManager))]
public class VillagerStarter : MonoBehaviour
{
void Reset()
{
AddMissingComponent<Animator>();
AddMissingComponent<NavMeshAgent>();
AddMissingComponent<NPCWander>();
AddMissingComponent<NPCAnimation>();
AddMissingComponent<CapsuleCollider>();
AddMissingComponent<NPCInteraction>();
AddMissingComponent<DialogueManager>();
SetupCollider(); // カプセルコライダーの設定
}
// 必要なコンポーネントがなければ追加する
private void AddMissingComponent<T>() where T : Component
{
if (GetComponent<T>() == null)
{
gameObject.AddComponent<T>();
Debug.Log($"[{gameObject.name}] に {typeof(T).Name} を追加しました。");
}
}
// カプセルコライダーの初期設定
private void SetupCollider()
{
CapsuleCollider collider = GetComponent<CapsuleCollider>();
if (collider != null)
{
collider.center = new Vector3(0, 1, 0);
collider.radius = 0.5f;
collider.height = 2f;
Debug.Log($"[{gameObject.name}] の CapsuleCollider を設定しました。");
}
}
}
追加し終わったらVillagerStarterコンポーネントは削除して大丈夫です。
UIなどの参照は何もないので指定する必要はありますが。


おわりに
案外簡単にできて良かった。
まぁChatGPT氏のおかげだけども。
会話システムはだいたいできたからひとまず良いかな。
次は洞窟でも作って、シーン遷移するような感じのものでも作ってみるか…。
まぁもう少しいじってみます(*^_^*)