さばのみぞれ煮

本日のおすすめです。
大好物です!

一文字ずつ表示することができるよ

だしまきたまごの続編です!
おまたせしました。おまたせしすぎたかもしれません。

基本はだしまきと同じです
追加のコードを書くことによって、
メッセージの表示スピードを変えることできるよ


using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class StoryManager : MonoBehaviour
{
    [SerializeField] private StoryData[] storyDatas;
    [SerializeField] private Image background;
    [SerializeField] private Image characterImage;
    [SerializeField] private TextMeshProUGUI storyText;
    [SerializeField] private TextMeshProUGUI characterName;

    [Header("===== テキスト演出設定 =====")]
    [SerializeField, Range(0.01f, 0.2f)]
    private float typingSpeed = 0.05f;

    [Header("===== 効果音設定 =====")]
    [SerializeField] private AudioClip typingSE;
    [SerializeField] private AudioClip nextSE;
    [SerializeField, Range(0f, 1f)]
    private float seVolume = 0.7f;

    public int storyIndex { get; private set; } = 0;
    public int textIndex { get; private set; } = 0;

    private bool isTyping = false;
    private string currentFullText = "";
    private Coroutine typingCoroutine = null;
    private AudioSource audioSource;
    private bool storyEnded = false;

    private void Start()
    {
        audioSource = GetComponent<AudioSource>();
        if (audioSource == null)
        {
            audioSource = gameObject.AddComponent<AudioSource>();
            audioSource.playOnAwake = false;
        }

        if (storyDatas == null || storyDatas.Length == 0)
        {
            Debug.LogError("storyDatasが空!");
            return;
        }

        SetStoryElement(storyIndex, textIndex);
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Space))
        {
            if (storyEnded)
            {
                Debug.Log("ストーリーは既に終了しています。");
                return;
            }

            if (isTyping)
            {
                if (typingCoroutine != null) StopCoroutine(typingCoroutine);
                storyText.maxVisibleCharacters = currentFullText.Length;
                isTyping = false;
                PlayNextSE();
            }
            else
            {
                Next();
            }
        }
    }

    public void Next()
    {
        if (storyEnded || storyIndex >= storyDatas.Length)
        {
            Debug.Log("ストーリーは既に終了しています。");
            return;
        }

        PlayNextSE();

        textIndex++;

        if (textIndex >= storyDatas[storyIndex].stories.Count)
        {
            textIndex = 0;
            storyIndex++;

            if (storyIndex >= storyDatas.Length)
            {
                storyEnded = true;
                Debug.Log("全てのストーリーが終了しました。");
                return;
            }
        }

        SetStoryElement(storyIndex, textIndex);
    }

    private void SetStoryElement(int _storyIndex, int _textIndex)
    {
        var currentStoryData = storyDatas[_storyIndex];
        var storyElement = currentStoryData.stories[_textIndex];

        background.sprite = storyElement.Background;
        characterImage.sprite = storyElement.CharacterImage;
        characterName.text = storyElement.CharacterName;

        currentFullText = storyElement.StoryText;

        if (typingCoroutine != null) StopCoroutine(typingCoroutine);

        storyText.text = currentFullText;
        storyText.maxVisibleCharacters = 0;
        isTyping = true;
        typingCoroutine = StartCoroutine(TypeText());
    }

    private IEnumerator TypeText()
    {
        int total = currentFullText.Length;
        isTyping = true;

        for (int i = 0; i <= total; i++)
        {
            storyText.maxVisibleCharacters = i;

            if (i < total && !char.IsWhiteSpace(currentFullText[i]) && typingSE != null)
            {
                audioSource.PlayOneShot(typingSE, seVolume);
            }

            yield return new WaitForSeconds(typingSpeed);
        }

        isTyping = false;
        typingCoroutine = null;
    }

    private void PlayNextSE()
    {
        if (nextSE != null)
            audioSource.PlayOneShot(nextSE, seVolume);
    }
}

なが~~~~~いです
ほぼ150行。
だしまきで掲載済みのもの、だいたい同じ意味のものはカットします。
こっちコピペすればおけ


Collections.Generic;
using System.Collections.Generic;

同じような処理をいろんな型で使い回しできるようにしてくれる!

コードを少なく書けるようにしてくれる!


テキスト演出の設定

[Header("===== テキスト演出設定 =====")]
[SerializeField, Range(0.01f, 0.2f)]
private float typingSpeed = 0.05f;

typingSpeed

タイピングスピード
インスペクターから設定できるようになる
0.01~0.2の間(Range)で設定できるよ
0.05f=1秒に20文字くらいで標準なスピード


効果音の設定

[Header("===== 効果音設定 =====")]
[SerializeField] private AudioClip typingSE;   // タイピング音
[SerializeField] private AudioClip nextSE;     // 次の文に進むときの音
[SerializeField, Range(0f, 1f)]
private float seVolume = 0.7f;
AudioClip

