paint-brush
Cách tối ưu hóa giao diện người dùng trong Unity: Nguyên nhân và giải pháp hiệu suất chậmtừ tác giả@sergeybegichev
683 lượt đọc
683 lượt đọc

Cách tối ưu hóa giao diện người dùng trong Unity: Nguyên nhân và giải pháp hiệu suất chậm

từ tác giả Sergei Begichev9m2024/08/25
Read on Terminal Reader

dài quá đọc không nổi

Xem cách tối ưu hóa hiệu suất UI trong Unity bằng hướng dẫn chi tiết này với nhiều thử nghiệm, lời khuyên thực tế và bài kiểm tra hiệu suất để chứng minh!
featured image - Cách tối ưu hóa giao diện người dùng trong Unity: Nguyên nhân và giải pháp hiệu suất chậm
Sergei Begichev HackerNoon profile picture

Xem cách tối ưu hóa hiệu suất UI trong Unity bằng hướng dẫn chi tiết này với nhiều thử nghiệm, lời khuyên thực tế và bài kiểm tra hiệu suất để chứng minh!


Xin chào! Tôi là Sergey Begichev, Nhà phát triển khách hàng tại Pixonic (MY.GAMES). Trong bài đăng này, tôi sẽ thảo luận về tối ưu hóa giao diện người dùng trong Unity3D. Mặc dù việc kết xuất một tập hợp các kết cấu có vẻ đơn giản, nhưng nó có thể dẫn đến các vấn đề đáng kể về hiệu suất. Ví dụ, trong dự án War Robots của chúng tôi, các phiên bản giao diện người dùng chưa được tối ưu hóa chiếm tới 30% tổng tải CPU — một con số đáng kinh ngạc!


Thông thường, vấn đề này phát sinh trong hai điều kiện: một là khi có nhiều đối tượng động và hai là khi các nhà thiết kế tạo ra các bố cục ưu tiên khả năng mở rộng đáng tin cậy trên các độ phân giải khác nhau. Ngay cả một UI nhỏ cũng có thể tạo ra tải đáng chú ý trong những trường hợp này. Hãy cùng khám phá cách thức hoạt động của nó, xác định nguyên nhân gây ra tải và thảo luận về các giải pháp tiềm năng.

Khuyến nghị của Unity

Đầu tiên, chúng ta hãy xem lại Khuyến nghị của Unity để tối ưu hóa UI, tôi đã tóm tắt thành sáu điểm chính:


  1. Chia nhỏ các bức tranh của bạn thành các bức tranh nhỏ hơn
  2. Xóa bỏ Raycast Target không cần thiết
  3. Tránh sử dụng các thành phần tốn kém (Danh sách lớn, dạng xem Lưới, v.v.)
  4. Tránh các nhóm bố trí
  5. Ẩn canvas thay vì Game Object (GO)
  6. Sử dụng hoạt hình một cách tối ưu


Trong khi các điểm 2 và 3 rõ ràng về mặt trực quan, các khuyến nghị còn lại có thể gây khó khăn khi hình dung trong thực tế. Ví dụ, lời khuyên "chia canvas của bạn thành các canvas con" chắc chắn là hữu ích, nhưng Unity không cung cấp hướng dẫn rõ ràng về các nguyên tắc đằng sau sự phân chia này. Riêng tôi, về mặt thực tế, tôi muốn biết nơi nào có ý nghĩa nhất để triển khai các canvas con.


Hãy cân nhắc lời khuyên “tránh các nhóm bố cục”. Mặc dù chúng có thể góp phần làm tăng tải UI, nhiều UI lớn đi kèm với nhiều nhóm bố cục và việc làm lại mọi thứ có thể tốn thời gian. Hơn nữa, những nhà thiết kế bố cục tránh các nhóm bố cục có thể thấy mình dành nhiều thời gian hơn đáng kể cho các tác vụ của mình. Do đó, sẽ rất hữu ích nếu hiểu khi nào nên tránh các nhóm như vậy, khi nào chúng có thể có lợi và cần thực hiện những hành động nào nếu chúng ta không thể loại bỏ chúng.


