UnityNovelProj Unity で VisualNovel を作成する

url: https://github.com/ganyariya/gnovel/pull/24
title: "選択肢をダイアログファイルから表示して選択肢にあったシナリオを再生できるようにする by ganyariya · Pull Request #24 · ganyariya/gnovel"
description: "Summary by CodeRabbit新機能会話中に「どのペットが好き?」の選択肢(犬・猫・うさぎ)を追加。猫を選ぶと「なぜ猫が好き?」の追加選択(かわいい・うそ)に進むネスト分岐を実装。選択用パネルから直感的に選べ、選択内容に応じた台詞へ分岐します。改善分岐会話の進行管理を最適化し、選択後の会話遷移がよりスムーズかつ安定しました。"
host: github.com
favicon: https://github.githubassets.com/favicons/favicon.svg
image: https://opengraph.githubassets.com/6838f74c88b6ea6f0704dfd056557946f8b5c6b238e061cbf472de3c1dcfe2c2/ganyariya/gnovel/pull/24

ダイアログファイルにおける選択肢の実装

選択肢ですが、下記のようなフォーマットで実装することになっています。 choice が選択肢節が始まるキーワードであり、各選択肢は - で表されます。

choice 構文が nest されることがあり、それを踏まえて構文解析をする必要があります。

choice "What pet do you like?"
{
  -Dog
    raelin "oh, dog!"
    "I love it!"
  -Cat
    raelin "oh, cat..."
    "I don't like it..."
    choice "Why do you like cat?"
    {
      -Cute
        raelin "tashikani... cat is cute."
      -Lie
        raelin "oh, really you don't like cat."
        "you should meet cute cat! you will like it!"
    }
    "cat question is end!"
  -Rabbit
    raelin "rabbit??"
    "what's is?"
}

ダイアログファイルの choice 構文を解析する

choice 構文があったら、該当の choice の階層の {} の range 範囲を計算します。 これは Conversation インスタンスから続く lines を先読みして、 {} を数えたうえで } のある位置を計算します。

これで choice "hoge" {<choices>} のダイアログファイルの範囲を求められます。

次にその範囲内をまた 1 行ずつ読み込み -ChoiceX という - で始まる箇所を調べます。 そして、次の - が始まるまで lines を読み込み、その範囲を該当の選択肢を選んだときの割り込みダイアログとします。

このように

  1. chioce "title" {<choices>} の範囲を、 Conversation インスタンスの lines を先読みすることで計算する
  2. <choices> をさらに解析して、 -ChoiceX という選択肢のタイトルとその選択肢を選んだときに実行するシナリオを計算する

を行います。

これらは LogicalLine の仕組みを使っており、ユーザに選択肢を表示させたうえで選択されるまで coroutine で待機します。

  1. choices の構文解析を行う
  2. ユーザに選択肢を表示する
    1. 選択が完了するまでここの yield で待機する
  3. ユーザがなにか選択したら、choice "title" {<choices>} の範囲をスキップさせる
    1. 選択肢の処理が完了したため
  4. 選ばれた選択肢のシナリオを Conversation 化し、強引にキューの先頭に割り込ませる
        public IEnumerator Execute(DialogueLineData lineData)
        {
            var choiceRawData = RipRawData(lineData);
            var choices = ParseChoices(choiceRawData);
 
            var panel = ChoicePanel.Instance;
            var title = lineData.dialogueData.rawData;
            var choiceTitles = choices.Select(x => x.title).ToArray();
            panel.Show(title, choiceTitles);
 
            // 選択されるまで待つ
            while (panel.IsEnteringChoice) yield return null;
 
            // これまで実行していたシナリオについて、選択肢が終わったら先に移動させる
            // ConversationManager が 1 行進めるため、 endIndex のままでいい (+1 しなくていい)
            DialogueSystemController.instance.ConversationManager.CurrentConversation.OverwriteProgress(choiceRawData
                .endIndex);
 
            // 選択された先のシナリオを実行する
            var selectedChoice = choices[panel.LastDecision.AnswerIndex];
            var newConversation = new Conversation(selectedChoice.lines);
            DialogueSystemController.instance.InterruptEnqueueConversation(newConversation);
        }