Đùa thôi, đặt cái tiêu đề câu “lai” cho zui ấy mà. Hôm nay mình đề cập đến một số chuyện linh tinh liên quan đến quá trình tìm đọc các demo trên mạng.

Thời buổi giờ, tìm các nguồn để tự học lập trình không thiếu. Thường các demo trên mạng, người ta thường viết ngắn gọn, chủ yếu để nhấn mạnh nội dung cao hơn muốn truyền tải, nên họ ko có quá để ý đến các việc lặt vặt như : tối ưu performance, hạn chế coupling, tránh hardcode …. Tuy nhiên trong quá trình làm việc, mình thấy khá nhiều developer hay “bợ nguyên xi” code demo vào project mà không chỉnh sửa lại :)) , sau đây là vài món mình hay bắt gặp :

1. Câu lệnh Object.Find() thần thánh

API này thì quá quen thuộc rồi nên chắc ai cũng biết, mấy bạn có thể đọc lại ở đây. Công dụng của nó là tìm GameObject trên scene tham số là tên của object.

Ví dụ ở đây, mình muốn “get” được cái Player object :

Best-non-practice-01

void Start()
{
var playerObject = GameObject.Find("Player");
}

Tâm lý của người mới học Unity thường là “UI cha, quá khỏe !! Vừa nhanh vừa tiện, mắc mớ gì không dùng”.

Cái dở của API này ở đâu ?

  • Vô hình chung, cái script ở trên nó bị “gắn chặt” với cái scene, không có khả năng dùng lại, vì nếu bê qua scene khác không có object với tên “Player” thì nó toi.
  • Đây là một API “nặng”, lượng object trong scene tăng lên đồng nghĩa với việc chạy api này chậm. Mình từng làm việc với một ông, chuyên để api này trong Update(), bật profiler bao giờ cũng thấy ngốn kha khá cho cái script đó.
  • Designer là người setup scene, sẽ chẳng có ai biết được rằng, họ phải đặt tên object trong game theo đúng như bạn đã hardcode trong script.

Giải pháp thay thế ?

Thực ra ngọn nguồn của API này là để giải quyết cái chuyện : móc nối, lưu “reference” đến các object trong scene để xử lí, đây là vấn đề muôn thuở khi làm game với Unity.

  • Sử dụng một trường GameObject trong script rồi drag&drop trong Inspector cũng là một giải pháp ổn. Mỗi một Object trên scene đều có một ID riêng và độc nhất (nếu ai mở file scene lên bằng notepad sẽ thấy), theo cách lưu này, script sẽ lưu trữ ID đến object, reference đến object sẽ đc nạp khi Unity load scene.

Best-non-practice-02

  • Nếu bạn cần một giải pháp tốt hơn cho việc “Liên lạc” giữa các đối tượng trong game, hay thử qua ObserverPattern.

2. Đặt những tác vụ nặng trong hàm Update()

Thi thoảng mọi người có thể thấy các hàm GetComponent(), RayCast() … được đặt trong Update(), FixedUpdate(). Đấy đều là những tác vụ khá là nặng, thường làm chậm game. Tốt nhất nên cache tất cả các component hay dùng lại, rồi sử dụng trong Update(). Món này chắc nhiều người đã gặp trong các bài viết “Best Practice” trên mạng

// DO
Rigidbody _myRigidbody;
Transform _myTransform;
void Awake()
{
_myRigidbody = GetComponent<Rigidbody>();
_myTransform = transform;
}
void Update()
{
_rigidbody.velocity = ...
_myTransform.position = ...
}
// DONT
void Update()
{
GetComponent<Rigidbody>().AddForce()...
transform.position = ...
}

Nói đi cũng nói lại, cũng có những trường hợp chúng ta buộc phải đặt LineCast, RayCast trong Update() hoặc FixedUpdate(), đây là chuyện bình thường, nói chung cố gắng hạn chế được thì tốt, thêm các điều kiện hoặc kiểm tra LayerMask cẩn thận chẳng hạn.

[UPDATE]

Có bạn hỏi mình về performance của việc gọi trực tiếp transform và cache Transform lại, cái này cũng nhiều thread trên mạng có test rồi, mình thực hiện lại. Đây là code để test :

