Naninovelコマンドから、スクリプト・サーバに接続する方法

IT・プログラミング

※ 本記事にはプロモーションが含まれています。

こんにちは! ねこです。

Unity のアセットである Naninovel。

こちら、ノベル制作ツールではあるんですが、例えば、RPG などの会話部分など、他のゲームのパーツとして利用することも可能です。

インテグレーションの方法 • Naninovel
Unityゲームエンジン用のフル機能を備えた、ライター向けで完全にカスタマイズ可能なビジュアルノベル拡張。

これらのテキストは、.nani 拡張子のテキストファイルによって再生されています。

さらに Naninovel は、プログラマ側が拡張しやすいように作られてまして、ほとんどのことであれば、元のコードを変更せずに拡張可能です。

会話部分のフレームだけでなく、もっと複雑なUIもカスタマイズして表示したり。

もちろん独自コマンドや変数も追加できたりします。

Naninovel コマンドから、スクリプトを実行?

そこで、ねこさん考えました。

ねこ アイコン
ねこ
コマンドからクライアント側の API をたたけるようにすれば、すごく便利じゃね?

例えば、下記のようなコマンドを実装するとします。

@executeUseCase GetItemsUseCase params:{item_id},{count}

簡単に説明すると、アイテムのID、個数をリクエストして、アイテムをゲットする GetItemsUseCase という UseCase を実行する @executeUseCase コマンドです。

コマンド内部では、GetItemsUseCase を非同期で実行するようになっています。

しかも、UseCase 名、パラメータを変えるだけで、他の UseCase にもアクセス可能に。

これに加えて、Naninovel スクリプト上から、簡単な条件分岐・変数の使用が可能だということを加味しますと。。

なんと! 以下のことを行うことが出来そうです。

  • 会話シーン内で、アイテム付与・フラグ管理を行える。
  • ログインボーナスや販売ページなども Naninovel スクリプトで完結。
  • 新規イベントの追加実装も Naninovel で完結。
  • さらに、上記の内容をアプリ更新なしで行える。

これができると、開発・運用の幅がだいぶ広がりそうですね 笑。

注意点!

ということを言いながらですが、こちら、ゲーム運用における銀の弾丸では全くありません

というのも、Naninovel 自体がノベル制作ツールとして完成されているので、その中の実装内容は、アセットバンドル、音楽、シムテムロジックなど多岐に渡ります。

そのため、いくら RPG のテキスト表示にも使えますよ、と、謳っているとはいえ、全く性格の違うものを一つに統合する必要があります。

そのほとんどの場合。実装を Naninovel の設計に元の設計を合わせる必要が出てくるはずです。

合わせる必要のある箇所は下記です。

  • 画面の表示周り(レイヤー、カメラなど)
  • BGM、SE(Audio Mixer で設計)
  • データの持ち方(Addressable で設計)

特に、アセットバンドル周り、イメージ、サウンドの設計周りの結合は、やってみるとわかるんですが、かなりしんどい箇所になります。

また、今後のスタンダードになる予定の SRP に完全に対応しているわけではありません。

正直、一から意図した動作を作るほうが簡単かもしれません。

それをクリアしても、外部テキストから API が操作できるのはセキュリティ的に大きな穴をあけることではあります。

はたして、実際に業務に応用可能なのかは甚だ疑問です。

そんな不完全な内容。なぜ書くかって?

だって、こういうのって考えるだけでも面白いですよね?

実装のサンプル

以下、上記の内容を実際に実装してみたサンプルになります。

大まかな流れとしては、ApiCliant というオブジェクトを有した CustomCommand クラスから 該当 UseCase を実行ます。

成功したら、gotoSuccess で設定したラベルに飛び、失敗なら gotoFailed ラベルというものです(未設定ならそのまま次の処理をおこないます)。

GetItemSample.nani

Naninovel で実行するアイテム取得サンプルスクリプトです。

Chara: アイテムID101を、1個あげるね。
# GetItem
@executeUseCase GetItemsUseCase params:101,1 gotoSuccess:.Success gotoFailed:.Failed
# Failed
Chara: 失敗しちゃった。もう一回あげるね。
@goto .GetItem
# Success
Chara: はい、あげた!

CustomCommand.cs

Naninovel コマンドに、API 接続機能を追加したベースクラスです。

using System;
using UniRx;

namespace Naninovel.Commands
{
    public abstract class CustomCommand : Command
    {
        private static IApiCliant apiCliant = default;

        /// <summary>
        /// Api Cliant.
        /// </summary>
        protected IApiCliant ApiCliant => apiCliant;

