Cut進行用セーブロードの構築1【Unity6】

絶対調味ロジック

システムコンフィグである「テキスト設定」の実装を始めようとしたところ、
既読スキップ」が関わってくるので、まずは「セーブロード」システムを先に構築した方が良いとのこと。

独自で採用しているノベルパートの「Cut方式」で、セーブロードができるのかどうか、
そこからの挑戦になります。

Cut進行システム要約(Unity 2.5DノベルADV)

ノベルパートの構築方法は「シーン」あらため「Cut」進行となっています。

以前の記事ノベル・アドベンチャー独自構築【Unity】で軽く触れていますが、
セーブシステムを作るにあたって、改めてまとめてみます。

基本コンセプト

この開発では物語の進行を Cut(カット)単位で管理する。
Cutとは「その瞬間の画面の完成状態」 を定義する単位。

1Cut(1オブジェクト)の中には次の情報が含まれる。

  • カメラの状態・カメラの位置
  • 表示するキャラクター・キャラクターのポーズ
  • キャラクターの位置
  • セリフ
  • 必要な演出

つまりCutは「その瞬間の画面を再構築するための状態セット

SceneとCutの役割分担

Unity Scene
大きな区切り 場所移動・チャプター・分岐・探索エリア・ミニゲーム・etc

Cut
時間進行の単位 Cut_001 → Cut_002 → Cut_003 → …

Hierarchy構造

Cutは、Hierarchy上に「時系列で上から下に並べる

  • 今どこを作っているか視覚的に分かる
  • ストーリーの流れを確認しやすい
  • デバッグが容易

Cutの内部構造(概念)

Cutは、空オブジェクトで作られ、その子に必要な情報を持たせる。

Cut_001【例】
├ CharacterControl
├ CameraSetting
├ Message
└ PositionMarkers

Cutが開始すると、キャラクター状態カメラ状態セリフ・演出が適用される。

Cut進行の基本動作

ゲームはCut番号を順番に進める

【例】currentCut = 0 入力などで進行。
currentCut++ ExecuteCut(currentCut)

Cut開始時の処理

Cutが開始すると画面状態を再構築する。

【例】ApplyCharacterState() ApplyCameraState() StartDialogue()

重要なのは「状態を積み重ねない」こと。
毎Cut、表示状態位置ポーズを再適用する。

セーブロードとの関係

セーブデータに基本的に保存するもの「現在Cut番号
ロード時には「ExecuteCut(savedCut)

これにより、キャラクターカメラセリフが再構築される。

この方式のメリット

  • 視覚的に分かりやすい ・・・・・・ Hierarchyを見るだけでストーリー進行が分かる。
  • セーブロードが簡単 ・・・・・・・ 状態を保存する必要が少ない。
  • デバッグしやすい ・・・・・・・・ 任意のCutから再生可能。
  • 自己流でも破綻しにくい ・・・・・ 状態を毎回再適用するため。

設計思想まとめ

この進行方式は「Cut = 完成状態」という考え方に基づく。
ゲームは、Cutを順番に実行して、画面の完成状態を更新することで、物語を進行させる

リップシンクの調整

セーブシステムを作るにあたって、口パクシステムもシンプルにしておこうと改修です。

口パクアニメは「Animation」と「Animator」を使用し、変数でのオンオフをしています。
目パチはスクリプトでの制御をしています。
口パクのほうもセーブに優しそうなスクリプト制御に変更しました。

ランダム性を加えられ、アニメーションデータをごっそりと削除することができました。
アイドル時にもAnimatorが動き続けているのが気になっていたので、すっきりです。
誤差程度らしいですが、スクリプト制御の方が軽い場合もあるとのことです。

セーブシステムの設計案

Cut進行の概要と会話システム含むスクリプト内容をClaudeに添付し、セーブシステムを考察してもらいます。

  • セーブボタン・ロードボタンを押すことで各々の画面をアクティブにする。
  • セーブ画面(スロットが並んでいる画面)で任意のスロット(ボタン)を選択しセーブする。
  • スロットにはセーブしたときのゲーム画面のスクリーンショットを表示する。
    (タイミングはセーブボタンを押した時と同じであり、セーブ画面を貫通してゲームメイン画面だけ)
  • スロットにはセーブしたときのテキストも任意の長さで表示する。
  • スロットにはセーブした年月日も表示する。
  • ロード画面もセーブ画面と全く同じで、スロットを選択したらロードされる。
  • 変数の保存はJsonで自動作成保存しているが、セーブデータもそれでいけるならそれで。
    (安全性や複雑性、一般的でないのであれば違う形式でももちろん可能・保存変数はセーブデータの中に入れたい)
  • 一般的なUnityでの保存フォルダはどうなっているのか?
  • スロット数の増減を柔軟に行えるようにはしたい。
  • 自動保存も実装しておきたい。その場合スロットは一番最初(01?00?)の場所。
  • 自動保存・スロット2の1ページに3スロット実装で良いと思う。しかし将来的には増減できるようにしておきたい。
  • セーブロードともにスロットは削除ボタンで削除できるようにする。
  • 閉じるボタンや右クリックなどで画面を閉じる。

