Naninovelは、非常に多機能で柔軟なビジュアルノベル支援アセットです。
このアセットは、「NaniNovelスクリプト」(.nani)という独自のスクリプトを使用してゲームを作るため、厳密には「ノーコードのアセット」ではないのですが、基本的にC#の知識なくこのアセットのみでビルドまで行うことを前提としています。
そのため、既存のゲームの一部にNaniNovelを組み込んでC#から制御しようとすると、一工夫必要です。その手順を紹介します。
なお、基本的な情報は公式ドキュメントにガイドがありますし、公式のサンプルプロジェクトもあるのですが、より柔軟な方法を解説する記事です。
まずはバージョン確認
NaniNovelは、UnityのPackageManagerからインポートできるものよりもさらに最新版が公式Discord内で配布されています。
そのチャンネルは恐らくアセット購入者限定になっているので、アセットを購入した方は公式Discord内のinfoチャンネルを読んで購入者登録をしておきましょう。
手動で初期化する
始めに、この記事を参考に、NaniNovelを自動で初期化させないよう設定します。
設定ウィンドウの[Naninovel] - [Engine]にある「Initialize On Application Load」と「Show Title UI」のチェックマークを外す
CameraとEventSystemをDisableにする
次に、ゲーム起動時など早い段階で初期化を行いますが、場合によっては前述の記事だけで解決されない問題が発生します。
初期化を行うとCameraとEventSystemを含むNaniNovelオブジェクト一式がシーン内に生成されるため、もし既存のシーンにCameraやEventSystemが存在している場合は
- 「AudioListnerが二つ存在する」という警告が発生
- 「EventSystemがシーン内に二つ存在する」という警告が発生
- NaniNovel側のCameraのせいで画面が真っ暗になる
- 画面全体を覆うボタンUIをDisableにしないとマウスがNaniNovel以外に機能しなくなる。
など、色々と不都合があるのです。
そのため、初期化後にNaniNovel側のCameraとEventSystemをDisableにする必要があります。
まずイベントシステムについては、PlayerSettings -> NaniNovel -> Input -> [Spawn Event System]と[Spawn Input Module]をDisableにするだけでOKです。元々シーンに配置しているものだけ使えば良いですからね。
続いて、カメラと画面全体を覆うメッセージ送りボタン(ContinueInputUI)をDisableにする処理ですが、これは「アクティブにも非アクティブにもできる」という汎用的な処理にしたほうが後々便利です。そのため、処理内容は次のようになります。
- 既存のCamera、EventSystemがシーン内に存在するなら、それをアクティブ切り替え。
※私の環境だけかもしれませんが、なぜかCamera.mainを使用するとエラーになったため、オブジェクト名とこの記事を参考に、非アクティブなオブジェクトも含め探す方法を採用しています。 - ICameraManagerを使ってNaniNovelカメラをアクティブ切り替え。
- IUIManagerを使ってNaniNovelの画面全体を覆うボタンをアクティブ切り替え。
はじめに、DontDestroyOnload内も含めたゲームオブジェクトを全て検索するメソッドを作っておきます。便利なのでstaticな自作ライブラリにでも置いておきましょう。
で、これを一部で使って下記のように書きます。
- 初期化
- アクティブ切り替え
このActivateNaninobelObjects()
メソッドはあとで何度も何度も登場しますので、覚えておいてください。
とりあえずこれでNaniNovelの初期化はするけど、既存のゲーム画面に変化は起こさないということを実現できました。
- 初期化は一度行えばOKで、シーンを切り替えた後に再度初期化する必要はありません。
- 初期化時に生成されたNaniNovel関連のオブジェクトはDontDestroyOnLoadで保持されるため、シーンを切り替えても残ります。
- 一度初期化した後に初期化しようとしてもエラーにはなりません。
NaniNovelスクリプトをC#から呼び出す
続いて、特定のNaniNovelスクリプトをC#スクリプトから個別に呼ぶ方法です。
Naninovelスクリプト自体はNaninovel.Scriptという型で定義可能ですが、その必要はありません。
これは簡単で、前述のコードを一部流用して
これだけでOKです。
NaniNovelスクリプトを終了する
で、NaniNovelスクリプトの会話が終わったらどうすればよいのかというと、次のことができれば概ねなんとかなりそうです。
- NaniNovelスクリプトが終了したことをC#側で受け取る
- どのNaniNovelスクリプトが終了したのかをC#側で受け取る
- NaniNovelオブジェクト一式を非アクティブにする
NaniNovelではカスタムコマンドといって、自分でコマンドを新設できるシステムが用意されています。
そこで、このようなクラスを用意します。
このクラスはオブジェクトにアタッチする必要はなく、作っておくだけでNaniNovelスクリプト内でコマンドとして使用できます。
これでNaniNovelスクリプトが正常に終了させ、ついでにどのスクリプトが終了したのかも検知することができました。
音量を同期する
既存のゲームシステムに音量調節機能がある場合、それをNaniNovelシステムの音量に反映させる必要があります。
これを初期化時(先述の初期化イベントEngine.OnInitializationFinished += () =>{ //ここ};
)と、先述のActivateNaniNovelObjects()
内(またはNaniNovelスクリプトを実行したとき)で呼び出せばOKです。
ノベル画面でSettingsを開くと、音量の値が既存のゲームシステムの音量と同期していることがわかります。
「タイトル画面に戻る」ボタンの挙動を変更
デフォルトのPrinterUIをいじくり回す場合、「タイトル画面に戻る」ボタンは便利なので流用したい機能の一つです。
そこで、タイトル画面に戻るボタンを押した場合の挙動を変更してしまいましょう。
また、PackageManagerからアセットをアップデートしてしまい意図せず修正箇所が元に戻ったりする可能性もあります。
恐いことをしているので緊張しましょう。
ControlPanelTitleButton
クラスの、ExitToTitleAsync()
メソッドのif
~return;
よりも後ろの部分を全てコメントアウトし、次のように書き替えます。
タイトル画面に戻る際の確認メッセージはControlPanelTitleButtonクラスのprotected static string ConfirmationMessage = "";
のなかに書かれているので、必要に応じて書き直しましょう。
Tips表示
前述の方法でNaniNovelオブジェクトが非表示になっているときにTipsを表示するのは、割と簡単です。Tipsボタンを作って、ダイアログUIのTipsボタン(ControlPanelTipsButtonクラス)が行っている処理をそのままボタンイベントとして(コピペして)登録すればよいだけです。
問題は、Tips画面で「RETURN」を押した時の挙動です。
NaniNovelオブジェクトが非表示の時に自作のTipsボタンを押してTipsを表示し、その後ReturnでTipsを閉じようとすると、NaniNovelオブジェクトが表示されたままになり、画面が真っ暗のままになってしまいます。
かといって、「RETURNを押したらNaniNovelオブジェクトを非表示にする」という処理にしてしまうと、ダイアログUIから本来の方法でTipsを表示させてReturnを押した際に、ダイアログUIごと非表示になってノベル画面が消えてしまいます。
そのため、Returnを押した時の挙動を「ノベルが進行中かどうかを確認して処理を分ける」という風にしなければなりません。
「ノベルが進行中か」は、先ほど作った「@endScript」と同様に「@startScript」といったカスタムコマンドを作って、そこで自作のstaticなboolなどを切り替えるだけでOKです。
最後に問題のReturnボタンの挙動ですが、これはTipsのReturnボタンが押された時の挙動を継承してoverrideで書き替えるクラスを作ってしまいましょう。
これで挙動を書き替えられました。
あとは、既存のTipsUIのなかのTipsReturnButtonコンポーネントを削除して、代わりにこのクラスをアタッチすれば、無事にどこからでも呼び出せる独立したTips機能が完成します。
未読TIPSがあるか
記事執筆時のNaniNovel最新版(PackageManagerからインポートできるVersion1.18.3)では、外部クラスから「未読TIPSがあるか」というチェックを行う機能が実装されていません。
公式DISCORDで開発者のかたに教えていただいた方法もなぜかうまくいかなかったので、かなり無理矢理な方法で行います。(v1.18.3の場合)
- TipsListItemに自作のタグを付与しておく
- 初期化直後にDontDestroyOnLoadも含む全オブジェクトを検索し、TipsListItemオブジェクトを無理矢理取得する
- そのTextの中身が「"???"か」でアンロック済みか、「太字か」で未読かを判定する
なお、こんなことをしなくても、この記事を読んでいるあなたが使っている最新版では正式に実装されている可能性があります。確認してみましょう。