[Unity] [導入のヒントとコードの最適化] プログラムを最適化するためのいくつかの導入概念、手法、および原則

この記事は、断続的に更新され続けます

この記事は主にジュニア プログラマー向けです. Unity の開発を容易にするために、いくつかのショートカット キーといくつかの一般的なヒントがこのブログで継続的に更新されます.

ホットキー

  1. Ctrl + S; クイックセーブ!何もすることがないなら、ここに2回来てください!
  2. Ctrl+Shift+F ; 階層ウィンドウで任意のオブジェクトを選択し、シーン ウィンドウでこのショートカット キーを押すと、オブジェクトの座標が現在のシーン ウィンドウの座標に設定されます。
  3. Alt+Mouse Left ; 階層ウィンドウでオブジェクトを折りたたんだり展開したりするときに、alt を押しながら左側の小さな三角形を押すと、すべてのサブオブジェクトが折りたたまれたり展開されたりします。

概念と知識

関数の実行順序

スクリプトの組み込み関数の実行順序は次のとおりです。
Awake ->OnEable -> Start -> FixedUpdate -> Update -> LateUpdate ->OnGUI ->Reset -> OnDisable ->OnDestroy

コライダーの性能順

コライダーのパフォーマンスと効率のおおよその順序は、スフィア コライダー > カプセル コライダー > ボックス コライダー > メッシュ コライダーです。衝突した固定点の数と計算の複雑さに依存します。( https://blog.csdn.net/qiaoquan3/article/details/51320312 )

タグとレイヤーの違い

Unity では、Tag と Layer の両方が、ゲーム オブジェクトをマークするために使用されるプロパティです。

タグは、さまざまなゲーム オブジェクトを識別するために使用され、通常はスクリプト内で特定のタイプのゲーム オブジェクトを見つけるために使用されます。たとえば、敵オブジェクトのグループ内で敵を見つけたり、小道具オブジェクトのグループ内で特定のタイプの小道具を見つけたりします。 .

レイヤーは、ゲーム オブジェクトの物理プロパティとレンダリング順序を制御するために使用され、通常は衝突検出とレンダリング効果を達成するために使用されます。たとえば、プレイヤー オブジェクトと敵オブジェクトの Layer を異なる値に設定して、プレイヤーと敵の間の衝突検出を実現できます。

したがって、タグとレイヤーの違いは、主な用途とアプリケーション シナリオにあります。タグは、ゲーム オブジェクトのタイプまたは特性をマークするために使用され、スクリプトで特定のタイプのゲーム オブジェクトを見つけて処理するために使用されます。レイヤーは、ゲーム オブジェクトの物理プロパティとレンダリング順序を制御するために使用され、通常は衝突検出とレンダリング効果を達成するために使用されます。
ここに画像の説明を挿入
たとえば、上の図では、プレイヤー レイヤーは建物や他のプレイヤー (またはトリガー) と衝突する必要があるだけなので、衝突と相互作用する必要があるオプションのみをチェックできます。これにより、他のレイヤーとの相互作用を大幅に節約できます。 (常に衝突状態にあるグラウンド層など) リソースの消費。

タグ、レイヤー、ゲームオブジェクト、および名前のうち、どのメソッドがより少ないリソースを消費するかを決定します

  • タグとレイヤー:
    タグとレイヤーを比較に使用する場合、Unity は比較に整数値を使用します。これらの整数値はメモリに格納されるため、リソースの消費は非常に少なくなります。
  • ゲームオブジェクト:
    通常、ゲームオブジェクトの比較にはオブジェクト参照が使用されます。つまり、オブジェクト参照の比較は通常、より多くのメモリを消費します。ただし、Start() 関数またはその他の初期化コードでそれらを使用するだけであれば、コストはそれほど大きくありません。
  • name:
    name プロパティは、各 GameObject の文字列変数です。文字列は整数やオブジェクト参照よりも多くのメモリを消費するため、通常比較するとより多くのメモリを消費します。

一般に、タグとレイヤーの消費が最も少なく、名前の消費が最も多くなります。ただし、これらの消費量の違いは通常小さく、頻繁に使用する場合にのみ顕著になるため、どちらを使用するかを選択する際には独自の判断を下す必要があります。

重要で便利な、覚えておくべき機能

卓球

Mathf.PingPong 関数は、指定された間隔内で前後に移動する値を生成できます。これには 2 つのパラメーターがあります。1 つ目は現在の時刻で、2 つ目は移動の合計時間です。

関数の戻り値は、間隔 [0, t] でラップアラウンドします。

以下は、指定された間隔内でオブジェクトを前後に移動させる簡単な例です。

public class PingPongExample : MonoBehaviour
{
    
    
    public float speed = 1f;
    public float length = 10f;

    private Vector3 startPosition;

    private void Start()
    {
    
    
        startPosition = transform.position;
    }

    private void Update()
    {
    
    
        float offset = Mathf.PingPong(Time.time * speed, length);
        transform.position = startPosition + Vector3.forward * offset;
    }
}

ラープ

変数補間を使用する: スクリプトを記述するときは、コードの可読性と柔軟性を向上させるために、transform.position = Vector3.Lerp(transform.position, targetPosition, speed * Time.deltaTime);この。
https://docs.unity3d.com/ScriptReference/30_search.html?q=Lerp。
ここに画像の説明を挿入

LateUpdate

LateUpdate() メソッドは、Update() メソッドの実行が終了した直後に呼び出される Unity の特別な関数です。通常、カメラの追従やオブジェクトの移動などの操作は Update() で行いますが、カメラの位置や回転が関係する場合は、これらの操作を LateUpdate() に入れることをお勧めします。

これは、Update() でカメラを追跡したり、オブジェクトを移動したりすると、他のオブジェクトの移動や回転によってカメラの位置と回転が不安定になり、画像のブレやその他の視覚的な問題が発生する可能性があるためです。これらの操作を LateUpdate() に入れることで、カメラの位置と回転が他のオブジェクトの操作の後に調整されることを保証できるため、これらの問題を回避できます。

さらに、Update() の一部の操作で LateUpdate() の結果を使用する必要がある場合は、FixedUpdate() を使用して、物理エンジンとレンダリング エンジン間の同期を確保する必要があることに注意してください。

つまり、LateUpdate() は、画面の揺れやその他の視覚的な問題を回避するために、Update() の後にカメラの位置と回転を調整する必要がある状況に適しています。

クランプ

Mathf.Clamp() 関数は、指定された範囲内で指定された値を制限できます. 値が最小値より小さい場合は最小値を返します. 値が最大値より大きい場合は最大値を返します. そうでない場合は最大値を返します.値自体を返します。

public static float Clamp(float 値, float min, float max);

このうち、value は制限する値、min は最小値、max は最大値を表します。

たとえば、プレーヤーのスコアを 0 から 100 の間で制限する必要がある場合は、次のコードを使用できます。

score = Mathf.Clamp(score, 0f, 100f);

複雑なシーンでも使用できます。
カメラがプレーヤーを追跡するシーンがあり、カメラの位置がシーンの境界を超えないように制限する必要があるとします。Mathf.Clamp() 関数を使用して、この関数を実現できます。
シーンの左下隅を (0, 0)、右上隅を (100, 100)、カメラのターゲットをプレイヤー、カメラとターゲットの距離を距離、カメラの位置は cameraPosition です。次に、次のコードを使用してカメラの位置を制限できます。

// 获取玩家当前位置
Vector3 playerPosition = player.transform.position;
// 计算摄像机应该在的位置
Vector3 cameraPosition = playerPosition - player.transform.forward * distance;
// 限制摄像机的位置不能超出场景边界
cameraPosition.x = Mathf.Clamp(cameraPosition.x, 0f, 100f);
cameraPosition.z = Mathf.Clamp(cameraPosition.z, 0f, 100f);
// 设置摄像机的位置
transform.position = cameraPosition;

呼び出す

Invoke() 関数は、指定された時間の後に指定された関数を呼び出すか、指定された間隔で指定された関数を繰り返し呼び出すことができます。(コルーチンも実行できることに注意してください。正式な形式では、多くの人がコルーチンの方が適していると言っていることがわかります (コルーチンは後ろにあります))

public void Invoke(string methodName, float time);
public void InvokeRepeating(string methodName, float time, float repeatRate);

その中で、methodName は呼び出す必要がある関数の名前を表し、time は遅延する必要がある時間または繰り返し呼び出す必要がある時間間隔を表し、repeatRate は繰り返し呼び出す必要がある時間間隔を表します。

コルーチン

コルーチンは、マルチタスクの並列処理のための Unity のメカニズムです。コルーチンは実行中に一時停止し、実行を続行する前に特定の条件が満たされるのを待つことができるため、遅延実行、アニメーション効果、非同期タスクの処理など、いくつかの非常に便利な機能を実現できます。Google が Invoke とコルーチンの違いを検索するとき、誰もがコルーチンを使用することをお勧めします. 以下は、コルーチンを使用して遅延実行を実装する例です:

using UnityEngine;
using System.Collections;

public class CoroutineExample : MonoBehaviour
{
    
    
    void Start()
    {
    
    
        StartCoroutine(DelayedFunction(2.0f));
    }

    IEnumerator DelayedFunction(float delay)
    {
    
    
        Debug.Log("Delay start");
        yield return new WaitForSeconds(delay);
        Debug.Log("Delay end");
    }
}

また、開発にはジェネレーターのようなものがよくあります. 私は通常、コルーチンを開いて、開始時に呼び出します. 以下は私の開発の例です:

using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using UnityEngine;


public class ResidentManager : MonoBehaviour
{
    
    

    [SerializeField] GameObject[] residentsPrefabs;

    int totalResidentNumber;

    private void Awake()
    {
    
    
        //变量初始化
    }
    // Start is called before the first frame update
    void Start()
    {
    
    
        //一些在其他脚本内awake中初始化的变量需要在start中初始化,
        //因为Start方法会在所有对象的Awake方法调用完毕后执行
        totalResidentNumber = GameManager.INSTANCE.totalResidentNumber;

        
        StartCoroutine(SpawnResident());
    }

    // Update is called once per frame
    void Update()
    {
    
    
    }

    //协程,生成resident
    IEnumerator SpawnResident()
    {
    
    
        for (int i = 0; i < totalResidentNumber; i++)
        {
    
    
            int randomIndex = Random.Range(0, residentsPrefabs.Length);
            GameObject resident = Instantiate(residentsPrefabs[randomIndex], transform.position, Quaternion.identity);
            resident.transform.parent = transform;
            yield return new WaitForSeconds(0.1f);
        }
    }
}

It can also be used in this way. コルーチン関数では、yield return ステートメントを使用して、一定時間待機したり、別のコルーチンの実行が終了するのを待機したりできます。

IEnumerator MyCoroutine() {
    
    
    // 等待 3 秒钟
    yield return new WaitForSeconds(3f);

    // 等待另一个协程的执行结束
    yield return StartCoroutine(AnotherCoroutine());
}

IEnumerator AnotherCoroutine() {
    
    
    // 协程的执行逻辑
}

コルーチン関数では、yield break ステートメントを使用して、コルーチンの実行を中断できます。

IEnumerator MyCoroutine() {
    
    
    while (true) {
    
    
        // 协程的执行逻辑

        // 如果满足某个条件,中断协程的执行
        if (someCondition) {
    
    
            yield break;
        }
    }
}

レイキャスト

Raycast は Unity で非常によく使われる機能で、カメラやオブジェクトからレイを発射し、レイが他のオブジェクトと交差するかどうかを検出し、交点や法線ベクトルなどの情報を取得するために使用されます。Raycast は次のように使用されます。

Physics.Raycast 関数を使用してレイをキャストします。この関数は、開始点、方向、およびレイの長さを渡す必要があり、レイヤー マスク、クエリ トリガーなどのいくつかのオプションのパラメーターも渡すことができます。

Ray ray = new Ray(transform.position, transform.forward);
float maxDistance = 100f;
int layerMask = LayerMask.GetMask("Default");
bool hitSomething = Physics.Raycast(ray, out RaycastHit hitInfo, maxDistance, layerMask);

光線が他のオブジェクトと交差する場合、Physics.Raycast 関数は true を返し、交点の情報を RaycastHit 構造体に埋め込みます。交点、法線ベクトル、交差オブジェクトなどの情報は、この構造から取得できます。

if (hitSomething) {
    
    
    Debug.Log("Hit something at " + hitInfo.point + " with normal " + hitInfo.normal + " on object " + hitInfo.collider.gameObject.name);
}

Debug.DrawLine 関数を使用してシーンに光線を描画できます。これはデバッグに便利です。(描画された光線はシーン ウィンドウにのみ表示され、比較的短い時間です。よりよく観察するために時間を延長できます。通常は 5 秒を使用します。)

Debug.DrawLine(ray.origin, ray.origin + ray.direction * 100, Color.red,5);

ここに画像の説明を挿入

Raycast は、マウス クリック検出、光線兵器ヒット検出、AI 衝突検出など、さまざまなシナリオで使用できます。
.

Physics.OverlapSphere

Physics.OverlapSphere は、指定された半径内のすべてのオブジェクトを検出するために Unity で使用される関数です。この関数は、検出半径と検出中心点の座標の 2 つのパラメータを受け入れます。半径内のすべてのコライダー コンポーネントを含む配列を返します。
例:

using UnityEngine;

public class Player : MonoBehaviour
{
    
    
    public float detectionRadius = 10f;

    private void Update()
    {
    
    
        // 获取所有与玩家距离小于detectionRadius的对象
        Collider[] colliders = Physics.OverlapSphere(transform.position, detectionRadius);

        // 遍历所有对象,查找敌人
        foreach (Collider collider in colliders)
        {
    
    
            Enemy enemy = collider.GetComponent<Enemy>();
            if (enemy != null)
            {
    
    
                enemy.Attack();
            }
        }
    }
}

Physics.OverlapSphere 関数を使用するとパフォーマンスが低下するため、できるだけ使用しないか、必要な場合にのみ使用する必要があることに注意してください。さらに、多数のオブジェクトを含むシーンでは、物理レイヤーを使用して、検出する必要のないオブジェクトを除外することを検討して、パフォーマンスを向上させることができます。

Physics.OverlapSphere(transform.position, detectionRadius, enemyLayer);

開発の原則

ゲームオブジェクトの数を最小限に抑える

いくつかのゲームオブジェクトを可能な限り組み合わせると、ドローコールの数が大幅に減少し、パフォーマンスが向上します。

プレハブの再利用

プレハブは非常に便利で、シーン内のオブジェクトをすばやくインスタンス化し、複数のシーンで再利用できます.1 つのプレハブを変更すると、すべてのプレハブ インスタンスに変更を適用できます.
プレハブ バリアント (https://docs.unity3d.com/Manual/PrefabVariants.html) もあり、オブジェクト指向の継承と同様に、「ツリー」プレハブを変更し、「リンゴの木」プレハブに変更を適用して、 「梨の木」「プレハブのような機能。

オブジェクトプール

オブジェクト プーリングは、既に作成されているオブジェクトを再利用するための手法です。これにより、オブジェクト作成のオーバーヘッドを回避し、パフォーマンスを向上させることができます。たとえば、弾丸の場合、各弾丸が個別にインスタンス化および破棄される場合、オーバーヘッドが非常に高くなります。オブジェクト プールに切り替えると、リソースを節約できます。以下に例を示します。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ObjectPool : MonoBehaviour {
    
    

    public GameObject bulletPrefab;
    public int poolSize = 20;

    private List<GameObject> bullets = new List<GameObject>();
	//创建了一个Bullet的对象池,通过实例化bulletPrefab来填充池子。
    void Start () {
    
    
        for (int i = 0; i < poolSize; i++) {
    
    
            GameObject bullet = Instantiate (bulletPrefab);
            bullet.SetActive (false);
            bullets.Add (bullet);
        }
    }
	//GetBullet函数用于获取一个Bullet对象,
	//它首先检查池子中是否有空闲的对象,
	//如果有,就返回其中一个;否则,就实例化一个新的Bullet对象。
    public GameObject GetBullet () {
    
    
        for (int i = 0; i < bullets.Count; i++) {
    
    
            if (!bullets[i].activeInHierarchy) {
    
    
                return bullets[i];
            }
        }

        GameObject bullet = Instantiate (bulletPrefab);
        bullet.SetActive (false);
        bullets.Add (bullet);

        return bullet;
    }
	//ReturnBullet函数用于将Bullet对象返回池子。
	//当我们使用完一个Bullet对象时,可以将其传递给ReturnBullet函数
	//,以便将其返回到池子中,而不是销毁它。
    public void ReturnBullet (GameObject bullet) {
    
    
        bullet.SetActive (false);
    }
}

実際の使用では、Bullet オブジェクトを作成する必要がある場合は、ObjectPool の GetBullet 関数を使用してオブジェクトを取得し、使用後に ObjectPool の ReturnBullet 関数を使用してオブジェクトをプールに返します。このようにして、Bullet オブジェクトの頻繁な作成と破棄を回避できるため、アプリケーションのパフォーマンスが向上します。

プロファイラーを使用したパフォーマンス分析

プロファイラーは、開発者がアプリケーションのパフォーマンスのボトルネックを特定し、アプリケーションのパフォーマンスを最適化するのに役立つ Unity の組み込みツールです。現在実行中のプログラムのボトルネックがどこにあるのかを調べるためにプロファイラーを使用することができます.このツールについては unity の公式チュートリアルで学びました. 使い方も検索できるので、とても便利だと思います。

条件付きコンパイル

さまざまなプラットフォームやコンパイル条件に応じてさまざまなコードを記述できるため、アプリケーションの移植性が向上します。条件付きコンパイルを使用すると、プラットフォームを変更して新しいプロジェクトを作成したり、ターゲット プラットフォームに応じてコードに手動で注釈を付けたりする必要はありません.すべてのコードを直接書き込むことができます.違いは、コンパイル時に自動的に決定されることです.ビルド オプションのターゲット プラットフォームに応じて、コードのどの部分をコンパイルする必要があるか パフォーマンスを犠牲にすることなくコンパイルされたコード。

開発時にしか使わない関数は UNITY_EDITOR でプリコンパイルすることができます. 以下は私が開発した VR ゲームの例です. デバッグ時にエディタとウィンドウの下でしか動作しないので, このように書くことができます.ここに画像の説明を挿入

以下は、条件付きコンパイルを使用してプラットフォーム依存の機能を実装する例です。

using UnityEngine;
using System.Collections;

public class PlatformExample : MonoBehaviour
{
    
    
#if UNITY_EDITOR
    void Start()
    {
    
    
        Debug.Log("Unity Editor");
    }
#elif UNITY_ANDROID
    void Start()
    {
    
    
        Debug.Log("Android");
    }
#elif UNITY_IOS
    void Start()
    {
    
    
        Debug.Log("iOS");
    }
#endif
}

ひと目でわかりますが、最も重要なのは利便性であり、他のプラットフォームのコードをコメントアウトする必要はありません。結局のところ、unity はクロスプラットフォーム エンジンでもあります。
その他の条件付きコンパイルの内容とプラットフォームの一覧については、公式 Web サイトを参照してください:
https://docs.unity3d.com/Manual/PlatformDependentCompilation.html

シングルトン パターンを使用する

コードを記述するときは、シングルトン パターンの使用を検討してください。これにより、コードがより簡潔で読みやすくなり、保守が容易になります。シングルトン パターンは、アプリケーション全体で特定の型のインスタンスが 1 つだけ存在するようにすることを目的とした一般的な設計パターンです。
C# でシングルトン パターンを実装する方法の簡単な例を次に示します。

public class Singleton
{
    
    
    private static Singleton instance;

    private Singleton() {
    
     }

    public static Singleton Instance
    {
    
    
        get
        {
    
    
            if (instance == null)
            {
    
    
                instance = new Singleton();
            }
            return instance;
        }
    }
}

この例では、Singleton というクラスを定義し、静的プライベート インスタンス変数 instance を宣言します。外部から新しいシングルトン インスタンスを作成できないようにするために、プライベート コンストラクターも宣言します。

このようにして、Singleton.Instance を介して Singleton クラスの唯一のインスタンスを取得し、アプリケーション全体に Singleton のインスタンスが 1 つだけ存在するようにします。

シングルトン パターンを使用すると、次のような多くの利点が得られます。

  • アプリケーション内のインスタンスが 1 つだけであることを確認して、リソースの浪費を減らします。
  • 他のコードがインスタンスにアクセスするためのグローバル アクセス ポイントを提供します。
  • コンストラクターでインスタンスを初期化するなどして、インスタンスをより細かく制御できます。

ただし、コードの複雑さや結合が増加する可能性など、シングルトン モードがもたらす可能性があるいくつかの問題にも注意を払う必要があります。
シングルトンモードの書き方は複数ありますし、プログラムのバグがなく、欲しい機能が実現できる限り、他の書き方もあります!

プロジェクトをリファクタリングしてください!

スクリプトでの機能配置計画

スクリプトの可読性を向上させるために、各関数の場所を次のように計画できます。

  1. まず、すべてのパブリック メンバー変数 (パブリック変数やパブリック プロパティなど) をスクリプトの先頭に配置することをお勧めします。これにより、他の開発者がスクリプトで表示されるメンバー変数を簡単に確認できるようになります。

  2. 次に、パブリック メンバー変数の後にスクリプトのライフサイクル関数 (Start や Update など) を配置することをお勧めします。これらの関数は通常、スクリプトを初期化し、フレームごとにスクリプトの状態を更新するために使用されるため、パブリック メンバー変数の後に配置する方が理にかなっています。

  3. 次に、他のオーバーロード関数、条件付きコンパイル関数、パブリック関数、およびプライベート関数を関数の論理的な順序に従ってグループ化し、各関数グループの間に特定の空白行を残して分割することをお勧めします。これにより、コードの可読性が向上し、開発者がコードのロジックと構造を理解しやすくなります。

  4. 補助計算関数がある場合は、それらを呼び出す関数の後に配置し、その役割を強調するために短い名前を付けることが推奨されます。

たとえば、関数の可能なシーケンスは次のとおりです。

public class ExampleScript : MonoBehaviour {
    
    
    // 公共成员变量
    public int someValue;

    // 生命周期函数
    void Start() {
    
    
        // ...
    }

    void Update() {
    
    
        // ...
    }

    // 其他重载函数和条件编译函数
    void OnTriggerEnter(Collider other) {
    
    
        // ...
    }

    #if UNITY_EDITOR
    void OnDrawGizmos() {
    
    
        // ...
    }
    #endif

    // 公共函数
    public void DoSomething() {
    
    
        // ...
    }

    // 私有函数
    private void DoAnotherThing() {
    
    
        // ...
    }

    // 辅助计算函数
    private void CalculateSomething() {
    
    
        // ...
    }

    // 调用辅助计算函数的函数
    private void PerformCalculations() {
    
    
        CalculateSomething();
        // ...
    }
}

上記のコードでは、パブリック メンバー変数、ライフ サイクル関数、その他のオーバーロードされた関数と条件付きコンパイル関数、パブリック関数、プライベート関数、および補助計算関数の順序が並べられているため、コードの可読性が向上し、他の開発者にとってより簡単になります。コードのロジックと構造を理解します。

プロジェクト フォルダ ディレクトリの計画

プロジェクトのフォルダー ディレクトリ構造は、通常、次のように配置する必要があります。

  • Assets: このフォルダーは、シーン、マテリアル、プリセット、スクリプト、テクスチャ、サウンドなど、すべてのゲーム リソースのルート ディレクトリです。
  • Scenes: このフォルダは、すべてのシーン ファイルが保存される場所です。
  • Scripts: このフォルダーは、すべてのスクリプト ファイルが保存される場所です。
  • Prefabs: このフォルダーは、すべてのプリセット ファイルが保存される場所です。プリセットは再利用可能なゲーム オブジェクトです。
  • マテリアル: このフォルダは、すべてのマテリアル ファイルが保存される場所です。
  • Textures: このフォルダは、すべてのテクスチャ ファイルが保存される場所です。
  • オーディオ: このフォルダには、すべてのサウンド ファイルが保存されます。
  • Plugins: このフォルダーには、サードパーティ ライブラリやその他のツールなど、すべてのプラグイン ファイルが保存されます。
  • エディター: このフォルダーは、すべてのエディター拡張機能が保存される場所です。

おすすめ

転載: blog.csdn.net/gongfpp/article/details/129117178