見出し画像

【Unity】ノベルゲームの雛形

(タイトル画像のイラストはいらすとや様よりお借りしました!
ウサギのイラストはぴょこというキャラクターだそうです)

はじめに

いつもお世話になっております。
さて、この度「3Dローグライクゲームの作り方」が100記事を超えました。
これも読んでくださる皆様のお陰です、ありがとうございます。
今回はそれを記念して、ちょっと新しいことをやってみたいと思いまして、2Dでノベルゲーム(と言ってもシナリオはありませんが)を作成することにしました。
ノベルゲームと言っても本当に単純に完成させる為、逆に恐らくどんなものにも組み込めるものになっているはずです。3Dでも、それこそ現在のローグライクにも応用すれば、簡単なシナリオをつけることも可能でしょう。
是非活用して下さい。
最終的にこんな感じの画面が作れます(イラストはいらすとや様より)。

スクリーンショット 2020-11-12 20.05.07

制作環境

今回のUnityバージョンは「2019.4.13f1」です(エディタは日本語にしてます)。
「Visual Studio 2019 for Mac」も使用させて頂きます。

今回の仕様

・本文ウィンドウと名前表示ウィンドウの表示
・文字送り
・背景の表示
・立ち絵の表示
・選択肢の実装
・(BGMの再生)
・(効果音の再生(文章の途中で再生は×))
・簡単なフェードイン・アウトの実装
・シーン切り替え
・上記全てをテキストファイルで一括管理
・次ページへ促すアイコンの表示
・ウィンドウを一時的に隠す機能の実装

加えて今回は説明にあたり、テキストファイル以外の外部ファイルは扱わないようにします。勿論アセットも使用しません。全てUnity(とVisualStudioとテキストエディター)で完結させます。作成するスクリプトも1つだけです。
(かっこがついているものは実装できても外部音声ファイルがないと確認できません、ご了承ください)

環境を整える

上記の「新規シーンの作成」までは行っておきましょう。
その上でフォルダを用意します。今回用意するのはこちら。

・Scenes(既にあるはずです)
・Scripts
・Animators
・Resources

更にResourcesフォルダ内に以下のフォルダを作成します。

・Animations
・Sprites
・Prefabs
・Texts
・(AudioClips) ← 音を鳴らす場合はこれも追加する

ウィンドウなどの配置決め

テキストウィンドウなどの位置を決めます。
まず、ヒエラルキータブの「+」ボタンから「UI」-「Canvas」を選択し、Canvasを作成します。
そのCanvasに空の子オブジェクトを作成し、名前を「GameManager」にします。
更にGameManagerの階層下に「UI」-「Panel」を作成し、名前を「BackgroundPanel」にします。
BackgroundPanelの階層下にPanelを二つ作成し、名前をそれぞれ「MainTextPanel」と「NameTextPanel」にします。その二つの階層下に更に「UI」-「テキスト」を作成し、名前を「MainText」と「NameText」にして下さい。
後BackgroundPanelの階層下に空の子オブジェクトを作成し、名前を「CharacterImages」にしましょう。
上記を行うとヒエラルキータブはこんな感じになります。

スクリーンショット 2020-11-10 16.17.12

そしてそれぞれのサイズや位置を調節します。するとこんな感じになります。

スクリーンショット 2020-11-09 23.16.04

参考までに、筆者の設定を載せます。

スクリーンショット 2020-11-09 23.18.19

スクリーンショット 2020-11-09 23.18.47

スクリーンショット 2020-11-09 23.19.17

スクリーンショット 2020-11-09 23.19.37

スクリーンショット 2020-11-09 23.21.20

スクリーンショット 2020-11-09 23.22.09

スクリーンショット 2020-11-10 16.17.42

ページ送りアイコン

次にページ送りのアイコンを作成します。これはスプライトを使用します。
プロジェクトタブ内のAssets/Resources/Spritesフォルダに移動し、その中に「右クリック」-「作成」-「スプライト」-「三角形」を作成します(名前は任意で構いません)。
そしてヒエラルキータブ内のBackgroundPanelの階層下に空の子オブジェクトを作成し、名前を「NextPageIcon」にします。その階層下に「UI」-「画像」を作成し、「ソース画像」に先程作成したスプライトを指定、「スプライトメッシュを使用」にチェックを入れて下さい。チェックを入れると三角形の形になったと思います。
後は位置と色を自由に決めましょう。筆者はこうなりました。

スクリーンショット 2020-11-09 23.57.56

ヒエラルキータブはこんな感じです。

スクリーンショット 2020-11-10 16.23.32

このアイコンにアニメーションをつけます。
プロジェクトタブ内のAssets/Animatorsフォルダ内に「右クリック」-「作成」-「アニメーターコントローラー」を作成、名前を「NextPageIconAnimator」にします。
更にAssets/Resources/Animationsフォルダ内に「右クリック」-「作成」-「アニメーション」を作成、名前を「NextPageIconAnimation」にします。
ヒエラルキータブに戻ります。
NextPageIconにコンポーネントを追加します。「コンポーネントを追加」-「その他」-「アニメーター」を選択します。するとAnimatorコンポーネントが追加されると思うので、コントローラーに先程作成したNextPageIconAnimatorを指定します。
プロジェクトタブ内のNextPageIconAnimatorをダブルクリックします。するとこんな画面が出てくると思います。

スクリーンショット 2020-11-10 0.20.20

この画面内にNextPageIconAnimationをドラッグ&ドロップします。
するとこんな感じになります。

スクリーンショット 2020-11-10 0.24.59

これでアイコンをアニメーションさせる用意ができました。次は実際にアニメーションを設定します。
プロジェクトタブ内のNextPageIconAnimationをダブルクリックします。するとこんな画面が出てきます。

スクリーンショット 2020-11-10 0.29.12

この画面を出している状態でヒエラルキータブ内のNextPageIconをクリックします。すると、「プロパティーを追加」ボタンを押せるようになるはずです。押した時に出てくるメニューの中から「RectTransform」の隣の三角形を選択します。するとプロパティがずらっと出てくると思うので、アニメーションさせたい箇所のプロパティの隣の+ボタンをクリックします(筆者は上下運動させたいので「Anchored Position」を選択しました)。
そして左上の赤丸ボタンをクリックします。するとこんな感じになると思います。

スクリーンショット 2020-11-10 0.48.19

この白い線を移動させて動作を記録します。例えばこんな感じです。

スクリーンショット 2020-11-10 0.50.17

この位置に線を移動させて、インスペクター上で値を変えます。

スクリーンショット 2020-11-10 0.51.35

点が出現しました。こんな感じでアニメーションを記録します。
終わったらもう一度赤丸ボタンを押して録画を終了します。
アニメーションはこれで完成ですが、ループさせたい場合はもう一工夫必要です。プロジェクトタブ内のNextPageIconAnimationをクリックし、インスペクタータブに表示された「ループ時間」にチェックを入れておきます。これでアニメーションをループさせることができます。
実行してみます。

ページ送りアイコンアニメーション

もしアニメーションが速すぎたり遅すぎたりする場合はアニメータータブ内の該当アニメーションのステートをクリックして選び、「速度」の設定を変えることでも対処できます。

まずは文字を表示してみる

手始めに文字を表示するスクリプトを書いてみましょう。プロジェクトタブ内のAssets/Scriptsフォルダ内に「右クリック」-「作成」-「C# スクリプト」を作成し、名前を「GameManager」にします。
このスクリプトをダブルクリックして開きましょう。そして、以下のように記述して下さい。

using UnityEngine;
using UnityEngine.UI;

// MonoBehaviourを継承することでオブジェクトにコンポーネントとして
// アタッチすることができるようになる
public class GameManager : MonoBehaviour
{
   // SerializeFieldと書くとprivateなパラメーターでも
   // インスペクター上で値を変更できる
   [SerializeField]
   private Text mainText;
   [SerializeField]
   private Text nameText;
   private string _text = "Hello,World!";
   
   // MonoBehaviourを継承している場合限定で
   // 最初の更新関数(Updateメソッド)が呼ばれる時に最初に呼ばれる
   private void Start()
   {
       // Main Textに指定したTextコンポーネントの
       // テキストのパラメーターに代入する
       mainText.text = _text;
   }
}

コードの解説はコメント文を読んでください。
スクリプトをビルドします。Visual Studioの画面上部の三角マークのボタンを押して下さい。これでエラーがなければ四角になったボタンをもう一度押して終了します。
Unityエディタに戻ります。GameManagerに先程作成したGameManagerスクリプトをドラッグ&ドロップします。そして以下のように設定します。

スクリーンショット 2020-11-10 2.15.31

それができたら、画面上部の真ん中にある三角形のボタンを押して下さい。こんな感じで表示されます。

スクリーンショット 2020-11-10 2.16.41

お決まりの定形文句ですね!

名前を表示

次は名前も表示してみます。パラメーターをもう一つ増やしてもいいですが、最終的にテキストファイルから読み出すことを考えて一つのパラメーターから名前も取得することにします。
GameManagerクラスを以下のように変更して下さい。

// パラメーターを追加
private const char SEPARATE_MAIN_START = '「';
private const char SEPARATE_MAIN_END = '」';

// パラメーターを変更
private string _text = "みにに「Hello,World!」";

// メソッドを変更
private void Start()
{
   ReadLine(_text);
}

/**
* 1行を読み出す
*/
private void ReadLine(string text)
{
   // '「'の位置で文字列を分ける
   string[] ts = text.Split(SEPARATE_MAIN_START);
   // 分けたときの最初の値、つまり"みにに"が代入される
   string name = ts[0];
   // 分けたときの次の値、つまり"Hello,World!」"が代入されるので
   // 最後の閉じ括弧を削除して代入(="Hello,World!")
   string main = ts[1].Remove(ts[1].LastIndexOf(SEPARATE_MAIN_END));
   nameText.text = name;
   mainText.text = main;
}

かぎかっこで囲われた部分が本文で、その直前の文字が名前を表すことにします(例:『名無しさん「こんにちは世界」』の場合、名前に「名無しさん」、本文に「こんにちは世界」が表示される)。
ビルドして、実行してみます。

スクリーンショット 2020-11-10 2.43.19

名前欄にも名前が表示されました。

文字を1文字ずつ表示

今のままだと文字が一斉に表示されてしまうので、1字ずつ文字送りできるようにします。
とりあえず1字だけ表示してみましょうか。
GameManagerクラスを変更して下さい。

// スクリプトの最初に記述
using System.Collections.Generic;

// パラメーターを追加
private Queue<char> _charQueue;

// メソッドを追加
/**
* 文を1文字ごとに区切り、キューに格納したものを返す
*/
private Queue<char> SeparateString(string str)
{
   // 文字列をchar型の配列にする = 1文字ごとに区切る
   char[] chars = str.ToCharArray();
   Queue<char> charQueue = new Queue<char>();
   // foreach文で配列charsに格納された文字を全て取り出し
   // キューに加える
   foreach (char c in chars) charQueue.Enqueue(c);
   return charQueue;
}

/**
* 1文字を出力する
*/
private void OutputChar()
{
   // キューから値を取り出し、キュー内からは削除する
   mainText.text += _charQueue.Dequeue();
}

// メソッドを変更
private void ReadLine(string text)
{
   string[] ts = text.Split('「');
   string name = ts[0];
   string main = ts[1].Remove(ts[1].LastIndexOf('」'));
   nameText.text = name;
   mainText.text = "";
   _charQueue = SeparateString(main);
}

private void Start()
{
   ReadLine(_text);
   OutputChar();
}

キューは先入先出し(FIFO)なので先に入れたものから取り出されます。
実行してみます。

スクリーンショット 2020-11-10 10.06.16

Helloの最初の「H」が表示されました。
これを待ち時間をつけて全て表示するコードがこちらです。コルーチンを使用します。
GameManagerクラスを変更して下さい。

using System.Collections;

// パラメーターを追加
[SerializeField]
private float captionSpeed = 0.2f;

// メソッドを変更
private bool OutputChar()
{
   // キューに何も格納されていなければfalseを返す
   if (_charQueue.Count <= 0) return false;
   mainText.text += _charQueue.Dequeue();
   return true;
}

// メソッドを追加
/**
* 文字送りするコルーチン
*/
private IEnumerator ShowChars(float wait)
{
   // OutputCharメソッドがfalseを返す(=キューが空になる)までループする
   while (OutputChar())
       // wait秒だけ待機
       yield return new WaitForSeconds(wait);
   // コルーチンを抜け出す
   yield break;
}

// メソッドを変更
private void Start()
{
   ReadLine(_text);
}

private void ReadLine(string text)
{
   string[] ts = text.Split(SEPARATE_MAIN_START);
   string name = ts[0];
   string main = ts[1].Remove(ts[1].LastIndexOf(SEPARATE_MAIN_END));
   nameText.text = name;
   mainText.text = "";
   _charQueue = SeparateString(main);
   // コルーチンを呼び出す
   StartCoroutine(ShowChars(captionSpeed));
}

実行してみます。

文字送り

文字を時間差で表示することができました。なお、文字送りのスピードを変えたいときはUnityエディタ上でGame ManagerコンポーネントのCaption Speedの値を変更します。

クリックされたら全文表示

今度は画面が左クリックされたら文字送りを待たずに全て表示されるようにしましょう。
GameManagerクラスを変更して下さい。

// メソッドを追加
/**
* 全文を表示する
*/
private void OutputAllChar()
{
   // コルーチンをストップ
   StopCoroutine(ShowChars(captionSpeed));
   // キューが空になるまで表示
   while (OutputChar()) ;
}