Sự mơ hồ trong các khuyến nghị của Unity là một vấn đề cốt lõi — thường không rõ ràng về những nguyên tắc chúng ta nên áp dụng cho những đề xuất này.

Nguyên tắc xây dựng UI

Để tối ưu hóa hiệu suất UI, điều cần thiết là phải hiểu cách Unity xây dựng UI. Hiểu các giai đoạn này rất quan trọng để tối ưu hóa UI hiệu quả trong Unity. Chúng ta có thể xác định ba giai đoạn chính trong quy trình này:


  1. Bố cục . Ban đầu, Unity sắp xếp tất cả các thành phần UI dựa trên kích thước và vị trí được chỉ định của chúng. Các vị trí này được tính toán theo các cạnh màn hình và các thành phần khác, tạo thành một chuỗi phụ thuộc.


  2. Xử lý hàng loạt . Tiếp theo, Unity nhóm các phần tử riêng lẻ thành các lô để kết xuất hiệu quả hơn. Vẽ một phần tử lớn luôn hiệu quả hơn so với việc kết xuất nhiều phần tử nhỏ hơn. (Để tìm hiểu sâu hơn về xử lý hàng loạt, hãy tham khảo bài viết này. )


  3. Rendering . Cuối cùng, Unity sẽ vẽ các lô đã thu thập. Càng ít lô thì quá trình render sẽ càng nhanh.


Mặc dù còn nhiều yếu tố khác liên quan đến quá trình này, nhưng ba giai đoạn này chiếm phần lớn các vấn đề, vì vậy bây giờ chúng ta hãy tập trung vào chúng.


Trong điều kiện lý tưởng, khi UI của chúng ta vẫn tĩnh — nghĩa là không có gì di chuyển hoặc thay đổi — chúng ta có thể xây dựng bố cục một lần, tạo một lô lớn duy nhất và hiển thị nó một cách hiệu quả.


Tuy nhiên, nếu chúng ta sửa đổi vị trí của ngay cả một phần tử, chúng ta phải tính toán lại vị trí của nó và xây dựng lại lô bị ảnh hưởng. Nếu các phần tử khác phụ thuộc vào vị trí này, khi đó chúng ta cũng sẽ cần tính toán lại vị trí của chúng, gây ra hiệu ứng tầng trong toàn bộ hệ thống phân cấp. Và càng nhiều phần tử cần điều chỉnh, tải lô càng cao.


Vì vậy, những thay đổi trong bố cục có thể gây ra hiệu ứng lan tỏa trên toàn bộ UI và mục tiêu của chúng ta là giảm thiểu số lượng thay đổi. (Ngoài ra, chúng ta có thể nhắm đến việc cô lập các thay đổi để ngăn chặn phản ứng dây chuyền.)


Ví dụ thực tế, vấn đề này đặc biệt rõ rệt khi sử dụng nhóm bố cục. Mỗi lần xây dựng lại bố cục, mọi LayoutElement đều thực hiện thao tác GetComponent, có thể tốn khá nhiều tài nguyên.

Nhiều bài kiểm tra

Hãy cùng xem xét một loạt ví dụ để so sánh kết quả hiệu suất. (Tất cả các thử nghiệm đều được thực hiện bằng Unity phiên bản 2022.3.24f1 trên thiết bị Google Pixel 1.)


Trong bài kiểm tra này, chúng ta sẽ tạo một nhóm bố cục gồm một phần tử duy nhất và phân tích hai tình huống: một là thay đổi kích thước của phần tử và hai là sử dụng thuộc tính FillAmount.


Những thay đổi của RectTransform:


FlllAmount thay đổi:


Trong ví dụ thứ hai, chúng ta sẽ thử làm điều tương tự, nhưng trong một nhóm bố cục có 8 phần tử. Trong trường hợp này, chúng ta vẫn chỉ thay đổi một phần tử.


Những thay đổi của RectTransform:


FlllAmount thay đổi:


Nếu trong ví dụ trước, các thay đổi đối với RectTransform dẫn đến tải 0,2 ms trên bố cục, thì lần này tải tăng lên 0,7 ms. Tương tự, tải từ việc cập nhật hàng loạt tăng từ 0,65 ms lên 1,10 ms.


