このセクションでは、ゲームが使用する実際のスクリプトと最適化を行う方法を示しています。また、最適化が動作する理由と、なぜそれらを適用すると特定の状況で有効になるのか詳しく説明します。
プロジェクトが円滑に実行されるように確認するチェックリストのようなものはありません。動きの遅いプロジェクトを最適化するには、プロファイラーで分析して無駄に処理時間がかかる部分を特定する必要があります。プロファイラーによる分析を行わない、または、その結果を十分理解せずに最適化することは、目隠ししながら最適化するようなものです。
どんなプロセス(物理的であったり、スクリプト、またはレンダリング)がゲームを遅くしているかを把握するために、ビルトインプロファイラーでのパフォーマンス測定を使用することができますが、実際の犯人を見つけるために、特定のスクリプトや方法を掘り下げるすることはできません。しかし、のゲームに特定の機能を無効にするスイッチを取り付けることにより、あなたはかなり最悪の犯人を絞り込むことができます。例えば、あなたが敵キャラクターの AI スクリプトを削除したら、フレームレートが 2 倍になったならば、あなたはそのスクリプト、または、それがゲームにもたらしたことを知り、最適化されなければならない。唯一の問題は、あなたが問題を見つける前に、異なる多くのものを試してみなければならないかもしれないことです。
モバイルデバイス上でのプロファイリングの詳細については、モバイルのための最適化実用ガイドを参照してください。
最初から高速化しつつ開発しようとするのは危険です。なぜなら、最適化されていないものを高速化したり、遅すぎるものを後から作り直すのは、時間の無駄だからです。ゲーム毎に最適化の方法は異なり、あるゲームでは有効だった最適化が、別のタイトルでは役に立たない事もあります。正しい決断をするには、ハードウェアに対する直感と知識が大切です。
スクリプトとゲームプレイ方法でよいゲームプレイとよいコード設計の間で交差する例としてのオブジェクトプーリングを提供しています。一時的なオブジェクトのためにオブジェクトプーリングを使うと、それらを作成、破棄するよりも高速です。メモリアロケーションをより簡易にし、動的メモリ割り当てとガベージコレクション作業を除けるからです。
Unity で書いたスクリプトは自動メモリ管理を使用しています。すべてのスクリプト言語はこれを行います。対照的に C や C++のような低水準言語は手動でメモリ割り当てをし、プログラマが直接メモリアドレスへの読み書きを許可します。そのため、作成するすべてのオブジェクトを削除する責任があります。C++でオブジェクトを作成する場合は、手動でメモリの割当てを解除する必要があります。スクリプト言語では、ObjectReference= NULL;
と書けば十分です。
メモ GameObject myGameObject;
または var myGameObject : GameObject;
ようなゲームオブジェクト変数を使用している場合、myGameObject = null;
としたとき、なぜそれは破壊されないのか?
Destroy(myGameObject);
の呼び出しでその参照とそのオブジェクトを削除しますUnity が何もできないオブジェクト、例えば、なにも継承していないクラスのインスタンス(対照的なのは、ほとんどのクラスや “スクリプト·コンポーネント” が MonoBehaviour から参照されている場合)を作成し、その後、それへの参照変数を null に設定すると、スクリプトと Unity が関連している限りは、そのオブジェクトは失われます;それにアクセスすることはできず、二度と見ることはできませんが、メモリに常駐しています。その後、少し時間がたつと、ガベージコレクタが実行され、どこからも参照されていないメモリ内容は削除されます。裏でメモリの各ブロックへの参照数が追跡・維持されているので、これが可能になっています。スクリプト言語が C++よりも遅い理由の一つがこれです。
オブジェクトが作成されるたびにメモリが割り当てられます。非常に多くのコードでは、あなたはそれを知らなくてもオブジェクトを作成しています。
Debug.Log("boo" + "hoo");
オブジェクトを作成します。
""
の代わりに System.String.Empty
を使う。クラスはオブジェクトであり、レファレンスとして振る舞う。Foo がクラスの場合、
Foo foo = new Foo();
MyFunction(foo);
その後、MyFunction は、ヒープに割り当てられた元の Foo オブジェクトへのレファレンスを受け取ります。MyFunction の内側の foo への変更は、目に見えるどこでも foo がレファレンスされるようになります。
クラスはデータであり、そのように振る舞います。Foo は構造体である場合、
Foo foo = new Foo();
MyFunction(foo);
その後 MyFunction は foo のコピーを受けます。foo はヒープに割り当てられることはなく、決してガベージが収集されることはありません。MyFunction が foo のコピーに変更する場合、他の foo は影響を受けません。
これの結論は、インスタンス化とデストロイの使用は、ガベージコレクタに多くのするべきことをやらせる ということです。これはゲームにおいて“支障”を引き起こすことがありえます。自動メモリ管理を理解するページが説明するように、インスタンス化とデストロイを囲んで、一般的なパフォーマンスでヒッチを回避する方法は他にもあります。何かが起こっていないとき手動でガベージコレクタをトリガーとし、または非常に多くの場合、未使用のメモリの大きなバックログを蓄積させないようにトリガーにします。
もう一つの理由はそれであり、具体的にはプレハブが最初にインスタンス化されたとき、時々の追加の物事が RAM にロードされなければならず、またはテクスチャやメッシュは、GPU にアップロードする必要があります。れは同様に支障を引き起こす可能性があり、オブジェクトプーリングとゲームを続ける代わりのレベルの負荷を発生します。
パペット(人形)の無限の箱を持っている「パペットマスター(人形使い)」を想像してみてください。キャラクターが表示されるスクリプトを呼び出すたびに、その人形の新しいコピーを箱から取り出し、ステージが終わるたびに、その現在のコピーを捨てます。オブジェクトプーリングは、ショーが始まる前にすべての人形を箱から出して、見えるべきでないときには舞台裏のテーブルの上ににすべての人形を置いておくのと同じようなものです。
1つの問題は、プールの作成は他の目的で使うヒープメモリの量を減らしてしまうことです。あなたが作成したばかりのプールの上にメモリを割り当てる続けると、より頻繁にガベージコレクションをトリガーする可能性があります。それだけでなく、すべてのコレクションは遅くなります。収集に要する時間は生きているオブジェクトの数が増加するにしたがって増大するためです。これらの問題を念頭に置き、あなたが大きすぎるプールを割り当てた場合、またはそれらをアクティブに維持している場合、明らかにパフォーマンスが悪くなることがあります。さらにまた、多くの種類のオブジェクトは、オブジェクトをプールするのに役に立ちません。例えば、ゲームは、かなりの時間を持続し、または大量に出現する敵のためにスペルエフェクトを含んでいるが、ゲームが進むにつれて徐々にキルされていきます。このような場合では、パフォーマンスの利点をオーバーヘッドが大幅に上回るのでオブジェクトプールは使用すべきではありません。
ここでは、単純な発射のための単純にスクリプトを並べて比較してます。ひとつはインスタンスを使用し、ひとつはオブジェクトプーリングを使用。
// GunWithInstantiate.js // GunWithObjectPooling.js
#pragma strict #pragma strict
var prefab : ProjectileWithInstantiate; var prefab : ProjectileWithObjectPooling;
var maximumInstanceCount = 10;
var power = 10.0; var power = 10.0;
private var instances : ProjectileWithObjectPooling[];
static var stackPosition = Vector3(-9999, -9999, -9999);
function Start () {
instances = new ProjectileWithObjectPooling[maximumInstanceCount];
for(var i = 0; i < maximumInstanceCount; i++) {
// 不使用のオブジェクトの山を任意の場所に置きます
instances[i] = Instantiate(prefab, stackPosition, Quaternion.identity);
// デフォルトで無効、これらのオブジェクトはまだ、有効になっていません
instances[i].enabled = false;
}
}
function Update () { function Update () {
if(Input.GetButtonDown("Fire1")) { if(Input.GetButtonDown("Fire1")) {
var instance : ProjectileWithInstantiate = var instance : ProjectileWithObjectPooling = GetNextAvailiableInstance();
Instantiate(prefab, transform.position, transform.rotation); if(instance != null) {
instance.velocity = transform.forward * power; instance.Initialize(transform, power);
} }
} }
}
function GetNextAvailiableInstance () : ProjectileWithObjectPooling {
for(var i = 0; i < maximumInstanceCount; i++) {
if(!instances[i].enabled) return instances[i];
}
return null;
}
// ProjectileWithInstantiate.js // ProjectileWithObjectPooling.js
#pragma strict #pragma strict
var gravity = 10.0; var gravity = 10.0;
var drag = 0.01; var drag = 0.01;
var lifetime = 10.0; var lifetime = 10.0;
var velocity : Vector3; var velocity : Vector3;
private var timer = 0.0; private var timer = 0.0;
function Initialize(parent : Transform, speed : float) {
transform.position = parent.position;
transform.rotation = parent.rotation;
velocity = parent.forward * speed;
timer = 0;
enabled = true;
}
function Update () { function Update () {
velocity -= velocity * drag * Time.deltaTime; velocity -= velocity * drag * Time.deltaTime;
velocity -= Vector3.up * gravity * Time.deltaTime; velocity -= Vector3.up * gravity * Time.deltaTime;
transform.position += velocity * Time.deltaTime; transform.position += velocity * Time.deltaTime;
timer += Time.deltaTime; timer += Time.deltaTime;
if(timer > lifetime) { if(timer > lifetime) {
transform.position = GunWithObjectPooling.stackPosition;
Destroy(gameObject); enabled = false;
} }
} }
もちろん、大型で複雑なゲームのためにすべてのあなたのプレハブのために働く一般的な解決策を確認したくなるでしょう。
スクリプトとゲームプレイ方法で与えられる“数百人の回転、ダイナミックな点灯、一度の画面上で集められたコイン”の例はどのようにスクリプトコードを書くか、セクションを実証するために使用されます。パーティクルシステムのような Unity コンポーネントやカスタムシェーダーは貧弱なモバイルハードウェアに負担をかけずに素晴らしい効果を作成するために使用できます。
この効果は大量のコインが落ち、バウンドして、回転する 2D の横スクロールのゲームの文脈で生かすことを想像してみてください。コインは動的にポイントライトに照らされています。私たちはゲームをより印象的にするために光きらめくコインをキャプチャしたい。
もし高性能なハードウェアを持っているのなら、この問題への標準的なアプローチを使用することができます。すべてのコインオブジェクトそれぞれに対して、頂点単位のライティング、もしくはフォワード、ディファードライティングを使ったシェーディングを行い、それからコインのハイライト部分の反射に、周辺に光があふれるイメージエフェクトのグローを追加します。
しかし、モバイルのハードウェアは多くのオブジェクトに窒息されるでしょう。グローエフェクトを適用するにはまったく問題外です。では、我々は何をしますか?
あなたは、そのすべてが同じように移動し、プレーヤーによって慎重に検査できない多数のオブジェクトを表示する場合、あなたは時間がなくてもパーティクルシステムを使用してそれらを大量に描画することができるかもしれません。ここにこの技術のいくつかのステレオタイプのアプリケーションがあります:
スプライトパッカーと呼ばれる無料のエディター拡張機能があり、アニメーションスプライトパーティクルシステムの作成が容易になります。テクスチャにあなたのオブジェクトのフレームをレンダリングし、次にパーティクルシステムにアニメーションスプライトシートとして使用することができます。私たちは回転するコインでそれを使用例として使用しています。
スプライトパッカーに含まれているプロジェクトは、まさにこの問題に対する解決策を示す例です。
それは低いコンピューティング性能で見事な効果を達成するために、すべての異なる種類のアセットのファミリーを使用しています:
readme ファイルはどのような機能が必要でどのように実装され、使用されたプロセスがどのようにシステムに動作するか、これを説明する例を含んでいます。これはそのファイルです:
問題は次のように定義さます。"一度に画面に表示する数百の動的に点灯して回転し、集められたコイン」
単純なアプローチは、コインをレンダリングするために粒子を使用しようとする代わりに、コインプレハブのコピーをまとめてインスタンス化することです。しかし、これは私たちが克服しなければならない多くの課題を引き合わせてくれます。
この例の最終目標または“話の教訓”は、あなたのゲームが本当に必要とするものがある場合、あなたは従来の手段でそれを達成しようとするとそれが遅れの原因となりますが、それは不可能であることを意味するものではありません。それはあなたがはるかに高速に実行される独自のシステム上に、いくつかの仕事を配置する必要があることを意味します。
これらは動的オブジェクトの数百または数千に関与している状況で適用される特定のスクリプトの最適化です。あなたのゲーム内のすべてのスクリプトにこれらの技術を適用することは恐ろしい考えです:これらは、実行時に大量のオブジェクトまたは大量のデータを扱う大規模なスクリプト用のツールや設計ガイドラインとして予約する必要があります。
コンピューターサイエンスでは、O(n)で示される操作の順序は、動作はそれが適用されるオブジェクトの数が増加して評価されなければならない回数(n)が増加する方法を指す。
例えば、基本的なソートアルゴリズムを検討してみる。n ある数を、最小から最大に(昇順)それらをソートしたい。
void sort(int[] arr) {
int i, j, newValue;
for (i = 1; i < arr.Length; i++) {
// 記録します
newValue = arr[i];
//大きいものをすべて右にシフト
j = i;
while (j > 0 && arr[j - 1] > newValue) {
arr[j] = arr[j - 1];
j--;
}
// 記録していた値を大きな値の左に置きます
arr[j] = newValue;
}
}
重要な部分は、ここで二つのループがあり、一つは他方の内側(のループ)があるということです。
for (i = 1; i < arr.Length; i++) {
...
j = i;
while (j > 0 && arr[j - 1] > newValue) {
...
j--;
}
}
それでは、アルゴリズムの最悪のケースを与えると仮定します:入力された数字をソートしますが、逆の順でされています。その場合、もっとも内側のループは j 回実行されます。平均して、i は 1 から arr.Length–1 へ行くように、j は arr.Length/2 になります。O(n) の条件で、arr.Length は、私たちの n があり、したがって、合計で、もっとも内側のループは、nn / 2* 回、または n2/2** 回実行します。しかし、我々は操作の実際の数ではなく、操作の数が増加する方法について話をしたいので、O(n) 項で、1/2 にすべての定数を固定します。だから、このアルゴリズムは O(n2) です。データセットが大きい場合には、操作の数が指数関数的に爆発するので、操作の順序は重要です。
O(n2) 操作のゲーム内の例が 100 の敵です。ここで、各敵の AI は、他のすべての敵の動きを考慮に入れます。マップをセルに分割させて、速くなるかもしれない。最寄りのセル内に各敵の動きを記録し、その後、各敵は最寄りのいくつかのセルにサンプリングされる。つまり、O(n) の操作になります。
あなたがゲームで 100 の敵を持っていると言うと彼らすべてがプレイヤーに向かって移動する。
// EnemyAI.js
var speed = 5.0;
function Update () {
transform.LookAt(GameObject.FindWithTag("Player").transform);
// 下のほうが、もっと悪い
//transform.LookAt(FindObjectOfType(Player).transform);
transform.position += transform.forward * speed * Time.deltaTime;
}
それは、それらが十分に存在し、同時に実行した場合遅くなる可能性があります。あまり知られていない事実: MonoBehaviour のコンポーネント·アクセサリーのすべて、transform、renderer、audio のようなことは GetComponent(Transform) の対応と同等で、彼らは実際に少し遅いです。GameObject.FindWithTag は、いくつかの場合において、例えば、内部ループで、またはインスタンスの多くで実行するスクリプトで、最適化されています。このスクリプトは少し遅いかもしれません。
これはスクリプトの改良版です。
// EnemyAI.js
var speed = 5.0;
private var myTransform : Transform;
private var playerTransform : Transform;
function Start () {
myTransform = transform;
playerTransform = GameObject.FindWithTag("Player").transform;
}
function Update () {
myTransform.LookAt(playerTransform);
myTransform.position += myTransform.forward * speed * Time.deltaTime;
}
超越関数( Mathf.Sin、Mathf.Pow 等)、除算及び平方根は、すべて乗算の時間の 100 倍ほどかかります。物事の壮大なスキームでは、まったく時間がないが、あなたがそれらをフレームごとに何千回も呼び出している場合、それは追加することができます。
これのもっとも一般的なケースはベクトル正規化です。何度も同じベクトル正規化している場合、一度代わりにそれを正規化し、後で使用するために結果をキャッシュすることを検討してください。
両方のベクトルの長さを使用して、それを正規化している場合、normalized プロパティーを使うより、長さの逆数でベクトルを乗算して正規化されたベクトルを得るために速いでしょう。
あなたは距離を比較している場合は、実際の距離を比較する必要はありません。代わりに、sqrMagnitude プロパティーを使用し、距離の二乗を比較し、平方根またはその 2 つを保存することができます。
もう一つは、あなたは定数 c によって何度も除算している場合には、代わりに逆数を掛けることができます。最初に 1.0/c を掛けて計算します。
あなたは高等な何かをしなければならない場合、あなたが、多くの場合、それを少なくやって、その結果をキャッシュすることで、それを最適化することができるかもしれません。例えば、レイキャストを使用した発射スクリプトを考えてみます。
// Bullet.js
var speed = 5.0;
function FixedUpdate () {
var distanceThisFrame = speed * Time.fixedDeltaTime;
var hit : RaycastHit;
// 毎フレームで、現在の場所から前方に、次のフレームでいるであろう場所に、レイを放ちます
if(Physics.Raycast(transform.position, transform.forward, hit, distanceThisFrame)) {
// 発射します
} else {
transform.position += transform.forward * distanceThisFrame;
}
}
直ちに、我々は deltaTime と fixedDeltaTime とアップデートで FixedUpdate に置き換えることで、スクリプトを向上させることができます。FixedUpdate は、フィジクスのアップデートを指し、これはフレームの更新よりも頻繁に起こります。しかし、n 秒ごとにレイキャスティングすることでさらに行われます。小さい n は大きな時間分解能を提供しますし、n が大きいとよりよいパフォーマンスを提供します。より大きく、より遅いあなたのターゲットがあり、時間的なエイリアシングが発生する前に大きな n があってもよいです(プレーヤーがターゲットを攻撃した所、ターゲットが n 秒前にいたところでの爆発のように見えます、または、プレーヤーはターゲットを攻撃しました、しかし、発射体は右を通り過ぎていきます)。
// BulletOptimized.js
var speed = 5.0;
var interval = 0.4; // これは 'n' 秒
private var begin : Vector3;
private var timer = 0.0;
private var hasHit = false;
private var timeTillImpact = 0.0;
private var hit : RaycastHit;
// 最初のインターバルを設定
function Start () {
begin = transform.position;
timer = interval+1;
}
function Update () {
// フレームより短いインターバルは不可
var usedInterval = interval;
if(Time.deltaTime > usedInterval) usedInterval = Time.deltaTime;
// インターバルごとに、インターバルの最初にいた場所から前方に、
// 次のインターバルの最初にいるであろう場所に、レイを発します。
if(!hasHit && timer >= usedInterval) {
timer = 0;
var distanceThisInterval = speed * usedInterval;
if(Physics.Raycast(begin, transform.forward, hit, distanceThisInterval)) {
hasHit = true;
if(speed != 0) timeTillImpact = hit.distance / speed;
}
begin += transform.forward * distanceThisInterval;
}
timer += Time.deltaTime;
// レイキャストが何かにヒットした後に、実際にヒットするために
// 弾丸がレイと同じ距離を進むまで待ちます。
if(hasHit && timer > timeTillImpact) {
// ヒットする
} else {
transform.position += transform.forward * speed * Time.deltaTime;
}
}
ただ関数を呼び出すこと自体にオーバーヘッドが少しあります。あなたはフレームあたり数千回と X= Mathf.Abs(x) "のようなものを呼び出している場合、その代わりに X=(X>0 X :-x ?); それだけでやる方がよいかもしれません。
Unity で使われる NVIDIA の PhysX 物理エンジンはモバイルで利用できますが、デスクトップよりもモバイルプラットフォーム上ではハードウェアの性能限界は、より簡単に達します。
ここではモバイル上で、よりよいパフォーマンスを得るためにフィジクスをチューニングするためのヒントを紹介します: -