表題の通り。なんとか自分の思っていたものが形になったので、自分用のメモを残します。2D用です
とりあえず動くようになった時点のものを上げてるので、最適化などは度外視です。ごめんね。
仕様
簡単な仕様は以下の通り。
- セリフはCSVで管理する
- 地の文のUIとセリフのUIをCSVで選択できる
- 画面のクリックしたところに合わせてテキストが出る。2文以上テキストがある場合は、画面のどこかをクリックすると2文目が表示される。
- 次の文が存在しないときは、クリックでUIが非表示になる。
- テキストを表示中は他のクリック判定のところをクリックしても現在のテキストの2文目が表示される ( ≒ 1つのグループの文を表示中は、他のグループの文に切り替わらない)
実装メモ
CSVの中身
CSVは以下のような感じです。
GropID,UI ID,LineNomber,Text
1001,1,1,これはグループ1の1文目
1002,2,1,これはグループ2の1文目
1003,2,1,これはグループ3の1文目
1003,1,2,これはグループ3の2文目
- GroupID
- セリフのひとまとめのグループです。このIDが同じセリフ群を一連のまとまりとして扱います
- ステージによって管理を分けたく、ステージ1で使うセリフには頭に10を、ステージ2で使うセリフには頭に20をつけるようにした。だから4桁
- UIID
- どのUIに合わせてセリフを出すかを決める
- 1なら地の文用UI、2ならセリフ用UIみたいな感じ
- 1と2がどっちかどっちかはコード内で決め打ち
- LineNumber
- GroupID内でのセリフの順番
- Text
- セリフ本文
コード内で、1文目スキップの文章を入れていないので、実際にUnityに突っ込んでいるCSVはいきなり1文目から始まっている。
また、そこそこ量があるので、ゲームスタート時にCSVを読み込んで辞書に突っ込むようにしている。
実際の動作イメージ

GroupIDを設定しているオブジェクトがクリックされたら、TextClickSystemにGroupIDを渡し、そのIDに基づいてテキストを表示するという仕組み。
文章を表示中は他のクリック判定部分をクリックしても判定が入らないように、かつ、画面のどこをクリックしても次の文章が表示されるように、セリフのUIの中に画面を覆うサイズのImageを仕込み、そちらにクリック判定を持たせています。

なんか…こんな感じ。
クリック判定は色々試行錯誤した結果、EventTiggerを使っています。
コード全文と簡単な使い方
使い方
- クリックするGameObject側の設定
- クリックしたいGameObjectをシーン上に追加、BoxCollider2Dをアタッチ
- GroupIDHolderをGameObjectにアタッチ。適当なGroupIDを設定
- EventTiggerを追加。Add New EventでPointer Clickを選ぶ
- None(Object)にはそのObject自身を設定する
- FunctionにはGroupIDHolderのOnObjectClickedを選択
- TextClickSystemとUIの設定
- 空のObjectを新しく作り、そこにTextClickSystemをアタッチ。
- そのObjectを親として、子にCanvasを作り必要なUIを突っ込んでいく
- クリック判定を持つImage
- 不透明度は0にしておく
- BoxCollider2Dを追加する
- EventTiggerを追加する。このときNone(Object)にいれるのは、TextClickSystemがアタッチされているObject。Fuctionには「OnImageClicked」を選択
- クリック判定を持つImage
- 追加したUIをTextClickSystemのInspectorでぽいぽい設定していく
- その他の設定
- HierarchyにEventSystemを追加
- MainCameraにPhysics2DRaycasterを追加

