ゼロから始めるゲーム制作2:ノベルゲームエンジンの選定 Utage vs Naninovel

ゼロから始めるゲーム制作2:ノベルゲームエンジンの選定 Utage vs Naninovel

書いた人 : HAYAO(PG) HAYAO(PG)

 こんにちは、HazeDenki株式会社のプログラマーを担当しているHAYAOと申します。


 前回の通り、数回分の記事のテーマは以下の通りになる想定です。


  1. ノベルゲームの基盤を作る。まず何をする?
  2. ノベルゲームエンジンの選定。宴とNaniNovelの特徴について
  3. 開発環境のセットアップ
  4. ノベルゲームエンジンのソースをAIとカスタマイズしていく方法
  5. NaniNovelの機能についての解説数本


 今回は2.ノベルゲームエンジンの選定について、調査した内容を共有します。

ノベルゲーム用エンジンの選定


 Unityで作ることは決めていたので、Unity用のノベルゲームエンジンをちょっと調べた中で拡張性が高いとされている「」エンジンと「NaniNovel」エンジンの調査を行いました。


結論から言えば、採用したのは「NaniNovel」エンジンです。


以下に調査した各エンジンの特徴を記載します。

そんなことはないぞッ!という内容があればコメントで教えてもらえると助かります



宴 -utage-



特色

 主にUnityのインスペクタとスプレッドシートを用いてゲームを作成していくことが前提の作りになっています。

 また、日本語のマニュアルやサポートが非常に充実しており、そこがNaniNovelに比べて日本人に大きいメリットと言えます。

 逆に英語圏の方には厳しい!

 NaniNovelは、基本すべて英語です。※翻訳の怪しかったNaniNovelの日本語マニュアルは廃止されました。


↓公式マニュアル


↓公式フォーラム

↓公式Discord


開発の感触

 文字表示や選択肢の制御から、各種機能の呼び出しや保存するパラメータなどもスプレッドシート上ですべてを入力していくのが大きな特徴です。

 独自機能を拡張した場合でも、基本スプレッドシートのシナリオファイル上から機能を呼び出す流れとなります。その他、Unityで必要な操作は基礎的な操作にとどまっており、専用の操作はほとんどインスペクタ上での設定。上記マニュアルを読んでいけば使いこなせます。


拡張性

 誰しもが気になると思う、プリセットにない機能を実装するための拡張性についてですが、基本的にSendMessage系の機能を用いるのがほとんどになると思われます。

 メッセージ(Command)をスプレッドシート上で定義し、シナリオ中に送信する。エンジンのメッセージ受取側でArg1の設定値を基に分岐処理を行い、任意の処理を実行する流れが基本でしょう。

※マニュアルより抜粋

        //SendMessageコマンドが実行されたタイミング
        void OnDoCommand(AdvCommandSendMessage command)
        {
            switch (command.Name)
            {
                case "DebugLog":
                    DebugLog(command);
                    break;
                case "InputField":
                    InputField(command);
                    break;
                case "AutoLoad":
                    AutoLoad(command);
                    break;
                default:
                    Debug.LogError("Unknown Message:" + command.Name);
                    break;
            }
        }


 上記のようなスイッチ文でメッセージを仕分ける形がマニュアルに記載されています。

 貼ったサンプル画像の選択行の実行時、case"DebugLog"の処理が実行されることになる形です。


 ここは賛否多少あれど、スプレッドシートでの操作に集約されているというのは管理や障壁の低さを考えると利点と言えます。

 半面、デバッグ作業は苦労する未来が見える・・・


 途中でミニゲーム用Sceneを呼び出す程度の拡張は十分SendMessageで事足りるため、一通り動かしてプリセットの演出に満足出来れば宴を採用する価値は高いです。


不満点

 スプレッドシートの操作が苦手だと当然キツイ。脚本をコピペしてゲームの形に整えていく作業は大変だと思います。シナリオ執筆者がスプレッドシートを常用している場合、障壁は少ないでしょうけど。


 また、NaniNovelと比べてカスタマイズの小回りが利かないです。ゼロから作った独自機能を呼び出す事は簡単にできるのですが、根幹機能の一部を差し替えたいとなった時に難しい。


 例えば、一部のシーンで文字表示を上から1文字1文字フェードインさせながら落下させたいと考えたとき。

 どのような処理でテキストウインドウにテキストを表示しているのか。どこを変えればテキストウインドウ上での表示アニメーションを変更できるのか調べたところ、こういったところは細かい変更を想定していない内容で、システム全体に影響を与えてしまう構造でした。


 細かいところには手が届かない印象です。

  

