Unityでゲームプレイの保存をするゴースト機能を作成する

今回はゲームプレイのゴーストを表示する機能を作成していきます。

以前作成した

UnityのAnimatorの保存と再生の機能を使うとゲームプレイ中のキャラクターのアニメーションを保存したり再生したりすることが出来ます。今回はその機能を使ってみようと思います。

の機能を自分なりに作成したものです。

今回の機能を作成すると、例えばレーシングゲームで自分の車のゴーストを表示させたり、アクションゲームで「このように攻略してください」といった例をゴーストとして表示する時に応用して使えると思います。

ただし完全に正確なゴーストを作成出来るわけでもなく多少ズレるかもしれません。

今回の機能ではキャラクターの動きを保存し、そのデータをその場で再生出来る機能だけでなくファイルにゴーストデータを保存して後からデータを読み出してゴーストの再生を出来るようにしていきます。

今回の機能を作成すると

↑のようなものが出来上がります。

動的に動く床のゲームオブジェクトとはスタートが同期していないのでゴーストキャラクターの位置と動く床の位置は一致しません。

スポンサーリンク

ゴースト機能の概要

まずはゴースト機能をどのように作るかを考えていきます。

キャラクターの動きを後から再生すればいいので、Update毎にキャラクターの位置や角度、アニメーションパラメータの値等をデータとして保持しておき、あらかじめ用意しておいたゴーストキャラに通常の操作キャラクターと同じAnimatorControllerを取り付け、ゴーストキャラクターの位置や角度、アニメーションを保持したデータで再生すれば出来そうです。

ただ毎回Updateメソッドが呼ばれる度にデータを追加していくとデータ量が多くなりすぎるので、データを取る時間間隔の設定とデータの数の制限を設ける事にします。

ゴースト機能の作成

それではゴースト機能を作成していきましょう。

操作キャラクターの準備

まずは通常の操作キャラクターを用意します。

キャラクターにはCharacterControllerを取り付け、コライダのサイズを調整します。

キャラクターにはAnimatorControllerを作成し取り付けます。

AnimatorControllerにはIdle、Walk、Jump状態を作成します。

ゴースト機能の操作キャラクターのAnimatorController

アニメーションパラメータにFloatのSpeed、TriggerのJumpを作成します。

Idle→WalkはSpeedがGreaterで0.1
Walk→IdleはSpeedがLessで0.1
Idle→Jump、Walk→IdleはJumpがトリガーされた時
Jump→IdleはHas Exit Timeにチェックを入れておきます。

キャラクターを動かすスクリプトは

を取り付けます。

最小限の移動とジャンプが出来るスクリプトです。

これで操作キャラクターは完成です。

ゴーストキャラクターの作成

ゴーストキャラクターはキャラクターのモデルにCharacterControllerと操作キャラクターに設定したAnimatorControllerをゴーストキャラクターにも取り付けます。

ゴーストキャラクターのモデルにはEthanを使用していて、EthanBodyとEthanGrassesにはデフォルトでEthaWhiteマテリアルが設定されています。

そこでゴーストキャラクター用のGhostMaterialを作成し、それをゴーストキャラクターのEthanBodyとEthanGrassesにドラッグ&ドロップします。

ゴーストキャラクター用のマテリアルを作成

GhostMaterialは↑のように半透明なマテリアルにします。

ゴーストキャラクターは

実際のゴーストキャラクター

↑のように半透明になります。

ゴーストキャラクターはGhostという名前にし、最初は非表示にする為インスペクタの名前の横のチェックを外しておきます。

ゴースト機能スクリプトの作成

ヒエラルキー上に空のゲームオブジェクトを作成し、名前をRecorderとします。

Recorderゲームオブジェクトには新しくRecorderスクリプトを作成し取り付けます。

フィールド宣言部とStartメソッド

まずはフィールド宣言とStartメソッドを見ていきます。

インスペクタで操作キャラクタースクリプトghostCharaを設定出来るようにし、そのキャラクターのデータを保持出来るようにします。

isRecordは今ゴーストを記録しているかどうか
maxDataNumはデータを保持する最大数で、この数字を超えたらゴーストの記録を停止します。
recordDurationはキャラクターのデータを取る時間間隔です。
animKeyはInputManagerのキーの名前を指定します。
elapsedTimeは前回記録してからの経過時間を入れます。
ghostDataはゴーストデータをクラスとして持たせて、そのインスタンスを入れておくフィールドです。
isPlayBackは今ゴーストを再生しているかどうか
ghostはゴーストとして動かすゴーストキャラクターのゲームオブジェクトをインスペクタで設定します。
waitTimeはゴーストの再生が終わった後は最初から再生し直しますが、その間の待ち時間を設定します。
saveDataFolderは保存先のフォルダのパスです。
saveFileNameは保存ファイル名です。