using UnityEngine;
using System.Collections;
using System.Diagnostics;
using Debug = UnityEngine.Debug;
public class TestCacheComponentPerformance : MonoBehaviour
{
/// Will use for Loop test in Update
[SerializeField] int runCount = 100000;
/// Use for Editor testing, change the runCount, and press this bool
/// in Inspector to run the test again
[SerializeField] bool test = false;
void Update ()
{
if (test)
{
RunTest();
test = false;
}
}
void RunTest()
{
Stopwatch watch = new Stopwatch();
var cacheTransform = transform;
// test direct transform
watch.Start();
for (int m = 0; m < runCount; m++)
{
transform.position = Vector3.zero;
}
watch.Stop();
Debug.Log("Direct call : " + watch.ElapsedMilliseconds);
watch.Reset();
// test getcomponent
watch.Start();
for (int m = 0; m < runCount; m++)
{
GetComponent<Transform>().position = Vector3.zero;
}
watch.Stop();
Debug.Log("GetComponent call : " + watch.ElapsedMilliseconds);
watch.Reset();
// test cache
watch.Start();
for (int m = 0; m < runCount; m++)
{
cacheTransform.position = Vector3.zero;
}
watch.Stop();
Debug.Log("Cached call : " + watch.ElapsedMilliseconds);
watch.Reset();
}
}

Mình thực hiện trên PC cấu hình đơn giản thế này :
CPU : i3-3210 @3.2 GHZ
RAM : vô tận =)))
Thu được kết quả thế này (loop 1triệu call (1 000 000)), đơn vị thời gian là milisecond

cache_performance

Các bạn tự rút ra kết luận hé. NOTE : cái này chỉ là test với component Transform hay dùng, còn đương nhiên với CustomComponent khác thì cached luôn nhanh hơn GetComponent() rồi.

3. Làm việc với các parameter của Animator mà không định nghĩa các const string để tối ưu

Sử dụng string theo kiểu cấp phát động trong C# bao giờ cũng không tốt, bộ nhớ dễ phân mảnh, GC phải hoạt động nhiều do allocate liên tục. Để tối ưu, dễ chỉnh sửa bảo trì, bạn nên :

  • Define các const string hay sử dụng, thay vì hardcode
  • Có thể định nghĩa các trường [SerializeField] trong script để chỉnh sửa trong Inspector
  • Cao tay hơn thì viết code Editor để đọc các parameter trong Animator ra 😉
//------- DO
const string JUMP_ANIMATOR_TRIGGER = "Jump";
[SerializeField] string walkAnimatorParameterName;// set it to "Jump" in Inspector
void Update()
{
_animator.SetTrigger(JUMP_ANIMATOR_TRIGGER);
_animator.SetTrigger(walkAnimatorParameterName);
}
//------- DONT
void Update()
{
_animator.SetTrigger("Jump");
}

Cá nhân mình hay sử dụng [SerializeField] để có thể chỉnh sửa AnimatorParameterName trong Inspector, vì trong quá trình làm việc, mấy parameters này hay bị thay đổi, không nên đặt chúng trong code. 😉

4. Sử dụng Resources.Load() quá nhiều

Resources.Load() cũng là một API thường thấy khi đọc các demo có liên quan đến việc thay skin, nạp một số asset ngay trong runtime. Nhìn chung, mục đích chính của API này là : giảm tải lượng asset được nạp lên RAM, chỉ load những asset cần thiết trong runtime chứ không load hết tất cả lên 1 lần lúc khởi chạy ứng dụng. Một số việc liên quan đến Resources như :

  • Phân loại skin của nhân vật, chỉ nạp những skin cần thiết trong game
  • Thay đổi texture, model cho object trong game ở runtime
  • Load một số data dạng xml, json, sql… runtime
  • ….v..v…

Best-non-practice-03

void Start()
{
var skin = Resources.Load("PlayerGirlSkin");
}

