Unity Tối ưu hóa trò chơi của bạn bằng Profiler

Hiệu suất là khía cạnh quan trọng của bất kỳ trò chơi nào và không có gì đáng ngạc nhiên, cho dù trò chơi có hay đến đâu nhưng nếu nó chạy kém trên máy của người dùng thì người dùng sẽ không cảm thấy thú vị bằng.

Vì không phải ai cũng có PC hoặc thiết bị cao cấp (nếu bạn đang nhắm mục tiêu đến thiết bị di động), điều quan trọng là phải lưu ý đến hiệu suất trong toàn bộ quá trình phát triển.

Có nhiều lý do khiến trò chơi có thể chạy chậm:

  • Kết xuất (Quá nhiều lưới có độ poly cao, trình đổ bóng phức tạp hoặc hiệu ứng hình ảnh)
  • Âm thanh (Chủ yếu là do cài đặt nhập âm thanh không chính xác)
  • Mã không được tối ưu hóa (Các tập lệnh chứa các hàm yêu cầu hiệu suất ở sai vị trí)

Trong hướng dẫn này, tôi sẽ hướng dẫn cách tối ưu hóa mã của bạn với sự trợ giúp của Unity Profiler.

Hồ sơ

Trước đây, hiệu suất gỡ lỗi trong Unity là một công việc tẻ nhạt, nhưng kể từ đó, một tính năng mới đã được thêm vào, được gọi là Profiler.

Profiler là một công cụ trong Unity cho phép bạn nhanh chóng xác định các điểm nghẽn trong trò chơi của mình bằng cách theo dõi mức tiêu thụ bộ nhớ, giúp đơn giản hóa đáng kể quá trình tối ưu hóa.

Cửa sổ hồ sơ thống nhất

Phần trình bày tệ

Hiệu suất kém có thể xảy ra bất cứ lúc nào: Giả sử bạn đang làm việc trên phiên bản kẻ thù và khi bạn đặt nó vào hiện trường, nó hoạt động tốt mà không gặp vấn đề gì, nhưng khi bạn sinh ra nhiều kẻ thù hơn, bạn có thể nhận thấy khung hình/giây (khung hình mỗi giây). ) bắt đầu giảm.

Kiểm tra ví dụ dưới đây:

Trong Cảnh, tôi có một Khối có tập lệnh đính kèm, nó di chuyển Khối từ bên này sang bên kia và hiển thị tên đối tượng:

SC_ShowName.cs

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

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
}

Nhìn vào số liệu thống kê, chúng ta có thể thấy rằng trò chơi chạy ở tốc độ hơn 800 khung hình / giây nên hầu như không ảnh hưởng gì đến hiệu suất.

Nhưng hãy xem điều gì sẽ xảy ra khi chúng ta nhân đôi khối lập phương 100 lần:

Fps giảm hơn 700 điểm!

LƯU Ý: Tất cả các thử nghiệm đã được thực hiện khi tắt Vsync

Nói chung, bạn nên bắt đầu tối ưu hóa khi trò chơi bắt đầu có hiện tượng giật, đơ hoặc khung hình/giây giảm xuống dưới 120.

Làm thế nào để sử dụng hồ sơ?

Để bắt đầu sử dụng Profiler, bạn sẽ cần:

  • Bắt đầu trò chơi của bạn bằng cách nhấn Play
  • Mở Profiler bằng cách vào Window -> Analysis -> Profiler (hoặc nhấn Ctrl + 7)

  • Cửa sổ mới sẽ xuất hiện trông giống như thế này:

Cửa sổ hồ sơ Unity 3D

  • Ban đầu nó có thể trông đáng sợ (đặc biệt là với tất cả các biểu đồ đó, v.v.), nhưng đó không phải là phần chúng ta sẽ xem xét.
  • Nhấp vào tab Dòng thời gian và thay đổi nó thành Thứ bậc:

  • Bạn sẽ nhận thấy 3 phần (EditorLoop, PlayerLoop và Profiler.CollectEditorStats):

  • Mở rộng PlayerLoop để xem tất cả các phần mà sức mạnh tính toán đang được sử dụng (LƯU Ý: Nếu giá trị PlayerLoop không cập nhật, hãy nhấp vào nút "Clear" ở đầu cửa sổ Profiler).

