見出し画像

【Unity】脱出ゲームを作ってみる⑥【会話イベント・画像表示・CSVの文字化け対応】

【脱出ゲーム】美術館に閉じ込められちゃった!がようやく配信できました!是非遊んでください!

GooglePlay

AppStore

今回は会話シーンの実装とCSVの文字化けの対応についてです。


1.会話シーンの実装

(1)セーブデータの有無で遷移シーンを変更する

今作ではタイトル画面で、「始める」ボタンをタップしたときに、セーブデータがある状態だとMainシーンに、ない状態だとOpningシーンに遷移します。

public void SceneChange() {
    if (!_dontSeBarrage) {
        //もしセーブデータがあれば
        if (ES3.KeyExists("SAVEDATA")){
            //セーブデータがなければ
            TriggerDelayGoToScene();
        } else
        {
            scene = "Opening";
            TriggerDelayGoToScene();
        }
    }
}

Opningシーンでは会話イベントがあり、会話イベントが終わった後にマニュアルを表示される、初回にマニュアルを強制的に一度見せる仕様となっています。

(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)にアタッチします。

TalkManagerをヒエラルキーに配置してください。

後は、会話リストを追加してください。
今回は話者名は使用しなかったのですが、エラーになりそうなので、スペースを入れています。

どんどん追加していけます。

以下参考記事

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);
            }
        }

続きます。

この記事が気に入ったらサポートをしてみませんか?