このチュートリアルでは、プレイヤーがアクションを巻き戻すことができるシンプルなゲームを作成します。 Unityでそれを行いますが、システムを他のエンジンに適合させることができます。 最初の部分では、この関数の基本を検討し、2番目では、そのスケルトンを記述し、より汎用的にします。
まず、このようなシステムを使用するゲームを見てみましょう。 このテクニックのさまざまなアプリケーションを研究してから、巻き戻し機能を備えた小さなゲームを作成します。
主な機能のデモンストレーション
Unityの最新バージョンとエンジンの経験が必要です。 ソースコードは公開され、結果を比較できます。
準備はいい? 行こう!
このシステムは他のゲームでどのように使用されていますか?
プリンスオブペルシャ:The Sands of Timeは、タイムリワインドメカニズムが統合された最初のゲームの1つでした。 プレイヤーが死んだとき、彼はゲームを再開するだけでなく、キャラクターがまだ生きているときに数秒前に巻き戻し、すぐに再試行することもできます。
プリンスオブペルシャ:忘れられた砂。 Sands Of Timeの 3部作では、時間の巻き戻しをゲームプレイに完全に統合しています。 これにより、プレーヤーは高速読み込みのために中断せず、ゲームに没頭したままになります。
このメカニズムは、ゲームプレイだけでなく、ゲーム自体の物語や宇宙にも統合されており、プロット全体で言及されています。
Braidのようなゲームでも同様のシステムが使用されており、ゲームプレイも時間の巻き戻しに密接に関連しています。 ゲームOverwatch Tracerのヒロインは、彼女を数秒前の場所に戻す 、つまりマルチプレイヤーゲームでも時間を巻き戻す能力を持っています。 一連のレーシングゲームGRIDには、スナップショットの仕組みもあります。レース中、プレイヤーは巻き戻しの小さなストックを持ち、車が重大な事故に遭ったときに使用できます。 これにより、レース終了時の事故から生じる煩わしさからプレイヤーを解放します。
GRIDでの深刻な衝突では、事故までゲームを巻き戻す機会があります
その他のユースケース
ただし、このシステムは、クイック保存の代わりとしてのみ使用することはできません。 別の使用例は、レースゲームおよび非同期マルチプレイヤーモードでの「ゴースト」の実装です。
リプレイ
これは、関数を使用するもう1つの興味深い方法です。 SUPERHOT 、 Wormsシリーズ、およびほとんどのスポーツゲームのようなゲームで使用されます。
スポーツのリプレイは、テレビで行うのと同じように機能します。ゲームプレイは、時には異なる角度から繰り返し表示されます。 これを行うために、ゲームでは、ビデオではなく、ユーザーのアクションが記録されます。これにより、さまざまな角度や角度からリプレイを再生できます。 ワームのゲームでは、リプレイにユーモアが表示されます。非常に面白いまたは効果的な殺害の即時再実行が表示されます。
SUPERHOTは動きも記録します。 レベルを通過すると、ゲームプレイ全体のリプレイが表示され、わずか数秒で収まります。
スーパーミートボーイで面白いリプレイ。 レベルを完了すると、プレーヤーは以前のすべての試行が互いに重なり合って表示されます。
スーパーミートボーイレベルの終わりにリプレイします。 以前の試行はすべて記録され、同時に再生されます。
タイムトライアルの幽霊
ゴーストレースは、プレイヤーが空いているトラックを運転して最高の時間を見せようとするテクニックです。 しかし同時に、彼はゴーストと競います。これは、プレイヤーの最高の前の試みのパスを正確に繰り返す半透明のマシンです。 それに直面することは不可能です。つまり、プレーヤーは最高の時間を達成することに集中できます。
一人で旅行しないようにするために、あなたは自分自身と競争することができます。 この機能は、 Need for SpeedシリーズからDiddy Kong Racingまで、ほとんどのレースゲームで使用されています。
Trackmania Nationsでのゴーストレース。 これは「銀色」の難易度です。つまり、プレイヤーがゴーストを通過すると銀メダルを獲得するということです。 車のモデルは交差していることに注意してください。つまり、ゴーストは重要ではなく、通過させることができます。
マルチプレイヤーモードのゴースト
関数を使用する別の方法は、マルチユーザー非同期モードでのゴーストです。 このめったに使用されない機能では、1人のプレイヤーのデータを記録することにより、マルチプレイヤーの試合が行われます。 データはゴーストレースの場合と同じ方法で適用され、他のプレイヤーとの競争のみが行われます。
この種の競争は、さまざまな困難を乗り切ることができるTrackmaniaゲームで使用されます。 そのような記録されたライダーは、報酬を受け取るために敗北しなければならない敵になります。
撮影モンタージュ
一部のゲームでは、巻き戻しは単なる楽しいツールです。 Team Fortress 2には、独自のビデオを作成できるリプレイエディターが組み込まれています。
Team Fortress 2のリプレイエディター。 記録された戦闘は、プレイヤーの目だけでなく、あらゆる視点から見ることができます。
機能を有効にすると、以前の一致を記録および表示できます。 プレイヤーが見るものだけでなく、 すべてが記録されることが非常に重要です。 これは、記録されたゲームの世界を動き回り、全員がどこにいるかを確認し、時間を管理できることを意味します。
実装方法
このシステムをテストするには、簡単なゲームが必要です。 作りましょう!
プレイヤー
シーンにキューブを作成します。これがプレイヤーのキャラクターになります。 次に、
Player.cs
という新しいC#スクリプトを作成し、
Update()
関数に次を追加します。
void Update() { transform.Translate (Vector3.forward * 3.0f * Time.deltaTime * Input.GetAxis ("Vertical")); transform.Rotate (Vector3.up * 200.0f * Time.deltaTime * Input.GetAxis ("Horizontal")); }
したがって、矢印を使用してキャラクターを制御できます。 スクリプトをキューブに添付します。 Playをクリックした後、移動できます。 上から立方体を見るようにカメラの角度を変更します。 最後に、 床面を作成し、各マテリアルに独自のマテリアルを割り当てて、ボイド内を移動しないようにします。 次のようなものが得られるはずです。
WSADと矢印キーでキューブを制御してみてください
タイムコントローラー
次に、新しいC#
TimeController.cs
を作成して、新しい空の
TimeController.cs
追加します。 彼はゲームの記録と巻き戻しを管理します。
これが機能するために、プレイヤーのキャラクターの動きを記録します。 巻き戻しボタンをクリックした後、キャラクターの座標を変更します。 まず、キャラクターを保存する変数を作成します。
public GameObject player;
結果のTimeControllerスロットにプレーヤーオブジェクトを割り当てて、プレーヤーとそのデータにアクセスできるようにします。
次に、プレーヤーデータを保存する配列を作成する必要があります。
public ArrayList playerPositions; void Start() { playerPositions = new ArrayList(); }
次に、プレーヤーの位置を継続的に記録する必要があります。 最後のフレームに保存されたプレーヤーの位置、プレーヤーが6フレーム戻った位置、およびプレーヤーが8秒前(または設定した録音時間)にあった位置があります。 再生キーを押すと、時間の巻き戻し機能を作成した結果として、位置の配列に戻り、フレームごとにそれらを割り当てます。
まず、データを保存しましょう:
void FixedUpdate() { playerPositions.Add (player.transform.position); }
FixedUpdate()
関数では、データを書き込みます。
FixedUpdate()
が使用されるのは、1秒あたり50サイクル(または選択した値)の一定の頻度で実行され、一定の間隔でデータを記録できるためです。
Update()
関数は、プロセッサが提供する頻度で実行されるため、作業が複雑になります。
このコードは、プレーヤーの位置をフレームごとに配列に保存します。 今それを適用する必要があります!
巻き戻しボタンのクリックテストを追加します。 これを行うには、ブール変数が必要です。
public bool isReversing = false;
そして、
Update()
関数をチェックインします。
void Update() { if(Input.GetKey(KeyCode.Space)) { isReversing = true; } else { isReversing = false; } }
ゲームを反対方向に実行するには 、記録する代わりにデータを使用する必要があります。 プレーヤーの位置を記録および適用するための新しいコードは次のようになります。
void FixedUpdate() { if(!isReversing) { playerPositions.Add (player.transform.position); } else { player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1]; playerPositions.RemoveAt(playerPositions.Count - 1); } }
そして、
TimeController
スクリプト全体は
TimeController
ようになります。
using UnityEngine; using System.Collections; public class TimeController: MonoBehaviour { public GameObject player; public ArrayList playerPositions; public bool isReversing = false; void Start() { playerPositions = new ArrayList(); } void Update() { if(Input.GetKey(KeyCode.Space)) { isReversing = true; } else { isReversing = false; } } void FixedUpdate() { if(!isReversing) { playerPositions.Add (player.transform.position); } else { player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1]; playerPositions.RemoveAt(playerPositions.Count - 1); } } }
また、
player
クラスに、
TimeController
巻き
TimeController
再生されない場合にのみ移動を実行するかどうかを確認するチェックを追加してください。 そうしないと、動作がおかしくなる可能性があります。
using UnityEngine; using System.Collections; public class Player: MonoBehaviour { private TimeController timeController; void Start() { timeController = FindObjectOfType(typeof(TimeController)) as TimeController; } void Update() { if(!timeController.isReversing) { transform.Translate (Vector3.forward * 3.0f * Time.deltaTime * Input.GetAxis ("Vertical")); transform.Rotate (Vector3.up * 200.0f * Time.deltaTime * Input.GetAxis ("Horizontal")); } } }
これらの新しい行は、起動時にシーン内の
TimeController
オブジェクトを自動的に検出し、実行時にチェックします。 巻き戻しが実行されていない場合にのみ、キャラクターを制御できます。
これで、世界中を移動し、スペースバーでその動きを巻き戻すことができます。 記事の最後にあるリンクからパッケージをダウンロードし、 TimeRewindingFunctionality01を開いて作業を確認できます!
しかし、ちょっと待ってください。単純なサイコロプレーヤーが、最後に残った方向を見続けるのはなぜですか。 オブジェクトの回転を記録することを推測しなかったからです!
これを行うには、データを保存および適用するために最初に作成された別の配列が必要です。
using UnityEngine; using System.Collections; public class TimeController: MonoBehaviour { public GameObject player; public ArrayList playerPositions; public ArrayList playerRotations; public bool isReversing = false; void Start() { playerPositions = new ArrayList(); playerRotations = new ArrayList(); } void Update() { if(Input.GetKey(KeyCode.Space)) { isReversing = true; } else { isReversing = false; } } void FixedUpdate() { if(!isReversing) { playerPositions.Add (player.transform.position); playerRotations.Add (player.transform.localEulerAngles); } else { player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1]; playerPositions.RemoveAt(playerPositions.Count - 1); player.transform.localEulerAngles = (Vector3) playerRotations[playerRotations.Count - 1]; playerRotations.RemoveAt(playerRotations.Count - 1); } } }
実行してみてください! TimeRewindingFunctionality02は拡張バージョンです。 これで、キューブプレーヤーは時間をさかのぼることができ、対応する瞬間に見たとおりに表示されます。
おわりに
完全に動作する時間巻き戻しシステムを備えたゲームの簡単なプロトタイプを作成しましたが、まだ完全とはほど遠い状態です。 さらに、はるかに安定した普遍的なものにし、興味深い効果を追加します。
まだやらなければならないことは次のとおりです。
- 12番目のフレームごとにのみ記録し、記録されたフレーム間で状態を補間して、データ量が大きくなりすぎないようにします
- 配列が大きくなりすぎず、ゲームがクラッシュしないように、プレーヤーの最後の75の位置とターンのみを記録します。
さらに、プレイヤーだけでなくこのシステムが機能するようにこのシステムを拡張する方法についても考えます。
- 1人のプレーヤーだけでなく記録する方法
- 巻き戻し効果を追加します(VHS信号のぼかしなど)
- 独自のクラスを使用して、配列ではなくプレーヤーの位置と回転を保存します
Unityプロジェクトアーカイブ
そこで、時間を前のポイントに巻き戻すことができるシンプルなゲームを作成しました。 これで、この機能を改善し、その使用をさらに面白くすることができます。
より少ないデータを書き込み、補間する
現時点では、プレーヤーの位置を記録し、1秒間に50回ターンします。 このような大量のデータはすぐに耐えられなくなり、特に複雑なゲームや弱いモバイルデバイスで顕著になります。
代わりに、1秒間に4回しか記録できず、これらのキーフレーム間の位置と回転を補間できます。 したがって、生産性の92%を節約できます。また、結果は一瞬で再現されるため、毎秒50フレームの記録と区別できません。
xフレームごとにキーフレームを記録することから始めます。 これを行うには、最初に新しい変数が必要です。
public int keyframe = 5; private int frameCounter = 0;
keyframe
変数は、プレイヤーデータを書き込む
FixedUpdate
メソッドの
keyframe
です。 現時点では、値5が割り当てられています。
FixedUpdate
、
FixedUpdate
メソッドの5サイクルごとにデータが記録されます。
FixedUpdate
は毎秒50回実行されるため、毎秒10フレームが記録されます。
frameCounter
変数は、次のキーフレームまでフレームカウンターとして使用されます。
次に、
FixedUpdate
関数の記録ブロックを次のように変更します。
if(!isReversing) { if(frameCounter < keyframe) { frameCounter += 1; } else { frameCounter = 0; playerPositions.Add (player.transform.position); playerRotations.Add (player.transform.localEulerAngles); } }
今すぐゲームを開始しようとすると、今すぐ巻き戻しにかかる時間が大幅に短縮されることがわかります。 記録するデータが少ないが、通常の速度で再生するために発生しました。 修正する必要があります。
まず、データを書き込まずに再生する
frameCounter
、別の
frameCounter
変数が必要です。
private int reverseCounter = 0;
データを書き込むのと同じ方法で使用するために、プレーヤーの位置を復元するコードを修正します。
FixedUpdate
関数は次のようになります。
void FixedUpdate() { if(!isReversing) { if(frameCounter < keyframe) { frameCounter += 1; } else { frameCounter = 0; playerPositions.Add (player.transform.position); playerRotations.Add (player.transform.localEulerAngles); } } else { if(reverseCounter > 0) { reverseCounter -= 1; } else { player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1]; playerPositions.RemoveAt(playerPositions.Count - 1); player.transform.localEulerAngles = (Vector3) playerRotations[playerRotations.Count - 1]; playerRotations.RemoveAt(playerRotations.Count - 1); reverseCounter = keyframe; } } }
今、時間を巻き戻すとき、プレーヤーはリアルタイムで前の位置にジャンプします!
しかし、私たちはこれをまったく望みませんでした。 これらのキーフレーム間の位置を補間する必要がありますが、これはもう少し複雑になります。 まず、4つの変数が必要です。
private Vector3 currentPosition; private Vector3 previousPosition; private Vector3 currentRotation; private Vector3 previousRotation;
それらは、現在のデータまで、現在のプレーヤーデータと1つの記録されたキーフレームを保存するので、それらの間を補間できます。
次に、この関数が必要です。
void RestorePositions() { int lastIndex = keyframes.Count - 1; int secondToLastIndex = keyframes.Count - 2; if(secondToLastIndex >= 0) { currentPosition = (Vector3) playerPositions[lastIndex]; previousPosition = (Vector3) playerPositions[secondToLastIndex]; playerPositions.RemoveAt(lastIndex); currentRotation = (Vector3) playerRotations[lastIndex]; previousRotation = (Vector3) playerRotations[secondToLastIndex]; playerRotations.RemoveAt(lastIndex); } }
補間を実行する位置変数と回転変数に関連情報を割り当てます。 2つの異なるポイントに対して呼び出すため、別の関数でこれを行います。
データ回復ブロックは次のようになります。
if(reverseCounter > 0) { reverseCounter -= 1; } else { reverseCounter = keyframe; RestorePositions(); } if(firstRun) { firstRun = false; RestorePositions(); } float interpolation = (float) reverseCounter / (float) keyframe; player.transform.position = Vector3.Lerp(previousPosition, currentPosition, interpolation); player.transform.localEulerAngles = Vector3.Lerp(previousRotation, currentRotation, interpolation);
関数を呼び出して、キーフレームの特定の間隔で配列から最後から最後から2番目のデータセットを取得します(この場合、これは5です )が、復元が実行される最初のサイクルでも呼び出す必要があります。 したがって、次のブロックが必要です。
if(firstRun) { firstRun = false; RestorePositions(); }
動作させるには、
firstRun
変数も必要です。
private bool firstRun = true;
スペースバーを放したときにリセットするには:
if(Input.GetKey(KeyCode.Space)) { isReversing = true; } else { isReversing = false; firstRun = true; }
補間の仕組みは次のとおりです。最後に保存されたキーフレームを使用する代わりに、このシステムは最後の最後から2番目のフレームを受け取り、それらの間でデータを補間します。 補間の量は、フレーム間の現在の距離に依存します。
補間は、現在および以前の位置(または回転)を渡すLerp関数によって実行されます。 次に、補間係数が計算されます。補間係数の値は0〜1です。 次に、プレーヤーは、保存された2つのポイントの間に配置されます。たとえば、最後のキーフレームへの途中で40%になります。
動きを遅くしてフレームごとに再生すると、これらのキーフレーム間でキャラクターがどのように移動するかを実際に見ることができますが、再生の過程では感知できません。
したがって、タイムリワインドスキームの複雑さを大幅に削減し、より安定させました。
固定数のフレームのみを記録する
保存できるフレームの数を大幅に減らすことにより、システムが大量のデータを保存しないようにすることができます。
現在は、アレイに書き込まれた大量のデータであり、長期間使用するようには設計されていません。 アレイが大きくなると、面倒になり、アクセスに時間がかかり、システム全体が不安定になります。
これを修正するために、配列が特定のサイズより大きくなったかどうかを確認するコードを追加できます。 保存する1秒あたりのフレーム数がわかっている場合、ゲームに干渉せず、複雑さを増さないように、保存に必要な巻き戻し時間を決定できます。 かなり複雑なプリンスオブペルシャでは、巻き戻し時間は約15秒に制限されており、より技術的に単純なブレードゲームでは、巻き戻しは無限になります。
if(playerPositions.Count > 128) { playerPositions.RemoveAt(0); playerRotations.RemoveAt(0); }
配列が特定のサイズを超えると、最初の要素を削除します。 そのため、プレーヤーが巻き戻すことができるデータを常に保存し、効率を妨げることはありません。 記録および再生コードの後に、このコードを
FixedUpdate
関数に挿入します。
独自のクラスを使用してプレーヤーデータを保存する
プレーヤーの位置と回転を2つの別々の配列に記録します。 これは機能しますが、2つの場所から同時にデータを読み書きすることを常に覚えておく必要があります。 ただし、これらすべてのデータを格納するためのクラスと、将来的には他のクラス(プロジェクトに必要な場合)を個別に作成することができます。
データのコンテナとして使用されるネイティブクラスのコードは次のとおりです。
public class Keyframe { public Vector3 position; public Vector3 rotation; public Keyframe(Vector3 position, Vector3 rotation) { this.position = position; this.rotation = rotation; } }
クラスの宣言を開始する前に、TimeController.csファイルに追加できます。 彼は、プレイヤーの位置と回転を維持するコンテナを作成します。 デザイナを使用すると、必要なすべての情報を使用して直接作成できます。
アルゴリズムの残りの部分は、新しいシステムで動作するように適合させる必要があります。 Startメソッドで、配列を初期化する必要があります。
keyframes = new ArrayList();
そして代わりに:
playerPositions.Add (player.transform.position); playerRotations.Add (player.transform.localEulerAngles);
Keyframeオブジェクトに直接保存できます。
keyframes.Add(new Keyframe(player.transform.position, player.transform.localEulerAngles));
ここでは、プレーヤーの位置と回転を1つのオブジェクトに追加し、それを単一の配列に追加します。これにより、アルゴリズムの複雑さが大幅に軽減されます。
ぼかし効果を追加して、巻き戻しが有効であることを示します
時間が巻き戻されていることを報告する何らかのサインが本当に必要です。 これまで私たちだけがこれについて知っていて、そのような振る舞いはプレイヤーにとって混乱させる可能性があります。 このような状況では、視覚的な(画面のわずかなぼやけなど)とサウンド(スローダウンして音楽を逆順で再生する)の両方について、実行中の巻き戻しについてプレーヤーに通知するさまざまな信号を追加すると便利です。
少しぼかしを追加して、 プリンスオブペルシャスタイルで何かをしましょう。
プリンスオブペルシャでの巻き戻し時間:忘れられた砂
Unityでは、複数のカメラエフェクトを重ねることができます。実験することで、プロジェクトに最適なカメラエフェクトを選択できます。
基本的な効果を使用するには、まずそれらをインポートする必要があります。 これを行うには、 Assets> Import Package> Effectsに移動し 、提供されているものをすべてインポートします。
視覚効果はカメラに直接適用できます。 [コンポーネント]> [画像効果]に移動し、 ぼかしとブルーム効果を追加します。 それらの組み合わせは、私たちが目指している良い効果を生み出します。
これらは基本設定です。 プロジェクトに応じてカスタマイズできます。
今すぐゲームを開始しようとすると、常にエフェクトが使用されます。
次に、有効化および無効化する方法を学習する必要があります。 これを行うには、
TimeController
エフェクトを
TimeController
インポートし
TimeController
。 それらを最初に追加します。
using UnityStandardAssets.ImageEffects;
TimeController
からカメラにアクセスするには、次の変数を追加します。
private Camera camera;
そして、
Start
関数で値を割り当てます。
camera = Camera.main;
次に、次のコードを追加して、時間を巻き戻すときに効果を有効にします。
void Update() { if(Input.GetKey(KeyCode.Space)) { isReversing = true; camera.GetComponent<Blur>().enabled = true; camera.GetComponent<Bloom>().enabled = true; } else { isReversing = false; firstRun = true; camera.GetComponent<Blur>().enabled = false; camera.GetComponent<Bloom>().enabled = false; } }
スペースバーを押すと、シーンの時間を巻き戻すだけでなく、カメラを巻き戻す効果を有効にして、何が起こっているかをプレーヤーに通知します。
すべての
TimeController
コードは次のようになります。
using UnityEngine; using System.Collections; using UnityStandardAssets.ImageEffects; public class Keyframe { public Vector3 position; public Vector3 rotation; public Keyframe(Vector3 position, Vector3 rotation) { this.position = position; this.rotation = rotation; } } public class TimeController: MonoBehaviour { public GameObject player; public ArrayList keyframes; public bool isReversing = false; public int keyframe = 5; private int frameCounter = 0; private int reverseCounter = 0; private Vector3 currentPosition; private Vector3 previousPosition; private Vector3 currentRotation; private Vector3 previousRotation; private Camera camera; private bool firstRun = true; void Start() { keyframes = new ArrayList(); camera = Camera.main; } void Update() { if(Input.GetKey(KeyCode.Space)) { isReversing = true; camera.GetComponent<Blur>().enabled = true; camera.GetComponent<Bloom>().enabled = true; } else { isReversing = false; firstRun = true; camera.GetComponent<Blur>().enabled = false; camera.GetComponent<Bloom>().enabled = false; } } void FixedUpdate() { if(!isReversing) { if(frameCounter < keyframe) { frameCounter += 1; } else { frameCounter = 0; keyframes.Add(new Keyframe(player.transform.position, player.transform.localEulerAngles)); } } else { if(reverseCounter > 0) { reverseCounter -= 1; } else { reverseCounter = keyframe; RestorePositions(); } if(firstRun) { firstRun = false; RestorePositions(); } float interpolation = (float) reverseCounter / (float) keyframe; player.transform.position = Vector3.Lerp(previousPosition, currentPosition, interpolation); player.transform.localEulerAngles = Vector3.Lerp(previousRotation, currentRotation, interpolation); } if(keyframes.Count > 128) { keyframes.RemoveAt(0); } } void RestorePositions() { int lastIndex = keyframes.Count - 1; int secondToLastIndex = keyframes.Count - 2; if(secondToLastIndex >= 0) { currentPosition = (keyframes[lastIndex] as Keyframe).position; previousPosition = (keyframes[secondToLastIndex] as Keyframe).position; currentRotation = (keyframes[lastIndex] as Keyframe).rotation; previousRotation = (keyframes[secondToLastIndex] as Keyframe).rotation; keyframes.RemoveAt(lastIndex); } } }
プロジェクトパッケージをダウンロードして 、試してみてください。
まとめると
タイムリワインドゲームが大幅に改善されました。 アルゴリズムは大幅に改善され、消費電力は90%削減され、安定しています。 時間が巻き戻されていることをプレイヤーに知らせる興味深い効果を追加しました。
それに基づいて実際のゲームを作成する時が来ました!