セーブシステム構築案

導入スクリプト

  • Sys_CutController【1Cutを管理】
    任意のCut番号を決める・Messenger終了通知・CutManagerへ進行通知
  • Sys_CutManager【Cutの進行管理】
    CutControllerの番号を上から登録していくリスト・Cut順番管理・ロード復帰・Cut開始
  • Sys_Messenger【独自会話システム】
    追加・修正あり
  • Sys_SaveManager【セーブロード実行】
    セーブ・ロード実行スクリプト・スクリーンショットの保存・JSON保存
  • Sys_ScreenshotManager【スクリーンショット保存】
    一番シンプルで一般的な方法・UI無しスクショ・PNG保存

Hierarchyの最終構造

NovelRoot
System_set
│ ├ CutManager
│ ├ SaveManager
│ └ ScreenshotManager

Cuts
├ Cut_001
│ ├ CutController
│ ├ Messenger
│ ├ CharacterPosition
│ └ CameraPosition

├ Cut_002
├ Cut_003

※必ず「Messenger」は「Cut」の子にする。

その他機能

  • 保存場所
    Unityの一般的保存場所:Application.persistentDataPath
  • セーブボタン
    Saveボタン押す → SaveManager.Save(slot) → Screenshot撮影 → SaveUI開く
  • AutoSave
    Cut開始時(CutManager):オートセーブはここに入れると安定します。

スクリプト作成と配置

新スクリプト作成と既存スクリプト修正を行い、オブジェクトにアタッチ・アサインしていきます。

Cut」に該当番号を振り、「CutManager」に順番に登録していきます。

この時点で、従来の「NextObject」での次Cut指定は廃止。
CutManagerに自動で順番通りの表示になるようにしてもらっています。

セリフ無しで演出のみのCut_003(ドアを開けてカメラが動くシーン)にシステムの行く手を阻まれます。

Cut進行やセーブは「セリフ(Sys_Messenger)」の終了が条件になっているので、
それを持たないCutの場合、挙動がおかしくなります。

なので、それを伝えるスクリプトを別途用意することになりました。
例外があるとやはり一筋縄ではいきませんね。

セーブ・ロードの発動は、仮で「F11」キー「F12」キーで行えるようにしてもらっています。
スロットも現時点ではテスト用のそれ1つです。

途中のトラブル(特にロード後の挙動)

  • ロード後に立ち絵が消える(よってリップシンクなどのエラーも起こる)
    原因:別Cut内の「キャラクター」を呼び出している場合、見当たらなくなって起こる現象。
    解決:Cut外にキャラクター本体を置き、位置などは従来通りスクリプトで制御する。
  • ロード後にCut_001に戻ってから、セーブ時のCutに移る
    原因:ロード時にすべてのCutをいったん全アクティブし、非アクティブにするため、一瞬Cut_001が表示する。
    解決:画面ちらつきは、暗幕やローディング画面実装にて解決予定。
  • セーブ時のCutに移る、はカメラが初期位置からセーブ時のCut位置に移る挙動
    原因:Cut時のカメラ移動は「DOTween」制御で行っているため、ロード時にそれが実行されている。
    解決:ロード時に「DOTween」の動きをさせずに、即終了させる。

スクリプトを書いてもらっている手前、中身が良く分からないので、テストプレイ時の問題挙動で説明するしかなく、
それがなかなか難しかったです。

「動画」で判断できるか聞いてみてもClaudeは「画像」でしか判断できないです、と。
GeminiChatGPTはたしか動画も見てくれます。
(動きの判断のみであり、雰囲気やその良し悪しは判断できませんと言われたこともありました)

ロード後に一瞬映るCut_001と気がつく前は、
セーブポイントとロードポイントが1Cutずれています、とかで原因を探ってしまうし、
ロード後にカメラが初期位置からセーブ時の位置に移動する挙動も、
Cut_001から早送りするようにロードされます、と相談してしまうしで、
自分でスクリプトを書いていないと、見当違いの相談が多くなってしまうのが弱点でした。

