Hero image

Lập trình nâng cao và kiến ​​trúc mã

Trang web này đã được dịch bằng máy để thuận tiện cho bạn. Chúng tôi không thể đảm bảo tính chính xác hoặc độ tin cậy của nội dung được dịch. Nếu bạn có thắc mắc về tính chính xác của nội dung được dịch, vui lòng tham khảo phiên bản tiếng Anh chính thức của trang web.

Đây là bài viết thứ tư trong loạt bài hướng dẫn tối ưu hóa dự án Unity của bạn. Hãy sử dụng chúng như một hướng dẫn để chạy ở tốc độ khung hình cao hơn với ít tài nguyên hơn. Sau khi đã thử áp dụng những phương pháp tốt nhất này, hãy nhớ xem thêm các trang khác trong loạt bài này:

Xem các hướng dẫn tối ưu hóa mới nhất của chúng tôi dành cho nhà phát triển và nghệ sĩ Unity 6:

Hiểu về vòng lặp người chơi (PlayerLoop) Unity.

Hãy đảm bảo bạn hiểu rõ thứ tự thực thi của vòng lặp khung hình trong Unity. Mỗi script Unity đều chạy một số hàm xử lý sự kiện theo một trình tự đã được xác định trước. Bạn cần hiểu sự khác biệt giữa Awake , Start , Update và các hàm khác tạo vòng đời của một script. Bạn có thể sử dụng API cấp thấp để thêm logic tùy chỉnh vào vòng lặp cập nhật của trình phát.

Xem sơ đồ quy trình vòng đời của Script để biết thứ tự thực thi cụ thể của các hàm sự kiện.

Hãy tìm hiểu về PlayerLoop và vòng đời của một script.

Hãy tìm hiểu về PlayerLoop và vòng đời của một script.

Xây dựng trình quản lý cập nhật tùy chỉnh

Một cách sử dụng phổ biến cho Update hoặc LateUpdate là chỉ thực thi logic khi một điều kiện nào đó được đáp ứng. Điều này có thể dẫn đến rất nhiều lệnh gọi lại trên mỗi khung hình mà thực chất không chạy bất kỳ đoạn mã nào ngoại trừ việc kiểm tra điều kiện này.

Mỗi khi Unity gọi một phương thức Message như Update hoặc LateUpdate, nó sẽ thực hiện một cuộc gọi tương tác , một cuộc gọi từ phía C/C++ đến phía C# được quản lý. Đối với số lượng đối tượng nhỏ, điều này không phải là vấn đề. Khi bạn có hàng nghìn đối tượng, chi phí phát sinh này bắt đầu trở nên đáng kể.

Hãy cân nhắc tạo một UpdateManager tùy chỉnh nếu bạn có một dự án lớn sử dụng Update hoặc LateUpdate theo cách này (ví dụ: một trò chơi thế giới mở). Hãy cho phép các đối tượng đang hoạt động đăng ký nhận thông báo từ UpdateManager này khi chúng muốn lệnh gọi lại và hủy đăng ký khi không cần nữa. Mô hình này có thể giảm thiểu nhiều cuộc gọi tương tác đến các đối tượng Monobehaviour của bạn. Hãy đăng ký các đối tượng đang hoạt động vào Trình quản lý cập nhật này khi chúng cần lệnh gọi lại và hủy đăng ký khi chúng không cần nữa. Mô hình này có thể giảm thiểu nhiều lệnh gọi tương tác đến các đối tượng Monobehaviour của bạn.

Hãy tham khảo các kỹ thuật tối ưu hóa dành riêng cho công cụ trò chơi để xem các ví dụ về triển khai.

Sơ đồ Trình quản lý cập nhật tùy chỉnh

Việc xây dựng Trình quản lý cập nhật tùy chỉnh giúp giảm thiểu các cuộc gọi tương tác.

Giảm thiểu mã chạy ở mọi khung hình

Hãy xem xét liệu đoạn mã đó có cần phải chạy trong mỗi khung hình hay không. Loại bỏ các logic không cần thiết khỏi các phương thức Update, LateUpdate và FixedUpdate. Các hàm xử lý sự kiện này là những nơi thuận tiện để đặt mã cần cập nhật mỗi khung hình, đồng thời tách biệt bất kỳ logic nào không cần cập nhật với tần suất đó. Khi có thể, chỉ thực thi logic khi có sự thay đổi.

