Unityでのリプレイについて

  • 26
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

ゲームを作っていると、リプレイ機能が欲しくなることが多々あると思います。
全部動画にして撮れば間違いないのですがそうはいかないのでここではプログラムで状況再現する方法を投稿します。

大切なことを書き忘れていました。
Rigidbodyなど使うと、リプレイはずれてしまいます。
なので、物理演算などは自前のプログラムを使いましょう。

リプレイなのにサンプルがないとはどういうことだ!
ということでサンプルを作りました。
http://www.unitygames.jp/game/ug7000800
プロジェクトはこちら
http://www1.axfc.net/u/3115412

出てくるキューブをクリックで消すだけのサンプルです。
1.乱数のシードを保存しよう!
何においても乱数が毎回同じにならなければ結果は変わってしまいます。
ゲームモードで乱数のシードを決めるのか、読み込むのかを変えましょう

if( GameMode.Play){
       // 乱数のシードを適当に設定 .
   Random.Seed = (Time.time%100)
}
else if( GameMode.RePlay){
       // シード値をゲームプレイのシード値に設定 .
       Random.Seed = LoadedSeed;
}

毎回シードが固定でしたらここはいらなくなります。とにもかくにもシードを合わせましょう。
2.入力ではなく位置で保存?
Unityの場合、同じ入力を同じタイミングで与えてもキャラクタの位置が微妙にずれることがあります。
(Inspecterのxyzが微妙にずれるのはあるあるです)
なので入力ではなく、入力して動いた位置を保存しておいてそれを読み込む・・・
としたいのですが、そうすると入力に対する移動方法がプレイとリプレイで変わってめんどうです。

2-2. 移動を丸めよう
誤差があるのなら移動した結果を丸めてしまえばいいのです!
Mathf.Floor() // 切捨て .
Mathf.Ceil()   // 切り上げ .
ここら辺の関数を使って小数点以下を切りましょう。
第2位ぐらいまではほしいので、
値を100倍した結果を丸めてさらに100分の1にしましょう。
ここらへんの話は独自に誤差の出ない移動システムを使っていたり、
誤差は誤差なので無視するのなら必要ないです。
3.スクリプトの実行順
ここが今回の話のメインになります。
Unityはスクリプトの実行順がランダムです。
よく使われるのは Script Execution Order
http://docs-jp.unity3d.com/Documentation/Components/class-ScriptExecution.html
ですが、Script Execution Orderには以下の二つの欠点があります。
(1) 同スクリプト内の実行順は制御できない
Enemy.csというスクリプトが全敵についていた場合、その敵の実行順は毎フレームランダムになってしまいます。
ゲームによっては誰が最初に行動するかが重要になるため、これがずれると再現もできなくなります。
(2)めんどくさい
すべてのスクリプトファイルを実行順つけていくと、すごくめんどくさいです。
これは以外と重要な要素で、開発がめんどくさいと情熱が失われます。

これら二つの問題を解決する方法は、ゲームを操る神をつくることです。
具体的には、以下のような実装をします。
(1) Update()関数はゲーム上で1つしかもたない
それを持つのが神です。他のスクリプトのUpdate()に変わるもの、例として Run() という関数を呼び出します。
(2) 神はすべての実行中スクリプトの情報を持っている
Update関数を消されたスクリプトは、そのままでは更新できません。
なのでスクリプトはゲーム内に現れたとき、神に更新する許可をとります。
Kami.CharCSList.Add(this);
といったように神のList<Char>に自分をAddしましょう。
using System.Collections.Generic
を使うことでListが使えます。
そして神のUpdate内で
foreach(Char char in CharCSList){
    char.Run();
}
とやってやることでリストに登録された順に実行します。
こうすると実は若干パフォーマンスも上がったりするという噂。
あなたも神を信じましょう。
4.そもそもどうやって情報を保存するのか?
ゲームによって保存に必要な情報は変わります。
基本的にプレイヤーの入力はすべて保存します。
保存するには保存・読み込み用のクラスをつくりましょう。
Class ReplayData{
      // 入力のあったフレーム群 .
      List<int> frame;
      // 入力位置 .
      List<Vector3> InputPos;
      ...
      ...
}
などゲームに必要なクラスを用意しておき、入力があった場合、ゲーム内で
if(Input.TouchCount > 0){
     // 実行処理 .
     Done(InputPos);
     // 実行時のフレームをリストに加える .
     ReplayData.frame.Add(m_NowFrame);
    // 入力位置をリストに加える .
    ReplayData.InputPos.Add(InputPos);
}
といったようなコードを書けばリストには加わります。
その加えたリストを実際に保存するのですが、
LitJson
http://lbv.github.io/litjson/
などクラスをJsonにしてくれるようなものを使うと便利です。
扱える人ならScriptableObjectにしてもいいでしょう。むしろ推奨です。
5.データを読み込んでリプレイをさせよう
4で吐き出したデータをつかってリプレイを作ります。
リプレイはゲームの通常プレイのシステムを使いまわしましょう。
ここでの条件は次のような条件が考えられます
(1) ゲームの結果にかかわる入力は一切無視(キャラクタの移動など)
(2) 実行速度をいじれる(倍速やポーズなど)
(3) ゲームクリア報酬などうっかりわたさない
(4) プレイ時と同じ実行フレームで同じ位置に入力させる

(1) はプレイヤーの移動などの入力ができてしまうと未来が変わるので、
リプレイモードだった場合無視するようにしましょう
ただカメラの移動などゲームの結果にかかわらないのでしたらやると楽しいです。

(2) は、実は2番であげた神を使うことで簡単にできます。
Application.TargetFrameRateでFPSはいじれますが、端末だと60FPSがきつかったりします。
そこで神のUpdateの中でforループをさせることで、2倍どころか10倍100倍も可能です。
神が実行順を管理するとこのようなメリットも生まれます。あなたも神を信じましょう。

(3) はうっかり忘れやすいことです。
通常プレイの処理を使いまわしているので、うっかりゲームクリア報酬を与えていないかちゃんとチェックしましょう。

(4) は処理の話です。
4番で保存したデータを読み込み、
Run(){
    if(!Active){
        return;
    }
    Frame++;
    // 今のフレームが保存データのフレームと一致していたら .
    if(Frame == ReplayData.frame[index]){
        // 実行処理を呼ぶ .
        Main.Done(ReplayData.InputPos[index]);
        // 参照インデックスを進める .
        index++;
        // インデックスがリプレイデータの範囲を超えてたらもうリプレイを終える .
        if(index >= ReplayData.frame.Count){
                Active = false;
        }
    }
}

といった風になります。

ようはリプレイというのは
同じ条件(Seed)で、同じタイミング(Frame)で、同じ位置に同じような入力をすれば動く
ということです。後はちょっとUnity独自の処理を気をつければできます。

時間があったらコードなど整理予定