![見出し画像](https://assets.st-note.com/production/uploads/images/144398204/rectangle_large_type_2_79d7e12dbe1433cc881defd563010890.png?width=800)
【Unity】脱出ゲームを作ってみる⑥【会話イベント・画像表示・CSVの文字化け対応】
【脱出ゲーム】美術館に閉じ込められちゃった!がようやく配信できました!是非遊んでください!
GooglePlay
AppStore
今回は会話シーンの実装とCSVの文字化けの対応についてです。
1.会話シーンの実装
(1)セーブデータの有無で遷移シーンを変更する
今作ではタイトル画面で、「始める」ボタンをタップしたときに、セーブデータがある状態だとMainシーンに、ない状態だとOpningシーンに遷移します。
public void SceneChange() {
if (!_dontSeBarrage) {
//もしセーブデータがあれば
if (ES3.KeyExists("SAVEDATA")){
//セーブデータがなければ
TriggerDelayGoToScene();
} else
{
scene = "Opening";
TriggerDelayGoToScene();
}
}
}
Opningシーンでは会話イベントがあり、会話イベントが終わった後にマニュアルを表示される、初回にマニュアルを強制的に一度見せる仕様となっています。
![](https://assets.st-note.com/img/1718811458813-W8RLvmD6P2.png?width=800)
(2)会話用スクリプトの作成
何回も会話があったり、登場人物が何人もいるのであれば、宴を導入しようと思いました(下記記事を参照)。
ただ、今回は既存のサンプルプロジェクトを改造するという方法かつ6月中には必ずリリースしたかった、さらに会話シーンは初回の1度きりなため、ChatGPTにそれっぽいコードを書いてもらってしのぐことにしました。
以下にコードを載せます。コードはOdin Inspector and Serializerを使用していますので、持ってないけど使いたい方はChatGPT等にぶち込んで修正してもらってください。
Talk.cs
using UnityEngine;
using Sirenix.OdinInspector;
[System.Serializable]
public class Talk
{
[LabelText("会話内容")]
[Tooltip("会話の内容を行ごとに設定します。")]
public string[] lines; // 会話の内容
[LabelText("話者名")]
[Tooltip("話者の名前を設定します。")]
public string speaker; // 話者の名前
[LabelText("話者の画像")]
[Tooltip("話者の画像を設定します。")]
public Sprite speakerImage; // 話者の画像
[LabelText("話者のフレーム")]
[Tooltip("話者を表示するフレームを設定します。")]
public GameObject speakerFrame; // 話者を表示するフレーム
[LabelText("会話用のSE")]
[Tooltip("会話用のSEを設定します。")]
public AudioClip dialogueSE; // 会話用のSE
}
TalkManager
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using Sirenix.OdinInspector;
using GoogleMobileAds.Api;
public class TalkManager : SerializedMonoBehaviour
{
[LabelText("会話UI管理")]
[Tooltip("会話UIを管理するクラスを設定します。")]
[SerializeField] private TalkUI talkUI; // 会話UIを管理するクラス
[LabelText("会話リスト")]
[Tooltip("すべての会話を設定します。")]
[SerializeField] private Talk[] talks; // すべての会話
private int currentTalkIndex = 0; // 現在の会話インデックス
private int currentLineIndex = 0; // 現在の行インデックス
private bool isTyping = false; // タイピング中かどうか
private Coroutine typingCoroutine; // タイピングコルーチン
//次の会話に進むためのボタン
[LabelText("次の会話ボタン")]
[Tooltip("次の会話に進むためのボタンを設定します。")]
[SerializeField] private Button nextButton;
[SerializeField] private GameObject manual;
void Start()
{
AdmobLibrary.RequestBanner(AdSize.Banner, AdPosition.Bottom, false);
SoundManager.i.StopBgm();
StartTalk();
nextButton.onClick.AddListener(OnTalkClicked);
}
// 会話を開始する
public void StartTalk()
{
PlayTalkSE();
if (talks.Length > 0)
{
ShowNextLine();
}
}
// 会話がクリックされたときの処理
public void OnTalkClicked()
{
if (isTyping)
{
// タイピング中の場合は行をすべて表示する
CompleteCurrentLine();
}
else
{
// タイピング中でない場合は次の行を表示する
ShowNextLine();
}
}
// 次の行を表示する
private void ShowNextLine()
{
// 会話が終了している場合は処理を終了する
if (currentLineIndex < talks[currentTalkIndex].lines.Length)
{
// タイピング中の場合はコルーチンを停止してすぐに表示する
typingCoroutine = StartCoroutine(TypeLine(talks[currentTalkIndex].lines[currentLineIndex]));
// 会話UIを更新
talkUI.UpdateUI(talks[currentTalkIndex]);
// 行インデックスを更新
currentLineIndex++;
}
else
{
// 次の会話に進む
currentTalkIndex++;
// 行インデックスをリセット
currentLineIndex = 0;
// 会話が終了している場合は処理を終了する
if (currentTalkIndex < talks.Length)
{
// 会話用のSEを再生
PlayTalkSE();
// 次の行を表示
ShowNextLine();
}
else
{
// 会話終了時の処理
EndTalk();
}
}
}
// 行を1文字ずつ表示する
private IEnumerator TypeLine(string line)
{
isTyping = true;
talkUI.SetTalkText("");
foreach (char letter in line.ToCharArray())
{
talkUI.AppendTalkText(letter.ToString());
yield return new WaitForSeconds(0.05f); // 文字表示のスピード調整
}
isTyping = false;
}
// 現在の行をすべて表示する
private void CompleteCurrentLine()
{
StopCoroutine(typingCoroutine);
talkUI.SetTalkText(talks[currentTalkIndex].lines[currentLineIndex - 1]);
isTyping = false;
}
// 会話用のSEを再生する
private void PlayTalkSE()
{
AudioClip se = talks[currentTalkIndex].dialogueSE;
if (se != null)
{
AudioSource.PlayClipAtPoint(se, Camera.main.transform.position);
}
}
// 会話を終了する
private void EndTalk()
{
// 会話終了時の処理
talkUI.HideTalkUI();
manual.SetActive(true);
}
}
TalkUI.cs
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using Sirenix.OdinInspector;
using GoogleMobileAds.Api;
public class TalkManager : SerializedMonoBehaviour
{
[LabelText("会話UI管理")]
[Tooltip("会話UIを管理するクラスを設定します。")]
[SerializeField] private TalkUI talkUI; // 会話UIを管理するクラス
[LabelText("会話リスト")]
[Tooltip("すべての会話を設定します。")]
[SerializeField] private Talk[] talks; // すべての会話
private int currentTalkIndex = 0; // 現在の会話インデックス
private int currentLineIndex = 0; // 現在の行インデックス
private bool isTyping = false; // タイピング中かどうか
private Coroutine typingCoroutine; // タイピングコルーチン
//次の会話に進むためのボタン
[LabelText("次の会話ボタン")]
[Tooltip("次の会話に進むためのボタンを設定します。")]
[SerializeField] private Button nextButton;
[SerializeField] private GameObject manual;
void Start()
{
AdmobLibrary.RequestBanner(AdSize.Banner, AdPosition.Bottom, false);
SoundManager.i.StopBgm();
StartTalk();
nextButton.onClick.AddListener(OnTalkClicked);
}
// 会話を開始する
public void StartTalk()
{
PlayTalkSE();
if (talks.Length > 0)
{
ShowNextLine();
}
}
// 会話がクリックされたときの処理
public void OnTalkClicked()
{
if (isTyping)
{
// タイピング中の場合は行をすべて表示する
CompleteCurrentLine();
}
else
{
// タイピング中でない場合は次の行を表示する
ShowNextLine();
}
}
// 次の行を表示する
private void ShowNextLine()
{
// 会話が終了している場合は処理を終了する
if (currentLineIndex < talks[currentTalkIndex].lines.Length)
{
// タイピング中の場合はコルーチンを停止してすぐに表示する
typingCoroutine = StartCoroutine(TypeLine(talks[currentTalkIndex].lines[currentLineIndex]));
// 会話UIを更新
talkUI.UpdateUI(talks[currentTalkIndex]);
// 行インデックスを更新
currentLineIndex++;
}
else
{
// 次の会話に進む
currentTalkIndex++;
// 行インデックスをリセット
currentLineIndex = 0;
// 会話が終了している場合は処理を終了する
if (currentTalkIndex < talks.Length)
{
// 会話用のSEを再生
PlayTalkSE();
// 次の行を表示
ShowNextLine();
}
else
{
// 会話終了時の処理
EndTalk();
}
}
}
// 行を1文字ずつ表示する
private IEnumerator TypeLine(string line)
{
isTyping = true;
talkUI.SetTalkText("");
foreach (char letter in line.ToCharArray())
{
talkUI.AppendTalkText(letter.ToString());
yield return new WaitForSeconds(0.05f); // 文字表示のスピード調整
}
isTyping = false;
}
// 現在の行をすべて表示する
private void CompleteCurrentLine()
{
StopCoroutine(typingCoroutine);
talkUI.SetTalkText(talks[currentTalkIndex].lines[currentLineIndex - 1]);
isTyping = false;
}
// 会話用のSEを再生する
private void PlayTalkSE()
{
AudioClip se = talks[currentTalkIndex].dialogueSE;
if (se != null)
{
AudioSource.PlayClipAtPoint(se, Camera.main.transform.position);
}
}
// 会話を終了する
private void EndTalk()
{
// 会話終了時の処理
talkUI.HideTalkUI();
manual.SetActive(true);
}
}
(3)会話スクリプトの使い方
TalkUIをCanvasの子オブジェクト(今回はTalkCanvas)にアタッチします。
![](https://assets.st-note.com/img/1718813349121-cpo80Lt808.png?width=800)
TalkManagerをヒエラルキーに配置してください。
![](https://assets.st-note.com/img/1718813552919-RKdMYQNLLu.png?width=800)
後は、会話リストを追加してください。
今回は話者名は使用しなかったのですが、エラーになりそうなので、スペースを入れています。
![](https://assets.st-note.com/img/1718813618405-HAeNV9fLAx.png)
以下参考記事
2.CSVの文字化け防止
ヒント機能のデータとして、CSVファイルを使用しますが、これが文字化けして困っていました。
以下の記事を参考に.editorconfigファイルをプロジェクト直下に置くことで直りました。
教えてくれた方ありがとうございます。
3.ヒント画像のアスペクト比を保持して表示する
ヒント機能利用時に画像を表示しますが、この画像のアスペクト比がバラバラで、そのまま表示すると画像が歪んでしまいましたので、以下のコードを実装して、元の画像のアスペクト比のまま、表示するようにしました。
// 表示する画像コンポーネント
[SerializeField, Tooltip("表示する画像")]
private Image _image;
///////////////////////中略//////////////////////
// 画像のアスペクト比を計算
float aspectRatio = sprite.bounds.size.x / sprite.bounds.size.y;
// 親オブジェクトのサイズを取得
float containerWidth = _image.rectTransform.rect.width;
float containerHeight = _image.rectTransform.rect.height;
// 画像の表示サイズを計算して設定
if (aspectRatio > 1)
{
// 横長の画像の場合
float width = containerWidth;
float height = width / aspectRatio;
if (height > containerHeight)
{
height = containerHeight;
width = height * aspectRatio;
}
_image.rectTransform.sizeDelta = new Vector2(width, height);
}
else
{
// 縦長の画像の場合
float height = containerHeight;
float width = height * aspectRatio;
if (width > containerWidth)
{
width = containerWidth;
height = width / aspectRatio;
}
_image.rectTransform.sizeDelta = new Vector2(width, height);
}
}
else
{
// 画像が存在しない場合は非表示にする
_image.gameObject.SetActive(false);
}
}
続きます。
この記事が気に入ったらサポートをしてみませんか?