StartメソッドではGhostCharaスクリプトからAnimatorコンポーネントを取得しanimatorに保持しておきます。

似たようなフィールド名になっていますが、ghostCharaはGhostCharaスクリプト、ghostはゴーストキャラクターのゲームオブジェクトになります。

ゴーストデータクラス

Recorderクラスの中にゴーストデータを保持するクラスを作成します。

後でクラスごとシリアライズするので[Serializable]アトリビュートをクラスに付けておきます。

それぞれリストでデータを保持出来るようにします。

データを記録するUpdateメソッド

次はデータを記録する処理を書いているUpdateメソッドを見ていきます。

isRecordでデータを記録していたら経過時間を足して記録間隔を超えていたらghostCharaスクリプトのTransformから位置と角度を取得しGhostDataクラスのリストに追加します。

ジャンプだけはアニメーションパラメータをトリガーで作成しているので、ジャンプキーを押したか押してないかのbool値を追加します。

記録したデータ数がmaxDataNumを超えたらStopRecordメソッドを呼び出しデータの保存をやめることにします。

UIのボタンを押した時に呼び出す処理

次にUIのボタンを押した時に呼び出す処理を記述します。

StartRecordメソッドは「ゴーストの保存」ボタンを押した時に呼び出す処理で、コルーチンの全停止とStopGhostメソッドを呼び出しゴーストの再生を止めます。

GhostDataクラスのインスタンスを作成しghostDataに参照を入れます。

StopRecordは「ゴーストの保存の停止」ボタンを押した時に呼び出します。

StartGhostは「ゴーストの再生」ボタンを押した時に呼び出す処理で、保存したゴーストを再生する処理です。

ゴーストが保存されていればisPlayBackをtrue、ゴーストキャラクターであるghostをアクティブにしてPlayBackメソッドをコルーチンで呼び出します。

StopGhostメソッドは「ゴーストの停止」ボタンを押した時に呼び出しゴーストの再生をやめゴーストキャラクターを非アクティブにします。

ゴーストの再生処理

ゴーストの再生処理はPlayBackメソッドで行います。

PlayBackメソッドではisPlayBackがtrueである間ghostDataから順番にデータを取り出しゴーストキャラクターの位置や角度、アニメーションの再生をします。

ゴーストデータはrecordDurationの間隔で保存したので、その間隔経過してから位置や角度、アニメーションを設定します。

保存データ数を超えた場合はまずSpeedを0、Jumpのトリガーをキャンセルした後、指定の時間待たせます。

その後ゴーストキャラの位置や角度を最初のデータの位置や角度にし、また最初から再生するようにします。

ファイルにゴーストデータを保存、読み出しする処理

最後はゴーストデータをファイルに保存したり読み出したりする処理を作成します。

Saveメソッドは「ゴーストデータをファイルに保存」ボタンを押した時に呼び出す処理で、ghostDataをJSON形式に変換したものを指定したファイルに一括で書き出しています。

LoadメソッドはファイルからJSON形式で保存したデータをreadAllTextに一括で読み出しそれをGhostDataクラスの形式に戻してghostDataに値を入れています。

Save、Loadメソッドの処理に関しては、

C#でファイルの読み込みと書き込みのサンプルを作成していきます。確認にはUnity付属のMonoDevelopを使用します。UnityでCSVファイルの読み込みと書き込みもしてみました。

を参照してください。

OnApplicationQuitはMonoBehaviourクラスで定義されているメソッドでアプリケーションの終了時に呼び出されます。

アプリケーション終了時にSaveメソッドを呼び出しファイルにデータを書き出しています。

ボタンのみでファイルに保存したい場合はOnApplicationQuitメソッドは要らないです。

これでスクリプトが完成しました。

UIの作成

スクリプトが完成したので、UIのボタンを作成しメソッドを呼び出すようにします。

UIのButtonを6つ作成しそれぞれ対応する名前を付けます。

「ゴーストの保存」ボタンはRecordButton
「ゴーストの保存の停止」ボタンはStopRecordButton
「ゴーストの再生」ボタンはPlayBackButton
「ゴーストの停止」ボタンはStopGhostButton
「ゴーストデータをファイルに保存」ボタンはOutputGhostDataButton
「ゴーストデータを読み込み」ボタンReadGhostDataButton

という名前にしました。

ゴースト機能操作ボタンのヒエラルキー

実際の並びは

ゴースト機能操作ボタンの配列

↑のようにしています。

ボタンのOnClickに呼び出す処理を設定する

次はボタンのOnClickに呼び出す処理を設定します。

RecordButtonはRecorderのStartRecordメソッドを指定します。

ボタンのOnClickにゴースト機能スクリプトのメソッドを指定

他のボタンもRecorderスクリプトの該当するメソッドを指定します。

