Unity là một công cụ tuyệt vời để phát triển game với nhiều chức năng được cung cấp sẵn. Nhưng bạn có biết rằng nếu sử dụng không đúng cách những chức năng sẵn này có thể dẫn đến vấn đề hiệu năng khiến game giật lag. Bài viết này sẽ cung cấp 7 điều cơ bản cần biết giúp tối ưu hiệu năng khi lập trình C# trong Unity (Optimization Unity). Đồng thời sử dụng API của Unity một cách hiệu quả.
Nội dung
- 7 lưu ý để Optimization Unity
- 1/ Tránh sử dụng Unity event rỗng như Awake, Start, Update
- 2/ Tránh truy xuất trực tiếp thuộc tính tag và name
- 3/ Cache kết quả của GetComponent
- 4/ Cache kết quả của transform
- 5/ Nhớ giải phóng tài nguyên không được tự động collect bởi GC
- 6/ Tránh khi dùng string Id khi tương tác với Animator và Material
- 7/ Loại bỏ logging code khi build bản release
- Tổng kết về Optimization Unity
7 lưu ý để Optimization Unity
1/ Tránh sử dụng Unity event rỗng như Awake, Start, Update
Các magic method như Awake, Start, Update,… được Unity tự động đưa vào một list và gọi bằng cách loop qua list đó khi chạy game. Quá trình đưa method vào list và gọi trong vòng lặp đều gây ra hao tốn tài nguyên memory và CPU do quá trình gọi lệnh từ C# xuống tầng native C++ của Unity. Ngoài ra, method rỗng vẫn sẽ được Unity đưa vào list, nên nếu không cần xử lý trong các magic method của Unity thì không viết định nghĩa trong code.
public class NewBehaviourScript : MonoBehaviour
{
// Xin đừng để magic method rỗng!!!
void Start()
{
}
void Update()
{
}
}
2/ Tránh truy xuất trực tiếp thuộc tính tag và name
Những class kế thừa từ UnityEngine.Object có thuộc tính tag và name. Hai thuộc tính này rất hữu ích để phân loại các game object trong Unity nhưng gây ra GC.Alloc không cần thiết. Bạn có thể nhìn đoạn code bên C# của hai thuộc tính này trong GitHub repository UnityCsReference để hiểu rõ hơn cách Unity truy xuất tag và name từ C#.
public extern string tag
{
[FreeFunction("GameObjectBindings::GetTag", HasExplicitThis = true)]
get;
[FreeFunction("GameObjectBindings::SetTag", HasExplicitThis = true)]
set;
}
public string name
{
get { return GetName(this); }
set { SetName(this, value); }
}
[FreeFunction("UnityEngineObjectBindings::GetName")]
extern static string GetName([NotNull("NullExceptionObject")] Object obj);
Bạn có thể thấy cả hai thuộc tính đều gọi về code native C++ của Unity. Quá trình chuyển đổi từ các object của C# sang C++ và ngược lại sẽ chiếm dụng thêm memory của thiết bị, cụ thể ở đây là quá trình chuyển đổi từ string trong C++ sang string trong C#. Do vậy, mỗi lần tag và name được gọi sẽ làm tăng lượng memory mà chương trình sử dụng, dẫn đến GC.Alloc tăng theo thời gian. Nếu giá trị của tag, name không thay đổi tại runtime thì bạn có thể cache giá trị của hai thuộc tính này.
private string name;
private string tag;
private void Awake()
{
this.name = gameObject.name;
this.tag = gameObject.tag;
{
Nếu giá trị của tag, name có thể thay đổi tại runtime thì bạn cần tự code một hệ thống tag, name đơn giản bằng C#. Lợi ích của việc tự code là hệ thống của bạn có thể hỗ trợ các tính năng Unity không có sẵn như multi-tag (nhiều tag trên cùng một game object),…
private List<string> names;
private List<string> tags;
private IEnumerable<string> GetNames()
{
return name;
}
private IEnumerable<string> GetTags()
{
return tags;
}
3/ Cache kết quả của GetComponent
Tương tự như tag và name, GetComponent cũng gọi xuống native code C++ của Unity dẫn đến mỗi lần gọi GetComponent sẽ gây GC.Alloc. Do vậy nên cache kết quả của GetComponent thay vì gọi mỗi lần cần sử dụng một component trên game object, đặc biệt khi cần truy xuất component liên tục mỗi frame trong Update.
private Rigidbody rigidbody;
private void Awake()
{
rigidbody = GetComponent<Rigidbody>();
}
4/ Cache kết quả của transform
Transform là một trong những component được truy xuất nhiều nhất trong Unity để thay đổi các thuộc tính như position, rotation, scale, thiết lập quan hệ parent giữa các game object. Thường chúng ta sẽ cập nhật nhiều thuộc tính một lúc như trong đoạn code sau.
private void SetTransform(Vector3 position, Quaternion rotation, Vector3 scale)
{
transform.position = position;
transform.rotation = rotation;
transform.scale = scale;
}
Khi transform được truy xuất, method GetTransform() được gọi trong Unity. Mặc dù GetTransform() được tối ưu và nhanh hơn GetComponent() nhưng vẫn chậm hơn so với việc cache giá trị transform. Bạn có thể sử dụng SetPositionAndRotation() để giảm thiểu số lần gọi GetTransform().
private void SetTransform(Vector3 position, Quaternion rotation, Vector3 scale)
{
var transformCache = transform;
transformCache.SetPositionAndRotation(position, rotation);
transformCache.localScale = scale;
}
5/ Nhớ giải phóng tài nguyên không được tự động collect bởi GC
Mặc dù Garbage Collector trong C# tự động lo liệu phần lớn việc giải phóng tài nguyên không được sử dụng, một số class trong Unity cần developer chủ động giải phóng như Texture2D, Sprite, Material, PlayableGraph,… Nếu bạn sinh ra object mới của các class này bằng keyword new hoặc method Create, đừng quên destroy nếu không muốn tràn RAM.
private void Start()
{
_texture = new Texture2D(8, 8);
_sprite = Sprite.Create(_texture, new Rect(0, 0, 8, 8), Vector2.zero);
_material = new Material(shader);
_graph = PlayableGraph.Create();
}
private void OnDestroy()
{
Destroy(_texture);
Destroy(_sprite);
Destroy(_material);
if (_graph.IsValid())
{
_graph.Destroy();
}
}
6/ Tránh khi dùng string Id khi tương tác với Animator và Material
Chúng ta cùng phân tích đoạn code sau:
_animator.Play("Wait");
_material.SetFloat("_Prop", 100f);
Đoạn code trên trông rất vô hại nhưng bên trong lõi Unity đang gọi đến hai method Animator.StringToHash() and Shader.PropertyToID(). Hai method này thực hiện việc chuyển đổi string sang một id có type int. Thật là lãng phí nếu phải thực hiện việc chuyển đổi từ string sang int mỗi lần muốn Play animation hay set giá trị của material. Các bạn cần cache giá trị của các id này dưới dạng int và sử dụng int id để thực hiện các lệnh.
public static class ShaderProperty
{
public static readonly int Color = Shader.PropertyToID("_Color");
public static readonly int Alpha = Shader.PropertyToID("_Alpha");
public static readonly int ZWrite = Shader.PropertyToID("_ZWrite");
}
public static class AnimationState
{
public static readonly int Idle = Animator.StringToHash("idle");
public static readonly int Walk = Animator.StringToHash("walk");
public static readonly int Run = Animator.StringToHash("run");
}
7/ Loại bỏ logging code khi build bản release
Unity cung cấp sẵn các method để logging như Debug.Log(), Debug.LogWarning(), Debug.LogError(). Chúng ta cần biết logging là một tiến trình rất nặng tiêu tốn nhiều tài nguyên CPU, RAM (xử lý string), I/O (nếu ghi log file ra đĩa cứng). Ví dụ một đoạn code log thường thấy như sau:
Debug.Log("Complete level " + currentLevel);
Cần chú ý ở trên ngoài việc tiêu tốn tài nguyên trong method Log, chúng ta còn tiêu tốn tài nguyên khi thực hiện việc nối string khi truyền “Complete level” + currentLevel. Chúng ta có thể set giá trị của UnityEngine.Debug.unityLogger.logEnabled bằng false để tắt log. Nhưng như thế là chưa đủ do việc logEnabled chỉ là một nhánh if trong các method Log, LogWarning, LogError, chúng ta vẫn gây tốn tài nguyên ở việc xử lý string khi truyền tham số.
Để hoàn toàn loại bỏ logging, chúng ta có thể sử dụng Scripting Symbol trong Unity. Đoạn code nằm trong #if và #endif UNITY_EDITOR chỉ được compile trên Unity Editor. Ở bản build sẽ hoàn toàn không có đoạn code này.
#if UNITY_EDITOR
Debug.LogError($"Error {e}");
#endif
Vấn đề ở đây là nếu trong game của bạn có 1000 nơi thực hiện log bạn sẽ phải vào 1000 nơi đó để thêm scripting symbol, như vậy không phải giải pháp tốt để dễ dàng maintain.
Giải pháp tốt nhất để Optimization Unity là sử dụng attribute Conditional. Nếu Symbol Scripting không được định nghĩa thì method có attribute Conditional sẽ bị loại bỏ tất cả các đoạn code gọi đến method đó. Do vậy, bạn nên tạo một wrapper class cho việc Debug và sử dụng attribute Conditional trên các method log của wrapper class.
public static class Debug
{
private const string MConditionalDefine = "DEBUG_LOG_ON";
[System.Diagnostics.Conditional(MConditionalDefine)]
public static void Log(object message)
{
UnityEngine.Debug.Log(message);
}
}
Để định nghĩa Scripting Symbol trong Unity bạn có thể vào chức năng Scripting Define Symbols ở trong Project Settings -> Player -> Other Settings.
Tổng kết về Optimization Unity
Việc sử dụng các tính năng do Unity cung cấp có thể dẫn đến những lỗi hiệu năng không mong muốn làm ảnh hưởng đến trải nghiệm người dùng. Hy vọng bài viết này đã cung cấp cho các bạn một số kiến thức cơ bản về Optimization Unity, tránh những lỗi hiệu năng phổ biến, dễ mắc phải khi lập trình game bằng Unity.
0 Lời bình