🎼

【Unity】ScenarioFlowによるシナリオ実装#1-4(ScenarioBookの読み方)

2023/04/15に公開

はじめに

こんにちは.伊都アキラです.
前回の記事では,メッセージを出力するScenarioMethodを追加し,デバックコンソールにHello, world!を表示しました.

今回は,その時に使用したScenarioBookの構造,および使い方について解説していきます.

ScenarioBookの構造

ScenarioBookは,ソースファイルに記述されたシナリオ,すなわち呼び出すべきScenarioMethodと,その順番を保持しているオブジェクトです.
そのインスタンスはコンストラクタで生成することはなく,IScenarioPublisher<>を利用して,ソースファイルからIScenarioBookに変換することで生成します.

ScenarioBookは名前の通り,現実にある紙の本と同じような構造を持っています.
ScenarioBookは複数のScenarioPageを持ち,ScenarioPageは複数のScenarioSentenceを持つ,といった具合です.

そして,各ScenarioSentenceに,それぞれ対応するScenarioMethodの情報が入っています.ScenarioSentenceを「読む」ことで,ScenarioMethodが実行されます.

ただし,実際にC#内で扱うのは,Book, Page, Sentenceに必要な機能をそれぞれ備えたIScenarioBook, IScenarioPage, IScenarioSentenceの三つのインターフェースです.それぞれが持つ機能を見ていきましょう.

IScenarioBook

IScenarioBookインターフェースは以下のようなメンバーを持っています.

IScenarioBook.cs
    public interface IScenarioBook
    {
        int MaxIndex { get; }

        int CurrentIndex { get; }

        IScenarioBook OpenTo(int n);

        IScenarioBook OpenLabel(string label);

        bool HasLabel(string label);

        IScenarioPage ReadPage();

        IEnumerable<IScenarioPage> ReadAll();
    }

以下,各メンバーの解説です.

MaxIndex

ScenarioBookが持つScenarioPageのうち,最後のページ番号(ScenarioPageの番号)です.
ScenarioBookのページ番号は0から数えるので,あるScenarioBookが4枚のScenarioPageを持つとき,MaxIndexは3になります.

CurrentIndex

現在開かれているScenarioPageの番号を指します.
ScenarioBookのページ番号は0から数えることに注意しましょう.

OpenTo

引数として渡したnページ目のScenarioPageを開きます.OpenTo(2)を呼び出した後,CurrentIndexは2になります.

OpenLabel

ScenarioBook中のScenarioSentenceには,ラベルを付加することができます.これは本に挟む栞のようなもので,与えたラベル名で,それが付加されている場所のScenarioSentenceを開くことができます.

例えばページ番号2の4文目に"A"というラベルが付加されているときにOpenLabel("A")を呼び出すと,その後のCurrentIndexは2に,ScenarioPageにおけるCurrentIndexは4になります.

HasLabel

ScenarioBook中に,引数として与えた名前のラベルが含まれているかどうかを返します.

ReadPage

現在開かれているScenarioPageを返します.
CurrentIndexが3なら,ページ番号3のScenarioPageが返されます.

ReadAll

ScenarioBookが持っているすべてのScenarioPageを返します.

IScenarioPage

IScenairoPageインターフェースは以下のようなメンバーを持っています.

IScenarioPage.cs
    public interface IScenarioPage
    {
        int MaxIndex { get; }

        int CurrentIndex { get; }

        IScenarioPage PointTo(int n);

        IScenarioSentence ReadSentence();

        IEnumerable<IScenarioSentence> ReadAll();
    }

以下,各メンバーの解説です.

MaxIndex

そのScenarioPageに含まれるScenarioSentenceの番号で,最大のものを返します.この番号は0から始まることに注意してください.
あるScenarioPageが4つのScenarioSentenceを持つとき,MaxIndexは3を返します.

CurrnetIndex

現在,指されているScenarioSentenceの番号を返します.
ScenarioPageの行番号(ScenarioSentenceの番号)は0から数えることに注意しましょう.

PointTo

引数として受け取ったn番目のScenarioSentenceを指します.
PointTo(2)を実行した後,CurrentIndexの値は2になります.

ReadSentence