そのたびに、各部分にデバッグログを追加して、詳細原因を探ってくれるのは頼もしかったです。
途中、余計な心配や作業をして怒られましたが…

Claudeさんはやっぱり、ちょっと厳しいw
職人気質なところを感じますね…

その点、同じ設定でもGeminiちゃんはまだ甘やかし味を感じますw

ロード挙動が良い感じになってきたので、
セーブ・ロードの仮ボタンを作って、システム第一段階を完了とします。

専用の「SaveData」フォルダが生成され、
中にJSONデータとスクリーンショットが保存されているのが分かります。

セーブロードの実機挙動

さすがに「バックログ」ほど簡単にはいきませんでした。
しかし「Cut進行方式」だとセーブすること自体は簡単だと、ノベルパートを構築しているときに教えてもらっていたので、
(セーブするものがシンプルらしい。なので保存記述もシンプルになる)
意を決して実装してみました。

セーブシステム用のアセットもいくつかあるようでしたが、
自分のゲーム構築に当てはめることができるのかどうか、余計な機能やシステム・素材などが紛れ込みそうだとか、
そういうことを考えてしまって、結局試すことなく、1から作成することにしました。

スクリプトは分からなくても、構成は把握できたので、とてもよかったと思っています。
Aiツールがなかったら作れませんでしたが(^ω^)

保存形式や場所などが決まったことにより、「既読」や「保存変数」などの機能も追加していけそうです。
次は仮UIを作り、セーブシステム第二段階を目指します。

Cut進行用セーブロードの構築2【Unity6】に、つづく。

システム第一段階まとめ

完成した機能

  • Cutシステム動作
  • セーブ(slot0固定)
  • ロード(slot0固定)
  • スクリーンショット保存
  • カメラ復元(IsLoadingフラグで即座に移動)
  • キャラ位置復元
  • ホイール操作復元
  • ロード後セリフ即表示
  • テスト用セーブ・ロードボタン

現在のスクリプト構成

  • Sys_CutManager.cs:Cut番号自動再生リスト
  • Sys_CutController.cs:Cut番号登録
  • Sys_SaveManager.cs:セーブシステム
  • Sys_ScreenshotManager.cs:スクリーンショット取得
  • Sys_Messenger.cs:会話システム
  • Sys_EndCut.cs:Messenger以外でのCut終了通知
  • Sys_GetPosition.cs:キャラクター位置取得
  • Cmd_Move_xyz.cs:DOTweenでの移動スクリプト
  • UI_SaveLoadButtons.cs:セーブ・ロードボタン用

重要な設計ポイント

Cut進行
セリフありのCut → MessengerがFinishConversation()でNextCut()
・演出のみのCut → Cmd_EndCutをアタッチしてEndCut()

セーブデータ
json ”sceneName”: “a01_Novel_1″,”cutIndex”: 3,”messageIndex”: 1,”saveTime”: “2026/03/17 22:29”

保存場所
・Application.persistentDataPath/SaveData/

ロード時の重要フラグ
Sys_CutManager.WillLoadFromSave
 → シーンロード後にStartCut(0)が呼ばれるのを防ぐ
Sys_SaveManager.IsLoading
 → DOTweenアニメーションをスキップして即座に移動
 → Messengerの文字送りをスキップして即座に表示

Hierarchy構造
Scene
System_set
│ ├ CutManager
│ ├ ScreenshotManager
│ └ …
SaveManager(ルートに置く・DontDestroyOnLoad)
CAMERA_Main(Cutの外)
CHARACTERS(Cutの外)
│ └ キャラクター立ち絵オブジェクト
Cut_001
Cut_002
└ …

現在の設計ルール(忘れないように)

  • キャラクター・カメラはCutの外に置く
  • Cutをまたいで使うオブジェクトはCutの外に置く
  • SaveManagerはルートオブジェクト必須
  • 演出のみのCutには必ずSys_EndCutをアタッチ
  • 複数Cmd_Move_xyzがある場合、EndCutを呼ぶのは最後の1つだけ

未解決・将来対応

  • ⚠️ 一瞬映り込み → ローディング画面で解決予定
  • ⚠️ スクリーンショットのUI映り込み

■ 第二段階

① SaveSlot UI設計
  ├ スクリーンショット表示
  ├ セーブ日時表示
  ├ ページ方式(1ページ3スロット)
  └ Deleteボタン

② オートセーブ(slot0)

③ ローディング画面

Cut進行用セーブロードの構築2【Unity6】に、つづく。

コメント