NaniNovel


$165!(高い)


特色

 NaniNovelスクリプトというファイル形式のテキスト形式のファイルを編集することで文章表示やCG表示などのノベルゲーム的制御の指定を行っていきます。

 IDEがVSCodeのプラグインでサポートされており、入力の補完機能があるのが非常に便利です。

Naninovel特有の機能の呼び出しは@コマンドで行う。
その候補を出してくれる神機能。

 公式マニュアルについて、項目は充実しており、全て英語です。また、購入後一定期間Discordの専用サポートもあり、NaniNovelがデフォルトで何が出来るのか把握するための手段について困ることはないでしょう。


↓公式マニュアル


長所

 NaniNovelの何より良いところは、素のUnityで出来ることに関して特殊な操作をほとんどすることなくゲームに組み込めることです。

 例えば背景も立ち絵もテキストウインドウもカメラも全て、UnityのUI上でPrefabs指定による設定が可能であることは大きい。これにより、複雑なレイヤー構造のキャラクターや3DCGの動的背景、特殊なシェーダやライト、ポストエフェクトを適用した画面を直観的に表示できます。

 制約が少ない分、独自UIはほとんどなく、素のUnityにおけるゲーム作りの方法を一通り知っておく必要があります。また、Utageに比べ高価格であり、C#によるコーディングが行えなければ、価格分の価値は発揮できないと言っていいです。

 機能拡張となると、既存コードの読み込みが必要になるほか、NaniNovelに関する非公式の開発ブログはほとんど見つかりません。当然ですが、Discordフォーラムも機能拡張に関しては簡潔な回答に終始する場合がほとんどです。

 

拡張性

 上記でも述べていますが、拡張性は直観的かつ高いです。

 現在のノベルゲーム作りに採用していますが、Unityで出来ることは今のところNaniNovelエンジン上でも全て出来ているレベルです。パッドコントローラ対応やアニメーションを使ったキャラ立ち絵のまばたき等は簡単に出来ます。


拡張例(NaniNovel使おうと思っている人以外見なくて良いかも)

 拡張した機能の例を全て紹介していると書き切れないので、特にノベルゲームの根幹として重要なテキストウインドウに関するカスタム例を紹介します。

 NaniNovelではデフォルトで5種類のテキストウインドウが用意されています

ダイアログ

ワイド

フルスクリーン

バブル(吹き出し)

チャット形式

 

 これらはNaniScriptにてシナリオを記述していく中で画面上に表示/非表示の命令が可能となっています。

@printer FullScreen //指定したテキストウインドウが表示される
ヒロイン:セリフ //呼び出したウインドウの中に左記のテキストが表示される


 既に公開されている動画で確認可能ですが、今回のノベルゲームで必要なテキストウィンドウは必須で2種類ありました。

1.フルスクリーンかつ、文字送りが縦にスクロールする形式。(動画0:21~参照)

2.スチル(CG)表示時に画面下部のワイドダイアログかつ文字送りがページ切り替え式。

 2つ目はデフォルトのワイド形式で実現可能ですが、1つ目は文字送りの機能を改造する必要がありました。


 各テキストウインドウはPrefab化されており、フルスクリーンのデフォルトPrefabを調査して改修を試みることになりましたが、嬉しいことに「テキスト表示時に処理を行う機能のための基底クラス」が用意されていました。

public abstract class TextRevealEffect : MonoBehaviour
{
    protected virtual RevealableText Text { get; } // 表示テキストオブジェクトを登録する。
    protected virtual IRevealInfo Info { get; } // 表示状態取得インターフェース
}


public interface IRevealInfo
{
    event Action OnChange;  // ★テキストが表示される度に発火
    int LastRevealedCharIndex { get; }  // 最後に表示された文字のインデックス
}


 指定のテキストオブジェクトの表示が変更される度に通知を飛ばしてくれる基底クラスが実装されているのであれば、それを継承したスクリプトを作り、Prefab内のテキストコンポーネントにアタッチするだけです。


 スクロール処理は、テキスト表示エリアの高さを取得し、それがテキストウインドウをはみ出していたらスクロール処理を行う形式としました。ウインドウからはみ出たテキストはマスク処理で隠します。


 内容はこんな感じです。