Để có kết quả tốt nhất, hãy hướng nhân vật trong trò chơi của bạn đến tình huống (hoặc địa điểm) mà trò chơi bị chậm nhất và đợi trong vài giây.

  • Sau khi chờ một chút, Dừng trò chơi và quan sát danh sách PlayerLoop

Bạn cần xem giá trị GC Alloc, viết tắt của Phân bổ thu gom rác. Đây là loại bộ nhớ đã được comComponent cấp phát nhưng không còn cần thiết nữa và đang chờ được Bộ sưu tập rác giải phóng. Lý tưởng nhất là mã không được tạo ra bất kỳ rác nào (hoặc càng gần 0 càng tốt).

Thời gian ms cũng là một giá trị quan trọng, nó cho biết mã đã chạy trong bao lâu tính bằng mili giây, vì vậy lý tưởng nhất là bạn cũng nên cố gắng giảm giá trị này (bằng cách lưu các giá trị vào bộ nhớ đệm, tránh gọi các hàm yêu cầu hiệu suất mỗi Cập nhật, v.v..).

Để xác định vị trí lỗi nhanh hơn, click vào cột GC Alloc để sắp xếp giá trị từ cao xuống thấp)

  • Trong biểu đồ Sử dụng CPU, nhấp vào bất kỳ đâu để chuyển sang khung đó. Cụ thể, chúng ta cần xem xét các mức cao nhất, trong đó khung hình/giây là thấp nhất:

Biểu đồ sử dụng CPU Unity

Đây là những gì Profiler tiết lộ:

GUI.Repaint đang phân bổ 45,4KB, khá nhiều, mở rộng nó sẽ tiết lộ thêm thông tin:

  • Nó cho thấy rằng hầu hết các phân bổ đều đến từ phương thức GUIUtility.BeginGUI() và OnGUI() trong tập lệnh SC_ShowName, biết rằng chúng ta có thể bắt đầu tối ưu hóa.

GUIUtility.BeginGUI() đại diện cho một phương thức OnGUI() trống (Có, ngay cả phương thức OnGUI() trống cũng phân bổ khá nhiều bộ nhớ).

Sử dụng Google (hoặc công cụ tìm kiếm khác) để tìm những cái tên mà bạn không nhận ra.

Đây là phần OnGUI() cần được tối ưu hóa:

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }

Tối ưu hóa

Hãy bắt đầu tối ưu hóa.

Mỗi tập lệnh SC_ShowName gọi phương thức OnGUI() của riêng nó, điều này không tốt vì chúng ta có 100 phiên bản. Vì vậy, những gì có thể được thực hiện về nó? Câu trả lời là: Để có một tập lệnh duy nhất với phương thức OnGUI() gọi phương thức GUI cho mỗi Khối.

  • Đầu tiên, tôi thay thế OnGUI() mặc định trong tập lệnh SC_ShowName bằng public void GUIMethod() sẽ được gọi từ tập lệnh khác:
    public void GUIMethod()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
  • Sau đó tôi tạo một tập lệnh mới và gọi nó là SC_GUIMethod:

SC_GUIPhương thức.cs

using UnityEngine;

public class SC_GUIMethod : MonoBehaviour
{
    SC_ShowName[] instances; //All instances where GUI method will be called

    void Start()
    {
        //Find all instances
        instances = FindObjectsOfType<SC_ShowName>();
    }

    void OnGUI()
    {
        for(int i = 0; i < instances.Length; i++)
        {
            instances[i].GUIMethod();
        }
    }
}