StopRecordButtonはStopRecordメソッド
PlayBackButtonはStartGhostメソッド
StopGhostButtonはStopGhostメソッド
OutputGhostDataButtonはSaveメソッド
ReadGhostDataButtonはLoadメソッド

をそれぞれ指定します。

これでUIのボタンを押した時に呼び出すメソッドの設定が終わりました。

ゴースト機能の完成

RecorderゲームオブジェクトのRecorderスクリプトの設定は、

Recorderスクリプトの設定

↑のようになります。

GhostCharaには操作キャラクターをドラッグ&ドロップし、Ghostにはゴーストキャラクターのゲームオブジェクトをドラッグ&ドロップします。

これでゴースト機能が完成しました。

ジャンプ途中で「ゴーストの保存の停止」ボタンや「ゴーストの再生」ボタンを押すとゴーストの再生の最後でジャンプ終了時に高い所で止まってしまいます。

これはジャンプ途中の位置までしか記録せず保存を停止している為です。

ジャンプアニメーションは前のデータでトリガーされているので最後まで再生されます。

記事の最初に記したように動く床とゴーストキャラクターの位置が一致しません。

またデータに記録間隔を長くすると、実際のジャンプと位置が一致しなくなります。

さらにはジャンプボタンを押したかでbool値をデータに入れているので、アニメーション自体がされなくなる事もあります。

その為、記録間隔自体の処理をなくすか、ジャンプアニメーションに関してはジャンプボタンを押した時の経過時間を保持しておき、その時間が過ぎたらJumpアニメーションのトリガーをするという方法にした方がいいかもしれません。

機能を確認すると記事の最初に紹介した動画のようになります。(^^)/

ジャンプデータを時間で計測するバージョン

完成したゴースト機能ではジャンプのデータをキーを押したかどうかで記録していますが、記録する間隔(recordDuration)が長いと押していてもデータが取れないことがあります。

そこでジャンプキーを押したかどうかはrecordDurationによらず常に確認するようにし、bool値のデータでなく押すまでの時間でデータを取るようにしてみます。

あらかじめ言っておきますが、時間で計測する為、本来の動きとのズレが発生します。

ですが、recordDurationを長めに取っても時間でデータを記録するのでジャンプデータの取りこぼしがありません。

Recordスクリプトの改造

ジャンプキーを押したかどうかを時間で保持する為にRecordスクリプトを改造していきます。

まずはフィールドを追加します。

位置や角度と同じタイミングでジャンプのデータを取るわけではないので、ゴーストの再生が終わったらisLoopResetをtrueにし、それに伴ってジャンプのゴーストデータも最初に戻します。

次はGhostDataクラスのジャンプのデータを書き換えます。

データはfloatで持つようにします。

次にUpdateメソッドでジャンプのデータを取る部分の変更です。

元のジャンプのデータを取る部分を削除し、if文の外に以下の処理を追加します。

ジャンプキーを押した時にデータの記録を取り始めた時間からの経過時間、または前回ジャンプキーを押した時からの経過時間をリストに保存します。

startTimeには現在のゲーム開始からの時間を入れておきます。

次にStartRecordメソッドにstartTimeにゲーム開始からのリアルタイム秒を入れておきます。

次にStartGhostメソッドでPlayBackAnimメソッドのコルーチン処理を追加します。

今まではPlayBackでジャンプのデータの再生もしてましたが、ジャンプはPlayBackAnimの方に移します。

PlayBackメソッドの処理を変更します。

isLoopResetがtrueの時はゴーストの再生が終わり、ジャンプとの同期を取る為に待つためyield return nullでその後の処理をさせません。

次は新しく作ったPlayBackAnimメソッドです。

ジャンプのデータは押した時間を記録していくので、位置や角度のデータと数が違います。

そこでisLoopResetがtrueの時にジャンプデータを最初に戻します。

ジャンプデータは前に押した時からの時間を保存しているので、その時間待った後にJumpアニメーションパラメータのトリガーをしています。

それ以外はyield return nullで何もしません。

これで修正は完了です。

確認すると、

↑のようにrecordDurationを0.2と長くしてもジャンプアニメーションがちゃんと再生されているのがわかります。

終わりに

位置や角度と同じようにジャンプデータを取るか、押すまでの経過時間でジャンプデータを取るかで多少変わります。

経過時間で取った方が記録間隔にかかわらずジャンプアニメーションを再生させる事が出来ますが、位置や角度と完全な同期をしていない為、多少ズレてしまうかもしれません。

兎にも角にも簡単なゲームプレイの保存をしてゴーストの再生をする機能は出来たのではないかと思います。

(゜゜)~

スポンサーリンク

記事をシェアして頂ける方はこちら

フォローして頂くとやる気が出ます