現在指されているScenarioSentenceを返します.
CurrentIndexが4なら,行番号が4のScenarioSentenceが返されます.

ReadAll

ScenarioPageが持つすべてのScenarioSentenceを返します.

IScenarioSentence

IScenairoSentenceインターフェースは以下のようなメンバーを持っています.

IScenarioSentence.cs
    public interface IScenarioSentence
    {
        object OnRead();
    }

メンバーはOnReadのみです.
これを実行すると,これに対応したScenarioMethodが呼び出されます.

返値の型がobjectとなっていますが,これは呼び出されたScenarioMethodの元々のメソッドの返値と一致します.返値の型がintのメソッドをScenarioMethodにしていたらintが,返値の型がboolのメソッドをScenarioMethodにしていたらboolが返ります.

そのため,しようと思えばキャストを行うことで,ScenarioMethodの返値を受け取ることは可能です.実際に,非同期処理をサポートするTaskFlowではOnReadの返値の型がUniTaskであるかどうかをチェックすることで,非同期処理をうまくコントロールしています.

ソースファイルとScenarioBookの関係

ScenarioMethodを好きな順番で呼び出すためのソースファイルを記述するときには,有効な記述範囲を<Page>と</Page>,二つのシンボルで閉じなければならないのでした.
このとき,上の方からシンボルで閉じられたブロックごとにページ番号が振られていき,ページのブロックごとに,上の行から順に行番号が振られていきます.

すなわち,シンボルで閉じられたブロックが3つあり,それぞれにScenarioMethodを2, 3, 4個書いたとすると,3つのScenarioPageを持ち,それぞれ2, 3, 4個のScenarioSentenceを持つScenarioBookが生成されます.

さて,このことを確かめてみましょう.まず,前回作成したMessageLogger.csを,以下のように拡張します.

MessageLogger.cs
using ScenarioFlow;
using UnityEngine;

public class MessageLogger : IReflectable
{
    [ScenarioMethod("message")]
    public void LogMessage(string message)
    {
        Debug.Log(message);
    }
    
    //追加
    [ScenarioMethod("index")]
    public void LogScenarioIndex(int pageIndex, int sentenceIndex)
    {
        Debug.Log($"{pageIndex}ページ・{sentenceIndex}行");
    }
}

引数としてページ番号,行番号を受け取り,それを表示するシンプルなメソッドです.ScenarioMethod名"index"として追加します.

そして,ScenarioMethodの引数の型としてintを使いたいので,PrimitiveDecoder.csint用のDecoderを追加します.

PrimitiveDecoder.cs
using ScenarioFlow;
using System;

public class PrimitiveDecoder : IReflectable
{
    [Decoder]
    public string StringDecoder(string source)
    {
        return source;
    }
    
    //追加
    [Decoder]
    public int IntDecoder(string source)
    {
        return int.TryParse(source, out int result) ?
            result :
            throw new ArgumentException($"Failed to decode {source}");
    }
}

次に,以下のようなソースファイルを用意しましょう.

では,前回のGameManger.csのうちReadScenarioBookに少しの変更を加え,実行してみましょう.

GameManger.cs
using ScenarioFlow;
using ScenarioFlow.ExcelFlow;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    //読み込むシナリオのソースファイル
    [SerializeField]
    private ExcelAsset excelAsset;

    private void Start()
    {
        //中略
    }

    //ScenarioBook中のScenarioMethodをすべて実行
    private void ReadScenarioBook(IScenarioBook scenarioBook)
    {
        foreach(IScenarioPage scenarioPage in scenarioBook.ReadAll())
        {
            //追加
            Debug.Log("ページ読みはじめ");

            foreach(IScenarioSentence scenarioSentence in scenarioPage.ReadAll())
            {
                scenarioSentence.OnRead();
            }
        }
    }
}

この時,以下のような結果が得られるはずです.


GameManger.csの実行結果

ソースファイルにおいてシンボルで閉じたブロックごとに,異なるページに分割されていることが分かりましたね.

おわりに

長くなってしまったので,今回はここで終わりにしたいと思います.

次回も引き続き,ScenarioBookについての解説をしていきます.
最後までお読みいただき,ありがとうございました.

Discussion