/**
* クリックしたときの処理
*/
private void OnClick()
{
   OutputAllChar();
}

// MonoBehaviourを継承している場合限定で
// 毎フレーム呼ばれる 
private void Update()
{
   // 左(=0)クリックされたらOnClickメソッドを呼び出し
   if (Input.GetMouseButtonDown(0)) OnClick();
}

実行してみます。画面をクリックすると全文字が一気に表示されたはずです。

次のページを表示

いつまでもHello,World!だけでは寂しいですね。ということで全文が表示されている時にクリックすると次のページが表示されるようにします。
前述の通り後からテキストを読み出すために一文でまとめたいと思います。
GameManagerクラスを変更して下さい。

// パラメーターを追加
private const char SEPARATE_PAGE = '&';
private Queue<string> _pageQueue;

// パラメーターを変更
private string _text =
    "みにに「Hello,World!」&みにに「これはテキスト表示のサンプルです」&名無し「こんにちは!」";

// メソッドを追加
/**
* 文字列を指定した区切り文字ごとに区切り、キューに格納したものを返す
*/
private Queue<string> SeparateString(string str, char sep)
{
   string[] strs = str.Split(sep);
   Queue<string> queue = new Queue<string>();
   foreach (string l in strs) queue.Enqueue(l);
   return queue;
}

/**
* 初期化する
*/
private void Init()
{
   _pageQueue = SeparateString(_text, SEPARATE_PAGE);
   ShowNextPage();
}

/**
* 次のページを表示する
*/
private bool ShowNextPage()
{
   if (_pageQueue.Count <= 0) return false;
   ReadLine(_pageQueue.Dequeue());
   return true;
}

// メソッドを変更
private void Start()
{
   Init();
}

private void OnClick()
{
   if (_charQueue.Count > 0) OutputAllChar();
   else
   {
       if (!ShowNextPage())
           // UnityエディタのPlayモードを終了する
           UnityEditor.EditorApplication.isPlaying = false;
   }
}

ページの区切り文字は「&」です。これをページを跨ぎたい箇所の間に挟んで下さい、(例:『a「b」&c「d」』なら本文には最初に「b」と表示され、クリックすると「d」と表示される)
実行してみます。最初はこの画面が表示されます。

スクリーンショット 2020-11-10 12.21.52

クリックすると次の画面が表示されます。

スクリーンショット 2020-11-10 12.22.07

更にクリックするとこの画面が表示されるはずです。

スクリーンショット 2020-11-10 12.22.26

この画面でクリックすると自動的にPlayモードが終了します。

ページ送りアイコンの非表示

今はアイコンが出現したままになっていますが、文字表示中は表示しないようにします。
GameManagerクラスを変更して下さい。

// パラメーターを追加
[SerializeField]
private GameObject nextPageIcon;

// メソッドを変更
private bool ShowNextPage()
{
   if (_pageQueue.Count <= 0) return false;
   // オブジェクトの表示/非表示を設定する
   nextPageIcon.SetActive(false);
   ReadLine(_pageQueue.Dequeue());
   return true;
}

private bool OutputChar()
{
   if (_charQueue.Count <= 0)
   {
       nextPageIcon.SetActive(true);
       return false;
   }
   mainText.text += _charQueue.Dequeue();
   return true;
}

private void OutputAllChar()
{
   StopCoroutine(ShowChars(captionSpeed));
   while (OutputChar()) ;
   nextPageIcon.SetActive(true);
}

ビルドしたら、実行前にGameManagerコンポーネントのNext Page Iconを設定しておくのを忘れないようにして下さい。
実行してみます。

スクリーンショット 2020-11-10 12.45.05

文の途中ではアイコンが表示されなくなりました。
これでテキスト表示の基本的な部分は完成しました!

背景の表示

ですがこれだけだと寂しいので背景の設定をしてみたいと思います。
GameManagerクラスを変更して下さい。

// パラメーターを追加
private const char SEPARATE_COMMAND = '!';
private const char COMMAND_SEPARATE_PARAM = '=';
private const string COMMAND_BACKGROUND = "background";
private const string COMMAND_SPRITE = "_sprite";
private const string COMMAND_COLOR = "_color";
[SerializeField]
private Image backgroundImage;
[SerializeField]
private string spritesDirectory = "Sprites/";

// パラメーターを変更
private string _text =
       "!background_sprite=\"background_sprite1\"&みにに「Hello,World!」&みにに「これはテキスト表示のサンプルです」&!background_sprite=\"background_sprite2\"!background_color=\"255,0,255\"&名無し「こんにちは!」";

// メソッドを追加
/**
* 背景の設定
*/
private void SetBackgroundImage(string cmd, string parameter)
{
   // 空白を削除し、背景コマンドの文字列も削除する
   cmd = cmd.Replace(" ", "").Replace(COMMAND_BACKGROUND, "");
   // ダブルクォーテーションで囲われた部分だけを取り出す
   parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
   switch (cmd)
   {
       case COMMAND_SPRITE:
           // Resourcesフォルダからスプライトを読み込み、インスタンス化する
           Sprite sp = Instantiate(Resources.Load<Sprite>(spritesDirectory + parameter));
           // 背景画像にインスタンス化したスプライトを設定する
           backgroundImage.sprite = sp;
           break;
       case COMMAND_COLOR:
           // 空白を削除し、カンマで文字を分ける
           string[] ps = parameter.Replace(" ", "").Split(',');
           // 分けた文字列(=引数)が4つ以上あるなら
           if (ps.Length > 3)
               // 透明度も設定する
               // 文字列をbyte型に直し、色を作成する
               backgroundImage.color = new Color32(byte.Parse(ps[0]), byte.Parse(ps[1]),
                                               byte.Parse(ps[2]), byte.Parse(ps[3]));
           else
               backgroundImage.color = new Color32(byte.Parse(ps[0]), byte.Parse(ps[1]), byte.Parse(ps[2]), 255);
           break;
   }
}

/**
* コマンドの読み出し
*/
private void ReadCommand(string cmdLine)
{
   // 最初の「!」を削除する
   cmdLine = cmdLine.Remove(0, 1);
   Queue<string> cmdQueue = SeparateString(cmdLine, SEPARATE_COMMAND);
   foreach (string cmd in cmdQueue)
   {
       // 「=」で分ける
       string[] cmds = cmd.Split(COMMAND_SEPARATE_PARAM);
       // もし背景コマンドの文字列が含まれていたら
       if (cmds[0].Contains(COMMAND_BACKGROUND))
           SetBackgroundImage(cmds[0], cmds[1]);
   }
}

// メソッドを変更
private void ReadLine(string text)
{
   // 最初が「!」だったら
   if (text[0].Equals(SEPARATE_COMMAND))
   {
       ReadCommand(text);
       ShowNextPage();
       return;
   }
   string[] ts = text.Split(SEPARATE_MAIN_START);
   string name = ts[0];
   string main = ts[1].Remove(ts[1].LastIndexOf(SEPARATE_MAIN_END));
   nameText.text = name;
   mainText.text = "";
   _charQueue = SeparateString(main);
   StartCoroutine(ShowChars(captionSpeed));
}

ビルドしたら、コンポーネントのBackground ImageにBackgroundPanelを設定しておきます。
これだけだとまだ動きません。背景画像も用意する必要があります。
ページ送りアイコンの画像を作成したように、任意のスプライトをAssets/Resources/Spritesフォルダの中に2つ作成し、名前をそれぞれ「background_sprite1」、「background_sprite2」にします。
そしてBackgroudPanelのImageの画像タイプを「シンプル」に設定し、「スプライトメッシュを使用」にチェックを入れておきます(これは普通の背景画像を扱う際はしなくていいです)。
実行してみます。

スクリーンショット 2020-11-10 15.15.39

最初にbackground_sprite1が読み込まれます。

スクリーンショット 2020-11-10 15.15.53

最後にbackground_sprite2が読み込まれ、色に「255,0,255,255」を指定した為マゼンタに変わっています。
このコマンドの使用方法は以下の通りです。

・「!」を最初につける(全コマンド共通)
・「!background_sprite="《スプライト名》"」で背景画像を設定する
・「!background_color="《r》,《g》,《b》,《a》"」で色を設定する

また、画像ファイルがあるフォルダをSprites Directoryで指定できます。
これだけでも簡単な紙芝居が作れそうです。

立ち絵の表示

背景があるなら立ち絵もあるといいですね。という訳で設定し......その前に、背景画像の表示と共通化できる部分は切り出して別メソッドにしておきます。
GameManagerクラスを変更して下さい。

// メソッドを追加
/**
* スプライトをファイルから読み出し、インスタンス化する
*/
private Sprite LoadSprite(string name)
{
   return Instantiate(Resources.Load<Sprite>(spritesDirectory + name));
}

/**
* パラメーターから色を作成する
*/
private Color ParameterToColor(string parameter)
{
   string[] ps = parameter.Replace(" ", "").Split(',');
   if (ps.Length > 3)
       return new Color32(byte.Parse(ps[0]), byte.Parse(ps[1]),
                                       byte.Parse(ps[2]), byte.Parse(ps[3]));
   else
       return new Color32(byte.Parse(ps[0]), byte.Parse(ps[1]),
                                       byte.Parse(ps[2]), 255);
}

/**
* 画像の設定
*/
private void SetImage(string cmd, string parameter, Image image)
{
   cmd = cmd.Replace(" ", "");
   parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
   switch (cmd)
   {
       case COMMAND_SPRITE:
           image.sprite = LoadSprite(parameter);
           break;
       case COMMAND_COLOR:
           image.color = ParameterToColor(parameter);
           break;
   }
}

// メソッドを変更
private void SetBackgroundImage(string cmd, string parameter)
{
   cmd = cmd.Replace(COMMAND_BACKGROUND, "");
   SetImage(cmd, parameter, backgroundImage);
}

実行してみて問題なく動くようであればOKです。
今度は立ち絵のプレハブを用意します。
CharacterImages階層下に新しく「UI」-「画像」を作成し、名前を「CharacterImage」にします。
ソース画像にbackground_sprite1(など、任意のスプライト)を設定します(こうしないとメニューが表示されない為)。
そして「スプライトメッシュを使用」(これは普通の立ち絵を使用する場合は要りません)と「アスペクト比を保存」(画像が引き延ばされないようにする為)にチェックを入れておきます。
そしてそのままCharacterImageをAssets/Resources/Prefabsフォルダにドラッグ&ドロップし、プレハブ化します(オブジェクト名が青くなったらOKです)。
最後にヒエラルキータブのCharacterImageを削除します。
これで立ち絵の準備はできました。
準備が終わった所で立ち絵を表示するコードを書いていきましょう。
GameManagerクラスを変更します。
(ここから先は巻きでいくためコードの説明を省略致します。わからなければ聞いて下さい)

// パラメーターを追加
private const string COMMAND_CHARACTER_IMAGE = "charaimg";
private const string COMMAND_SIZE = "_size";
private const string COMMAND_POSITION = "_pos";
private const string COMMAND_ROTATION = "_rotate";
private const string CHARACTER_IMAGE_PREFAB = "CharacterImage";
[SerializeField]
private GameObject characterImages;
[SerializeField]
private string prefabsDirectory = "Prefabs/";
private List<Image> _charaImageList = new List<Image>();

// パラメーターを変更
private string _text =
       "!background_sprite=\"background_sprite1\"!charaimg_sprite=\"polygon\"=\"background_sprite2\""+
       "!charaimg_size=\"polygon\"=\"500, 500, 1\"&みにに「Hello,World!」&みにに「これはテキスト表示のサンプルです」" +
       "&!background_sprite=\"background_sprite2\"!background_color=\"255,0,255\"!charaimg_pos=\"polygon\"=\"-500, 500, 0\"&名無し「こんにちは!」";

// メソッドを追加
/**
* 立ち絵の設定
*/
private void SetCharacterImage(string name, string cmd, string parameter)
{
   cmd = cmd.Replace(COMMAND_CHARACTER_IMAGE, "");
   name = name.Substring(name.IndexOf('"') + 1, name.LastIndexOf('"') - name.IndexOf('"') - 1);
   Image image = _charaImageList.Find(n => n.name == name);
   if (image == null)
   {
       image = Instantiate(Resources.Load<Image>(prefabsDirectory + CHARACTER_IMAGE_PREFAB), characterImages.transform);
       image.name = name;
       _charaImageList.Add(image);
   }
   SetImage(cmd, parameter, image);
}

/**
* パラメーターからベクトルを取得する
*/
private Vector3 ParameterToVector3(string parameter)
{
   string[] ps = parameter.Replace(" ", "").Split(',');
   return new Vector3(float.Parse(ps[0]), float.Parse(ps[1]), float.Parse(ps[2]));
}

// メソッドを変更
private void ReadCommand(string cmdLine)
{
   cmdLine = cmdLine.Remove(0, 1);
   Queue<string> cmdQueue = SeparateString(cmdLine, SEPARATE_COMMAND);
   foreach (string cmd in cmdQueue)
   {
       string[] cmds = cmd.Split(COMMAND_SEPARATE_PARAM);
       if (cmds[0].Contains(COMMAND_BACKGROUND))
           SetBackgroundImage(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_CHARACTER_IMAGE))
           SetCharacterImage(cmds[1], cmds[0], cmds[2]);
   }
}