Một vài chú ý khi sử dụng API này :

  • Đây cũng là một API nặng, có thể làm “giật” khung hình nếu bạn gọi nó giữa chừng trong quá trình chạy game.
  • Tất cả Sprite đặt trong thư mục Resources sẽ KHÔNG ĐƯỢC PACK bởi SpritePacker !! Như mình đã đề cập ở trên, data trong thư mục Resources sẽ được giữ nguyên, Unity không hề encrypt thứ gì trong này, và texture, sprite cũng thế. Đây là nguyên nhân chính khiến ai đặt các thể loại skin của nhân vật trong Resources thường làm tăng drawcall trong game. Như dưới hình mình có ví dụ cho 2 trường hợp khi dùng và không dùng thư mục Resources để chứa các sprite (drawcall là 1 vs 4) :
    capture_05272016_235646capture_05272016_235809

Nói thế không có nghĩa là cắt hoàn toàn việc sử dụng API này. Ở trên mình đã nêu ra ở trên vài công dụng và vài điểm bất lợi của nó, tùy project mà sử dụng ;). Mấy bạn có thể đọc thêm ngay tại trang Unity Learning để biết thêm vài tips về thư mục Resources này. Chú ý, câu đầu tiên trong trang Unity Learning, họ viết thế này :
Best Practices for the Resources System : Don’t use it.
Đại khái zậy hé, cũng đừng ham hố xài cái Resource này nhiều quá.

5. Biến nào trong script cũng đặt public

Unity có một thế mạnh là cho phép người ta chỉnh sửa các biến public trong script. Cũng chính từ đó mà hầu như các dev cứ đặt public cho mọi biến, hàm trong script. Điều này không tốt chút nào, phá vỡ các nguyên lý về đóng mở, interface … trong thiết  kế OOP. Thay vào đó, sử dụng attribute [SerializeField] cũng hoàn toàn cho phép bạn edit các biến của script trong Inspector Window. Nên dùng cách này nhé

// -----DO
[SerializeField] int playerHP;
[serializeField] int bullet;
//---- DONT
public int playerHP;
public int bullet;

7 thoughts on “[Unity3D] Những thứ bạn thường thấy trong các demo trên mạng, nhưng tốt nhất là không nên dùng :))

  1. Cho mình hỏi chút. Sao lại phải cache _myTransform = transform; vậy?
    Dùng trực tiếp transform thì k tối ưu hả à bạn?

    Like

    1. À, cái này có một thời gian cũng nhiều thread tranh cãi trên mạng. Trong nhiều bài viết kiểu “best practice”, đa số mọi người khi “soi” assembly ra, sẽ thấy bên dưới của property transform sẽ như thế này :
      public extern Transform transform
      {
      [WrapperlessIcall]
      [MethodImpl (MethodImplOptions.InternalCall)]
      get;
      }
      Nghĩa là tốn thêm một lời gọi hàm khác, cho nên sẽ luôn chậm hơn so với việc cached. Bạn có thể đọc qua 1 thread ở đây.
      Sau này cũng có các thread so sánh tốc độ thực thi của việc cache và không cache, vd ở đây, và họ kết luận rằng cache có tối ưu hơn, nhưng không thật sự nhiều.
      À, Note cái là, mấy bài test này chỉ nhắm tới transform – component hay dùng nhất, còn với các custom component khác thì đương nhiên là cache vẫn nhanh hơn GetComponent hé 😉
      Về sau này mình chưa có tìm thêm bài viết nào đào sâu hơn. Khi nào chắc chắn thì mình update bài viết sau.

      Like

    1. Hớ, thì cũng đâu khác gì nhau nhiều đâu. Đào dll của nó lên thì cũng chỉ tới được một cái InternalCall thoy, bên dưới nó làm cái gì thì chả ai biết được :v . Mà xưa giờ mình cũng chả thấy chỗ nào khuyên dùng các lệnh Find này

      Like

  2. Mình có thể tổng quát hoá lại như sau
    1. Biến nào load từ vùng nhớ, nên cached lại ( ví dụ các biến từ tên hàm có tiền tố Load, Fetch )
    2. Biến nào truy cập liên tục mà phải tìm kiếm, => cached lại ( ví dù các đối tượng phải tìm kiếm từ các hàm có tiền tố Find ).
    3. So sánh byte thì lẹ hơn so sánh với string => vì vậy người ta thường dùng enum thay thế cho string

    Like

Leave a comment