全文
試行錯誤してたときのログもそのまま。
public class GroupIDHolder : MonoBehaviour
{
public int GroupID; // GroupIDを整数型として設定
public void OnObjectClicked() // クリック時に呼ばれる
{
Debug.Log("クリックされました");
TextClickSystem.Instance.OnObjectClicked(GroupID);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using System.IO;
public class TextClickSystem : MonoBehaviour
{
public static TextClickSystem Instance { get; private set; }
[System.Serializable]
public class TextData
{
public int GroupID;
public int UIID;
public int LineNumber;
public string Text;
}
// UI要素
public TextMeshProUGUI textTMP;
public GameObject image_text;
public GameObject image_serihu;
public GameObject image;
private int selectedGroupID;
private int currentLineIndex = 0;
private bool isDisplayingText = false;
// CSVデータを保持する辞書
private Dictionary<int, List<TextData>> textDataDictionary = new Dictionary<int, List<TextData>>();
private void Awake()
{
// シングルトンパターンの実装
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject); // 既にインスタンスが存在する場合、重複しないように破棄する
}
}
private void Start()
{
image_text.SetActive(false);
image_serihu.SetActive(false);
textTMP.gameObject.SetActive(false);
image.SetActive(false);
// ゲーム開始時にCSVを読み込む
LoadTextData();
}
// ゲーム開始時にCSVファイルを一度だけ読み込む
private void LoadTextData()
{
string path = Path.Combine(Application.streamingAssetsPath, "text_data.csv");
string[] lines = File.ReadAllLines(path);
foreach (string line in lines)
{
if (string.IsNullOrWhiteSpace(line)) continue;
string[] fields = line.Split(',');
if (fields.Length < 4)
{
Debug.LogError($"無効な行の形式: {line}");
continue;
}
if (int.TryParse(fields[0].Trim(), out int groupID) &&
int.TryParse(fields[1].Trim(), out int uiID) &&
int.TryParse(fields[2].Trim(), out int lineNumber))
{
TextData data = new TextData
{
GroupID = groupID,
UIID = uiID,
LineNumber = lineNumber,
Text = fields[3].Trim('\"')
};
// GroupIDごとにデータを格納
if (!textDataDictionary.ContainsKey(groupID))
{
textDataDictionary[groupID] = new List<TextData>();
}
textDataDictionary[groupID].Add(data);
}
else
{
Debug.LogError($"行の整数を解析できませんでした: {line}");
}
}
// 各GroupIDのリストをLineNumberでソート
foreach (var group in textDataDictionary)
{
group.Value.Sort((a, b) => a.LineNumber.CompareTo(b.LineNumber));
}
}
public void OnImageClicked()
{
Debug.Log("imageがクリックされました");
if (currentLineIndex < textDataDictionary[selectedGroupID].Count - 1)
{
currentLineIndex++;
ShowText();
}
else
{
HideTextUI();
}
}
// GroupIDが渡されたときに、すでに読み込んだデータから選択されたグループのデータを取得
public void OnObjectClicked(int groupID)
{
selectedGroupID = groupID;
Debug.Log("GroupIDをTextClickSystemに受け渡しました!");
// 最初のテキストを表示
currentLineIndex = 0;
ShowText();
}
private void ShowText()
{
if (currentLineIndex < textDataDictionary[selectedGroupID].Count)
{
TextData currentText = textDataDictionary[selectedGroupID][currentLineIndex];
image_serihu.SetActive(currentText.UIID == 1);
image_text.SetActive(currentText.UIID == 2);
image.SetActive(true);
textTMP.text = currentText.Text;
textTMP.gameObject.SetActive(true);
isDisplayingText = true;
}
else
{
HideTextUI();
}
}
private void HideTextUI()
{
image_serihu.SetActive(false);
image_text.SetActive(false);
image.SetActive(false);
textTMP.gameObject.SetActive(false);
isDisplayingText = false;
currentLineIndex = 0;
}
}
動かないときは?
EventSystemをHierarchyに追加しているか
UIを作ったら、勝手に追加されますが、UIのPrefabを追加したときには追加されない。1敗
MainCameraにPhysics2DRaycasterを追加しているか
EventSystemも置いてるし、オブジェクトでEventTiggerも設定しているのに動かないなら、これの可能性が高い。2敗。
他のObjectとの位置関係を確認する
一番前に持ってきたらだいだい反応するでしょ!多分ね!
まとめ
余裕ができたら、もうちょっとコードをきれいにしたい
コメント