private void SetImage(string cmd, string parameter, Image image)
{
   cmd = cmd.Replace(" ", "");
   parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
   switch (cmd)
   {
       case COMMAND_SPRITE:
           image.sprite = LoadSprite(parameter);
           break;
       case COMMAND_COLOR:
           image.color = ParameterToColor(parameter);
           break;
       case COMMAND_SIZE:
           image.GetComponent<RectTransform>().sizeDelta = ParameterToVector3(parameter);
           break;
       case COMMAND_POSITION:
           image.GetComponent<RectTransform>().anchoredPosition = ParameterToVector3(parameter);
           break;
       case COMMAND_ROTATION:
           image.GetComponent<RectTransform>().eulerAngles = ParameterToVector3(parameter);
           break;
   }
}

実行してみます。
1ページ目はこんな感じになるはずです。

スクリーンショット 2020-11-10 20.59.18

中央に立ち絵が表示されました。これが3ページ目にはこうなります。

スクリーンショット 2020-11-10 20.59.30

立ち絵が移動しました。
使い方は以下です。

立ち絵画像を設定する
「!charaimg_sprite="《オブジェクト名》"="《スプライト名》"」
色を設定する
「!charaimg_color="《オブジェクト名》"="《r》,《g》,《b》,《a》"」
サイズを設定する
「!charaimg_size="《オブジェクト名》"="《x》,《y》,1"」
位置を設定する
「!charaimg_pos="《オブジェクト名》"="《x》,《y》,0"」
回転を設定する
「!charaimg_rotate="《オブジェクト名》"="《x》,《y》,《z》"」

《オブジェクト名》はシーンの中で唯一の値にして下さい。省略することはできません。

テキストファイルからデータを読み込む

そろそろいちパラメーターでデータを記述するのは大変になってきたので、この辺りでテキストファイルからデータを読み込むことができるようにします。
GameManagerクラスを変更して下さい。

// パラメーターを追加
[SerializeField]
private string textFile = "Texts/Scenario";

// パラメーターを変更
private string _text = "";

// メソッドを追加
/**
* テキストファイルを読み込む
*/
private string LoadTextFile(string fname)
{
   TextAsset textasset = Resources.Load<TextAsset>(fname);
   return textasset.text.Replace("\n", "").Replace("\r", "");
}

// メソッドを変更
private void Init()
{
   _text = LoadTextFile(textFile);
   _pageQueue = SeparateString(_text, SEPARATE_PAGE);
   ShowNextPage();
}

ビルドしたら、Assets/Resources/Textsフォルダに「Scenario.txt」というテキストファイルを作成します。これはUnityエディターではできないので、任意のテキストエディター(例えばWindowsならメモ帳など)で作成します。
そこに以下のように記述して下さい。

!background_sprite="background_sprite1"
!charaimg_sprite="polygon"="background_sprite2"
!charaimg_size="polygon"="500, 500, 1"

&みにに「Hello,World!」
&みにに「これはテキスト表示のサンプルです」

&!background_sprite="background_sprite2"
!background_color="255,0,255"
!charaimg_pos="polygon"="-500, 500, 0"

&名無し「こんにちは!」

改行コードは読み込む際に削除しているのでいくら改行しても構いません(空白はNG)。
わかりやすいようにして下さい。
実行すると先ほどと同じように表示されるはずです。

名前ウィンドウの非表示/立ち絵の非表示・削除

だいぶ見やすくなったところで、次です。
今度は立ち絵や名前ウィンドウを非表示にしてみたいと思います。
GameManagerクラスを変更して下さい。

// パラメーターを追加
private const string COMMAND_ACTIVE = "_active";
private const string COMMAND_DELETE = "_delete";

// メソッドを追加
/**
* パラメーターからboolを取得する
*/
private bool ParameterToBool(string parameter)
{
   string p = parameter.Replace(" ", "");
   return p.Equals("true") || p.Equals("TRUE");
}

// メソッドを変更
private void ReadLine(string text)
{
   if (text[0].Equals(SEPARATE_COMMAND))
   {
       ReadCommand(text);
       ShowNextPage();
       return;
   }
   string[] ts = text.Split(SEPARATE_MAIN_START);
   string name = ts[0];
   string main = ts[1].Remove(ts[1].LastIndexOf(SEPARATE_MAIN_END));
   if (name.Equals("")) nameText.transform.parent.gameObject.SetActive(false);
   else
   {
       nameText.text = name;
       nameText.transform.parent.gameObject.SetActive(true);
   }
   mainText.text = "";
   _charQueue = SeparateString(main);
   StartCoroutine(ShowChars(captionSpeed));
}

private void SetImage(string cmd, string parameter, Image image)
{
   cmd = cmd.Replace(" ", "");
   parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
   switch (cmd)
   {
       case COMMAND_SPRITE:
           image.sprite = LoadSprite(parameter);
           break;
       case COMMAND_COLOR:
           image.color = ParameterToColor(parameter);
           break;
       case COMMAND_SIZE:
           image.GetComponent<RectTransform>().sizeDelta = ParameterToVector3(parameter);
           break;
       case COMMAND_POSITION:
           image.GetComponent<RectTransform>().anchoredPosition = ParameterToVector3(parameter);
           break;
       case COMMAND_ROTATION:
           image.GetComponent<RectTransform>().eulerAngles = ParameterToVector3(parameter);
           break;
       case COMMAND_ACTIVE:
           image.gameObject.SetActive(ParameterToBool(parameter));
           break;
       case COMMAND_DELETE:
           _charaImageList.Remove(image);
           Destroy(image.gameObject);
           break;
   }
}

テキストファイルの中身を以下に変更して下さい。

!background_sprite="background_sprite1"
!charaimg_sprite="polygon"="background_sprite2"
!charaimg_size="polygon"="500, 500, 1"
!charaimg_active="polygon"="false"

&みにに「Hello,World!」

&!charaimg_active="polygon"="true"

&みにに「これはテキスト表示のサンプルです」

&!charaimg_active="polygon"="false"
!background_sprite="background_sprite2"
!background_color="255,0,255"
!charaimg_pos="polygon"="-500, 500, 0"

&名無し「こんにちは!」

&!charaimg_delete="polygon"=""

&「ポリゴンを削除しました」

実行してみます。最後のページが以下のようになり、ヒエラルキータブからpolygonが事前に削除されればOKです。

スクリーンショット 2020-11-10 22.44.37

今回追加した仕様は以下の通りです。

・かぎかっこ前に文字が無ければ名前欄を非表示にする
表示・非表示を設定する
「!charaimg_active="《オブジェクト名》"="《true(TRUE)orそれ以外》"」
削除する
「!charaimg_delete="《オブジェクト名》"=""」

立ち絵をアニメーションさせる

折角なのでアニメーションも付けたいと思います。
GameManagerクラスを変更して下さい。

// スクリプトの最初に記述
using System.IO;
using UnityEditor;
using UnityEngine.SceneManagement;

// パラメーターを追加
private const char COMMAND_SEPARATE_ANIM = '%';
private const string COMMAND_ANIM = "_anim";
[SerializeField]
private string animationsDirectory = "Animations/";
[SerializeField]
private string overrideAnimationClipName = "Clip";

// メソッドを追加
/**
* パラメーターからアニメーションクリップを生成する
*/
private AnimationClip ParameterToAnimationClip(Image image, string[] parameters)
{
   string[] ps = parameters[0].Replace(" ", "").Split(',');
   string path = animationsDirectory + SceneManager.GetActiveScene().name + "/" + image.name;
   AnimationClip prevAnimation = Resources.Load<AnimationClip>(path + "/" + ps[0]);
   AnimationClip animation;
   #if UNITY_EDITOR
       if (ps[3].Equals("Replay") && prevAnimation != null)
           return Instantiate(prevAnimation);
       animation = new AnimationClip();
       Color startcolor = image.color;
       Vector3[] start = new Vector3[3];
       start[0] = image.GetComponent<RectTransform>().sizeDelta;
       start[1] = image.GetComponent<RectTransform>().anchoredPosition;
       Color endcolor = startcolor;
       if (parameters[1] != "") endcolor = ParameterToColor(parameters[1]);
       Vector3[] end = new Vector3[3];
       for (int i = 0; i < 2; i++)
       {
           if (parameters[i + 2] != "")
               end[i] = ParameterToVector3(parameters[i + 2]);
           else end[i] = start[i];
       }
       AnimationCurve[,] curves = new AnimationCurve[4, 4];
       if (ps[3].Equals("EaseInOut"))
       {
           curves[0, 0] = AnimationCurve.EaseInOut(float.Parse(ps[1]), startcolor.r, float.Parse(ps[2]), endcolor.r);
           curves[0, 1] = AnimationCurve.EaseInOut(float.Parse(ps[1]), startcolor.g, float.Parse(ps[2]), endcolor.g);
           curves[0, 2] = AnimationCurve.EaseInOut(float.Parse(ps[1]), startcolor.b, float.Parse(ps[2]), endcolor.b);
           curves[0, 3] = AnimationCurve.EaseInOut(float.Parse(ps[1]), startcolor.a, float.Parse(ps[2]), endcolor.a);
           for (int i = 0; i < 2; i++)
           {
               curves[i + 1, 0] = AnimationCurve.EaseInOut(float.Parse(ps[1]), start[i].x, float.Parse(ps[2]), end[i].x);
               curves[i + 1, 1] = AnimationCurve.EaseInOut(float.Parse(ps[1]), start[i].y, float.Parse(ps[2]), end[i].y);
               curves[i + 1, 2] = AnimationCurve.EaseInOut(float.Parse(ps[1]), start[i].z, float.Parse(ps[2]), end[i].z);
           }
       }
       else
       {
           curves[0, 0] = AnimationCurve.Linear(float.Parse(ps[1]), startcolor.r, float.Parse(ps[2]), endcolor.r);
           curves[0, 1] = AnimationCurve.Linear(float.Parse(ps[1]), startcolor.g, float.Parse(ps[2]), endcolor.g);
           curves[0, 2] = AnimationCurve.Linear(float.Parse(ps[1]), startcolor.b, float.Parse(ps[2]), endcolor.b);
           curves[0, 3] = AnimationCurve.Linear(float.Parse(ps[1]), startcolor.a, float.Parse(ps[2]), endcolor.a);
           for (int i = 0; i < 2; i++)
           {
               curves[i + 1, 0] = AnimationCurve.Linear(float.Parse(ps[1]), start[i].x, float.Parse(ps[2]), end[i].x);
               curves[i + 1, 1] = AnimationCurve.Linear(float.Parse(ps[1]), start[i].y, float.Parse(ps[2]), end[i].y);
               curves[i + 1, 2] = AnimationCurve.Linear(float.Parse(ps[1]), start[i].z, float.Parse(ps[2]), end[i].z);
           }
       }
       string[] b1 = { "r", "g", "b", "a" };
       for (int i = 0; i < 4; i++)
       {
           AnimationUtility.SetEditorCurve(
               animation,
               EditorCurveBinding.FloatCurve("", typeof(Image), "m_Color." + b1[i]),
               curves[0, i]
           );
       }
       string[] a = { "m_SizeDelta", "m_AnchoredPosition" };
       string[] b2 = { "x", "y", "z" };
       for (int i = 0; i < 2; i++)
       {
           for (int j = 0; j < 3; j++)
           {
               AnimationUtility.SetEditorCurve(
                   animation,
                   EditorCurveBinding.FloatCurve("", typeof(RectTransform), a[i] + "." + b2[j]),
                   curves[i + 1, j]
               );
           }   
       }
       if (!Directory.Exists("Assets/Resources/" + path))
           Directory.CreateDirectory("Assets/Resources/" + path);
       AssetDatabase.CreateAsset(animation, "Assets/Resources/" + path + "/" + ps[0] + ".anim");
       AssetDatabase.ImportAsset("Assets/Resources/" + path + "/" + ps[0] + ".anim");
   #elif UNITY_STANDALONE
       animation = prevAnimation;
   #endif
   return Instantiate(animation);
}

**
* アニメーションを画像に設定する
*/
private void ImageSetAnimation(Image image, string parameter)
{
   Animator animator = image.GetComponent<Animator>();
   AnimationClip clip = ParameterToAnimationClip(image, parameter.Split(COMMAND_SEPARATE_ANIM));
   AnimatorOverrideController overrideController;
   if (animator.runtimeAnimatorController is AnimatorOverrideController)
       overrideController = (AnimatorOverrideController)animator.runtimeAnimatorController;
   else
   {
       overrideController = new AnimatorOverrideController();
       overrideController.runtimeAnimatorController = animator.runtimeAnimatorController;
       animator.runtimeAnimatorController = overrideController;
   }
   overrideController[overrideAnimationClipName] = clip;
   animator.Update(0.0f);
   animator.Play(overrideAnimationClipName, 0);
}

// メソッドを変更
private void SetImage(string cmd, string parameter, Image image)
{
   cmd = cmd.Replace(" ", "");
   parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
   switch (cmd)
   {
       case COMMAND_SPRITE:
           image.sprite = LoadSprite(parameter);
           break;
       case COMMAND_COLOR:
           image.color = ParameterToColor(parameter);
           break;
       case COMMAND_SIZE:
           image.GetComponent<RectTransform>().sizeDelta = ParameterToVector3(parameter);
           break;
       case COMMAND_POSITION:
           image.GetComponent<RectTransform>().anchoredPosition = ParameterToVector3(parameter);
           break;
       case COMMAND_ROTATION:
           image.GetComponent<RectTransform>().eulerAngles = ParameterToVector3(parameter);
           break;
       case COMMAND_ACTIVE:
           image.gameObject.SetActive(ParameterToBool(parameter));
           break;
       case COMMAND_DELETE:
           _charaImageList.Remove(image);
           Destroy(image.gameObject);
           break;
       case COMMAND_ANIM:
           ImageSetAnimation(image, parameter);
           break;
   }
}

