【Unity】ScenarioFlowによるシナリオ実装#3-2(シナリオ進行としてのセリフの表示)
はじめに
こんにちは.伊都アキラです.
前回は,シナリオ進行に欠かせない要素の一つである,セリフ表示のプログラムを作成しました.
ただし,ScenarioMethodとしては実行をしておらず,通常のメソッドとして呼び出し,テストを行いました.
今回は,基本的なDecoderとProgressorを実装してシナリオ進行の実行環境を整えるとともに,前回作成したセリフ表示プログラムをScenarioMethodとして実行します.
Decoderの作成
手始めに,文字列をScenarioMethodの引数として使用する型に変換するためのDecoderを作成していきます.
現時点ではまだ,引数としてはstring
型を取るものしか作成していませんが,基本的な型についてはここで一度に作成してしまいましょう.
次のPrimitiveDecoder.cs,およびAsyncDecoder.csを作成します.
using ScenarioFlow;
using System;
using System.Linq;
namespace AliceStandard.Decoder
{
public class PrimitiveDecoder : IReflectable
{
[Decoder]
public int IntDecoder(string source)
{
return int.TryParse(source, out int result) ?
result :
throw new ArgumentException(DecodeErrorMessage<int>(source));
}
[Decoder]
public int[] IntArrayDecoder(string[] sources)
{
return sources.Select(s => IntDecoder(s)).ToArray();
}
[Decoder]
public float FloatDecoder(string source)
{
return float.TryParse(source, out float result) ?
result :
throw new ArgumentException(DecodeErrorMessage<float>(source));
}
[Decoder]
public float[] FloatArrayDecoder(string[] sources)
{
return sources.Select(s => FloatDecoder(s)).ToArray();
}
[Decoder]
public bool BoolDecoder(string source)
{
return bool.TryParse(source, out bool result) ?
result :
throw new ArgumentException(DecodeErrorMessage<bool>(source));
}
[Decoder]
public bool[] BoolArrayDecoder(string[] sources)
{
return sources.Select(s => BoolDecoder(s)).ToArray();
}
[Decoder]
public string StringDecoder(string source)
{
return source;
}
[Decoder]
public string[] StringArrayDecoder(string[] sources)
{
return sources.Select(s => StringDecoder(s)).ToArray();
}
private string DecodeErrorMessage<T>(string source)
{
return $"'{source}' can't be converted into '{typeof(T).Name}'";
}
}
}
using ScenarioFlow;
using ScenarioFlow.TaskFlow;
using System;
using System.Threading;
namespace AliceStandard.Decoder
{
public class AsyncDecoder : IReflectable
{
private readonly ICancellationTokenDecoder cancellationTokenDecoder;
public AsyncDecoder(ICancellationTokenDecoder cancellationTokenDecoder)
{
this.cancellationTokenDecoder = cancellationTokenDecoder ?? throw new ArgumentNullException(nameof(cancellationTokenDecoder));
}
[Decoder]
public CancellationToken CancellationTokenDecoder(string source)
{
return cancellationTokenDecoder.Decode(source);
}
}
}
CancellationToken
型のDecoderは,ICancellationTokenDecoderインターフェースを使って作るのでした.
これで,ScenarioMethodの引数としてひとまず次の型が使えるようになります.
- PrimitiveDecoder
- int
- float
- bool
- string
- AsyncDecoder
- CancellationToken
CancellationToken
に関しては配列用のDecoderを定義していませんが,その型の役割上,ScenarioMethodの引数としてその配列を取らせる必要はないでしょう(それに,CancellationToken
型の配列をDecoderで渡そうとするとおかしなことになります.).
Progressorの作成
次に,基本的なProgressorをいくつか作成します.
ProgressorにはINextProgressorとICancellationProgressorがあり,前者は一つの処理が終わった後,次の処理へ移行する許可を出すための進行命令を,後者は一つの処理をキャンセルするためのキャンセル命令を発行するために必要です.
INextProgressor
はNotifyNextAsyncを,ICancellationProgressor
はNotifyCancellationAsyncを実装する必要があります.
それぞれの処理が完了した時点で,進行命令及びキャンセル命令が発行されます.
さて,今回は次の4つのProgressorを作成します.
クラス名 | 命令発行のトリガー | Next/Cancellation |
---|---|---|
KeyProgressor | キー入力 | 両対応 |
ButtonProgressor | ボタン | 両対応 |
CompositeAnyNextProgressor | 他のNextProgressorすべて | Next |
CompositeAnyCancellationProgressor | 他のCancellationProgressorすべて | Cancellation |
KeyProgressor
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using ScenarioFlow.TaskFlow;
using System;
using System.Threading;
using UnityEngine;
namespace AliceStandard.Progressor
{
public class KeyProgressor : INextProgressor, ICancellationProgressor
{
private readonly KeyCode[] keyCodes;
public KeyProgressor(Settings settings)
{
this.keyCodes = settings.KeyCodes ?? throw new ArgumentNullException(nameof(settings.KeyCodes));
}
public UniTask NotifyNextAsync(CancellationToken cancellationToken)
{
return AwaitAnyKeyDownAsync(cancellationToken);
}
public UniTask NotifyCancellationAsync(CancellationToken cancellationToken)
{
return AwaitAnyKeyDownAsync(cancellationToken);
}
private UniTask AwaitAnyKeyDownAsync(CancellationToken cancellationToken)
{
//キーのどれかが押されたら完了
return keyCodes.Length > 0 ? UniTaskAsyncEnumerable.EveryUpdate()
.SelectMany(_ => keyCodes.ToUniTaskAsyncEnumerable().Select(keyCode => Input.GetKeyDown(keyCode)))
.Where(x => x)
.FirstOrDefaultAsync(cancellationToken: cancellationToken) :
UniTask.Never(cancellationToken);
}
[Serializable]
public class Settings
{
public KeyCode[] KeyCodes;
}
}
}
NotifyNextAsync
とNotifyCancellationAsync
は,どちらもAwaitAnyKeyDownAsync
の完了をもって処理完了とします.
そして,AwaitAnyKeyDownAsync
は,keyCodes
に登録されたキーのどれか一つが押されたときに完了します.
すなわちKeyProgressor
は,特定のキーを押すと進行命令あるいはキャンセル命令を発行するProgressorです.
ButtonProgressor
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using ScenarioFlow.TaskFlow;
using System;
using System.Threading;
using UnityEngine.UI;
namespace AliceStandard.Progressor
{
public class ButtonProgressor : INextProgressor, ICancellationProgressor
{
private readonly Button[] buttons;
public ButtonProgressor(Settings settings)
{
this.buttons = settings.Buttons ?? throw new ArgumentNullException(nameof(settings.Buttons));
}
public UniTask NotifyNextAsync(CancellationToken cancellationToken)
{
return AwaitAnyButtonClikedAsync(cancellationToken);
}
public UniTask NotifyCancellationAsync(CancellationToken cancellationToken)
{
return AwaitAnyButtonClikedAsync(cancellationToken);
}
private UniTask AwaitAnyButtonClikedAsync(CancellationToken cancellationToken)
{
//ボタンのどれかが押されたら完了
return buttons.Length > 0 ? buttons.ToUniTaskAsyncEnumerable()
.SelectMany(button => button.OnClickAsAsyncEnumerable())
.FirstOrDefaultAsync(cancellationToken: cancellationToken) :
UniTask.Never(cancellationToken);
}
[Serializable]
public class Settings
{
public Button[] Buttons;
}
}
}
ButtonProgressor
は,トリガーをキー入力ではなくボタンの押下としている点以外はKeyProgressor
と同じです.
NotifyNextAsync
もNotifyCancellationAsync
も登録されたボタンのどれか一つを押すと処理が完了するので,これは特定のボタンの押下により進行命令およびキャンセル命令を発行するProgressorです.
CompositeAnyNext(Cancellation)Progressor
using Cysharp.Threading.Tasks;
using ScenarioFlow.TaskFlow;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace AliceStandard.Progressor
{
public class CompositeAnyNextProgressor : INextProgressor
{
private readonly INextProgressor[] nextProgressors;
public CompositeAnyNextProgressor(IEnumerable<INextProgressor> nextProgressors)
{
if (nextProgressors == null)
throw new ArgumentNullException(nameof(nextProgressors));
this.nextProgressors = nextProgressors.ToArray();
}
public UniTask NotifyNextAsync(CancellationToken cancellationToken)
{
return nextProgressors.Length > 0 ?
UniTask.WhenAny(nextProgressors.Select(nextProgressor => nextProgressor.NotifyNextAsync(cancellationToken))) :
UniTask.Never(cancellationToken);
}
}
}
using Cysharp.Threading.Tasks;
using ScenarioFlow.TaskFlow;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace AliceStandard.Progressor
{
public class CompositeAnyCancellationProgressor : ICancellationProgressor
{
private readonly ICancellationProgressor[] cancellationProgressors;
public CompositeAnyCancellationProgressor(IEnumerable<ICancellationProgressor> cancellationProgressors)
{
if (cancellationProgressors == null)
throw new ArgumentNullException(nameof(cancellationProgressors));
this.cancellationProgressors = cancellationProgressors.ToArray();
}
public UniTask NotifyCancellationAsync(CancellationToken cancellationToken)
{
return cancellationProgressors.Length > 0 ?
UniTask.WhenAny(cancellationProgressors.Select(cancellationProgressor => cancellationProgressor.NotifyCancellationAsync(cancellationToken))) :
UniTask.Never(cancellationToken);
}
}
}
2つの役割はほぼ同じなので,まとめて解説します.
Progressorは,当然ですが作成しただけでは意味がありません.
適切なクラスのコンストラクタに渡すことで,初めて使えるようになります.
例えば,NextProgressorについては,次のようにScenarioTaskExecuterTokenCodeDecorator
クラスのコンストラクタに渡して初めて意味を持ちます.
var scenarioBookReader = new ScenarioBookReader(
new ScenarioTaskExecuterTokenCodeDecorator(
new ScenarioTaskExecuter(tokenCodeHolder),
nextProgressor,
tokenCodeHolder));
ここで,渡せるNextProgressorの数は一つです.
先ほど作成したKeyProgressor
とButtonProgressor
の両方を渡すことはできません.
では,キーの押下またはボタンの押下を進行あるいはキャンセルのトリガーとしたい場合は,そのためのクラスをまた作らなければならないのでしょうか.
そうではなく,上記の2つのクラスのように,自身が特定のインターフェースを実装しながら,実際の処理は他の実装に任せるようなものを用意します.
例えば上記のCompositeAnyNextProgressor
は自身がINextProgressorを実装しながらも,コンストラクタに複数のINextProgressorを要求し,それらの内,最も早かった処理の完了をもって自身のNotifyNextAsync
の完了としています.
このとき,CompositeAnyNextProgressor
はあくまで一つのクラスですから,複数の実装をあたかも一つの実装のように見せることができます.
INextProgressor
を一つしか要求しないScenarioTaskExecuterTokenCodeDecorator
にも,KeyProgressor
とButtonProgressor
を渡したCompositeAnyNextProgressor
一つを渡すことで,専用のクラスを作ることなく,ボタン押下をトリガーとするクラスとキー押下をトリガーとするクラスを組み合わせて,ボタン押下またはキー押下をトリガーとするクラスを作ることができますね.
ScenarioMethodとしてのLineWriterの実行
では,作成したDecoderとProgressorを使って,前回に作成したLineWriter
を実行していきます.
まず,次のGameManager.cs
を用意してください.
using AliceStandard.Decoder;
using AliceStandard.Line;
using AliceStandard.Progressor;
using Cysharp.Threading.Tasks;
using ScenarioFlow;
using ScenarioFlow.ExcelFlow;
using ScenarioFlow.TaskFlow;
using UnityEngine;
using System;
public class GameManager : MonoBehaviour
{
//------各設定
[SerializeField]
private LineWriter.Settings lineWriterSettings;
[SerializeField]
private KeyProgressor.Settings keyProgressorSettings;
[SerializeField]
private ButtonProgressor.Settings buttonProgressorSettings;
//------
//実行したいソースファイル
[SerializeField]
private ExcelAsset excelAsset;
//Disposable
private IDisposable[] disposables;
private async UniTaskVoid Start()
{
//CancellationTokenを取得
var cancellationToken = this.GetCancellationTokenOnDestroy();
//------Progressorの準備
var keyProgressor = new KeyProgressor(keyProgressorSettings);
var buttonProgressor = new ButtonProgressor(buttonProgressorSettings);
//使用するNextProgressorをここに
INextProgressor nextProgressor = new CompositeAnyNextProgressor(
new INextProgressor[]
{
keyProgressor,
buttonProgressor,
});
//使用するCancellationProgressorをここに
ICancellationProgressor cancellationProgressor = new CompositeAnyCancellationProgressor(
new ICancellationProgressor[]
{
keyProgressor,
buttonProgressor,
});
//------
//------ScenarioBookReaderの準備
var tokenCodeHolder = new TokenCodeHolder();
var scenarioBookReader = new ScenarioBookReader(
new ScenarioTaskExecuterTokenCodeDecorator(
new ScenarioTaskExecuter(tokenCodeHolder),
nextProgressor,
tokenCodeHolder));
//------
//------CancellationToken用のDecoderの準備
var cancellationTokenDecoder = new CancellationTokenDecoder(tokenCodeHolder);
var cancellationTokenDecoderTokenCodeDecorator = new CancellationTokenDecoderTokenCodeDecorator(
cancellationTokenDecoder,
new CancellationProgressorTokenCodeDecorator(cancellationProgressor, tokenCodeHolder),
tokenCodeHolder);
//-----
//ScenarioPublisherの準備
var excelScenarioPublisher = new ExcelScenarioPublisher(
new ScenarioMethodSeacher(
new IReflectable[]
{
//使用するScenarioMethodとDecoderをここに
new PrimitiveDecoder(),
new AsyncDecoder(cancellationTokenDecoderTokenCodeDecorator),
new LineWriter(lineWriterSettings),
}));
//Disposableの登録
disposables = new IDisposable[]
{
cancellationTokenDecoder,
cancellationTokenDecoderTokenCodeDecorator,
};
//ScenarioBookの作成
var scenarioBook = excelScenarioPublisher.Publish(excelAsset);
//ScenarioBookを実行
await scenarioBookReader.ReadScenarioBookAsync(scenarioBook, cancellationToken);
}
//最後にDisposableをDispose
private void OnDestroy()
{
foreach (var disposable in disposables)
{
disposable.Dispose();
}
}
}
次に,適当なボタンをCanvasUIに配置します.
以下の画像では,名前をButtonNextとしています.また,見やすさのためにここではボタンの色を赤っぽくしていますが,実使用では透明にしています.
透明なボタンを配置している
次に,シナリオのソースファイルを準備します.
シナリオのソースファイル
以下はコピペ用です.タブ区切りになっているので,Excelファイルに直接貼り付けることができます.
ScenarioMethod Param1 Param2 Param3
<Page>
line.erase std
line.write アリス 私はアリス。 std
line.write アリス 好きな動物は<color=orange>うさぎ</color>。 std
line.write アリス とってもかわいいよね! std
line.write マッチ売りの少女 私はマッチ売りの少女。 std
line.write マッチ売りの少女 <color=red>マッチ</color>はいりませんか? std
line.erase std
</Page>
最後に,GameManager
に必要な設定を行い,実行しましょう.
エンターキー,スペースキー,またはボタンのクリックによって,シナリオが進むことを確認してください.
おわりに
今回は,シナリオの実行環境を整えるために基本的なDecoderとProgressorを作成しました.
自動進行などの便利機能はまだ作成していませんが,これで最低限のシナリオ進行を行うことができるようになります.
次回以降はしばらく,キャラクターの表示,背景の表示など,目に見えて分かるシナリオ進行の要素をどんどん足していきます.
Discussion
2023/05/21 13:00
GameManager.csに間違いがあったので修正しました.
2023/06/16
PrimitiveDecoder.csのStringArrayDecoderにDecoder属性が付加されていなかったので,修正しました.