Nếu bạn cần sử dụng hàm Update , hãy cân nhắc chạy đoạn mã này sau mỗi n khung hình. Đây là một cách để áp dụng kỹ thuật phân chia thời gian (time slicing), một kỹ thuật phổ biến để phân bổ khối lượng công việc lớn trên nhiều khung xử lý. Trong ví dụ này, chúng ta chạy hàm ExampleExpensiveFunction một lần mỗi ba khung hình:

private int interval = 3;

void Update()

{

nếu (Time.frameCount % interval == 0)

{

ExampleExpensiveFunction();

}

}

Tốt hơn nữa, nếu ExampleExpensiveFunction thực hiện một số thao tác trên một tập dữ liệu, hãy cân nhắc sử dụng phân chia thời gian để thao tác trên một tập con khác nhau của dữ liệu đó trong mỗi khung hình. Bằng cách thực hiện 1/ n công việc mỗi khung hình thay vì toàn bộ công việc mỗi n khung hình, bạn sẽ có được hiệu năng ổn định và dễ dự đoán hơn, thay vì gặp phải tình trạng CPU tăng đột biến định kỳ.

Mấu chốt là phải xen kẽ việc này với các công việc khác chạy trên các khung hình khác. Trong ví dụ này, bạn có thể "lên lịch" cho các hàm tốn kém khác khi Time.frameCount % interval == 1 hoặc Time.frameCount % interval == 2 .

Ngoài ra, bạn cũng có thể sử dụng lớp UpdateManager tùy chỉnh (bên dưới) và cập nhật các đối tượng đã đăng ký sau mỗi n khung hình.

Lưu trữ kết quả của các hàm tốn kém vào bộ nhớ cache.

Tốt nhất nên lưu trữ các tham chiếu trong phương thức Awake hoặc Start để tránh gọi chúng trong phương thức Update.

Dưới đây là một ví dụ minh họa việc sử dụng không hiệu quả khi gọi hàm GetComponent lặp đi lặp lại:

void Update()

{

Renderer myRenderer = GetComponent();

ExampleFunction(myRenderer);

}

Việc chỉ gọi hàm GetComponent một lần sẽ hiệu quả hơn, vì kết quả của hàm được lưu vào bộ nhớ cache. Kết quả được lưu trong bộ nhớ cache có thể được sử dụng lại trong hàm Update mà không cần gọi thêm hàm GetComponent nào nữa.

Trình kết xuất riêng tư myRenderer;

void Start()

{

myRenderer = GetComponent();

}

void Update()

{

ExampleFunction(myRenderer);

}

Ghi chú : Trong các phiên bản Unity trước Unity 2020.2, GameObject.Find , GameObject.GetComponent và Camera.main từng rất tốn tài nguyên, tuy nhiên điều này không còn đúng nữa. Tuy nhiên, tốt nhất là nên tránh gọi chúng trong các phương thức Update và làm theo cách trên bằng cách lưu trữ kết quả vào bộ nhớ cache.

Tránh sử dụng các sự kiện Unity trống và các câu lệnh ghi nhật ký gỡ lỗi.

Các câu lệnh ghi nhật ký (đặc biệt là trong Update , LateUpdate hoặc FixedUpdate ) có thể làm giảm hiệu năng. Vô hiệu hóa các câu lệnh ghi nhật ký trước khi thực hiện bản dựng.

Để thực hiện việc này dễ dàng hơn, hãy cân nhắc tạo một thuộc tính có điều kiện cùng với một chỉ thị tiền xử lý. Ví dụ, tạo một lớp tùy chỉnh như sau:

public static class Logging

{

[System.Diagnostics.Conditional("ENABLE_LOG")]

static public void Log(object message)

{

UnityEngine.Debug.Log(message);

}

}

Tạo thông báo nhật ký của bạn bằng lớp tùy chỉnh. Nếu bạn vô hiệu hóa bộ xử lý trước ENABLE_LOG trong Cài đặt Trình phát , tất cả các câu lệnh Log của bạn sẽ biến mất ngay lập tức.

Điều tương tự cũng áp dụng cho các trường hợp sử dụng khác của lớp Debug, chẳng hạn như Debug.DrawLine và Debug.DrawRay. Những công nghệ này cũng chỉ được sử dụng trong giai đoạn phát triển và có thể ảnh hưởng đáng kể đến hiệu năng.