アニメーションを適用するために以下のことを行う必要があります。
まずAssets/Animatorフォルダに新しくアニメーターコントローラーを作成し、名前を「CharacterImageAnimator」にして下さい。
更にAssets/Resources/Animationフォルダに新しく空のアニメーションを作成し、名前を「Clip」にします。
CharacterImageAnimatorを開きます。「右クリック」-「ステートの作成」-「空」で二つステートを作成します。
ステートの設定の「Write Defaults」のチェックをどちらも外します。
そして遷移を以下のようにして下さい。

スクリーンショット 2020-11-11 14.51.42

遷移の設定はこんな感じです。

スクリーンショット 2020-11-12 16.57.20

Clipの方にはMotionにClipアニメーションを指定します。
そしてプレハブのCharacterImageにアニメーターコンポーネントを追加し、
コントローラーにCharacterImageAnimatorを指定します。
テストします。テキストファイルに以下を記述して下さい。

!background_sprite="background_sprite1"
!charaimg_sprite="polygon"="background_sprite2"
!charaimg_size="polygon"="500, 500, 1"
!charaimg_rotate="polygon"="30,30,0"

&みにに「Hello,World!」

&!charaimg_active="polygon"="true"
!charaimg_anim="polygon"="anim,0,1,EaseInOut%255,255,255,0%1000,1000,1%1000,500,0"

&みにに「これはテキスト表示のサンプルです」

&!charaimg_active="polygon"="true"
!background_sprite="background_sprite2"
!background_color="255,0,255"
!charaimg_anim="polygon"="anim,,,Replay"

&名無し「こんにちは!」

&!charaimg_delete="polygon"=""

&「ポリゴンを削除しました」

実行すると、こんな感じになります。

立ち絵アニメーション

立ち絵をアニメーションさせる方法は次の通りです。

立ち絵をアニメーション
「!charaimg_anim="《オブジェクト名》"="《設定》%《色》%《サイズ》%《位置》"」
設定には次を代入
「《アニメーション名》,《開始時間》,《終了時間》,《実行方法》,」
には次を代入
「《r》,《g》,《b》,《a》」※省略可
サイズには次を代入
「《x》,《y》,1」※省略可
位置(移動先)には次を代入
《x》,《y》,0※省略可

《実行方法》には「EaseInOut」又は「Replay」又は「それ以外(Linear)」を指定しましょう。EaseInOutとLinearはアニメーションを作成(あるいは上書き)しますが、アニメーションの仕方が若干違います。
Replayは既に作成済みのアニメーションをもう一度呼び出し、上書きはしません。
《アニメーション名》にはその立ち絵オブジェクトで唯一の値を使用して下さい。ただしReplayを指定する場合は既にあるものを使用するようにしましょう。
回転などをアニメーションする場合は自動では作成できないので自分で作成して下さい。
作成したアニメーションを使用したい場合はAnimations/《シーン名》/《アニメーションさせるオブジェクト名》フォルダ内に入れます。
例えばこんな感じです。

スクリーンショット 2020-11-11 20.48.03

自動で作成した場合もこの中に入ります。
(まあ細かいことは使っていく内にわかると思います)

ラベルジャンプの実装

立ち絵ができたところで選択肢の実装もしましょう。
ですがその前に指定したラベルまでジャンプするようにします。
ということでまずはテキストをラベル分けします。
GameManagerクラスを変更して下さい。

// スクリプトの最初に記述
using System.Linq;

// パラメーターを追加
private const char SEPARATE_SUBSCENE = '#';
private Dictionary<string, Queue<string>> _subScenes =
       new Dictionary<string, Queue<string>>();

// メソッドを変更
private void Init()
{
   _text = LoadTextFile(textFile);
   Queue<string> subScenes = SeparateString(_text, SEPARATE_SUBSCENE);
   foreach (string subScene in subScenes)
   {
       if (subScene.Equals("")) continue;
       Queue<string> pages = SeparateString(subScene, SEPARATE_PAGE);
       _subScenes[pages.Dequeue()] = pages;
   }
   _pageQueue = _subScenes.First().Value;
   ShowNextPage();
}

これでテストします。

#START&
!background_sprite="background_sprite1"
!charaimg_sprite="polygon"="background_sprite2"
!charaimg_size="polygon"="500, 500, 1"
!charaimg_rotate="polygon"="30,30,0"

&みにに「Hello,World!」

&!charaimg_active="polygon"="true"
!charaimg_anim="polygon"="anim,0,1,EaseInOut%255,255,255,0%1000,1000,0%1000,500,0"

&みにに「これはテキスト表示のサンプルです」

&!charaimg_active="polygon"="true"
!background_sprite="background_sprite2"
!background_color="255,0,255"
!charaimg_anim="polygon"="anim,,,Replay"

&名無し「こんにちは!」

#END&
!charaimg_delete="polygon"=""

&「ポリゴンを削除しました」

実行してみます。最後の「ポリゴンを削除しました」が出なくなったはずです。
次にラベルジャンプの機能をつけます。
GameManagerクラスを変更して下さい。

// パラメーターを追加
private const string COMMAND_JUMP = "jump_to";

// メソッドを追加
/**
* 対応するラベルまでジャンプする
*/
private void JumpTo(string parameter)
{
   parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
   _pageQueue = _subScenes[parameter];
}

// メソッドを変更
private void ReadCommand(string cmdLine)
{
   cmdLine = cmdLine.Remove(0, 1);
   Queue<string> cmdQueue = SeparateString(cmdLine, SEPARATE_COMMAND);
   foreach (string cmd in cmdQueue)
   {
       string[] cmds = cmd.Split(COMMAND_SEPARATE_PARAM);
       if (cmds[0].Contains(COMMAND_BACKGROUND))
           SetBackgroundImage(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_CHARACTER_IMAGE))
           SetCharacterImage(cmds[1], cmds[0], cmds[2]);
       if (cmds[0].Contains(COMMAND_JUMP))
           JumpTo(cmds[1]);
   }
}

テキストファイルを以下のように変更して下さい。

#START&
!background_sprite="background_sprite1"
!charaimg_sprite="polygon"="background_sprite2"
!charaimg_size="polygon"="500, 500, 1"
!charaimg_rotate="polygon"="30,30,0"

&みにに「Hello,World!」

&!charaimg_active="polygon"="true"
!charaimg_anim="polygon"="anim,0,1,EaseInOut%255,255,255,0%1000,1000,0%1000,500,0"

&みにに「これはテキスト表示のサンプルです」

&!charaimg_active="polygon"="true"
!background_sprite="background_sprite2"
!background_color="255,0,255"
!charaimg_anim="polygon"="anim,,,Replay"

&名無し「こんにちは!」
&!jump_to="NEXT"

#END&
!charaimg_delete="polygon"=""

&「ポリゴンを削除しました」

#NEXT&
「ジャンプしました!」
&!jump_to="END"

「ジャンプしました!」が先に表示されればOKです。
使い方は以下の通りです。

・「#《ラベル名》」でラベルを設定
・ゲーム開始時には最初のラベルのテキストが読み込まれる
・「!jump_to="《ラベル名》"」で指定したラベルまでジャンプ/巻き戻る

選択肢の実装

用意ができたところで選択肢の実装をしましょう。
とりあえず選択肢のボタンを表示してみたいと思います。
GameManagerクラスを変更して下さい。

// パラメーターを追加
private const string COMMAND_SELECT = "select";
private const string COMMAND_TEXT = "_text";
private const string SELECT_BUTTON_PREFAB = "SelectButton";
[SerializeField]
private GameObject selectButtons;
private List<Button> _selectButtonList = new List<Button>();

// メソッドを追加
/**
* 選択肢の設定
*/
private void SetSelectButton(string name, string cmd, string parameter)
{
   cmd = cmd.Replace(COMMAND_SELECT, "");
   name = name.Substring(name.IndexOf('"') + 1, name.LastIndexOf('"') - name.IndexOf('"') - 1);
   Button button = _selectButtonList.Find(n => n.name == name);
   if (button == null)
   {
       button = Instantiate(Resources.Load<Button>(prefabsDirectory + SELECT_BUTTON_PREFAB), selectButtons.transform);
       button.name = name;
       _selectButtonList.Add(button);
   }
   SetImage(cmd, parameter, button.image);
}

// メソッドを変更
private void ReadCommand(string cmdLine)
{
   cmdLine = cmdLine.Remove(0, 1);
   Queue<string> cmdQueue = SeparateString(cmdLine, SEPARATE_COMMAND);
   foreach (string cmd in cmdQueue)
   {
       string[] cmds = cmd.Split(COMMAND_SEPARATE_PARAM);
       if (cmds[0].Contains(COMMAND_BACKGROUND))
           SetBackgroundImage(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_CHARACTER_IMAGE))
           SetCharacterImage(cmds[1], cmds[0], cmds[2]);
       if (cmds[0].Contains(COMMAND_JUMP))
           JumpTo(cmds[1]);
       if (cmds[0].Contains(COMMAND_SELECT))
           SetSelectButton(cmds[1], cmds[0], cmds[2]);
   }
}

private void SetImage(string cmd, string parameter, Image image)
{
   cmd = cmd.Replace(" ", "");
   parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
   switch (cmd)
   {
       case COMMAND_TEXT:
           image.GetComponentInChildren<Text>().text = parameter;
           break;
       case COMMAND_SPRITE:
           image.sprite = LoadSprite(parameter);
           break;
       case COMMAND_COLOR:
           image.color = ParameterToColor(parameter);
           break;
       case COMMAND_SIZE:
           image.GetComponent<RectTransform>().sizeDelta = ParameterToVector3(parameter);
           break;
       case COMMAND_POSITION:
           image.GetComponent<RectTransform>().anchoredPosition = ParameterToVector3(parameter);
           break;
       case COMMAND_ROTATION:
           image.GetComponent<RectTransform>().eulerAngles = ParameterToVector3(parameter);
           break;
       case COMMAND_ACTIVE:
           image.gameObject.SetActive(ParameterToBool(parameter));
           break;
       case COMMAND_DELETE:
           _charaImageList.Remove(image);
           Destroy(image.gameObject);
           break;
       case COMMAND_ANIM:
           ImageSetAnimation(image, parameter);
           break;
   }
}​

選択肢をまとめるオブジェクトと選択肢のプレハブを作成するのを忘れていました。なので今作成します。
ヒエラルキータブ内のBackgroundPanelに空の子オブジェクトを作成し、名前を「SelectButtons」にして下さい。それをGame ManagerコンポーネントのSelect Buttonsに設定します。あとは配置やレイアウトコンポーネントを自由に設定して下さい。
次にSelectButtonsの階層下に「UI」-「Button」でボタンを作成し、名前を「SelectButton」にします。そして自由に色などを決め、Assets/Resources/Prefabsフォルダーに入れてプレハブ化して下さい。
テストします。テキストファイルに以下を書いて下さい。

&みにに「Hello,World!」

&!charaimg_active="polygon"="true"
!charaimg_anim="polygon"="anim,0,1,EaseInOut%255,255,255,0%1000,1000,0%1000,500,0"

&みにに「これはテキスト表示のサンプルです」

&!charaimg_active="polygon"="true"
!background_sprite="background_sprite2"
!background_color="255,0,255"
!charaimg_anim="polygon"="anim,,,Replay"

&名無し「こんにちは!」
&!select_text="NEXT1"="こんにちは"
!select_text="NEXT2"="こんばんは"
!select_text="NEXT3"="おはようございます"
&!jump_to="NEXT1"

#END&
!charaimg_delete="polygon"=""

&「ポリゴンを削除しました」

#NEXT1&
「こんにちはを選んだ」
&!jump_to="END"

#NEXT2&
「こんばんはを選んだ」
&!jump_to="END"

#NEXT3&
「おはようございますを選んだ」
&!jump_to="END"

こんな感じに表示されればOKです。

スクリーンショット 2020-11-12 0.48.11

ただ、今のままだと勝手にNEXT1のラベルに飛んでしまいます。かといって最後のjump_toを削除してしまうと実行を終了してしまうので、なんとかする必要があります。
GameManagerクラスを変更して下さい。

// メソッドを変更
private void OnClick()
{
   if (_charQueue.Count > 0) OutputAllChar();
   else
   {
       if (_selectButtonList.Count > 0) return;
       if (!ShowNextPage())
           EditorApplication.isPlaying = false;
   }
}

private void ReadLine(string text)
{
   if (text[0].Equals(SEPARATE_COMMAND))
   {
       ReadCommand(text);
       if (_selectButtonList.Count > 0) return;
       ShowNextPage();
       return;
   }
   string[] ts = text.Split(SEPARATE_MAIN_START);
   string name = ts[0];
   string main = ts[1].Remove(ts[1].LastIndexOf(SEPARATE_MAIN_END));
   if (name.Equals("")) nameText.transform.parent.gameObject.SetActive(false);
   else
   {
       nameText.text = name;
       nameText.transform.parent.gameObject.SetActive(true);
   }
   mainText.text = "";
   _charQueue = SeparateString(main);
   StartCoroutine(ShowChars(captionSpeed));
}

これで実行してみます。

スクリーンショット 2020-11-12 1.10.33