オーディオクリップ
音のかたまりで、音を鳴らすことに必要

float seVolume

全体の効果音の大きさを調節できるようになるよ


private bool isTyping = false;                    // 今タイピング中?
private string currentFullText = "";              // 表示予定の全文(一時保存)
private Coroutine typingCoroutine = null;         // タイピング中の一時中止
private AudioSource audioSource;                  // 再生中に効果音を調整してくれる
Audio Source

オーディオクリップを読み込んで
再生・停止・音量調整ができるようになる


    private bool isTyping = false;
    private string currentFullText = "";
    private Coroutine typingCoroutine = null;
    private AudioSource audioSource;
    private bool storyEnded = false;
bool isTyping
private bool isTyping = false;

今タイピング進行中か判定してくれる
これでタイピングが終わってから
次に勧めるようになるように制御してくれる

string currentFullText = "";

いま表示しようとしている全文を一時的に保存
文字を一文字表示するための元データ

typingCoroutine
Coroutine typingCoroutine = null;

いま表示しているところのタイピング音の一時停止
nullでいまタイピングしていない状態

AudioSource audioSource;

効果音を再生するのに必要
これを使ってタイピング音と次にいくときの音をだす

bool storyEnded
bool storyEnded = false;


ストーリーが全て完了したかどうか確認する
なくなったら終わるための処理をする