(実際のプログラムを単純化して抜粋)

    public class RevealScrollRect : TextRevealEffect
    {
        [Tooltip("Viewport RectTransform of the ScrollRect.")]
        public RectTransform viewport;

        // 画面ギリギリまで文字が表示されないように余白設定
        public float textSpace = 40f;

        public float scrollTime = 0.5f;

        private void OnEnable() //プリンタが呼び出されたときに実行
        {
            Info.OnChange += HandleRevealChanged;//テキストに変更があったら毎回HandleRevealChanged()を呼び出す
            Text.ForceMeshUpdate();
        }

        private async void HandleRevealChanged() 
        {
            Text.ForceMeshUpdate();
            float addedHeight = OverflowingHeight(); //テキストの追加で高さが表示領域をはみ出したらいくつスクロールするか計算
            if (addedHeight > 0)
            {
                    await AnimateScrollY(addedHeight, scrollTime);//スクロールアニメーション処理
            }
        }

        private async UniTask AnimateScrollY(float addedHeight, float duration)
        {
            // 現在のテキストエリアの anchoredPosition を取得
            Vector2 pos = Text.rectTransform.anchoredPosition;
            float startY = pos.y;
            float targetY = startY + addedHeight;


            // DOTween を使用して anchoredPosition.y を targetY へアニメーションさせる(スクロール)
            await Text.rectTransform.DOAnchorPosY(targetY, duration)
                                  .SetEase(Ease.OutCubic)
                                  .SetLink(gameObject)
                                  .AsyncWaitForCompletion();
        }

        public float OverflowingHeight()
        {
            if (Text.text == null || viewport == null) return 0;

            float contentHeight = Text.preferredHeight; //テキストエリアの高さ
            float viewportHeight = viewport.rect.height; //表示ウインドウ領域の高さ
            float scrollOffset = Text.rectTransform.anchoredPosition.y; // 既にスクロールされた分は anchoredPosition.y の値で把握


            // 実際の表示テキストの高さ
            float textHeight = contentHeight - scrollOffset;


            // textHeight が viewportHeight+余白 を超過した分スクロール
            float res = textHeight - viewportHeight - textSpace;

            return res;
        }
  }


 文字表示情報が変更されたことを通知してくれる仕組みがあれば、文字送りも文字表示アニメーションも色々出来る気がしてきませんか?


 この細かさの基底クラスが無数に用意されているのがNaniNovelです。


欠点

・価格

 高いです。165ドル。機能拡張を行わないのであれば、正直Utageと出来ることの差はそこまでありません。

 NaniNovelがUnity知識への依存度が高い分、UtageでもNaniNovelでも企画の要望を満たせる状態の場合、Utageの方が開発しやすい場合が多いのではないでしょうか。


・詳細なプログラム設計と適切なサンプルがマニュアルにない

 例えば先ほど紹介した、「テキスト表示時に処理を行う機能のための基底クラス」について、マニュアルはこれです

 指定したテキストオブジェクトに変更があったらアクションが飛ぶ基底クラスがあるよ!とは書いていない。

 ここに書いてあることは、よくわからない。

 デバッグしながら設定値の意味を把握する作業を何度やったかわかりません。


 この欠点により、ハードルが爆上がり・・・と思いきや、今の時代裏技があります。

 AIにエンジンのソースコードを読んでもらえば良いのです。

 (Discordフォーラムで質問した時も何度か「AIに助けてもらいなさい」と言われました。。)

 この方法で、NaniNovelは無限の可能性を手に入れます。自分がどのようにAIとNaniNovelの調査作業を行っていったかについては今後の記事で書く予定です。


総括

 話の冒頭でも書きましたが、採用したエンジンはNaniNovelです。今後、開発を進めていくうちに致命的な不具合などに遭遇しなければ、、これは変わらないでしょう。


 昔Naninovelに挫折した方も、AIと一緒に挑戦すれば世界が変わるかもしれません。

 次回は、AIとのコーディング環境セットアップと自分の開発の進め方を共有しようと思います。


 ここまで読んでくださった方、ありがとうございました。

 また次回。HazeDenkiをよろしくお願いいたします。

 

記事一覧へ戻る

コメント (2)

コメントを残す

0/1000
☆ MALK ☆ 23分前
i'm not a fan of ai, but i'm excited for new developments you guys make!
herba 1時間前
thank you for writing in comprehensive detail ! (and gosh ! in english too ! thank you ! ; ;) it was an interesting read ! i love how that engine synergizes greatly with your prior programming experience !