勝手に先に進むことはなくなりました。
しかし今度は何をしても先に進めなくなってしまいました。これをなんとかしましょう。
GameManagerクラスを変更して下さい。

// メソッドを追加
/**
* 選択肢がクリックされた
*/
private void SelectButtonOnClick(string label)
{
   foreach (Button button in _selectButtonList) Destroy(button.gameObject);
   _selectButtonList.Clear();
   JumpTo('"' + label + '"');
   ShowNextPage();
}

// メソッドを変更
private void SetSelectButton(string name, string cmd, string parameter)
{
   cmd = cmd.Replace(COMMAND_SELECT, "");
   name = name.Substring(name.IndexOf('"') + 1, name.LastIndexOf('"') - name.IndexOf('"') - 1);
   Button button = _selectButtonList.Find(n => n.name == name);
   if (button == null)
   {
       button = Instantiate(Resources.Load<Button>(prefabsDirectory + SELECT_BUTTON_PREFAB), selectButtons.transform);
       button.name = name;
       button.onClick.AddListener(() => SelectButtonOnClick(name));
       _selectButtonList.Add(button);
   }
   SetImage(cmd, parameter, button.image);
}

実行してみます。

スクリーンショット 2020-11-12 1.35.09

他のところにもジャンプできるようになりました!
ということで選択肢の出し方は以下です。

選択肢のテキストを設定する
「!select_text="《ジャンプ先ラベル名》"="《選択肢のテキスト》"」
※他、立ち絵画像のように画像を設定することも可能です。その際はcharaimgの代わりにselectと書いて下さい。

ウェイトの実装

アニメーションなどしている間は文字が表示されないほうがいいこともありますよね。そういう時のために待機時間を設定できるようにします。
GameManagerクラスを変更して下さい。

// パラメーターを追加
private const string COMMAND_WAIT_TIME = "wait";
private float _waitTime = 0;

// メソッドを追加
/**
* 待機時間を設定する
*/
private void SetWaitTime(string parameter)
{
   parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
   _waitTime = float.Parse(parameter);
}

/**
* 次の読み込みを待機するコルーチン
*/
private IEnumerator WaitForCommand()
{
   yield return new WaitForSeconds(_waitTime);
   _waitTime = 0;
   ShowNextPage();
   yield break;
}

// メソッドを変更
private void ReadLine(string text)
{
   if (text[0].Equals(SEPARATE_COMMAND))
   {
       ReadCommand(text);
       if (_selectButtonList.Count > 0) return;
       if (_waitTime > 0)
       {
           StartCoroutine(WaitForCommand());
           return;
       }
       ShowNextPage();
       return;
   }
   string[] ts = text.Split(SEPARATE_MAIN_START);
   string name = ts[0];
   string main = ts[1].Remove(ts[1].LastIndexOf(SEPARATE_MAIN_END));
   if (name.Equals("")) nameText.transform.parent.gameObject.SetActive(false);
   else
   {
       nameText.text = name;
       nameText.transform.parent.gameObject.SetActive(true);
   }
   mainText.text = "";
   _charQueue = SeparateString(main);
   StartCoroutine(ShowChars(captionSpeed));
}

private void ReadCommand(string cmdLine)
{
   cmdLine = cmdLine.Remove(0, 1);
   Queue<string> cmdQueue = SeparateString(cmdLine, SEPARATE_COMMAND);
   foreach (string cmd in cmdQueue)
   {
       string[] cmds = cmd.Split(COMMAND_SEPARATE_PARAM);
       if (cmds[0].Contains(COMMAND_BACKGROUND))
           SetBackgroundImage(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_CHARACTER_IMAGE))
           SetCharacterImage(cmds[1], cmds[0], cmds[2]);
       if (cmds[0].Contains(COMMAND_JUMP))
           JumpTo(cmds[1]);
       if (cmds[0].Contains(COMMAND_SELECT))
           SetSelectButton(cmds[1], cmds[0], cmds[2]);
       if (cmds[0].Contains(COMMAND_WAIT_TIME))
           SetWaitTime(cmds[1]);
   }
}

private void OutputAllChar()
{
   StopCoroutine(ShowChars(captionSpeed));
   while (OutputChar()) ;
   _waitTime = 0;
   nextPageIcon.SetActive(true);
}

テキストファイルを次のように編集します。

#START&
!background_sprite="background_sprite1"
!charaimg_sprite="polygon"="background_sprite2"
!charaimg_size="polygon"="500, 500, 1"
!charaimg_rotate="polygon"="30,30,0"

&みにに「Hello,World!」

&!charaimg_active="polygon"="true"
!charaimg_anim="polygon"="anim,0,1,EaseInOut%255,255,255,0%1000,1000,0%1000,500,0"

&みにに「これはテキスト表示のサンプルです」

&!charaimg_active="polygon"="true"
!background_sprite="background_sprite2"
!background_color="255,0,255"
!charaimg_anim="polygon"="anim,,,Replay"
!wait="5"

&名無し「こんにちは!」
&!select_text="NEXT1"="こんにちは"
!select_text="NEXT2"="こんばんは"
!select_text="NEXT3"="おはようございます"
&!jump_to="NEXT1"

#END&
!charaimg_delete="polygon"=""

&「ポリゴンを削除しました」

#NEXT1&
「こんにちはを選んだ」
&!jump_to="END"

#NEXT2&
「こんばんはを選んだ」
&!jump_to="END"

#NEXT3&
「おはようございますを選んだ」
&!jump_to="END"

実行すると2回目のアニメーション時、次の文字が表示されるのに時間がかかるはずです(5秒)。
使い方は以下の通りです。

・「!wait="《秒数》"」で指定した秒数、文字送りを待機

BGM・効果音の再生

ゲームなのですから音も流せるといいですね。という訳で実装します。
GameManagerクラスを変更して下さい。

// パラメーターを追加
private const string COMMAND_BGM = "bgm";
private const string COMMAND_SE = "se";
private const string COMMAND_PLAY = "_play";
private const string COMMAND_MUTE = "_mute";
private const string COMMAND_SOUND = "_sound";
private const string COMMAND_VOLUME = "_volume";
private const string COMMAND_PRIORITY = "_priority";
private const string COMMAND_LOOP = "_loop";
private const string SE_AUDIOSOURCE_PREFAB = "SEAudioSource";
[SerializeField]
private AudioSource bgmAudioSource;
[SerializeField]
private GameObject seAudioSources;
[SerializeField]
private string audioClipsDirectory = "AudioClips/";
private List<AudioSource> _seList = new List<AudioSource>();

// メソッドを追加
/**
* BGMの設定
*/
private void SetBackgroundMusic(string cmd, string parameter)
{
   cmd = cmd.Replace(COMMAND_BGM, "");
   SetAudioSource(cmd, parameter, bgmAudioSource);
}

/**
* 効果音の設定
*/
private void SetSoundEffect(string name, string cmd, string parameter)
{
   cmd = cmd.Replace(COMMAND_SE, "");
   name = name.Substring(name.IndexOf('"') + 1, name.LastIndexOf('"') - name.IndexOf('"') - 1);
   AudioSource audio = _seList.Find(n => n.name == name);
   if (audio == null)
   {
       audio = Instantiate(Resources.Load<AudioSource>(prefabsDirectory + SE_AUDIOSOURCE_PREFAB), seAudioSources.transform);
       audio.name = name;
       _seList.Add(audio);
   }
   SetAudioSource(cmd, parameter, audio);
}

/**
* 音声の設定
*/
private void SetAudioSource(string cmd, string parameter, AudioSource audio)
{
   cmd = cmd.Replace(" ", "");
   parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
   switch (cmd)
   {
       case COMMAND_PLAY:
           audio.Play();
           break;
       case COMMAND_MUTE:
           audio.mute = ParameterToBool(parameter);
           break;
       case COMMAND_SOUND:
           audio.clip = LoadAudioClip(parameter);
           break;
       case COMMAND_VOLUME:
           audio.volume = float.Parse(parameter);
           break;
       case COMMAND_PRIORITY:
           audio.priority = int.Parse(parameter);
           break;
       case COMMAND_LOOP:
           audio.loop = ParameterToBool(parameter);
           break;
       case COMMAND_ACTIVE:
           audio.gameObject.SetActive(ParameterToBool(parameter));
           break;
       case COMMAND_DELETE:
           _seList.Remove(audio);
           Destroy(audio.gameObject);
           break;
   }
}

/**
* 音声ファイルを読み出し、インスタンス化する
*/
private AudioClip LoadAudioClip(string name)
{
   return Instantiate(Resources.Load<AudioClip>(audioClipsDirectory + name));
}

// メソッドを変更
private void ReadCommand(string cmdLine)
{
   cmdLine = cmdLine.Remove(0, 1);
   Queue<string> cmdQueue = SeparateString(cmdLine, SEPARATE_COMMAND);
   foreach (string cmd in cmdQueue)
   {
       string[] cmds = cmd.Split(COMMAND_SEPARATE_PARAM);
       if (cmds[0].Contains(COMMAND_BACKGROUND))
           SetBackgroundImage(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_CHARACTER_IMAGE))
           SetCharacterImage(cmds[1], cmds[0], cmds[2]);
       if (cmds[0].Contains(COMMAND_JUMP))
           JumpTo(cmds[1]);
       if (cmds[0].Contains(COMMAND_SELECT))
           SetSelectButton(cmds[1], cmds[0], cmds[2]);
       if (cmds[0].Contains(COMMAND_WAIT_TIME))
           SetWaitTime(cmds[1]);
       if (cmds[0].Contains(COMMAND_BGM))
           SetBackgroundMusic(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_SE))
           SetSoundEffect(cmds[1], cmds[0], cmds[2]);
   }
}

BGM再生用のオブジェクトとSE再生用のプレハブを用意します。
まずヒエラルキータブのGameManagerに空の子オブジェクトを作成し、名前を「BGMAudioSource」にします。そして「コンポーネントを追加」-「オーディオ」-「オーディオソース」を追加し、これをGame ManagerコンポーネントのBgm Audio Sourceに設定して下さい。
更にGameManagerに空の子オブジェクトを作成し、名前を「SEAudioSources」にします。その階層下に空の子オブジェクトを作成し、名前を「SEAudioSource」にしましょう。そして先ほどと同じようにAudio Sourceコンポーネントを追加し、プレハブ化して下さい。SEAudioSourcesはGame ManagerコンポーネントのSe Audio Sourcesに設定しておきます。
実行してみます。何か適当なBGMとSEを用意し、名前をそれぞれ「bgm1」、「se1」にしましょう。
テキストファイルを編集して下さい。

#START&
!background_sprite="background_sprite1"
!charaimg_sprite="polygon"="background_sprite2"
!charaimg_size="polygon"="500, 500, 1"
!charaimg_rotate="polygon"="30,30,0"
!bgm_sound="bgm1"!bgm_loop="true"
!se_sound="sesample"="se1"
!se_priority="sesample"="150"
!se_play="sesample"=""

&みにに「Hello,World!」

&!charaimg_active="polygon"="true"
!charaimg_anim="polygon"="anim,0,1,EaseInOut%255,255,255,0%1000,1000,0%1000,500,0"
!bgm_play=""

&みにに「これはテキスト表示のサンプルです」

&!charaimg_active="polygon"="true"
!background_sprite="background_sprite2"
!background_color="255,0,255"
!charaimg_anim="polygon"="anim,,,Replay"
!wait="5"

&名無し「こんにちは!」
&!select_text="NEXT1"="こんにちは"
!select_text="NEXT2"="こんばんは"
!select_text="NEXT3"="おはようございます"
&!jump_to="NEXT1"

#END&
!charaimg_delete="polygon"=""

&「ポリゴンを削除しました」

#NEXT1&
!se_play="sesample"=""
&「こんにちはを選んだ」
&!jump_to="END"

#NEXT2&
「こんばんはを選んだ」
&!jump_to="END"

#NEXT3&
「おはようございますを選んだ」
&!jump_to="END"

最初に効果音が一回流れて、次のページでBGMが流れ始めます。そして「こんにちは」を選んだ時のみもう一度効果音が流れます。
音のフェードも実装します。
GameManagerクラスを変更して下さい。

// パラメーターを追加
private const string COMMAND_FADE = "_fade";

// メソッドを追加
/**
* 音のフェードを行うコルーチン
*/
private IEnumerator FadeSound(AudioSource audio, float time, float volume)
{
   float vo = (volume - audio.volume) / (time / Time.deltaTime);
   bool isOut = audio.volume > volume;
   while ((!isOut && audio.volume < volume) || (isOut && audio.volume > volume))
   {
       audio.volume += vo;
       yield return null;
   }
   audio.volume = volume;
}

/**
* 音声にフェードをかける
*/
private void FadeSound(string parameter, AudioSource audio)
{
   string[] ps = parameter.Replace(" ", "").Split(',');
   StartCoroutine(FadeSound(audio, int.Parse(ps[0]), int.Parse(ps[1])));
}

// メソッドを変更
private void SetAudioSource(string cmd, string parameter, AudioSource audio)
{
   cmd = cmd.Replace(" ", "");
   parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
   switch (cmd)
   {
       case COMMAND_PLAY:
           audio.Play();
           break;
       case COMMAND_MUTE:
           audio.mute = ParameterToBool(parameter);
           break;
       case COMMAND_SOUND:
           audio.clip = LoadAudioClip(parameter);
           break;
       case COMMAND_VOLUME:
           audio.volume = float.Parse(parameter);
           break;
       case COMMAND_PRIORITY:
           audio.priority = int.Parse(parameter);
           break;
       case COMMAND_LOOP:
           audio.loop = ParameterToBool(parameter);
           break;
       case COMMAND_FADE:
           FadeSound(audio, parameter);
           break;
       case COMMAND_ACTIVE:
           audio.gameObject.SetActive(ParameterToBool(parameter));
           break;
       case COMMAND_DELETE:
           _seList.Remove(audio);
           Destroy(audio.gameObject);
           break;
   }
}

