A-Nest 2021/10/23 17:57

開発者向け - Naninovelの活用

どうも、ご無沙汰しております。
プログラマのゆっくりみかんです。

前回「書けて無くて申し訳ない」と言いながら、また結局書けてませんでした・・・
なので、今後はきっちりルール化して、定期的に書いていこうと思います。

さて、またえらく時間がかかってしまったのですが、開発はもちろん毎日続いています。
ただ、目に見える変化が中々出せなくて
「うーん、Ci-enの記事書くにも何を書くか・・・」
と悩んだままズルズル行ってしまいました。
しかしまあ、いい加減悩むぐらいなら何でもいいから書かなきゃ、となってサークル全体で引き締めていこうぜ、ってなった感じです。

さて、今回は前回の続きの話でも。

前回、テキスト表示アセットについて色々書いてましたが、特に変わりは無くNaninovelを採用して今に至ります。
こちらのアセットですね。

Naninovel

糞高いですが、値下げをちょくちょくしている様なので、タイミングを見計らうと負担が少なく済みます。
そして、値段に見合うアセットです。

そして、Naninovelを使うにあたって得たノウハウを色々書いていこうかな、と。
正直、細かすぎて分かりやすく書こうとするとすごく大変なので、内容がかなりわかりにくくなっちゃってる気がします。
なので、不明点等あれば、コメントを頂ければ答えれる限りは答えます。
そして、あまり幅広い話じゃなくて申し訳ないです。

細かな設定の癖

NaninovelはUnity上でキャラクタ等々の設定を行っていくアセットです。

こんな感じの設定ですね。
英単語を読めば大まかな意味はわかるのですが、いくつか癖のある項目がありました。
Posesの中に「Visible」という項目があります。
まあ、普通に解釈したら
「そのポーズを表示するか、しないか」
という理解になると思うのですが、Visibleにチェックを入れるとキャラクタの表示がどうも妙な挙動になるようです。
バグなのか仕様なのか判別が付かないですが、とにかく直感には反する設定です。
時間があれば原因を追及してバグだったらバグレポートを出してもいいのですが、そこまで見ている時間はありません。
まあとにかく、そういう
「あれ?」
となる部分がいくつもあって結構時間を浪費してしまいました。
まあ、新しく使うアセットにこういうのは付き物なので、しょうがないですね。

カスタム変数

Naninovelで完結したゲーム、つまりノベルゲームを作るだけなら話は早いのですが、うちのゲームは色々内部で連携をする必要がありました。
その際に一番悩んだのが、こちらで用意した変数との連携です。