        /// <summary>
        /// Set api cliant.
        /// </summary>
        /// <param name="apiCliant"></param>
        public static void SetApiCliant(IApiCliant apiCliant) => CustomCommand.apiCliant = apiCliant;
    }
}

ExecuteUseCase.cs

実際の executeUseCase コマンドの実装部分です。

using Naninovel;
using Naninovel.Commands;
using Cysharp.Threading.Tasks;
using UniRx;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Text;

namespace Naninovel
{
    [CommandAlias("executeUseCase")]
    public class ExecuteUseCase : CustomCommand
    {
        [ParameterAlias(NamelessParameterAlias), RequiredParameter]
        public StringParameter UseCaseClassName = default;

        [ParameterAlias("params")]
        public StringListParameter Params = default;

        [ParameterAlias("gotoSuccess"), IDEResource(ScriptsConfiguration.DefaultScriptsPathPrefix, 0)]
        public NamedStringParameter GotoSuccessPath;

        [ParameterAlias("gotoFailed"), IDEResource(ScriptsConfiguration.DefaultScriptsPathPrefix, 0)]
        public NamedStringParameter GotoFailedPath;

        public override async UniTask ExecuteAsync (CancellationToken cancellationToken = default)
        {
            // UseCase を文字列から生成
            Type useCaseType = Type.GetType("Sample." + UseCaseClassName);
            INaninovelUseCase useCase = (INaninovelUseCase)Activator.CreateInstance(useCaseType);
            List<NullableString> paramaters = Params.Value.ToList();

            useCase.Initialize(ApiCliant, paramaters);

            // UseCase を実行
            bool isSuccess = await useCase.RunAsync();

            // 成功なら gotoSuccess ラベルへ、失敗なら gotoFailed ラベルへ。
            var builder = new StringBuilder();

            if (isSuccess == true && Assigned(GotoSuccessPath))
                builder.AppendLine($"{CommandScriptLine.IdentifierLiteral}{nameof(Goto)} {GotoSuccessPath.Name ?? string.Empty}{(GotoSuccessPath.NamedValue.HasValue ? $".{GotoSuccessPath.NamedValue.Value}" : string.Empty)}");
            else if (isSuccess == false && Assigned(GotoFailedPath))
                builder.AppendLine($"{CommandScriptLine.IdentifierLiteral}{nameof(Gosub)} {GotoFailedPath.Name ?? string.Empty}{(GotoFailedPath.NamedValue.HasValue ? $".{GotoFailedPath.NamedValue.Value}" : string.Empty)}");

            var onSelectScript = builder.ToString().TrimFull();
            var autoPlay = !Assigned(GotoSuccessPath) && !Assigned(GotoFailedPath);

            var player = Engine.GetService<IScriptPlayer>();
            var script = Script.FromScriptText($"Execute the usecase `{UseCaseClassName}` script", onSelectScript);
            var playlist = new ScriptPlaylist(script);
            await player.PlayTransientAsync(playlist);
                
            if (autoPlay && !player.Playing)
            {
                var nextIndex = player.PlayedIndex + 1;
                player.Play(player.Playlist, nextIndex);
                return;
            }
        }
    }
}

GetItemsUseCase.cs

アイテムを取得する UseCase サンプルです。

using System.Collections;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Naninovel;
using UnityEngine;

namespace Sample
{
    public interface INaninovelUseCase 
    {
        /// <summary>
        /// Initialize
        /// </summary>
        /// <param name="apiCliant">Api cliant.</param>
        /// <param name="paramaters">Send paramaters.</param>
        void Initialize(IApiCliant apiCliant, List<NullableString> paramaters);

        /// <summary>
        /// Run the use case.
        /// </summary>
        /// <returns>"True" is success to execute method.</returns>
        UniTask<bool> RunAsync();
    }

    public class GetItemsUseCase : INaninovelUseCase
    {
        private int itemId = default;
        private int count = default;

        private IApiCliant apiCliant = default;

        /// <summary>
        /// Initialize
        /// </summary>
        /// <param name="apiCliant">Api cliant.</param>
        /// <param name="paramaters">Send paramaters.</param>
        public void Initialize(IApiCliant apiCliant, List<NullableInteger> paramaters)
        {
            this.apiCliant = apiCliant;
            itemId = paramaters[0]?.Value;
            count = paramaters[1]?.Value;
        }

        /// <summary>
        /// Run the use case.
        /// </summary>
        /// <returns>"True" is success to execute method.</returns>
        public async UniTask<bool> RunAsync()
        {
            bool isSuccess = await [ここに実際にアイテムを取得する処理]
            return isSuccess;
        }
    }
}