Mặc dù chúng ta vẫn chỉ sửa đổi một phần tử, nhưng kích thước tăng lên của bố cục sẽ ảnh hưởng đáng kể đến tải trong quá trình xây dựng lại.


Ngược lại, khi chúng ta điều chỉnh FillAmount của một phần tử, chúng ta không thấy tải tăng, ngay cả khi có nhiều phần tử hơn. Điều này là do việc sửa đổi FillAmount không kích hoạt việc xây dựng lại bố cục, dẫn đến chỉ tăng nhẹ tải cập nhật theo đợt.


Rõ ràng, sử dụng FillAmount là lựa chọn hiệu quả hơn trong trường hợp này. Tuy nhiên, tình hình trở nên phức tạp hơn khi chúng ta thay đổi tỷ lệ hoặc vị trí của một phần tử. Trong những trường hợp này, việc thay thế các cơ chế tích hợp của Unity không kích hoạt xây dựng lại bố cục là một thách thức.


Đây là lúc SubCanvas phát huy tác dụng. Hãy cùng xem xét kết quả khi chúng ta đóng gói một phần tử có thể thay đổi trong SubCanvas.


Chúng ta sẽ tạo một nhóm bố cục với 8 phần tử, một trong số đó sẽ được đặt trong SubCanvas, sau đó sửa đổi phép biến đổi của nó.


Những thay đổi của RectTransform trong SubCanvas:


Như kết quả cho thấy, việc đóng gói một phần tử duy nhất trong SubCanvas gần như loại bỏ được tải trên bố cục; điều này là do SubCanvas cô lập mọi thay đổi, ngăn chặn việc xây dựng lại ở các cấp cao hơn của hệ thống phân cấp.


Tuy nhiên, điều quan trọng cần lưu ý là những thay đổi bên trong canvas sẽ không ảnh hưởng đến vị trí của các thành phần bên ngoài canvas. Do đó, nếu chúng ta mở rộng các thành phần quá nhiều, sẽ có nguy cơ chúng có thể chồng lấn với các thành phần lân cận.


Chúng ta hãy tiến hành bằng cách gói 8 phần tử bố cục trong một SubCanvas:


Ví dụ trước đó chứng minh rằng, trong khi tải trên layout vẫn thấp, thì bản cập nhật hàng loạt đã tăng gấp đôi. Điều này có nghĩa là, mặc dù việc chia các phần tử thành nhiều SubCanvases giúp giảm tải khi xây dựng layout, nhưng nó lại làm tăng tải khi lắp ráp hàng loạt. Do đó, điều này có thể dẫn đến một tác động tiêu cực tổng thể.


Bây giờ, chúng ta hãy tiến hành một thí nghiệm khác. Đầu tiên, chúng ta sẽ tạo một nhóm bố cục với 8 thành phần và sau đó sửa đổi một trong các thành phần bố cục bằng trình hoạt hình.


Bộ hoạt hình sẽ điều chỉnh RectTransform thành một giá trị mới:


Ở đây, chúng ta thấy kết quả tương tự như trong ví dụ thứ hai, nơi chúng ta thay đổi mọi thứ theo cách thủ công. Điều này hợp lý vì không có sự khác biệt nào giữa những gì chúng ta sử dụng để thay đổi RectTransform.


Bộ hoạt hình thay đổi RectTransform thành một giá trị tương tự:


Các nhà làm phim hoạt hình trước đây đã gặp phải vấn đề là họ sẽ liên tục ghi đè cùng một giá trị trong mỗi khung hình, ngay cả khi giá trị đó vẫn không thay đổi. Điều này vô tình kích hoạt việc xây dựng lại bố cục. May mắn thay, các phiên bản mới hơn của Unity đã giải quyết được vấn đề này, loại bỏ nhu cầu chuyển sang các phương án thay thế sự chuyển tiếp phương pháp chỉ nhằm cải thiện hiệu suất.


Bây giờ, chúng ta hãy xem xét cách thay đổi giá trị văn bản hoạt động trong nhóm bố cục có 8 phần tử và liệu nó có kích hoạt việc xây dựng lại bố cục hay không:


Chúng ta thấy rằng quá trình xây dựng lại cũng được kích hoạt.


Bây giờ, chúng ta sẽ thay đổi giá trị của TextMechPro trong nhóm bố cục gồm 8 phần tử:


TextMechPro cũng kích hoạt việc xây dựng lại bố cục và thậm chí có vẻ như nó đặt nhiều tải hơn vào việc tạo hàng loạt và hiển thị so với Text thông thường.


Thay đổi giá trị TextMechPro trong SubCanvas trong nhóm bố cục gồm 8 phần tử:


SubCanvas đã cô lập hiệu quả các thay đổi, ngăn chặn việc xây dựng lại bố cục. Tuy nhiên, mặc dù tải trọng khi cập nhật hàng loạt đã giảm, nhưng vẫn tương đối cao. Điều này trở thành mối quan tâm khi làm việc với văn bản, vì mỗi chữ cái được coi là một kết cấu riêng biệt. Do đó, việc sửa đổi văn bản sẽ ảnh hưởng đến nhiều kết cấu.


Bây giờ, hãy đánh giá tải phát sinh khi bật và tắt GameObject (GO) trong nhóm bố cục.


Bật và tắt GameObject bên trong nhóm bố cục gồm 8 phần tử:


Như chúng ta có thể thấy, việc bật hoặc tắt GO cũng kích hoạt việc xây dựng lại bố cục.


Bật GO bên trong SubCanvas với nhóm bố cục gồm 8 phần tử:


Trong trường hợp này, SubCanvas cũng giúp giảm tải.


Bây giờ, hãy kiểm tra tải là bao nhiêu nếu chúng ta bật hoặc tắt toàn bộ GO bằng một nhóm bố cục:


Như kết quả cho thấy, tải đã đạt đến mức cao nhất từ trước đến nay. Việc kích hoạt phần tử gốc sẽ kích hoạt việc xây dựng lại bố cục cho các phần tử con, điều này dẫn đến tải đáng kể trên cả quá trình tạo khối và kết xuất.


Vậy, chúng ta có thể làm gì nếu cần bật hoặc tắt toàn bộ các thành phần UI mà không tạo ra quá nhiều tải? Thay vì bật và tắt chính GO, bạn chỉ cần tắt thành phần Canvas hoặc Canvas Group. Ngoài ra, đặt kênh alpha của Canvas Group thành 0 có thể đạt được hiệu ứng tương tự trong khi tránh các vấn đề về hiệu suất.



Sau đây là những gì xảy ra với tải khi chúng ta vô hiệu hóa thành phần Canvas Group. Vì GO vẫn được bật trong khi canvas bị vô hiệu hóa, nên bố cục được bảo toàn nhưng chỉ đơn giản là không hiển thị. Cách tiếp cận này không chỉ dẫn đến tải bố cục thấp mà còn giảm đáng kể tải khi tạo hàng loạt và kết xuất.


Tiếp theo, chúng ta hãy xem xét tác động của việc thay đổi SiblingIndex trong nhóm bố cục.


Thay đổi SiblingIndex bên trong nhóm bố cục gồm 8 phần tử:


Như đã quan sát, tải vẫn đáng kể, ở mức 0,7 ms để cập nhật bố cục. Điều này chỉ ra rõ ràng rằng các sửa đổi đối với SiblingIndex cũng kích hoạt việc xây dựng lại bố cục.


Bây giờ, hãy thử nghiệm một cách tiếp cận khác. Thay vì thay đổi SiblingIndex, chúng ta sẽ hoán đổi kết cấu của hai phần tử trong nhóm bố cục.


Hoán đổi kết cấu của hai thành phần trong nhóm bố cục gồm 8 thành phần:


Như chúng ta có thể thấy, tình hình không được cải thiện; thực tế là nó còn tệ hơn. Việc thay thế kết cấu cũng kích hoạt việc xây dựng lại.


Bây giờ, hãy tạo một nhóm bố cục tùy chỉnh. Chúng ta sẽ xây dựng 8 phần tử và chỉ cần hoán đổi vị trí của hai phần tử trong số đó.