テストしてみます。
テキストファイルを編集して下さい。

#START&
!background_sprite="background_sprite1"
!charaimg_sprite="polygon"="background_sprite2"
!charaimg_size="polygon"="500, 500, 1"
!charaimg_rotate="polygon"="30,30,0"
!bgm_sound="bgm1"!bgm_loop="true"
!bgm_volume="0"
!se_sound="sesample"="se1"
!se_priority="sesample"="150"
!se_play="sesample"=""

&みにに「Hello,World!」

&!charaimg_active="polygon"="true"
!charaimg_anim="polygon"="anim,0,1,EaseInOut%255,255,255,0%1000,1000,0%1000,500,0"
!bgm_play=""!bgm_fade="5,1"

&みにに「これはテキスト表示のサンプルです」

&!charaimg_active="polygon"="true"
!background_sprite="background_sprite2"
!background_color="255,0,255"
!charaimg_anim="polygon"="anim,,,Replay"
!wait="5"

&名無し「こんにちは!」
&!select_text="NEXT1"="こんにちは"
!select_text="NEXT2"="こんばんは"
!select_text="NEXT3"="おはようございます"
&!jump_to="NEXT1"

#END&
!charaimg_delete="polygon"=""
!bgm_mute="false"
&「ポリゴンを削除しました」

#NEXT1&
!se_play="sesample"=""
&「こんにちはを選んだ」
&!jump_to="END"

#NEXT2&
!bgm_fade="5,0"
&「こんばんはを選んだ」
&!jump_to="END"

#NEXT3&
「おはようございますを選んだ」
&!jump_to="END"

最初に流れるBGMがフェードインして流れます。また、「こんばんは」を選ぶとフェードアウトしていきます。
ということで音声の実装が完了しました。
追加した仕様はこちらです。まずはBGMです。

BGMを設定する
「!bgm_sound="《オーディオクリップ名》"」
BGMを流す
「!bgm_play=""」
一時停止(true)・再開する(false)
「!bgm_mute="《true(TRUE)orそれ以外》"」
音量を設定する
「!bgm_volume="《音量》"」
優先度を設定する
「!bgm_priority="《優先度》"」
ループ再生を設定する
「!bgm_loop="《true(TRUE)orそれ以外》"」

効果音はこんな感じです。基本はBGMと同じですね。

効果音を設定する
「!se_sound="《オブジェクト名》"="《オーディオクリップ名》"」
効果音を流す
「!se_play="《オブジェクト名》"=""」
一時停止(true)・再開する(false)
「!se_mute="《オブジェクト名》"="《true(TRUE)orそれ以外》"」
音量を設定する
「!se_volume="《オブジェクト名》"="《音量》"」
優先度を設定する
「!se_priority="《オブジェクト名》"="《優先度》"」
ループ再生を設定する
「!se_loop="《オブジェクト名》"="《true(TRUE)orそれ以外》"」
アクティブ・非アクティブを設定する

「!se_active="《オブジェクト名》"="《true(TRUE)orそれ以外》"」
削除する
「!se_delete="《オブジェクト名》"=""」

画面のフェードイン・アウト

音にフェードをかけられるなら、画面もフェードできるとなおいいですね。
それをするには文字ウィンドウより前にパネルを用意し、アニメーションさせればいいでしょう。
まずヒエラルキータブ内のGameManager階層下にPanelを作成し、名前を「ForegroundPanel」にします。そしてImageコンポーネントの「レイキャストターゲット」のチェックを外しておきます。
次に作成したForegroundPanelにアニメーターコンポーネントを追加し、コントローラーに「CharacterImageAnimator」を設定します。
(ちなみに今のヒエラルキータブはこんな感じになっています)

スクリーンショット 2020-11-12 13.43.00

コードを書きます。
GameManagerクラスを変更して下さい。

// パラメーターを追加
private const string COMMAND_FOREGROUND = "foreground";
[SerializeField]
private Image foregroundImage;

// メソッドを追加
/**
* 前景の設定
*/
private void SetForegroundImage(string cmd, string parameter)
{
   cmd = cmd.Replace(COMMAND_FOREGROUND, "");
   SetImage(cmd, parameter, foregroundImage);
}

// メソッドを変更
private void ReadCommand(string cmdLine)
{
   cmdLine = cmdLine.Remove(0, 1);
   Queue<string> cmdQueue = SeparateString(cmdLine, SEPARATE_COMMAND);
   foreach (string cmd in cmdQueue)
   {
       string[] cmds = cmd.Split(COMMAND_SEPARATE_PARAM);
       if (cmds[0].Contains(COMMAND_BACKGROUND))
           SetBackgroundImage(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_FOREGROUND))
           SetForegroundImage(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_CHARACTER_IMAGE))
           SetCharacterImage(cmds[1], cmds[0], cmds[2]);
       if (cmds[0].Contains(COMMAND_JUMP))
           JumpTo(cmds[1]);
       if (cmds[0].Contains(COMMAND_SELECT))
           SetSelectButton(cmds[1], cmds[0], cmds[2]);
       if (cmds[0].Contains(COMMAND_WAIT_TIME))
           SetWaitTime(cmds[1]);
       if (cmds[0].Contains(COMMAND_BGM))
           SetBackgroundMusic(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_SE))
           SetSoundEffect(cmds[1], cmds[0], cmds[2]);
   }
}

ビルドしたら、Game ManagerコンポーネントのForeground ImageにForegroundPanelを設定しておきます。
テストします。
テキストファイルを以下のように編集して下さい。

#START&
!foreground_color="0,0,0"
!foreground_anim="fadein,0,2,%0,0,0,0%%"
!wait="2"
!background_sprite="background_sprite1"
!charaimg_sprite="polygon"="background_sprite2"
!charaimg_size="polygon"="500, 500, 1"
!charaimg_rotate="polygon"="30,30,0"
!bgm_sound="bgm1"!bgm_loop="true"
!bgm_volume="0"
!se_sound="sesample"="se1"
!se_priority="sesample"="150"
!se_play="sesample"=""

&みにに「Hello,World!」

&!charaimg_active="polygon"="true"
!charaimg_anim="polygon"="anim,0,1,EaseInOut%255,255,255,0%1000,1000,0%1000,500,0"
!bgm_play=""!bgm_fade="5,1"

&みにに「これはテキスト表示のサンプルです」

&!charaimg_active="polygon"="true"
!background_sprite="background_sprite2"
!background_color="255,0,255"
!charaimg_anim="polygon"="anim,,,Replay"
!wait="5"

&名無し「こんにちは!」
&!select_text="NEXT1"="こんにちは"
!select_text="NEXT2"="こんばんは"
!select_text="NEXT3"="おはようございます"
&!jump_to="NEXT1"

#END&
!charaimg_delete="polygon"=""
!bgm_mute="false"
&「ポリゴンを削除しました」
&!foreground_anim="fadeout,0,2,%0,0,0%%"
!wait="2"

#NEXT1&
!se_play="sesample"=""
&「こんにちはを選んだ」
&!jump_to="END"

#NEXT2&
!bgm_fade="5,0"
&「こんばんはを選んだ」
&!jump_to="END"

#NEXT3&
「おはようございますを選んだ」
&!jump_to="END"

実行すると以下のようになります。

フェードインアウト2d

フェードイン・アウトの方法はこちらです。

フェードイン
!foreground_color="0,0,0"(任意の色で初期化する)
!foreground_anim="fadein,0,2,%0,0,0,0%%"(アニメーションで透明度を下げる)
フェードアウト
!foreground_color="0,0,0,0"(任意の色で初期化する)
!foreground_anim="fadeout,0,2,%0,0,0%%"(アニメーションで透明度を上げる)

シーン切り替え

当然、シーンが一つだけということはありえないと思います。勿論簡単なゲームならそういうこともあるでしょうが、一つのテキストファイルに全ての処理を書くのは大変でしょう。シーン切り替えの必要がありますね。
ということで、その実装をしていきます。
GameManagerクラスを変更して下さい。

// パラメーターを追加
private const string COMMAND_CHANGE_SCENE = "scene";

// メソッドを追加
/**
* 対応するシーンに切り替える
*/
private void ChangeNextScene(string parameter)
{
   parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
   SceneManager.LoadSceneAsync(parameter);
}

// メソッドを変更
/**
* コマンドの読み出し
*/
private void ReadCommand(string cmdLine)
{
   cmdLine = cmdLine.Remove(0, 1);
   Queue<string> cmdQueue = SeparateString(cmdLine, SEPARATE_COMMAND);
   foreach (string cmd in cmdQueue)
   {
       string[] cmds = cmd.Split(COMMAND_SEPARATE_PARAM);
       if (cmds[0].Contains(COMMAND_BACKGROUND))
           SetBackgroundImage(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_FOREGROUND))
           SetForegroundImage(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_CHARACTER_IMAGE))
           SetCharacterImage(cmds[1], cmds[0], cmds[2]);
       if (cmds[0].Contains(COMMAND_JUMP))
           JumpTo(cmds[1]);
       if (cmds[0].Contains(COMMAND_SELECT))
           SetSelectButton(cmds[1], cmds[0], cmds[2]);
       if (cmds[0].Contains(COMMAND_WAIT_TIME))
           SetWaitTime(cmds[1]);
       if (cmds[0].Contains(COMMAND_BGM))
           SetBackgroundMusic(cmds[0], cmds[1]);
       if (cmds[0].Contains(COMMAND_SE))
           SetSoundEffect(cmds[1], cmds[0], cmds[2]);
       if (cmds[0].Contains(COMMAND_CHANGE_SCENE))
           ChangeNextScene(cmds[1]);
   }
}

テキストファイルに以下を記述して下さい。

#START&
!foreground_color="0,0,0"
!foreground_anim="fadein,0,2,%0,0,0,0%%"
!wait="2"
!background_sprite="background_sprite1"
!charaimg_sprite="polygon"="background_sprite2"
!charaimg_size="polygon"="500, 500, 1"
!charaimg_rotate="polygon"="30,30,0"
!bgm_sound="bgm1"!bgm_loop="true"
!bgm_volume="0"
!se_sound="sesample"="se1"
!se_priority="sesample"="150"
!se_play="sesample"=""

&みにに「Hello,World!」

&!charaimg_active="polygon"="true"
!charaimg_anim="polygon"="anim,0,1,EaseInOut%255,255,255,0%1000,1000,0%1000,500,0"
!bgm_play=""!bgm_fade="5,1"

&みにに「これはテキスト表示のサンプルです」

&!wait="5"
&!charaimg_active="polygon"="true"
!background_sprite="background_sprite2"
!background_color="255,0,255"
!charaimg_anim="polygon"="anim,,,Replay"
!wait="5"

&名無し「こんにちは!」
&!select_text="NEXT1"="こんにちは"
!select_text="NEXT2"="こんばんは"
!select_text="NEXT3"="おはようございます"
&!jump_to="NEXT1"

#END&
!charaimg_delete="polygon"=""
!bgm_mute="false"
&「ポリゴンを削除しました」
&!foreground_anim="fadeout,0,2,%0,0,0%%"
!wait="2"&
!scene="NextScene"

#NEXT1&
!se_play="sesample"=""
&「こんにちはを選んだ」
&!jump_to="END"

#NEXT2&
!bgm_fade="5,0"
&「こんばんはを選んだ」
&!jump_to="END"

#NEXT3&
「おはようございますを選んだ」
&!jump_to="END"

テストの為に以下のことをする必要があります。
新しいシーンでもノベルゲームを続けるのであれば、現在のGameManagerを一旦プレハブ化します。ただし削除はしないでください。
次にMainSceneを保存してから、新しいシーンをAssets/Scenesフォルダ内に作成します。名前を「NextScene」にして保存して下さい。
ヒエラルキータブにCanvasを作成し、その中にプレハブ化したGameManagerを入れましょう。こんな感じになればOKです。

スクリーンショット 2020-11-12 17.53.56

Game ManagerコンポーネントのText Fileの項目を「Texts/Scenario2」にします。
テキストファイルを「Scenario2.txt」という名前で作成します。それに以下のように記述して下さい。

#START&
!foreground_color="0,0,0"
!foreground_anim="fadein,0,2,%0,0,0,0%%"
!wait="2"
!se_sound="sesample"="se1"
!se_priority="sesample"="150"
!se_play="sesample"=""

&「シーンが切り替わりました!」

Unityエディターの上部の「ファイル」-「ビルド設定」をクリックします。すると以下のような画面が現れるはずです。

スクリーンショット 2020-11-12 18.04.05

この画面の、「ビルドに含まれるシーン」にMainSceneとNextSceneをドラッグ&ドロップします。するとこんな感じになります。

スクリーンショット 2020-11-12 18.06.08

NextSceneを保存したら、MainSceneを開き直して実行します。
最後にシーンが切り替わり、この画面が表示されればOKです。

スクリーンショット 2020-11-12 18.20.27

シーン切り替えの方法はこちら。

「!scene="《シーン名》"」でシーンが切り替わる

メッセージウィンドウの非表示

