Trang chủ Program Tối ưu unity game – Scripting only

Tối ưu unity game – Scripting only

bởi root

Lời mở đầu

Trong bất kì tựa game nào, trải nghiệm người dùng luôn là 1 phần quan trọng. Ngoài gameplay và cốt truyện, đó còn là trải nghiệm về sự mượt mà của đồ họa, sự ổn định trong kết nối mạng (đối với game multiplayer), sự phản ứng nhạy với thao tác người dùng, và kể cả dung lượng bộ cài.

Từ khi những Game Engine như Unity, Unreal ra đời, những tool này đã đơn giản hóa việc làm game đi rất nhiều. Người người làm game, nhà nhà làm game. Điều đó đồng nghĩa với khi đã bắt đầu làm game, ta phải chạy đua với rất nhiều nhà phát triển khác. Nếu muốn vượt lên trên top đầu thì buộc phải nâng cao chất lượng sản phẩm. 

Game tối ưu tồi tệ thể hiện ra bên ngoài như low frame rate, freeze, crash, input lag, loading time lâu, cơ chế vật lí không tạo cảm giác thực tế, và kể cả việc xài hao pin (1 yếu tố thường bị bỏ qua). Chỉ 1 trong số các điều trên thôi có thể khiến sự chú ý của người chơi tập trung vào những thứ ta làm dở, mặc kệ rằng còn nhiều tính năng khác hay ho.

Mục đích chính của việc tối ưu là sử dụng tài nguyên một cách có hiệu quả nhất. Ta phải kiểm soát các yếu tố sau: CPU phải tính toán bao nhiêu phép tính, độ phức tạp của các thuật toán, RAM (memory) sử dụng bao nhiêu, GPU sử dụng bao nhiêu, VRAM (dung lượng lưu trữ) bao nhiêu, và còn nhiều thứ khác nữa,….

Kiểm soát được các thành phần trên, ta sẽ hạn chế được bottleneck (thuật ngữ được sử dụng khi 1 thành phần phải xử lí nhiều hơn các thành phần khác, dẫn đến hệ thống sẽ bị quá tải ở điểm này và làm các thành phần khác chạy không đủ công suất → lãng phí hiệu năng của hệ thống)
Nội dung về tối ưu game sẽ chia làm nhiều phần, để mở đầu tôi giới thiệu với anh em bài đầu tiên là Tối ưu về Scripting trong Unity.

Công cụ Unity Profiler

Profiler là 1 công cụ tích hợp bên trong Unity, để truy cập, anh em vào đường dẫn Window > Analysis > Profiler. Profiler show ra những thông tin như tài nguyên sử dụng và độ nặng của các system chạy theo thời gian thực. Các system có thể cung cấp thông tin được list ra dưới đây:

  • Mức sử dụng CPU
  • Mức sử dụng GPU
  • Memory
  • Audio
  • Physics 2D, 3D
  • Network
  • Video

Kết quả của Profiler sẽ là bằng chứng chứng minh phương pháp tối ưu mà ta áp dụng có thực sự hiệu quả hay không bằng cách so sánh kết quả đo lường tại 2 thời điểm trước và sau khi áp dụng 1 biện pháp tối ưu.

Các kĩ thuật tối ưu Scripting

Clean code

Clean Code đảm bảo mã nguồn có chất lượng tốt, giúp ích cho việc cộng tác nhóm, dễ dàng bảo trì và mở rộng hệ thống. Với việc Optimize thường được làm ở cuối giai đoạn hoàn thiện sản phẩm, việc phải làm việc với 1 mã nguồn sạch đẹp sẽ dễ chịu hơn với phải làm việc với 1 mã nguồn bốc mùi.

 

Các kĩ thuật Clean code được giới thiệu trong đầu sách cùng tên của tác giả Robert C.Martin, quyển này thì khá nổi tiếng rồi và là cuốn sách gối đầu giường của rất nhiều anh em dev. Anh em nào lười đọc thì có thể tham khảo các bản tóm tắt trên google hoặc xem bài viết của Sơn.

Bỏ đi các hàm callback rỗng

Mục đích chính của việc viết các script kế thừa từ MonoBehavior là sử dụng các hàm callback mặc định của Unity như Awake, Start, Update, FixedUpdate.Về thứ tự gọi của các hàm này anh em có thể tham khảo ở link tại đây https://docs.unity3d.com/Manual/ExecutionOrder.html

Hình dưới đây minh họa việc 2 hàm callback này mặc định được thêm vào khi anh em tạo 1 script mới trong Unity:

Bất cứ khi nào 1 script MonoBehavior được khởi tạo trong Scene, Unity sẽ add mặc định các  hàm callback từ các script đó vào các list để gọi vào những thời điểm cụ thể.Tuy nhiên, Unity sẽ add tất cả các hàm callback vào list, bất kể việc các hàm callback đó có thân hàm hay không. 

Việc quên xóa 1 số hàm callback này sẽ không gây ra hậu quả gì to tát đáng kể, tuy nhiên nếu trong 1 Scene với số lượng script Mono lên đến hàng nghìn, việc để hàng nghìn hàm rỗng như này có thể gây vấn đề với CPU, khiến Scene khởi tạo lâu hơn, cụ thể là vào thời điểm mỗi Prefab được tạo mới qua hàm GameObject.Instantiate().

Ok và để chứng minh, tôi làm 1 bài test nho nhỏ. Làm 1 test Scene có các GameObject, gắn vào mỗi GameObject 2 Component là EmptyClassComponent với thân class rỗng, EmptyCallbackComponent với một hàm Update() rỗng.

Và dưới đây là kết quả test trên 30,000 GameObject, nếu bật EmptyClassComponent, CPU Usage Area trong Profiler trông không có gì nhiều. Tuy nhiên nếu bật EmptyCallbackComponent, vùng usage này tăng đột biến như hình bên dưới

 

Trong 1 project thường làm thì việc có hơn 30,000 object trong 1 scene là khá hiếm, chắc trừ mấy thể loại Battle Simulator hoặc Massive Battlefield ra. Tuy nhiên anh em cần lưu ý rằng các script MonoBehavior mới có các hàm callback như Update, không phải GameObject. Và việc 1 GameObject có gắn nhiều MonoBehavior script là điều bình thường, và nếu các thành phần con của GameObject đó lại gắn các script Mono nữa thì việc số lượng script cần phải được kiểm soát chặt chẽ.

Cách xử lí cho trường hợp này là hàm callback nào không dùng thì xóa đi.

Cache lại component reference

Việc lặp lại tính toán là một lỗi phổ biến khi code, đặc biệt là việc GetComponent() tôi nghĩ nhiều anh em hay gặp. Ví dụ dưới đây tôi có đoạn code check component máu của một đối tượng, nếu giá trị máu nhỏ hơn 0 thì trigger trạng thái chết.

 

Như anh em đã thấy, với mỗi lần hàm này được gọi, nó yêu cầu 5 phép tìm kiếm reference đến Component. Xét về mặt hiệu năng thì không được ổn lắm, tệ hơn là nếu hàm kiểm tra này được gọi trong Update.

Giải pháp cho trường hợp này là những Component này có thể được lấy reference và cache lại 1 lần trong hàm Awake để sử dụng cho những lần sau.

 

Memory để cache mỗi reference này tốn khoảng 32-64bits vào memory mỗi lần, tuy nhiên CPU lại đỡ phải tính toán. Đánh đổi như này cũng đáng phết 😀

Tránh việc lấy string properties từ gameobject

So sánh ‘tag’ là một việc khá phổ biến khi code. Anh em có thể dùng để đánh dấu Ally và Enemy chẳng hạn, để có những xử lí phù hợp cho từng loại đối tượng. Tuy nhiên việc sử dụng so sánh  tag như nào cũng là 1 điều cần lưu ý, nếu dùng sai cách có thể gây lãng phí memory. Đoạn code dưới đây là 1 ví dụ về việc bị leak memory qua mỗi vòng lặp

 

Tốt hơn thì nên nhận dạng Object thông qua Component và classtype. Tuy nhiên có vài trường hợp chúng ta buộc phải sử dụng tag để phân biệt. Vào trường hợp đó, GameObject trong unity đã có sẵn 1 hàm để so sánh tag là CompareTag()

Dưới đây là 1 bài test nho nhỏ để chứng minh hiệu năng của 2 cách trên. Tôi có đoạn code, mỗi khi bấm phím 1 sẽ so sánh tag bằng property .tag, bấm phím 2 sẽ so sánh bằng hàm CompareTag(), mỗi lần bấm sẽ chạy 10 triệu lần

 

Và sau đây là kết quảTheo như kết quả hình bên trên, việc sử dụng so sánh .tag tiêu tốn 400MB GC Allocated, tốn hơn 2000ms để chạy xong. Sử dụng CompareTag() chỉ tốn 1000ms để chạy xong và không cần dùng tới GC Allocated.

 