Bài đăng trên blog này về 10.000 lệnh gọi Update giải thích cách Unity thực thi Monobehaviour.Update .

Việc thêm chỉ thị tiền xử lý tùy chỉnh cho phép bạn phân chia các tập lệnh của mình.

Việc thêm chỉ thị tiền xử lý tùy chỉnh cho phép bạn phân chia các tập lệnh của mình.

Tắt tính năng ghi nhật ký dấu vết ngăn xếp.

Sử dụng các tùy chọn Stack Trace trong Cài đặt Trình phát để kiểm soát loại thông báo nhật ký nào sẽ hiển thị.

Nếu ứng dụng của bạn đang ghi nhật ký lỗi hoặc thông báo cảnh báo trong bản phát hành (ví dụ: để tạo báo cáo sự cố trong môi trường thực tế), hãy tắt tính năng theo dõi ngăn xếp để cải thiện hiệu suất.

Tìm hiểu thêm về ghi nhật ký Stack Trace .

Tùy chọn dấu vết ngăn xếp

Tùy chọn dấu vết ngăn xếp

Sử dụng giá trị băm thay vì tham số chuỗi.

Unity không sử dụng tên chuỗi để truy cập các thuộc tính của animator, material và shader bên trong hệ thống. Để tăng tốc độ, tất cả tên thuộc tính đều được băm thành ID thuộc tính, và các ID này thực sự được sử dụng để truy cập các thuộc tính đó.

Khi sử dụng phương thức Set hoặc Get trên animator, material hoặc shader, hãy sử dụng phương thức trả về giá trị số nguyên thay vì phương thức trả về giá trị chuỗi. Các phương thức xử lý chuỗi chỉ đơn giản là thực hiện băm chuỗi rồi chuyển tiếp ID đã băm đến các phương thức xử lý giá trị số nguyên.

Sử dụng Animator.StringToHash cho tên thuộc tính Animator và Shader.PropertyToID cho tên thuộc tính vật liệu và shader. Lấy các mã băm này trong quá trình khởi tạo và lưu trữ chúng trong các biến để sử dụng khi cần thiết cho phương thức Get hoặc Set.

Liên quan đến vấn đề này là việc lựa chọn cấu trúc dữ liệu, điều này ảnh hưởng đến hiệu năng khi bạn lặp lại hàng nghìn lần mỗi khung hình. Hãy tham khảo hướng dẫn về cấu trúc dữ liệu trong C# trên MSDN để có cái nhìn tổng quan về cách chọn cấu trúc phù hợp.

Gộp các đối tượng của bạn

Việc khởi tạo và hủy bỏ có thể tạo ra các điểm tắc nghẽn về bộ nhớ và quá trình thu gom rác (GC), nhìn chung là một quá trình chậm. Hãy áp dụng các kỹ thuật quản lý nhóm đối tượng khi bạn cần tạo ra một số lượng lớn đối tượng để giúp tránh tình trạng quá tải bộ nhớ do thu gom rác (GC) .

Quản lý đối tượng (Object pooling) là một mẫu thiết kế có thể tối ưu hóa hiệu năng bằng cách giảm công suất xử lý cần thiết của CPU để thực hiện các lệnh tạo và hủy lặp đi lặp lại. Thay vào đó, với kỹ thuật quản lý nhóm đối tượng (object pooling), GameObjects hiện có có thể được tái sử dụng nhiều lần.

Chức năng chính của việc quản lý đối tượng theo nhóm (object pooling) là tạo các đối tượng trước và lưu trữ chúng trong một nhóm, thay vì tạo và hủy chúng theo yêu cầu. Khi cần một vật phẩm nào đó, nó sẽ được lấy từ kho và sử dụng. Khi không còn cần thiết nữa, nó sẽ được trả lại bể chứa thay vì bị tiêu hủy.

Thay vì thường xuyên tạo và hủy GameObjects (ví dụ: bắn đạn từ súng), hãy sử dụng các nhóm đối tượng được cấp phát trước có thể được tái sử dụng và tái chế.

Điều này giúp giảm số lượng phân bổ được quản lý trong dự án của bạn và có thể ngăn ngừa các vấn đề về thu gom rác.

Unity tích hợp sẵn tính năng quản lý nhóm đối tượng thông qua không gian tên UnityEngine.Pool . Có sẵn trong Unity 2021 LTS trở lên, namespace này hỗ trợ quản lý các nhóm đối tượng, tự động hóa các khía cạnh như vòng đời đối tượng và kiểm soát kích thước nhóm.