プレイヤーが任意でウィンドウの表示・非表示を切り替えられるといいですね。という訳で右クリックされたらウィンドウを非表示にします。
GameManagerクラスを変更して下さい。

// メソッドを追加
/**
* 右クリックした時の処理
*/
private void OnClickRight()
{
   GameObject mainWindow = mainText.transform.parent.gameObject;
   GameObject nameWindow = nameText.transform.parent.gameObject;
   mainWindow.SetActive(!mainWindow.activeSelf);
   nameWindow.SetActive(mainWindow.activeSelf);
   if (_charQueue.Count <= 0)
           nextPageIcon.SetActive(mainWindow.activeSelf);
}

// メソッドを変更
private void Update()
{
   if (Input.GetMouseButtonDown(0)) OnClick();
   if (Input.GetMouseButtonDown(1)) OnClickRight();
}

とりあえずこれで実行してみます。右クリックするとウィンドウが消えたと思います。

スクリーンショット 2020-11-12 18.40.07

ただしこれには一つ問題があります。ウィンドウが消えても読み込みが止まらず、クリックにも反応してしまうことです。
これを修正するには、このようにします。
GameManagerクラスを変更して下さい。

// メソッドを変更
private void OnClick()
{
   if (!mainText.transform.parent.gameObject.activeSelf) return;
   if (_charQueue.Count > 0) OutputAllChar();
   else
   {
       if (_selectButtonList.Count > 0) return;
       if (!ShowNextPage())
           EditorApplication.isPlaying = false;
   }
}

private IEnumerator ShowChars(float wait)
{
   while (true)
   {
       if (mainText.transform.parent.gameObject.activeSelf)
       {
           if (!OutputChar()) break;
       }
       yield return new WaitForSeconds(wait);
   }
   yield break;
}

これで大丈夫なはずです。

※2020/11/13追記
名前表示ウィンドウの表示にバグがありましたので修正します。
GameManagerクラスを変更して下さい。

private void OnClickRight()
{
   GameObject mainWindow = mainText.transform.parent.gameObject;
   GameObject nameWindow = nameText.transform.parent.gameObject;
   mainWindow.SetActive(!mainWindow.activeSelf);
   if (nameText.text.Length > 0)
       nameWindow.SetActive(mainWindow.activeSelf);
   if (_charQueue.Count <= 0)
       nextPageIcon.SetActive(mainWindow.activeSelf);
}

private void ReadLine(string text)
{
   if (text[0].Equals(SEPARATE_COMMAND))
   {
       ReadCommand(text);
       if (_selectButtonList.Count > 0) return;
       if (_waitTime > 0)
       {
           StartCoroutine(WaitForCommand());
           return;
       }
       ShowNextPage();
       return;
   }
   string[] ts = text.Split(SEPARATE_MAIN_START);
   string name = ts[0];
   string main = ts[1].Remove(ts[1].LastIndexOf(SEPARATE_MAIN_END));
   nameText.text = name;
   if (name.Equals("")) nameText.transform.parent.gameObject.SetActive(false);
   else nameText.transform.parent.gameObject.SetActive(true);
   mainText.text = "";
   _charQueue = SeparateString(main);
   StartCoroutine(ShowChars(captionSpeed));
}

まとめ

以上でサウンドノベルで必要な最低限の機能は実装できたかと思います。
勿論他にも実装しなければいけないものはありますが、それらの実装をするとなると長くなり過ぎてしまうので(既に6万字近い)とりあえずここで終了しておきます。
(需要があればもしかしたら続きを書くかもしれません......)
最後に今回作成したGameManagerスクリプトを載せて終わりたいと思います。これで8万字超えですね!