Naninovelは自分で独自のコマンドを作る事が出来るので、最初はそれを使って対処していました。
例えば、会話の中でアイテムを持っているか確認する時は
「@checkItem」
みたいなコマンドを作ったり。
最初はこれで行けると思ったんですが、想定よりも作らなきゃいけない独自コマンドが増えて、キリが無かったです。
なので、Naninovelの変数マネージャ自体を置き換える形で、自分のゲームの様々な情報にアクセス出来るカスタムクラスを作りました。

	/// <summary>
	/// ゲームのフラグデータを一緒に読み込み/設定するCustomVariableManager
	/// </summary>
	[InitializeAtRuntime(@override: typeof(CustomVariableManager))]
	public class ShinhokenVariableManager : IStatefulService<GameStateMap>, IStatefulService<GlobalStateMap>, ICustomVariableManager {
		[Serializable]
		public class GlobalState {
			public SerializableLiteralStringMap GlobalVariableMap;
		}
        
        ~~~

こんな感じですね。
わざわざ全部一から書く必要は無く、既存の変数マネージャクラスを持ってきて、必要な部分を改造する感じです。

VariableExistsメソッドやGetVariableValue等々、メソッド名は分かりやすいので置き換えるべきクラスを置き換えて行く感じですね。

こういうマネージャレベルのクラスが割と気軽にカスタマイズ出来るのは、Naninovelの良いところですねぇ。
構造が分かりやすい。

こういうクラスを作ると、Naninovelの構文の中で

@if Money > 1000	;お金が1000以上あるかどうか

みたいな形で、自分が用意したカスタム変数やカスタムな状況を存分に利用出来る様になります。
これで、一々細々としたカスタムコマンドを作らなくても良くなりました。

マスタの取り込みについて

ゲームを作る上で、多くの場合マスタデータというものを作る必要が出てきます。

こういう感じのものですね。
イベントデータだったり、アイテムデータだったり、そういったものをまとめて管理する為にデータを集約します。
多くの場合はExcelで管理をしていると思いますが、うちの場合はNotionという無料サービスを利用しています。
これが非常に便利で使い勝手の良いツールなので、いつかこれに関する記事も書いてみたいですねぇ。

まあ、それはさておき。

最初、会話データをNaninovelに取り込む際、Naninovelの変数に会話自体を突っ込んで固定スクリプトを再生する感じの実装でした。
こんな感じですね。

MobFemale: {Kaiwa}	;Kaiwa変数には会話データが含まれている

このやり方だと1人の会話を差し替えるだけの形なので、複数人で会話させるとか、会話の途中に演出を入れるとか、そういった事が一切出来ません。
僕はそれでもまあいいと思っていたのですが、がふさんから「それは拙い」と突っ込みが入って、考え直す事になりました。
次に考えたのが、会話データで誰が喋っているかをプログラムで認識してカウントし、会話スクリプトの中で発言人数分ループで回す、というやり方でした。
今考えるとかなり酷いやり方です・・・。

@checkPerson		;会話データを解析してPerson変数に発話する人、Kaiwa変数に会話データを入れ、人数のカウントをPersonCount変数に入れるカスタムコマンド
#START				;会話スタートのラベル
@if PersonCount > 0	;会話人数が0以上の場合
{Person}: {Kaiwa}
@nextPerson			;次の人間をPerson変数へ、会話データをKaiwa変数に突っ込み、人数カウントを減らす
@goto .START
@endif

こんなんですね。いやぁ、酷い。
複数人の会話という部分はクリアしましたが、細かい演出を入れたいという要求がクリア出来てませんし、何よりNaninovelは文字列変数の中にNaninovelのコマンドを入れても、それを解釈してくれない実装でした。
会話の中で、特定の文字だけ大きくしたい、とかそういう場合にはコマンドを使うのですが、そういったコマンドが使えなかったのです。
Naninovelの良さを超絶スポイルしてしまっている。

そしてある日、マスタデータにNaninovelのスクリプトそのものを突っ込んで、取り込み時にスクリプトそのものを生成すればいい、とようやく気付きます。
というわけで、こういう感じにマスタデータを取り込む際にNaniScriptのファイル生成をするコードを書きました。

			#region Naninovel関連初期化
			if (configuration is null)
				configuration = ProjectConfigurationProvider.LoadOrDefault<ScriptsConfiguration>();	//Naninovelのプロジェクト設定の初期化
			if (editorResources is null)
				editorResources = EditorResources.LoadOrDefault();	//Naninovelのエディタ設定の初期化
			#endregion

			var naniScriptPath = $"Assets/_ShinHoken/Naninovel/{type}/";	//Naninovelの会話を突っ込むパス
			var assetFilePath = naniScriptPath + id + ".nani";			//Naninovelのスクリプトのフルパス
			var existFile = File.Exists(assetFilePath);					//既にファイルが存在しているか?

			//ディレクトリが無ければ生成
			if (!Directory.Exists(naniScriptPath)) {
				Directory.CreateDirectory(naniScriptPath);
				AssetDatabase.Refresh();
			}

			//スクリプトがサブルーチンなら最後にreturnと、念のためにstopを追加
			if (isSubRoutine) {
				naniScriptText += Environment.NewLine + "@return" + Environment.NewLine + "@stop";
			}

			//ファイル書き込み
			File.WriteAllText(assetFilePath, naniScriptText);

			//ファイルを新規作成した場合
			if (!existFile) {
				//アセットデータベースに追加
				AssetDatabase.ImportAsset(assetFilePath);
				AssetDatabase.Refresh();
			} else {
				//既存のNaniscriptを登録解除
				editorResources.RemoveAllRecordsWithPath(configuration.Loader.PathPrefix, id, configuration.Loader.PathPrefix);
			}
	
			//アセットのGUIDを取得
			var guid = AssetDatabase.AssetPathToGUID(assetFilePath);
			//Naniscriptの一覧に作成したNaniscriptを追加
			editorResources.AddRecord(configuration.Loader.PathPrefix, configuration.Loader.PathPrefix, id, guid);

			//データを保存
			EditorUtility.SetDirty(editorResources);
			AssetDatabase.SaveAssets();

こうする事で、マスタデータにNaninovelのスクリプトそのものを書けば取り込んでスクリプトを生成してくれる様になったわけです。
そして、生成されたスクリプトを呼び出せばそれでいいわけです。
Naninovelへのスクリプト追加周りは僕が勝手に解析してこれで行けるだろう、という推測で組んだ処理なので、もしかしたら危うい所があるかも知れません。
何にせよ、これでどんな演出でもマスタデータに書くだけで可能になりました。

発想って本当に大切ですね。
最初、このやり方が何故か全然思いつかなかったんですよ。
でも、ある日風呂に入ってちんこを洗ってる時に唐突に
「あ、こうすりゃいいじゃん」
って思いつきました。
そして、そういう思いつきの集合体が「ゲーム作りの経験」になるわけですね。

とりあえず

今回はまあこのくらいで。
かなり苦労しましたが、今回のテキスト周りでのノウハウはとても強い今後の糧になりそうです。
会話シーンがないゲームなんてないですしね。
次回作以降は、会話周りについては一切迷う必要がなくなる事でしょう。
こういった感じで今後より良い作品を出来るだけ素早く作る為の様々な工夫をしているので、寛大な心でお待ち頂ければ幸いです。

それでは、また!
作業に戻ります・・・!

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

最新の記事

月別アーカイブ

記事を検索