audioSource = GetComponent<AudioSource>();
if (audioSource == null)
{
    audioSource = gameObject.AddComponent<AudioSource>();
    audioSource.playOnAwake = false;
AudioSource

オーディオソースが同じゲームオブジェクトにあるか探してくれる
なかったら自動的に追加してくれて、
playOnAwake = false(シーンが始まる時に音がならなくなる)ように
設定してくれる
スクリプトが音声再生用の準備を完了させてくれる


if (isTyping)
{   
    if (typingCoroutine != null) StopCoroutine(typingCoroutine);
    storyText.maxVisibleCharacters = currentFullText.Length;
    isTyping = false;
    PlayNextSE();
}
else
{
    Next();
}

タイピング中のとき(isTyping == true)
一文字ずつ表示していたtypingCoroutine を強制的に終わらせる

storyText.maxVisibleCharacters = currentFullText.Le ngth;
良いやつのテキストを使って一度に全文を出してくれる

isTyping = false;
もうタイピングが終わったよって更新してくれる

PlayNextSE();
スキップしたよの効果音を鳴らしてくれる


こんかいのおさらい

using System.Collections; //C#で型を決めたリストを作る
using System.Collections.Generic; //同じ処理を使い回せるようにするよ
using TMPro; //いいテキストを使うよ
using UnityEngine; //標準のC#を使うよ
using UnityEngine.UI; //標準で基本のC#を使うよ

public class StoryManager : MonoBehaviour //StoryManagerの初期スクリプトを使うよ
{
    [SerializeField] private StoryData[] storyDatas; //ストーリーデータのスクリプト
    [SerializeField] private Image background; //背景
    [SerializeField] private Image characterImage; //キャラの立ち絵
    [SerializeField] private TextMeshProUGUI storyText; //ストーリーのテキスト
    [SerializeField] private TextMeshProUGUI characterName; //キャラの名前

    [Header("===== テキスト演出設定 =====")]
    [SerializeField, Range(0.01f, 0.2f)] //0.01fから0.2fの間で設定できるよ
    private float typingSpeed = 0.05f; //0.05fで設定してるよ

    [Header("===== 効果音設定 =====")]
    [SerializeField] private AudioClip typingSE; //タイピング音
    [SerializeField] private AudioClip nextSE; //次に進むときの音
    [SerializeField, Range(0f, 1f)] //0fから1fの間で設定できるよ
    private float seVolume = 0.7f; //0.7fで設定してるよ

    public int storyIndex { get; private set; } = 0; //storyIndexを誰でも設定変更できるよ
    public int textIndex { get; private set; } = 0; //textIndexを誰でも設定変更できるよ

    private bool isTyping = false; //タイピング進行中か判定してくれる
    private string currentFullText = ""; //いま表示しようとしている全文を一時的に保存してくれる
    private Coroutine typingCoroutine = null; //いま表示しようとしているところのタイピング音の一時停止
    private AudioSource audioSource; //効果音を再生するのに必要
    private bool storyEnded = false; //ストーリーが終わったか確認してくれる 終わったら終わるための処理に入る

    private void Start() //すたーと!
    {
        audioSource = GetComponent<AudioSource>(); //同じゲームオブジェクトに音があるか確認してくれる
        if (audioSource == null) //音がなかったら自動的に音をつけてくれる
        {
            audioSource = gameObject.AddComponent<AudioSource>(); //音を追加してほしいよ~
            audioSource.playOnAwake = false; //音が鳴らないようにしてくれる
        }

        if (storyDatas == null || storyDatas.Length == 0) 
        { //ストーリーデータがないときに挙動がおかしくならないようにしてくれる
            Debug.LogError("storyDatasが空!"); 
            return;
        } //ストーリーデータがないときにインスペクターで設定してねって教えてくれる

        SetStoryElement(storyIndex, textIndex); //どこの処理をしているか教えてくれる
    }

    private void Update() //更新!
    {
        if (Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Space))
        { //左クリックかスペースキーを押されたときを検知してくれる
            if (storyEnded) //このストーリーが終わったかどうかのチェックと分岐
            {
                Debug.Log("ストーリーは既に終了しています。"); //その時に終わっていることを教えてくれる
                return;
            }

            if (isTyping) //タイピング中のとき
            {
                if (typingCoroutine != null) StopCoroutine(typingCoroutine); //タイピングを終わらせる
                storyText.maxVisibleCharacters = currentFullText.Length; //全文表示
                isTyping = false; //タイピングの終わりを更新してくれる
                PlayNextSE(); //スキップしたよの効果音
            }
            else //まだ途中だよ
            {
                Next(); //つぎ!
            }
        }
    }

    public void Next() //つぎを始める!
    {
        if (storyEnded || storyIndex >= storyDatas.Length) // 全部見たか判定してくれる 
        {
            Debug.Log("ストーリーは既に終了しています。"); //終わったか教えてくれる
            return;
        }

        PlayNextSE(); //つぎへの効果音を鳴らすよ

        textIndex++; //つぎのテキストを表示するよ

        if (textIndex >= storyDatas[storyIndex].stories.Count) //全部見たか判定してくれる
        {
            textIndex = 0; //いまのテキスト表示が終わったよ
            storyIndex++; //いまのストーリーが終わったからつぎに進む準備をするよ

            if (storyIndex >= storyDatas.Length) //全部見たか教えてくれる
            {
                storyEnded = true; //ストーリーが終わったことを確認したら終わる準備に入る
                Debug.Log("全てのストーリーが終了しました。"); //終わったら教えてくれる
                return;
            }
        }

        SetStoryElement(storyIndex, textIndex);
    } //指定した位置を画面に全部反映してくれる、やっと動ける

    private void SetStoryElement(int _storyIndex, int _textIndex)
    { //セリフ変わったりキャラが変わるよ
        var currentStoryData = storyDatas[_storyIndex]; //いま処理しているところを編集できるようにしてくれる
        var storyElement = currentStoryData.stories[_textIndex]; //いま表示するべきものを取り出して表示するとこに入れてるよ

        background.sprite = storyElement.Background;
        characterImage.sprite = storyElement.CharacterImage;
        characterName.text = storyElement.CharacterName; //いま表示するべきものを反映してくれる
    
        currentFullText = storyElement.StoryText; //いま表示する全文を保存してくれてる

        if (typingCoroutine != null) StopCoroutine(typingCoroutine); //タイピングを終わらせてくれる

        storyText.text = currentFullText; //ストーリーテキストを全文セットしてくれる
        storyText.maxVisibleCharacters = 0; //何文字まで表示するかを非表示
        isTyping = true; //いまタイピング中だよ
        typingCoroutine = StartCoroutine(TypeText()); //一文字ずつ表示して音をだすループに入る
    }

    private IEnumerator TypeText() //タイピングテキストをできるように待機中
    {
        int total = currentFullText.Length; //全文の文字数を取得するよ
        isTyping = true; //いまタイピング中を確認

        for (int i = 0; i <= total; i++) //全表示終わりをループ
        {
            storyText.maxVisibleCharacters = i; //何文字まで出すかを指定できる

            if (i < total && !char.IsWhiteSpace(currentFullText[i]) && typingSE != null)
            { //文字と効果音を設定している場合だけ
                audioSource.PlayOneShot(typingSE, seVolume);
            } //タイピング音を鳴らしてくれる 文字や効果音がないときは鳴らさない

            yield return new WaitForSeconds(typingSpeed); //つぎの文字まで待機するよ
        }

        isTyping = false; //タイピング終わり
        typingCoroutine = null; //再開をさせないで終了判定してくれる
    }

    private void PlayNextSE() //つぎへの効果音
    {
        if (nextSE != null) //つぎのテキストにいくとき一度だけ
            audioSource.PlayOneShot(nextSE, seVolume); //つぎへ進む効果音の音量調整
    }
}

なが~~~いけど、書いてることはちょーシンプルです

さばのみぞれ煮ってすごい美味しくてだいすき
週に1回は食べたい

いただきます🌟