GameManagerスクリプト

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class GameManager : MonoBehaviour
{
   private const char SEPARATE_SUBSCENE = '#';
   private const char SEPARATE_PAGE = '&';
   private const char SEPARATE_COMMAND = '!';
   private const char SEPARATE_MAIN_START = '「';
   private const char SEPARATE_MAIN_END = '」';

   private const char COMMAND_SEPARATE_PARAM = '=';
   private const char COMMAND_SEPARATE_ANIM = '%';
   private const string COMMAND_BACKGROUND = "background";
   private const string COMMAND_FOREGROUND = "foreground";
   private const string COMMAND_CHARACTER_IMAGE = "charaimg";
   private const string COMMAND_BGM = "bgm";
   private const string COMMAND_SE = "se";
   private const string COMMAND_JUMP = "jump_to";
   private const string COMMAND_SELECT = "select";
   private const string COMMAND_WAIT_TIME = "wait";
   private const string COMMAND_CHANGE_SCENE = "scene";
   private const string COMMAND_TEXT = "_text";
   private const string COMMAND_SPRITE = "_sprite";
   private const string COMMAND_COLOR = "_color";
   private const string COMMAND_SIZE = "_size";
   private const string COMMAND_POSITION = "_pos";
   private const string COMMAND_ROTATION = "_rotate";
   private const string COMMAND_ACTIVE = "_active";
   private const string COMMAND_DELETE = "_delete";
   private const string COMMAND_ANIM = "_anim";
   private const string COMMAND_PLAY = "_play";
   private const string COMMAND_MUTE = "_mute";
   private const string COMMAND_SOUND = "_sound";
   private const string COMMAND_VOLUME = "_volume";
   private const string COMMAND_PRIORITY = "_priority";
   private const string COMMAND_LOOP = "_loop";
   private const string COMMAND_FADE = "_fade";

   private const string CHARACTER_IMAGE_PREFAB = "CharacterImage";
   private const string SELECT_BUTTON_PREFAB = "SelectButton";
   private const string SE_AUDIOSOURCE_PREFAB = "SEAudioSource";

   [SerializeField]
   private Image backgroundImage;
   [SerializeField]
   private Image foregroundImage;
   [SerializeField]
   private GameObject characterImages;
   [SerializeField]
   private GameObject selectButtons;
   [SerializeField]
   private Text mainText;
   [SerializeField]
   private Text nameText;
   [SerializeField]
   private GameObject nextPageIcon;
   [SerializeField]
   private AudioSource bgmAudioSource;
   [SerializeField]
   private GameObject seAudioSources;
   [SerializeField]
   private string spritesDirectory = "Sprites/";
   [SerializeField]
   private string prefabsDirectory = "Prefabs/";
   [SerializeField]
   private string audioClipsDirectory = "AudioClips/";
   [SerializeField]
   private string textFile = "Texts/Scenario";
   [SerializeField]
   private string animationsDirectory = "Animations/";
   [SerializeField]
   private string overrideAnimationClipName = "Clip";
   [SerializeField]
   private float captionSpeed = 0.2f;
   private float _waitTime = 0;
   private string _text = "";
   private Dictionary<string, Queue<string>> _subScenes =
       new Dictionary<string, Queue<string>>();
   private Queue<string> _pageQueue;
   private Queue<char> _charQueue;
   private List<Image> _charaImageList = new List<Image>();
   private List<Button> _selectButtonList = new List<Button>();
   private List<AudioSource> _seList = new List<AudioSource>();

   private void Start()
   {
       Init();
   }

   private void Update()
   {
       if (Input.GetMouseButtonDown(0)) OnClick();
       if (Input.GetMouseButtonDown(1)) OnClickRight();
   }

   /**
    * 初期化する
    */
   private void Init()
   {
       _text = LoadTextFile(textFile);
       Queue<string> subScenes = SeparateString(_text, SEPARATE_SUBSCENE);
       foreach (string subScene in subScenes)
       {
           if (subScene.Equals("")) continue;
           Queue<string> pages = SeparateString(subScene, SEPARATE_PAGE);
           _subScenes[pages.Dequeue()] = pages;
       }
       _pageQueue = _subScenes.First().Value;
       ShowNextPage();
   }

   /**
    * クリックしたときの処理
    */
   private void OnClick()
   {
       if (!mainText.transform.parent.gameObject.activeSelf) return;
       if (_charQueue.Count > 0) OutputAllChar();
       else
       {
           if (_selectButtonList.Count > 0) return;
           if (!ShowNextPage())
               EditorApplication.isPlaying = false;
       }
   }

   /**
    * 右クリックした時の処理
    */
   private void OnClickRight()
   {
       GameObject mainWindow = mainText.transform.parent.gameObject;
       GameObject nameWindow = nameText.transform.parent.gameObject;
       mainWindow.SetActive(!mainWindow.activeSelf);
       if (nameText.text.Length > 0)
           nameWindow.SetActive(mainWindow.activeSelf);
       if (_charQueue.Count <= 0)
           nextPageIcon.SetActive(mainWindow.activeSelf);
   }

   /**
    * 文字送りするコルーチン
    */
   private IEnumerator ShowChars(float wait)
   {
       while (true)
       {
           if (mainText.transform.parent.gameObject.activeSelf)
           {
               if (!OutputChar()) break;
           }
           yield return new WaitForSeconds(wait);
       }
       yield break;
   }

   /**
    * 次の読み込みを待機するコルーチン
    */
   private IEnumerator WaitForCommand()
   {
       yield return new WaitForSeconds(_waitTime);
       _waitTime = 0;
       ShowNextPage();
       yield break;
   }

   /**
    * 音のフェードを行うコルーチン
    */
   private IEnumerator FadeSound(AudioSource audio, float time, float volume)
   {
       float vo = (volume - audio.volume) / (time / Time.deltaTime);
       bool isOut = audio.volume > volume;
       while ((!isOut && audio.volume < volume) || (isOut && audio.volume > volume))
       {
           audio.volume += vo;
           yield return null;
       }
       audio.volume = volume;
   }

   /**
    * 1文字を出力する
    */
   private bool OutputChar()
   {
       if (_charQueue.Count <= 0)
       {
           nextPageIcon.SetActive(true);
           return false;
       }
       mainText.text += _charQueue.Dequeue();
       return true;
   }

   /**
    * 全文を表示する
    */
   private void OutputAllChar()
   {
       StopCoroutine(ShowChars(captionSpeed));
       while (OutputChar()) ;
       _waitTime = 0;
       nextPageIcon.SetActive(true);
   }

   /**
    * 次のページを表示する
    */
   private bool ShowNextPage()
   {
       if (_pageQueue.Count <= 0) return false;
       nextPageIcon.SetActive(false);
       ReadLine(_pageQueue.Dequeue());
       return true;
   }

   /**
    * 文字列を指定した区切り文字ごとに区切り、キューに格納したものを返す
    */
   private Queue<string> SeparateString(string str, char sep)
   {
       string[] strs = str.Split(sep);
       Queue<string> queue = new Queue<string>();
       foreach (string l in strs) queue.Enqueue(l);
       return queue;
   }

   /**
    * 文を1文字ごとに区切り、キューに格納したものを返す
    */
   private Queue<char> SeparateString(string str)
   {
       char[] chars = str.ToCharArray();
       Queue<char> charQueue = new Queue<char>();
       foreach (char c in chars) charQueue.Enqueue(c);
       return charQueue;
   }

   /**
    * 1行を読み出す
    */
   private void ReadLine(string text)
   {
       if (text[0].Equals(SEPARATE_COMMAND))
       {
           ReadCommand(text);
           if (_selectButtonList.Count > 0) return;
           if (_waitTime > 0)
           {
               StartCoroutine(WaitForCommand());
               return;
           }
           ShowNextPage();
           return;
       }
       string[] ts = text.Split(SEPARATE_MAIN_START);
       string name = ts[0];
       string main = ts[1].Remove(ts[1].LastIndexOf(SEPARATE_MAIN_END));
       nameText.text = name;
       if (name.Equals("")) nameText.transform.parent.gameObject.SetActive(false);
       else nameText.transform.parent.gameObject.SetActive(true);
       mainText.text = "";
       _charQueue = SeparateString(main);
       StartCoroutine(ShowChars(captionSpeed));
   }

   /**
    * コマンドの読み出し
    */
   private void ReadCommand(string cmdLine)
   {
       cmdLine = cmdLine.Remove(0, 1);
       Queue<string> cmdQueue = SeparateString(cmdLine, SEPARATE_COMMAND);
       foreach (string cmd in cmdQueue)
       {
           string[] cmds = cmd.Split(COMMAND_SEPARATE_PARAM);
           if (cmds[0].Contains(COMMAND_BACKGROUND))
               SetBackgroundImage(cmds[0], cmds[1]);
           if (cmds[0].Contains(COMMAND_FOREGROUND))
               SetForegroundImage(cmds[0], cmds[1]);
           if (cmds[0].Contains(COMMAND_CHARACTER_IMAGE))
               SetCharacterImage(cmds[1], cmds[0], cmds[2]);
           if (cmds[0].Contains(COMMAND_JUMP))
               JumpTo(cmds[1]);
           if (cmds[0].Contains(COMMAND_SELECT))
               SetSelectButton(cmds[1], cmds[0], cmds[2]);
           if (cmds[0].Contains(COMMAND_WAIT_TIME))
               SetWaitTime(cmds[1]);
           if (cmds[0].Contains(COMMAND_BGM))
               SetBackgroundMusic(cmds[0], cmds[1]);
           if (cmds[0].Contains(COMMAND_SE))
               SetSoundEffect(cmds[1], cmds[0], cmds[2]);
           if (cmds[0].Contains(COMMAND_CHANGE_SCENE))
               ChangeNextScene(cmds[1]);
       }
   }

   /**
    * 対応するシーンに切り替える
    */
   private void ChangeNextScene(string parameter)
   {
       parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
       SceneManager.LoadSceneAsync(parameter);
   }

   /**
    * 対応するラベルまでジャンプする
    */
   private void JumpTo(string parameter)
   {
       parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
       _pageQueue = _subScenes[parameter];
   }

   /**
    * 待機時間を設定する
    */
   private void SetWaitTime(string parameter)
   {
       parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
       _waitTime = float.Parse(parameter);
   }

   /**
    * 背景の設定
    */
   private void SetBackgroundImage(string cmd, string parameter)
   {
       cmd = cmd.Replace(COMMAND_BACKGROUND, "");
       SetImage(cmd, parameter, backgroundImage);
   }

   /**
    * 前景の設定
    */
   private void SetForegroundImage(string cmd, string parameter)
   {
       cmd = cmd.Replace(COMMAND_FOREGROUND, "");
       SetImage(cmd, parameter, foregroundImage);
   }

   /**
    * 立ち絵の設定
    */
   private void SetCharacterImage(string name, string cmd, string parameter)
   {
       cmd = cmd.Replace(COMMAND_CHARACTER_IMAGE, "");
       name = name.Substring(name.IndexOf('"') + 1, name.LastIndexOf('"') - name.IndexOf('"') - 1);
       Image image = _charaImageList.Find(n => n.name == name);
       if (image == null)
       {
           image = Instantiate(Resources.Load<Image>(prefabsDirectory + CHARACTER_IMAGE_PREFAB), characterImages.transform);
           image.name = name;
           _charaImageList.Add(image);
       }
       SetImage(cmd, parameter, image);
   }

   /**
    * 選択肢の設定
    */
   private void SetSelectButton(string name, string cmd, string parameter)
   {
       cmd = cmd.Replace(COMMAND_SELECT, "");
       name = name.Substring(name.IndexOf('"') + 1, name.LastIndexOf('"') - name.IndexOf('"') - 1);
       Button button = _selectButtonList.Find(n => n.name == name);
       if (button == null)
       {
           button = Instantiate(Resources.Load<Button>(prefabsDirectory + SELECT_BUTTON_PREFAB), selectButtons.transform);
           button.name = name;
           button.onClick.AddListener(() => SelectButtonOnClick(name));
           _selectButtonList.Add(button);
       }
       SetImage(cmd, parameter, button.image);
   }

   /**
    * 選択肢がクリックされた
    */
   private void SelectButtonOnClick(string label)
   {
       foreach (Button button in _selectButtonList) Destroy(button.gameObject);
       _selectButtonList.Clear();
       JumpTo('"' + label + '"');
       ShowNextPage();
   }

   /**
    * 画像の設定
    */
   private void SetImage(string cmd, string parameter, Image image)
   {
       cmd = cmd.Replace(" ", "");
       parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
       switch (cmd)
       {
           case COMMAND_TEXT:
               image.GetComponentInChildren<Text>().text = parameter;
               break;
           case COMMAND_SPRITE:
               image.sprite = LoadSprite(parameter);
               break;
           case COMMAND_COLOR:
               image.color = ParameterToColor(parameter);
               break;
           case COMMAND_SIZE:
               image.GetComponent<RectTransform>().sizeDelta = ParameterToVector3(parameter);
               break;
           case COMMAND_POSITION:
               image.GetComponent<RectTransform>().anchoredPosition = ParameterToVector3(parameter);
               break;
           case COMMAND_ROTATION:
               image.GetComponent<RectTransform>().eulerAngles = ParameterToVector3(parameter);
               break;
           case COMMAND_ACTIVE:
               image.gameObject.SetActive(ParameterToBool(parameter));
               break;
           case COMMAND_DELETE:
               _charaImageList.Remove(image);
               Destroy(image.gameObject);
               break;
           case COMMAND_ANIM:
               ImageSetAnimation(image, parameter);
               break;
       }
   }

   /**
    * スプライトをファイルから読み出し、インスタンス化する
    */
   private Sprite LoadSprite(string name)
   {
       return Instantiate(Resources.Load<Sprite>(spritesDirectory + name));
   }

   /**
    * パラメーターから色を作成する
    */
   private Color ParameterToColor(string parameter)
   {
       string[] ps = parameter.Replace(" ", "").Split(',');
       if (ps.Length > 3)
           return new Color32(byte.Parse(ps[0]), byte.Parse(ps[1]),
                                           byte.Parse(ps[2]), byte.Parse(ps[3]));
       else
           return new Color32(byte.Parse(ps[0]), byte.Parse(ps[1]),
                                           byte.Parse(ps[2]), 255);
   }

   /**
    * パラメーターからベクトルを取得する
    */
   private Vector3 ParameterToVector3(string parameter)
   {
       string[] ps = parameter.Replace(" ", "").Split(',');
       return new Vector3(float.Parse(ps[0]), float.Parse(ps[1]), float.Parse(ps[2]));
   }

   /**
    * パラメーターからboolを取得する
    */
   private bool ParameterToBool(string parameter)
   {
       string p = parameter.Replace(" ", "");
       return p.Equals("true") || p.Equals("TRUE");
   }

   /**
    * アニメーションを画像に設定する
    */
   private void ImageSetAnimation(Image image, string parameter)
   {
       Animator animator = image.GetComponent<Animator>();
       AnimationClip clip = ParameterToAnimationClip(image, parameter.Split(COMMAND_SEPARATE_ANIM));
       AnimatorOverrideController overrideController;
       if (animator.runtimeAnimatorController is AnimatorOverrideController)
           overrideController = (AnimatorOverrideController)animator.runtimeAnimatorController;
       else
       {
           overrideController = new AnimatorOverrideController();
           overrideController.runtimeAnimatorController = animator.runtimeAnimatorController;
           animator.runtimeAnimatorController = overrideController;
       }
       overrideController[overrideAnimationClipName] = clip;
       animator.Update(0.0f);
       animator.Play(overrideAnimationClipName, 0);
   }

   /**
    * パラメーターからアニメーションクリップを生成する
    */
   private AnimationClip ParameterToAnimationClip(Image image, string[] parameters)
   {
       string[] ps = parameters[0].Replace(" ", "").Split(',');
       string path = animationsDirectory + SceneManager.GetActiveScene().name + "/" + image.name;
       AnimationClip prevAnimation = Resources.Load<AnimationClip>(path + "/" + ps[0]);
       AnimationClip animation;
       #if UNITY_EDITOR
           if (ps[3].Equals("Replay") && prevAnimation != null)
               return Instantiate(prevAnimation);
           animation = new AnimationClip();
           Color startcolor = image.color;
           Vector3[] start = new Vector3[3];
           start[0] = image.GetComponent<RectTransform>().sizeDelta;
           start[1] = image.GetComponent<RectTransform>().anchoredPosition;
           Color endcolor = startcolor;
           if (parameters[1] != "") endcolor = ParameterToColor(parameters[1]);
           Vector3[] end = new Vector3[3];
           for (int i = 0; i < 2; i++)
           {
               if (parameters[i + 2] != "")
                   end[i] = ParameterToVector3(parameters[i + 2]);
               else end[i] = start[i];
           }
           AnimationCurve[,] curves = new AnimationCurve[4, 4];
           if (ps[3].Equals("EaseInOut"))
           {
               curves[0, 0] = AnimationCurve.EaseInOut(float.Parse(ps[1]), startcolor.r, float.Parse(ps[2]), endcolor.r);
               curves[0, 1] = AnimationCurve.EaseInOut(float.Parse(ps[1]), startcolor.g, float.Parse(ps[2]), endcolor.g);
               curves[0, 2] = AnimationCurve.EaseInOut(float.Parse(ps[1]), startcolor.b, float.Parse(ps[2]), endcolor.b);
               curves[0, 3] = AnimationCurve.EaseInOut(float.Parse(ps[1]), startcolor.a, float.Parse(ps[2]), endcolor.a);
               for (int i = 0; i < 2; i++)
               {
                   curves[i + 1, 0] = AnimationCurve.EaseInOut(float.Parse(ps[1]), start[i].x, float.Parse(ps[2]), end[i].x);
                   curves[i + 1, 1] = AnimationCurve.EaseInOut(float.Parse(ps[1]), start[i].y, float.Parse(ps[2]), end[i].y);
                   curves[i + 1, 2] = AnimationCurve.EaseInOut(float.Parse(ps[1]), start[i].z, float.Parse(ps[2]), end[i].z);
               }
           }
           else
           {
               curves[0, 0] = AnimationCurve.Linear(float.Parse(ps[1]), startcolor.r, float.Parse(ps[2]), endcolor.r);
               curves[0, 1] = AnimationCurve.Linear(float.Parse(ps[1]), startcolor.g, float.Parse(ps[2]), endcolor.g);
               curves[0, 2] = AnimationCurve.Linear(float.Parse(ps[1]), startcolor.b, float.Parse(ps[2]), endcolor.b);
               curves[0, 3] = AnimationCurve.Linear(float.Parse(ps[1]), startcolor.a, float.Parse(ps[2]), endcolor.a);
               for (int i = 0; i < 2; i++)
               {
                   curves[i + 1, 0] = AnimationCurve.Linear(float.Parse(ps[1]), start[i].x, float.Parse(ps[2]), end[i].x);
                   curves[i + 1, 1] = AnimationCurve.Linear(float.Parse(ps[1]), start[i].y, float.Parse(ps[2]), end[i].y);
                   curves[i + 1, 2] = AnimationCurve.Linear(float.Parse(ps[1]), start[i].z, float.Parse(ps[2]), end[i].z);
               }
           }
           string[] b1 = { "r", "g", "b", "a" };
           for (int i = 0; i < 4; i++)
           {
               AnimationUtility.SetEditorCurve(
                   animation,
                   EditorCurveBinding.FloatCurve("", typeof(Image), "m_Color." + b1[i]),
                   curves[0, i]
               );
           }
           string[] a = { "m_SizeDelta", "m_AnchoredPosition" };
           string[] b2 = { "x", "y", "z" };
           for (int i = 0; i < 2; i++)
           {
               for (int j = 0; j < 3; j++)
               {
                   AnimationUtility.SetEditorCurve(
                       animation,
                       EditorCurveBinding.FloatCurve("", typeof(RectTransform), a[i] + "." + b2[j]),
                       curves[i + 1, j]
                   );
               }   
           }
           if (!Directory.Exists("Assets/Resources/" + path))
               Directory.CreateDirectory("Assets/Resources/" + path);
           AssetDatabase.CreateAsset(animation, "Assets/Resources/" + path + "/" + ps[0] + ".anim");
           AssetDatabase.ImportAsset("Assets/Resources/" + path + "/" + ps[0] + ".anim");
       #elif UNITY_STANDALONE
           animation = prevAnimation;
       #endif
       return Instantiate(animation);
   }

   /**
    * BGMの設定
    */
   private void SetBackgroundMusic(string cmd, string parameter)
   {
       cmd = cmd.Replace(COMMAND_BGM, "");
       SetAudioSource(cmd, parameter, bgmAudioSource);
   }

   /**
    * 効果音の設定
    */
   private void SetSoundEffect(string name, string cmd, string parameter)
   {
       cmd = cmd.Replace(COMMAND_SE, "");
       name = name.Substring(name.IndexOf('"') + 1, name.LastIndexOf('"') - name.IndexOf('"') - 1);
       AudioSource audio = _seList.Find(n => n.name == name);
       if (audio == null)
       {
           audio = Instantiate(Resources.Load<AudioSource>(prefabsDirectory + SE_AUDIOSOURCE_PREFAB), seAudioSources.transform);
           audio.name = name;
           _seList.Add(audio);
       }
       SetAudioSource(cmd, parameter, audio);
   }

   /**
    * 音声の設定
    */
   private void SetAudioSource(string cmd, string parameter, AudioSource audio)
   {
       cmd = cmd.Replace(" ", "");
       parameter = parameter.Substring(parameter.IndexOf('"') + 1, parameter.LastIndexOf('"') - parameter.IndexOf('"') - 1);
       switch (cmd)
       {
           case COMMAND_PLAY:
               audio.Play();
               break;
           case COMMAND_MUTE:
               audio.mute = ParameterToBool(parameter);
               break;
           case COMMAND_SOUND:
               audio.clip = LoadAudioClip(parameter);
               break;
           case COMMAND_VOLUME:
               audio.volume = float.Parse(parameter);
               break;
           case COMMAND_PRIORITY:
               audio.priority = int.Parse(parameter);
               break;
           case COMMAND_LOOP:
               audio.loop = ParameterToBool(parameter);
               break;
           case COMMAND_FADE:
               FadeSound(audio, parameter);
               break;
           case COMMAND_ACTIVE:
               audio.gameObject.SetActive(ParameterToBool(parameter));
               break;
           case COMMAND_DELETE:
               _seList.Remove(audio);
               Destroy(audio.gameObject);
               break;
       }
   }

   /**
    * 音声ファイルを読み出し、インスタンス化する
    */
   private AudioClip LoadAudioClip(string name)
   {
       return Instantiate(Resources.Load<AudioClip>(audioClipsDirectory + name));
   }

   /**
    * 音声にフェードをかける
    */
   private void FadeSound(AudioSource audio, string parameter)
   {
       string[] ps = parameter.Replace(" ", "").Split(',');
       StartCoroutine(FadeSound(audio, int.Parse(ps[0]), int.Parse(ps[1])));
   }

   /**
    * テキストファイルを読み込む
    */
   private string LoadTextFile(string fname)
   {
       TextAsset textasset = Resources.Load<TextAsset>(fname);
       return textasset.text.Replace("\n", "").Replace("\r", "");
   }

}

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