コメント

  1. 匿名 より:

    こんにちは!
    この記事のやり方で複数のゴーストを同時に再生することって、
    できたりするのでしょうか?

    • こんにちは(^^)/

      この記事の内容を改造すれば複数のゴーストの同時再生も出来ると思います。

      改造するべき点としては、

      記録したデータはghostDataに保存し、それをghostdata.datというファイル名で保存しているので、

      すでにghostdata.datファイルが存在していたとしたら、次に記録するデータをghostdata2.datというようにファイル名の最後の数字を足していくなどして別ファイルに保存します。

      この記事ではゴースト用のキャラクターをあらかじめヒエラルキー上に配置しておき、インスペクタのチェックを外して見えないようにしていますが、データを再生する分のキャラクターをあらかじめ用意する他に

      ゴースト再生用の空のゲームオブジェクトを作成し、そこで再生機能を管理し、ゴーストデータファイルがある分だけゴースト用のキャラクターのプレハブをインスタンス化するという感じでもいいかもしれません。

      そもそもファイルにデータを保存しないのであれば、ghostDataをリストや配列にしてそこに複数のデータを保存し、再生機能の時に対応するキャラクターでghostDataに保存したデータを再生させてやるだけです(ファイルに出力しないのでゲーム終了と共にデータは消えます)。

      最初のデータはghostData[0]に保存し、次のデータはghostData[1]に保存していくといったようにスクリプトを変更します。

      ↑のような感じでdataNumの数値を増やしていきます。

      複数のキャラクターを操作し、それぞれ別個でデータを保存する場合はghostCharaをリストや配列にし、データを保存したいキャラクターを指定し、それぞれデータを保存するという感じで出来るような気がします。

      まとめとしては、再生は再生用のキャラクターを保存したデータで移動させたりアニメーションさせているだけなので、複数の再生用キャラクターを作り、それぞれのデータで動かしてあげるという風に変更すれば可能だと思います。(._.)

      • 匿名 より:

        ファイルにデータを保存しない方法で、複数のゴーストを動かすことに
        成功したのですが、1体のゴーストの再生が終わったら他のゴーストも一緒に止まってしまうのですが、どうしたら良いでしょうか・・・

        初歩的な質問だったらごめんなさい(>_<)

        • おそらくコルーチン1つでデータを操作している為に1つのデータの終わりを検知した時に他のデータも初期化してしまうので起きていると思われます。

          その為、ゴースト1体事に1つのコルーチンを使って動かすように改造します。

          試しに2つのファイルghostdata.datとghostdata2.datファイルにあらかじめデータが入っているとして再生機能を改造して作ってみました。

          ファイルからデータを読み込んでそれを再生する部分だけ改造したので、データがあるかどうか?の確認やデータの保存に関しての機能は省いております。

          複数のゴーストを出現させる為にヒエラルキー上に置いたゴーストキャラクターをアセットフォルダにドラッグ&ドロップしプレハブにし、ゴーストデータ分をインスタンス化して出現させるように変更します。

          まずはフィールド宣言部分でghostData等を配列に変えます。

          ghostPrefabにはゴーストキャラクターのプレハブを設定します。

          インスタンス化したものはghostInsに入れていきます。

          ゴーストのデータ数を別途データとして持っておきます。

          次にStartメソッドで配列の要素数を確保します。

          次にStartGhostメソッドです。

          ゴーストデータ数分繰り返し、ghostPrefabをインスタンス化し初期位置と初期角度を最初のデータに設定します。

          それぞれのコルーチンを別個に停止したい場合もあるので、StartCoroutineの戻り値をCoroutine型のフィールドに入れ、それぞれ停止出来るようにします(今回は一括で停止させてますが)。

          コルーチンで実行するメソッドにはゴーストデータの番号を引数として渡しています。

          次にStopGhostメソッドです。

          ここではコルーチンの全停止とghostInsにいれたゴーストキャラクターの削除をしています。

          次はPlayBackとPlayBackAnimメソッドです。

          引数でゴーストデータの番号を受け取るので、その番号に応じたゴーストキャラクターインスタンスの位置やアニメーションをデータに基づいて動かしてあげます。

          ゴーストデータの最後までいったかどうかも配列としてフィールドを用意したので他のゴーストキャラクターとは別個に判定をすることが出来ます。

          最後にファイルからデータを読み取るLoadメソッドです。

          こちらのメソッドではサンプルなので、2つしかデータがないとして2つのファイルからそれぞれのGhostData型のghostDatasの配列にデータを入れています。

          ghostDatasに個々のGhostDataのインスタンスを生成しています。

          急いで作ったので細かい部分は見ていませんが、コルーチンを分ければそれぞれのデータの干渉はなくなります。

          実際に試したサンプルはこちらです。

          複数のゴーストを再生したサンプル

          • 匿名 より:

            ありがとうございます!
            無事にやりたいことができました!