Một string properties nữa là ‘name’ cũng có thể được đem ra so sánh, tuy nhiên đen ở chỗ là Unity chưa cung cấp phương thức Compare cho thuộc tính này, nên anh em cân nhắc phương án sử dụng. Có thể so sánh bằng classtype, ví dụ như có thể xem một Object có phải Ally không bằng cách xem script mà nó gắn vào có thuộc typeOf(Ally) không.

Sử dụng data structure hợp lý

Trong gêm, sẽ có lúc chúng ta phải quản lý, lưu trữ một nhóm các đối tượng và thực hiện các phép toán như tìm kiếm, so sánh,…. Trong phần này tôi sẽ so sánh 3 kiểu phổ biến là Array, List, và Dictionary

Ví dụ mẫu như sau: tạo 1 Array int[], 1 List và 1 Dictionary<int, int>, cấp phát cho mỗi thằng 10 triệu phần tử, mỗi phần tử là 1 số nguyên Random từ 0 đến 100

Phép so sánh thứ nhất là so sánh tốc độ duyệt phần tử, cách làm như sau:

  • Đặt 1 điểm log ở trước và sau khi duyệt phần tử
  • Tiến hành duyệt phần tử trong 3 kiểu lưu trữ bên trên bằng lệnh for và foreach

Phép so sánh thứ hai, tôi so sánh tốc độ tìm kiếm phần tử, các hàm cụ thể được sử dụng như sau:

1
2
3
4
5
6
7
//check tồn tại
bool containsKey = intDictionary.ContainsKey(key);
bool containsValue = intList.Contains(value);
 
//tìm kiếm
int value = intDictionary[key];
int index = intList.FindIndex(item => item == value);

Cụ thể nội dung cũng như cách setup bài test tôi sẽ để link phía bên dưới:

http://www.theappguruz.com/blog/make-your-games-run-10-times-faster-by-understanding-arrays-lists-and-dictionaries-in-detail

Cân nhắc việc sử dụng bình phương khoảng cách thay vì khoảng cách

CPU máy tính có thể thực hiện hàng tỉ phép tính mỗi giây, việc nhân các số float chỉ là muỗi đốt, nhưng việc khai căn thì lại là 1 chuyện khác.

Khi yêu cầu Vector3 tính khoảng cách bằng thông số ‘magnitude’ hoặc hàm Distance(), ta yêu cầu máy tính thực hiện 1 phép khai căn (theo công thức pytago =), cpu tốn kha khá tài nguyên để thực hiện. (do việc khai căn là lấy số gần đúng, nên số lượng phép tính nhiều hơn). Chi tiết về quy tắc hoạt động của phép tính khai căn mình sẽ để link phía bên dưới bài viết.

Tối ưu các phép toán lượng giác

Tương tự như phép khai căn, phép tính lượng giác cũng là 1 phép tính phức tạp. Nên thay vì phải tính toán lại lúc runtime, ta làm 1 script hoặc một cái gì đó lưu lại kết quả đã tính để sử dụng cho những lần sau. Ví dụ như script bên dưới lưu kết quả phép tính sin cho các góc từ 0 ==>360 độ

Lời kết:

Thông tin được đưa ra trong bài viết dựa trên nguồn tham khảo là ebook “Unity Game Optimiation 3rd version” và doc của Unity. Việc tối ưu game không bao giờ là quá sớm hoặc quá muộn, việc nắm rõ các phương pháp tối ưu giúp chúng ta cẩn trọng hơn trong quá trình làm, từ đó đưa ra các phương án hợp lý cho từng bài toán cụ thể. 

Cảm ơn anh em nào đã kiên nhẫn đọc đến đây :D, bên trên là 7 phương pháp tối ưu về Scripting mà tôi nghĩ là quan trọng nhất, những phương pháp ít phổ biến hơn tôi sẽ tổng hợp trong những bài viết sắp tới.

Nguồn tham khảo:

https://docs.unity3d.com/Manual/ExecutionOrder.html

http://www.theappguruz.com/blog/make-your-games-run-10-times-faster-by-understanding-arrays-lists-and-dictionaries-in-detail

https://www.quora.com/How-do-computers-calculate-square-roots

https://en.wikipedia.org/wiki/Methods_of_computing_square_roots

https://learn.unity.com/tutorial/memory-management-in-unity#5c7f8528edbc2a002053b59b

Nhấn để đánh giá bài viết!
[Số đánh giá: 0 Trung bình: 0]

Có thể bạn quan tâm

Để lại bình luận