Tìm hiểu cách tạo một hệ thống quản lý đối tượng đơn giản trong Unity tại đây . Bạn cũng có thể thấy mô hình gộp đối tượng (object pooling), và nhiều mô hình khác, được triển khai trong một cảnh Unity trong dự án mẫu này có sẵn trên Unity Asset Store.

Quản lý nhóm đối tượng trong Unity

Quản lý nhóm đối tượng trong Unity

Khai thác sức mạnh của ScriptableObjects

Hãy lưu trữ các giá trị hoặc cài đặt tĩnh không thay đổi trong một ScriptableObject thay vì MonoBehaviour. ScriptableObject là một tài nguyên nằm bên trong dự án mà bạn chỉ cần thiết lập một lần duy nhất.

MonoBehaviours có thêm chi phí xử lý vì chúng yêu cầu một GameObject – và theo mặc định là một Transform – để đóng vai trò là đối tượng chủ. Điều đó có nghĩa là bạn cần tạo rất nhiều dữ liệu không sử dụng trước khi lưu trữ một giá trị duy nhất. ScriptableObject giúp giảm thiểu lượng bộ nhớ sử dụng bằng cách loại bỏ GameObject và Transform. Nó cũng lưu trữ dữ liệu ở cấp độ dự án, điều này rất hữu ích nếu bạn cần truy cập cùng một dữ liệu từ nhiều cảnh khác nhau.

Một trường hợp sử dụng phổ biến là có nhiều GameObjects dựa vào cùng một dữ liệu trùng lặp, dữ liệu này không cần phải thay đổi trong quá trình chạy. Thay vì lưu trữ dữ liệu cục bộ trùng lặp trên mỗi GameObject, bạn có thể chuyển nó vào một ScriptableObject. Sau đó, mỗi đối tượng lưu trữ một tham chiếu đến tài nguyên dữ liệu được chia sẻ, thay vì sao chép chính dữ liệu đó. Đây là một lợi ích có thể mang lại những cải tiến đáng kể về hiệu suất trong các dự án có hàng nghìn đối tượng.

Tạo các trường trong ScriptableObject để lưu trữ các giá trị hoặc cài đặt của bạn, sau đó tham chiếu đến ScriptableObject đó trong MonoBehaviours của bạn.

Việc sử dụng các trường từ ScriptableObject có thể ngăn ngừa việc trùng lặp dữ liệu không cần thiết mỗi khi bạn khởi tạo một đối tượng với MonoBehaviour đó.

Trong thiết kế phần mềm, đây là một phương pháp tối ưu hóa được gọi là mô hình flyweight. Việc tái cấu trúc mã của bạn theo cách này bằng cách sử dụng ScriptableObjects sẽ tránh việc sao chép nhiều giá trị và giảm thiểu lượng bộ nhớ tiêu thụ. Tìm hiểu thêm về mẫu thiết kế flyweight và nhiều mẫu khác, cũng như các nguyên tắc thiết kế trong sách điện tử " Nâng tầm mã nguồn của bạn với các mẫu thiết kế và SOLID" .

Hãy xem video hướng dẫn Giới thiệu về ScriptableObjects này để hiểu cách ScriptableObjects có thể mang lại lợi ích cho dự án của bạn. Vui lòng tham khảo tài liệu Unity tại đây cũng như hướng dẫn kỹ thuật "Tạo kiến ​​trúc trò chơi dạng mô-đun trong Unity với ScriptableObjects" .

Trong ví dụ này, một ScriptableObject có tên là Inventory chứa các thiết lập cho nhiều GameObjects khác nhau.

Trong ví dụ này, một ScriptableObject có tên là Inventory chứa các thiết lập cho nhiều GameObjects khác nhau.

Thêm mẹo dành cho Unity 6

Bạn có thể tìm thấy nhiều mẹo và kinh nghiệm thực tiễn tốt nhất dành cho các nhà phát triển và người sáng tạo Unity chuyên nghiệp tại trung tâm kinh nghiệm thực tiễn tốt nhất Unity . Hãy lựa chọn từ hơn 30 hướng dẫn được tạo ra bởi các chuyên gia trong ngành, các kỹ sư và nghệ sĩ kỹ thuật Unity , giúp bạn phát triển hiệu quả với bộ công cụ và hệ thống của Unity.