SC_GUIMethod sẽ được gắn vào một đối tượng ngẫu nhiên trong cảnh và gọi tất cả các phương thức GUI.

  • Chúng ta đã chuyển từ việc có 100 phương thức OnGUI() riêng lẻ sang chỉ có một phương thức, hãy nhấn play và xem kết quả:

  • GUIUtility.BeginGUI() hiện chỉ phân bổ 368B thay vì 36,7KB, một mức giảm lớn!

Tuy nhiên, phương thức OnGUI() vẫn đang phân bổ bộ nhớ, nhưng vì chúng ta biết nó chỉ gọi GUIMethod() từ tập lệnh SC_ShowName nên chúng ta sẽ chuyển thẳng sang phần gỡ lỗi phương thức đó.

Nhưng Profiler chỉ hiển thị thông tin chung, làm cách nào để chúng ta biết chính xác những gì đang xảy ra bên trong phương thức?

Để gỡ lỗi bên trong phương thức, Unity có một API tiện dụng được gọi là Profiler.BeginSample

Profiler.BeginSample cho phép bạn ghi lại một phần cụ thể của tập lệnh, hiển thị thời gian hoàn thành và dung lượng bộ nhớ được phân bổ.

  • Trước khi sử dụng lớp Profiler trong mã, chúng ta cần nhập vùng tên UnityEngine.Profiling ở đầu tập lệnh:
using UnityEngine.Profiling;
  • Mẫu Profiler được chụp bằng cách thêm Profiler.BeginSample("SOME_NAME"); khi bắt đầu chụp và thêm Profiler.EndSample(); vào cuối quá trình chụp, như cái này:
        Profiler.BeginSample("SOME_CODE");
        //...your code goes here
        Profiler.EndSample();

Vì tôi không biết phần nào của GUIMethod() đang gây ra việc phân bổ bộ nhớ nên tôi đã đính kèm từng dòng trong Profiler.BeginSample và Profiler.EndSample (Nhưng nếu phương thức của bạn có nhiều dòng thì bạn chắc chắn không cần phải gửi kèm theo mỗi dòng, chỉ cần chia nó thành các phần chẵn rồi làm việc từ đó).

Đây là phương pháp cuối cùng với Mẫu Hồ sơ được triển khai:

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
        Profiler.EndSample();
    }
  • Bây giờ tôi nhấn Play và xem nó hiển thị gì trong Profiler:
  • Để thuận tiện, tôi đã tìm kiếm "sc_show_" trong Profiler, vì tất cả các mẫu đều bắt đầu bằng tên đó.

  • Thật thú vị... Rất nhiều bộ nhớ đang được phân bổ trong sc_show_names phần 3, tương ứng với phần mã này:
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);

Sau khi tìm kiếm trên Google, tôi phát hiện ra rằng việc lấy tên của Object sẽ tiêu tốn khá nhiều bộ nhớ. Giải pháp là gán tên Đối tượng cho một biến chuỗi trong void Start(), theo cách đó nó sẽ chỉ được gọi một lần.

Đây là mã được tối ưu hóa:

SC_ShowName.cs

using UnityEngine;
using UnityEngine.Profiling;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    string objectName = "";

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
        objectName = gameObject.name; //Store Object name to a variable
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), objectName);
        Profiler.EndSample();
    }
}
  • Hãy xem Profiler đang hiển thị những gì:

Tất cả các mẫu đều được phân bổ 0B, do đó không còn bộ nhớ nào được phân bổ nữa.

Bài viết được đề xuất
Mẹo tối ưu hóa cho Unity
Cải thiện hiệu suất của trò chơi di động trong Unity
Trình tạo bảng quảng cáo cho sự thống nhất
Cách sử dụng Cập nhật trong Unity
Cài đặt nhập clip âm thanh Unity để có hiệu suất tốt nhất
Làm thế nào để trở thành một lập trình viên giỏi hơn trong Unity
Các khái niệm cơ bản của thiết kế trò chơi