Nhóm bố cục tùy chỉnh với 8 thành phần:


Tải thực sự đã giảm đáng kể — và điều này là bình thường. Trong ví dụ này, tập lệnh chỉ hoán đổi vị trí của hai phần tử, loại bỏ các hoạt động GetComponent nặng nề và nhu cầu tính toán lại vị trí của tất cả các phần tử. Do đó, việc cập nhật ít hơn cần thiết cho việc xử lý hàng loạt. Mặc dù cách tiếp cận này có vẻ như là một giải pháp toàn diện, nhưng điều quan trọng cần lưu ý là việc thực hiện các phép tính trong tập lệnh cũng góp phần vào tổng tải.


Khi chúng ta đưa thêm tính phức tạp vào nhóm bố cục, tải sẽ tăng lên, nhưng không nhất thiết phải phản ánh trong phần Bố cục vì các phép tính diễn ra trong các tập lệnh. Vì vậy, điều quan trọng là tự mình theo dõi hiệu quả của mã. Tuy nhiên, đối với các nhóm bố cục đơn giản, các giải pháp tùy chỉnh có thể là một lựa chọn tuyệt vời.

Kết luận

Việc xây dựng lại bố cục là một thách thức đáng kể. Để giải quyết vấn đề này, chúng ta phải xác định nguyên nhân gốc rễ của nó, có thể khác nhau. Sau đây là các yếu tố chính dẫn đến việc xây dựng lại bố cục:


  1. Hoạt ảnh của các thành phần: chuyển động, tỷ lệ, xoay (bất kỳ thay đổi nào của phép biến đổi)
  2. Thay thế các sprite
  3. Viết lại văn bản
  4. Bật và tắt GO, thêm/xóa GO
  5. Thay đổi chỉ mục anh chị em


Điều quan trọng là phải làm nổi bật một số khía cạnh không còn gây ra vấn đề trong các phiên bản Unity mới hơn nhưng lại gây ra vấn đề trong các phiên bản trước đó: ghi đè cùng một văn bản và liên tục thiết lập cùng một giá trị bằng một trình hoạt hình.


Bây giờ chúng ta đã xác định được các yếu tố kích hoạt việc xây dựng lại bố cục, hãy tóm tắt các giải pháp của chúng ta:


  1. Wrap một GameObject (GO) kích hoạt việc xây dựng lại trong SubCanvas. Cách tiếp cận này cô lập các thay đổi, ngăn không cho chúng ảnh hưởng đến các phần tử khác trong hệ thống phân cấp. Tuy nhiên, hãy thận trọng — quá nhiều SubCanvas có thể làm tăng đáng kể tải khi xử lý hàng loạt.


  2. Bật và tắt SubCanvas hoặc Canvas Group thay vì GO. Sử dụng một nhóm đối tượng thay vì tạo GO mới. Phương pháp này bảo toàn bố cục trong bộ nhớ, cho phép kích hoạt nhanh các thành phần mà không cần phải xây dựng lại.


  3. Sử dụng hoạt ảnh shader. Thay đổi kết cấu bằng shader sẽ không kích hoạt việc xây dựng lại bố cục. Tuy nhiên, hãy nhớ rằng kết cấu có thể chồng chéo với các thành phần khác. Phương pháp này có hiệu quả phục vụ mục đích tương tự như sử dụng SubCanvases, nhưng nó yêu cầu phải viết shader.


  4. Thay thế nhóm bố cục của Unity bằng một nhóm bố cục tùy chỉnh. Một trong những vấn đề chính với các nhóm bố cục của Unity là mỗi LayoutElement gọi GetComponent trong quá trình xây dựng lại, điều này tốn nhiều tài nguyên. Việc tạo một nhóm bố cục tùy chỉnh có thể giải quyết vấn đề này, nhưng nó cũng có những thách thức riêng. Các thành phần tùy chỉnh có thể có các yêu cầu hoạt động cụ thể mà bạn cần hiểu để sử dụng hiệu quả. Tuy nhiên, cách tiếp cận này có thể hiệu quả hơn, đặc biệt là đối với các tình huống nhóm bố cục đơn giản hơn.