---
# System prepended metadata

title: 'Dịch sách: Chương 2 (từ trang 90) - Unity Game Optimization'

---

# Dịch sách: Chương 2 (từ trang 90) - Unity Game Optimization

### Chú thích

1. Callback: hàm A được gọi là callback khi nó được truyền vào làm tham số của một hàm B nào đó và được kích hoạt sau khi hàm B được gọi.
2. Render: là một quá trình tạo một hình ảnh 2D hay 3D từ một các mô hình bằng cách sử dụng phần mềm máy tính. 
3.  Tuần tự hóa - Serialization: là quá trình chuyển trạng thái (dữ liệu) của một object sang dạng byte để lưu trữ object.
4. Phân giải tuần tự - Deserialization:  là quá trình khôi phục lại object từ dạng byte sau khi chúng được tuần tự hóa. 
5. Prefabs: là một thành phần đặc biệt cho phép cấu hình, lưu trữ các GameObject để có thể tái sử dụng sau này. 
6. ScriptableObject: một container có thể sử dụng để chứa một lượng lớn dữ liệu, độc lập khỏi các vật thể (thể hiện của lớp). nhìn chung, nơi này sẽ chứa các thông tin mà bình thường sẽ được “dùng chung” giữa nhiều vật thể. 
7. MonoBehaviour: một lớp định nghĩa các vật thể “có một hành động”. hành động ở đây có thể hiểu là Update(). Nó định nghĩa một số hàm ảo: Start(), Awake(), Update(), FixedUpdate(), OnGUI() 

8. Bất đồng bộ: có thể hiểu là tạo các luồng, các sự kiện được xử lý độc lập với luồng chính.  

##  I. "Tắt" những kịch bản và vật thể không cần thiết

Tại một thời điểm, đặc biệt là khi xây dựng thế giới mở và rộng lớn, bối cảnh phải xử lý rất nhiều. Càng có nhiều đối tượng được gọi bởi hàm *Update()*, hiệu năng hệ thống càng suy giảm. Tuy nhiên, việc xử lý các đối tượng nằm ngoài tầm nhìn của người chơi là hoàn toàn không cần thiết. Điều này có thể không khả thi trong những trò chơi mô phỏng cả một thành phố rộng lớn, nơi mà tất cả các mô phỏng phải được thực hiện đồng thời, nhưng lại khả thi trong những trò chơi với góc nhìn thứ nhất hoặc những trò chơi đua xe, khi mà người chơi phải di chuyển trong một khu vực rộng lớn và những đối tượng không nằm trong tầm nhìn có thể tạm thời bị vô hiệu hóa mà không gây nên bất kỳ ảnh hưởng rõ rệt nào đến trải nghiệm của người chơi. 

### 1. "Tắt" các đối tượng không trong tầm nhìn

+ Vào một số thời điểm, chúng ta sẽ muốn một vài thành phần hay vật thể sẽ được "tắt" khi mà chúng không thể thấy được. Tính năng render được xây dựng sẵn trong Unity có khả năng tránh render các vật thể không nằm trong tầm nhìn của camera (dựa vào một kỹ thuật với tên gọi **Frustum Culling**) và không render các vật thể bị ẩn sau các vật thể khác (kỹ thuật **Occlusion Culling** - sẽ được thảo luận kỹ ở chương 6), tuy nhiên chúng chỉ các kỹ thuật nhằm tối ưu khi render. Hai cách tiếp cận trên không ảnh hưởng gì đến các tác vụ mà CPU phải gánh, ví dụ như các tác vụ AI, giao diện người dùng hay logic game. Chúng ta phải có cách điều khiển riêng. 

+ Một giải pháp hiệu quả là sử dụng hàm `callbacks` *OnBecameVisible()* và *OnBecameInvisible()*. Đúng như tên gọi, những lời gọi đến các hàm này được sử dụng khi một vật thể chuyển đổi giữa 2 trạng thái: “nhìn thấy được” , "không nhìn được" đối với camera. Nếu có nhiều camera (trong các game nhiều người chơi): *OnBecameVisible()*  được gọi khi vật thể được ít nhất 1 người chơi nhìn thấy, *OnBecameInvisible()* được gọi khi không ai còn thấy được vật thể.

+ Vì các `callbacks` liên quan đến tính khả kiến của vật thể phải giao tiếp với luồng render, GameObject phải có một thành phần có thể render đính kèm, ví dụ như *MeshRenderer* hoặc *SkinnedMeshRenderer*. Cần đảm bảo rằng các thành phần mà ta muốn nhận được `callback` về tính khả kiến cũng được đi kèm trong cùng *GameObject* ví dụ như vật thể có thể render được và không phải cha hoặc con *GameObject*; ngược lại, chúng ta không thể thực hiện lời gọi. 

+ <u>Chú ý:</u> Unity cũng đến các camera ẩn trong cửa sổ **Scene** khi xem xét tới *OnBecameVisible()* hay *OnBecameInvisible()*. Nếu chúng ta thấy rằng các phương thức trên không được gọi đúng trong chế độ chơi thử nghiệm, chắc chắn rằng bạn đã đưa camera của cửa sổ **Scene** ra xa khỏi mọi thứ hoặc "tắt" toàn bộ cửa sổ **Scene**. 

+ Cụ thể, để kích hoạt hoặc tắt các thành phần của một vật thể, ta có thể thêm những hàm sau: 

  ```c++
  Void OnBecameVisible() { enable = true; }
  Void OnBecameInvisible() { enable = false; }
  ```

+ Bên cạnh đó, để bật hay tắt toàn bộ vật thể, chúng ta có thể cài đặt bằng cách: 

  ```c++
  Void OnBecameVisible() { gameObject.SetActive(true); }
  Void OnBecameInvisible() { gameObject.SetActive(false); } 
  ```

+ Tuy nhiên, cần lưu ý rằng nếu tắt các GameObject có chứa các vật thể có thể render, hoặc một trong các vật thể cha của chúng, sẽ không thể gọi *OnBecameVisible()* bởi vì không có biểu diễn đồ họa cho camera có thể thấy và thực thi lời gọi. Chúng ta nên đặt thành phần ở một *GameObject* con, và có một đoạn mã “tắt”, để thực thể có thể render luôn có thể nhìn thấy được hoặc có thể tái kích hoạt. 

### 2. "Tắt" vật thể căn cứ vào khoảng cách

+ Trong nhiều trường hợp khác, ta sẽ muốn các thành phần hoặc GameObject được "tắt" sau khi không còn nằm trong tầm nhìn của người chơi (đủ xa đến mức gần như không thể nhìn thấy). Một cách xử lý tốt trong trường hợp này là để cho các nhân vật không phải người chơi đứng yên, bất động tới khi người chơi di chuyển tới vị trí đủ gần để quan sát hành động của chúng. 

+ Đoạn code sau đây là một thủ tục đơn giản với mục đích kiểm tra định kỳ khoảng cách với một thực thể cụ thể và tự động tắt khi mà nó ở quá xa: 

  ```c++
  [SerializeField] GameObject _target;
  [SerializeField] float _maxDistance;
  [SerializeField] int _coroutineFrameDelay;
  void Start() {
   StartCoroutine(DisableAtADistance());
  }
  
  IEnumerator DisableAtADistance() {
   while(true) {
   	float distSqrd = (transform.position - _target.transform.position).sqrMagnitude;
   	if (distSqrd < _maxDistance * _maxDistance) {
   		enabled = true;
   	} else {
   		enabled = false;
   	}
  	for (int i = 0; i < _coroutineFrameDelay; ++i) {
   		yield return new WaitForEndOfFrame();
    	}
   }
  }
  ```

+ Ta nên gán nhân vật của người chơi (hoặc bất cứ vật thể nào chúng ta muốn lấy mốc so sánh) vào biến *_target* trong cửa sổ **Inspector**, định nghĩa khoảng cách lớn nhất ở biến *_maxDistance* và thay đổi tần suất cập nhật sử dụng biến *_coroutineFrameDelay*. Tại thời điểm bất kỳ khi mà vật thể đi xa hơn *_maxDistance* tính từ *_target*, nó sẽ bị tắt và tái kích hoạt lại khi di chuyển vào trong vùng gần hơn. 

+ Một phương pháp thậm chí còn hiệu quả hơn sẽ được đề cập ngay sau đây, đó là sử dụng bình phương khoảng cách thay vì khoảng cách. 

## II. **Sử dụng giá trị bình phương khoảng cách** 

+ CPU là một công cụ tính toán mạnh mẽ khi nhân số thực dấu phẩy động, tuy nhiên tính toán căn bậc hai luôn lại là một phép toán khó với CPU. Mỗi một lần tính khoảng cách sử dụng *Vector3* với thuộc tính *magnitude* hay phương thức *Distance()*, ta đang sử dụng phép tính căn bậc hai, phép toán tiêu tốn nhiều tài nguyên CPU hơn bình thường. 

+ Tuy nhiên, lớp *Vector3* có thuộc tính *sqrMagnitude* – tính toán giá trị bình phương khoảng cách. Tức là ta sẽ sử bình phương khoảng cách để so sánh thay vì so sánh khoảng cách bình thường. Nhờ vậy mà không tốn nhiều chi phí để tính toán giá trị căn bậc hai. 

+ Để minh họa, xem đoạn code dưới đây 

  ```c++
  float distance = (transform.position –other.transform.position).Distance();
  if (distance < targetDistance) {
   // do stuff
  }
  ```

  Có thể thay đoạn code trên bằng đoạn code dưới đây mà kết quả gần như không đổi: 

  ```c++
  float distanceSqrd = (transform.position – other.transform.position).sqrMagnitude;
  if (distanceSqrd < (targetDistance * targetDistance)) {
   // do stuff
  }
  ```

+ Lý do cho kết quả gần giống nhau là do độ chính xác của dấu phẩy động. Chúng ta có thể mất đi một phần độ chính xác so với khi sử dụng giá trị căn bậc hai, vì giá trị sẽ được điều chỉnh tới một số có thể biểu diễn được; nó có thể chính xác hoặc gần hơn với một con số được biểu diễn chính xác hơn, hoặc nhiều khả năng, nó sẽ trả lại một số có độ chính xác kém hơn. Kết quả là phép so sánh không hoàn toàn giống nhau, nhưng, trong hầu hết các trường hợp, điều này có thể chấp nhận được do hiệu suất có thể tăng khá đáng kể đối với mỗi lệnh được thay thế theo cách này. 

+ Nếu mất mát nhỏ trong độ chính xác là không quan trọng, thì thủ thuật tăng hiệu suất này nên được sử dụng. Tuy nhiên, nếu độ chính xác là rất quan trọng (chẳng hạn như chạy một mô phỏng chính xác không gian thiên hà với quy mô lớn), thì không nên áp dụng cách này. 

+ Ngoài ra, kỹ thuật này có thể được sử dụng cho bất kỳ phép tính căn bậc hai nào, không chỉ cho khoảng cách. Đây chỉ là ví dụ phổ biến nhất mà bạn có thể gặp và cho thấy tầm quan trọng của thuộc tính *sqrMagnitude* trong lớp *Vector3*. Đây là một thuộc tính mà Unity Technologies đưa ra có chủ đích cho chúng ta sử dụng theo cách này. 

## III. Tối thiểu hóa phân giải tuần tự 

+ Hệ thống tuần tự hóa của Unity được sử dụng chủ yếu cho phối cảnh, Prefabs, ScriptableObject, và những kiểu vật thể (mà có thể lấy được từ ScriptableObject). Khi một trong các thực thể được lưu vào đĩa, nó được chuyển sang dạng text sử dụng định dạng **Yet Another Markup Language (YAML)**, và có thể được chuyển đổi trở lại kiểu thực thể sau đó. Tất cả GameObject và các thành phần của chứng có thể được `serilized` khi mà một Prefab hoặc bối cảnh được `serialized`, bao gồm cả các trường *private* hoặc *protected* và tất cả thành phần của chúng, cũng như các GameObject con và thành phần của chúng. 

+ Khi mà ứng dụng được xây dựng, dữ liệu được tuần tự hóa này sẽ được bó lại cùng nhau trong các tệp dữ liệu nhị phân gọi là các *serialized files* trong Unity. Đọc và phân giải tuần tự các tệp tin này cần thời gian chạy rất lâu do vậy tất cả các hành vi phân giải tuần tự sẽ phải đánh đổi bằng hiệu năng. 

+ Việc phân giải tuần tự được thực hiện mỗi lần hàm *Resources.Load()*  được gọi (để load các file nằm trong một thư mục  *Resources*). Một khi dữ liệu đã được tải từ đĩa vào bộ nhớ, việc tải lại dữ liệu sau đó  sử dụng tham chiếu sẽ rất nhanh, tuy nhiên truy cập đĩa là điều luôn bắt buộc và phải diễn ra ít nhất một lần. Một cách tự nhiên, dữ liệu cần phân giải tuần tự càng lớn, thời gian xử lý càng lâu. Bởi vì mỗi một thành phần của Prefab đều cần tuần tự hóa, do đó sự phân cấp càng sâu, thì càng nhiều dữ liệu cần phân giải tuần tự. 

+ Điều này có thể là một trở ngại đối với Prefabs có phân cấp sâu, Prefabs với nhiều GameObject rỗng (bởi vì mỗi *GameObject* luôn bao gồm ít nhất một thành phần *Transform*), và đặc biệt nghiêm trọng hơn với giao diện người dùng Prefab khi chúng luôn có xu hướng chứa nhiều thành phần hơn bình thường. 

+ Việc tải một khối dữ liệu đã được tuần tự hóa có thể tạo gánh nặng rất lớn lên CPU trong lần đầu tiên khởi chạy, thời gian tải có xu hướng tăng nếu các dữ liệu này được yêu cầu ngay khi khởi tạo bối cảnh. Nghiêm trọng hơn là việc phân giải tuần tự có thể khiến các khung hình nếu chúng được load trong thời gian chạy. Dưới đây là một vài cách tiếp cận để giảm thiểu chi phí khi phân giải tuần tự. 

### 1. Cắt giảm dung lượng các vật thể cần tuần tự hóa

+ Ta nên tìm cách đơn giản hóa lưu trữ bằng việc giảm dung lượng của từng đối tượng càng thấp càng tốt hoặc chia nhỏ chúng thành các mảnh nhỏ hơn, sau đó chúng ta có thể load từng mảnh nhỏ một. Cách làm này có khiến việc quản lý các Prefab gặp nhiều khó khăn bởi vì Unity không hỗ trợ mạng liên kết các Prefab, do đó ta phải tự cài đặt một hệ thống quản lý. Các UI Prefab cũng là những đối tượng phù hợp với cách xử lý phân mảnh này bởi ta không cần toàn bộ giao diện tại một thời điểm, mà có thể tải các mảnh này tại các thời điểm khác nhau. 

### 2. Tải các thực thể tuần tự không đồng bộ

+ Các Prefab và các nội dung cần tuần tự hóa khác có thể được tải một cách bất đồng bộ qua hàm *Resources.LoadAsync()*. Hàm này giảm bớt gánh nắng luồng chính bằng cách sử dụng các luồng phụ để đọc từ đĩa. Để kiểm tra các dữ liệu đã được tải hay chưa, có thể thực hiện lời gọi *isDone* của đối tượng *ResourceRequest* được trả về trong lời gọi hàm trước đó. 

+ Cách làm này không phù hợp với các Prefab mà chúng ta cần ngay lập tức khi khởi tạo game, nhưng những Prefab mà game cần sau này đều là những đối tượng điển hình để áp dụng việc tải bất đồng bộ nếu ta có thể tạo một hệ thống quản lý được việc xử lý bất đồng bộ. 

### 3. **Giữ các thực thể tuần tự đã được tải trong bộ nhớ** 

+ Như đã đề cập, khi một đối tượng đã được load, nó sẽ được giữ lại trong bộ nhớ và có thể sao chép đối tượng nếu cần tái sử dụng sau này, ví dụ như tạo ra nhiều bản sao chép của Prefab. Ta có thể giải phóng bộ nhớ cho các đối tượng này, bằng việc gọi hàm *Resources.Unload()*. Tuy nhiên, nếu bộ nhớ còn trống nhiều, có thể lựa lưu lại dự liệu trên bộ nhớ, nhờ đó giảm thiểu thời gian tải lại từ đĩa sau này. Lẽ đương nhiên, giải pháp này tốn rất nhiều bộ nhớ và khiến việc quản lý quản lý bộ nhớ có thể gặp nhiều rủi ro, cần cẩn trọng khi sử dụng giải pháp này. 

### 4. **Chuyển các dữ liệu thường vào trong ScriptableObject** 

+ Khi ta có nhiều thuộc tính có thể dùng chung giữa trong các thành phần của các Prefab: điểm, độ mạnh, tốc độ… thì khi tuần tự hóa chúng vào bộ nhớ, mỗi Prefab sẽ lưu lại một bản copy của các giá trị này, gây lãng phí tài nguyên và ảnh hưởng đến hiệu năng. Một cách tiếp cận hợp lý hơn đó là tuần tự hóa các dữ liệu chung này trong một *ScriptableObject*, sau đó có thể tải và sử dụng. Phương án này giúp cắt giảm lượng dữ liệu được lưu trong các tệp tuần tự hóa của các Prefab và do đó có thể giảm thời gian tải các bối cảnh bằng việc tránh lặp lại. 

## IV.  Tải bối cảnh bổ sung bất đồng bộ 

+ Một bối cảnh mới có thể được tải bằng các cách sau:thay thế hoàn toàn bối cảnh hiện tại hoặc tải bổ sung thêm nội dung mới vào mà không cần phải bỏ đi khung cảnh trước đó(lựa chọn 1 trong 2 cách bằng tham số *LoadSceneMode* của *SceneManager.LoadScene()*) 

+ Một chế độ khác của việc tải bối cảnh là hoàn thiện một cách đồng bộ hoặc bất đồng bộ, luôn có những lý do nên sử dụng cả hai chế độ này. Tải đồng bộ bằng lời gọi hàm *SceneManager.LoadScene()* sẽ khóa luồng chính lại cho đến khi bối cảnh hoàn thiện. Điều này thường đem đến trải nghiệm cho người dùng tệ, khi mà game bị dừng lại cho đến khi nội dung được tải vào (dù là theo cách thay thế hoặc bổ sung). Phương án này tốt nhất nên dùng khi chúng ta muốn đưa người chơi sẵn sàng càng sớm càng tốt, hoặc chúng ta không có thời gian để chờ các vật thể trong bối cảnh xuất hiện, thường được dừng khi tải các màn đầu của game hoặc trở về màn hình menu. 

+ Tuy nhiên, để tải các bối cảnh trong tương lai theo cách giảm thiểu ảnh hưởng tới hiệu năng hơn, khiến người chơi luôn có cảm giác đang “trong trạng thái sẵn sàng”. Lựa chọn tải theo cách bổ sung bất đồng bộ sẽ mang lại lợi ích to lớn: bối cảnh sẽ vẫn được tải ở trong nền mà không mang đến ảnh hưởng đáng kể đến trải nghiệm người chơi. Việc này được thực hiện qua lời gọi *SceneManager.LoadSceneAsync()* và truyền vào tham số *LoadSceneMode.Additive*. 

+ Điều quan trọng cần phải hiểu rằng các bối cảnh không cứng nhắc tuân theo game level. Trong hầu hết mọi game, người chơi tường bị kẹt trong một màn chơi tại một vài thời điểm, nhưng Unity có thể hỗ trợ nhiều bối cảnh tải một cách mô phỏng qua việc tải bổ sung, cho phép mỗi bối cảnh có thể thể hiện một đoạn nhỏ của một màn chơi. Ví dụ: ta có thể khởi tạo bối cảnh đầu tiên cho màn chơi *(Scene-1-1a)*, khi người chơi đến gần điểm kết thúc, tải bất đồng bộ và bổ sung bối cảnh tiếp theo *(Scene-1-1b)*, và lặp lại trong suốt quá trình chơi. 

+ Việc khai thác tính năng này sẽ yêu cầu hệ thống liên tục kiểm tra vị trí của người chơi hoặc sử dụng điểm kích hoạt để phát thông báo rằng người chơi sắp vượt qua màn tiếp theo và tiến hành tải không đồng bộ vào thời điểm thích hợp. Một vấn đề quan trọng cần lưu ý khác là nội dung của bối cảnh sẽ không xuất hiện ngay lập tức vì việc tải bất đồng bộ sẽ dàn trải khối lượng tải trên một số lượng khung hình nhất định để tối thiểu ảnh hưởng có thể nhận thấy. Cần đảm bảo rằng khi thực thi tải bất đồng bộ cần có thời gian đủ lâu để người chơi không tự dưng thấy đối tượng xuất hiện trong trò chơi. 

+ Bối cảnh còn có thể được xóa khỏi bộ nhớ. Điều này giúp tiết kiệm bộ nhớ và cải thiện thời gian chạy bởi không cần phải gọi lại các hàm *Update()* mà không cần dùng đến nữa. Có thể giảm tải bằng cách đồng bộ *SceneManager.UnloadScene()* hoặc bất đồng bộ *SceneManager.UnloadSceneAsync()*. Hiệu năng có thể được cải thiện bởi chúng ta chỉ sử dụng những gì cần căn cứ vào vị trí của người chơi, tuy nhiên nên nhớ rằng không thể dỡ bỏ các phần nhỏ từ một bối cảnh tổng thể. Nếu tệp bối cảnh gốc rất lớn thì việc loại bỏ nó đồng nghĩa với việc loại bỏ mọi thứ. Bối cảnh gốc phải được chia thành nhiều bối cảnh nhỏ hơn để tải hoặc loại bỏ khi cần. Tương tự vậy, chúng ta chỉ nên loại bỏ một bối cảnh chỉ khi chắc chắn rằng người chơi không thể thấy các sự vật trong đó nữa, hoặc không thì họ sẽ thấy chúng như “biến mất”. Một khía cạnh khác cần lưu ý đó là việc loại một bối cảnh sẽ kích hoạt hàm hủy (destructor) của nhiều thực thể trong nó, giải phóng lượng lớn bộ nhớ và kích hoạt bộ xử lý bộ nhớ rác. Giải pháp này giúp cho việc sử dụng bộ nhớ hiệu quả hơn. 

+ Tóm lại, cách tiếp cận này yêu cầu việc thiết kế bối cảnh thực sự hiệu quả, thiết kế kịch bản, kiểm thử và soát lỗi một cách nghiêm túc, nhưng những hiệu quả nó đem lại là không thể phủ nhận. Khi sử dụng hợp lý, giải pháp này đặc biệt hiệu quả trong việc giải quyết vấn đề chuyển đổi một cách liền mạch, tối ưu thời gian chạy và cải thiện trải nghiệm người chơi. 

## V.  Tùy chỉnh lớp Update()

+ Về cơ bản, nguy cơ ảnh hưởng lớn đến hiệu năng trò chơi nằm ở cách mà các đối tượng trong trò chơi được cập nhật. Khi mà quá nhiều *MonoBehaviour* được viết để gọi định kỳ một vài hàm, có thể dẫn đến việc quá nhiều phương thức được kích hoạt trong một khung hình. 

+ Ví dụ: có hàng ngàn *MonoBehaviour* được khởi tạo cùng nhau khi bắt đầu một bối cảnh, chúng có cùng chu kì cập nhật là 500 ms. Điều này có thể dẫn đến việc chúng cùng được kích hoạt tại một khung hình, đẩy khối lượng của CPU tăng vọt, sau đó lắng xuống và cứ tiếp tục  như vậy. Một cách lý tưởng, chúng ta muốn các lần kích hoạt này được giãn đều trên trục thời gian. 

+ Một số giải pháp sau được đề xuất: 
  + Tạo một thời gian nghỉ ngẫu nhiên khi bộ đếm hết thời gian hoặc bắt đầu một quy trình kích hoạt. 
  + Giãn các thời điểm khởi tạo để chỉ phải xử lý một lượng vừa phải tại mỗi khung hình. 
  + Chuyển trách nhiệm gọi các hàm kích hoạt cập nhật cho một lớp điều hành, qua đó quản lý số lượng cập nhật. 

+ Để thực sự làm chủ quá trình, hai giải pháp đầu có quá nhiều rủi ro không thể đoán định. Một phương án tiếp cận tiềm năng hơn để tối ưu hàm cập nhật đó là không sử dụng hàm *Update()* hoặc chỉ sử dụng nó một lần duy nhất. Khi Unity gọi đến hàm *Update()*, nó sẽ hành xử theo cách cơ bản nhất, có nghĩa là nó sẽ luôn gọi hàng ngàn hàm *Update()* một cách độc lập thay vì chỉ một và đi qua một điểm “cầu” duy nhất (Native-Managed Bridge). Lời gọi hàng ngàn phương thức Update() riêng biệt không phải là một lượng công việc nhỏ lên CPU chủ yếu do điểm “cầu”. Chúng ta hoàn toàn có thể tối giản những lời gọi này bằng việc tạo ra một lớp quản lý có trách nhiệm quản lý các vật thể được cập nhập và khi cần chúng ta chỉ cần gọi phương thức Update() này.’

+ Thực tế, giải pháp này đã được ưu tiên áp dụng ngày từ khi các nhà phát triển Unity bắt đầu những dự án, bởi nó cho phép điều khiển một cách triệt để các việc cập nhật vật thể khi nào và như thế nào. Cách này có thể ứng dụng cho menu dừng, hiệu ứng thời gian nghỉ, hoặc xử lý ưu tiên một số tác vụ quan trọng hay không xử lý các tác vụ kém ưu tiên hơn nếu CPU đang sắp quá tải trong khung hình hiện tại. 

+ Tất cả các vật thể đều muốn gia nhập một hệ thống như vậy phải có chung một điểm “đi vào”. Ta có thể đạt được điều này qua lớp *Interface* với từ khóa *interface*. Khi class nào đó implement một *Interface*, , nó buộc phải cài đặt các phương thực mà *Interface* quy định. Trong C#, các lớp chỉ có thể được kế thừa từ một lớp cơ sở nhưng số lượng *Interface* có thể implement là không giới hạn. 

+ Ví dụ một lớp implement  *Interface* và phải cài đặt  hàm *OnUpdate()* mà *Interface* quy định: 

  ```c++
  public interface IUpdateable {
   void OnUpdate(float dt);
  }
  
  public class UpdateableComponent : MonoBehaviour, IUpdateable {
   public virtual void OnUpdate(float dt) {}
  }
  ```

+ Phương thức *OnUpdate()* của lớp *UpdateableComponent* nhận vào tham số dt là vi phân khoảng thời gian giữa các lần cập nhật , và có thể cho phép giãn thời gian theo ý muốn chứ không giống như lời gọi *Time.deltaTime* thường được sử dụng trong *Update()*.

+ Unity sẽ tự động phát hiện và gọi các phương thức tên là Update(), nhưng không gọi đến cái tên OnUpdate(), nên ta phải tự định nghĩa cách để gọi các hàm cập nhật này, đây cũng là mục tiêu của cả phương pháp này. Ví dụ: chúng ta có thể giao việc kiểm soát “cách cập nhật” các vật thể cho một lớp GameLogic. 

+ Trong suốt quá trình khởi tạo các hàm *OnUpdate()* này, chúng ta phải có thông báo với GameLogic về sự tồn tại cũng như biến mất của các lớp được quản lý để GameLogic có thể biết được khi nào cần gọi hay không. 

+ Ví dụ tiếp theo đây mô phỏng cách cài đặt các thông báo như vậy. Cần lưu ý rằng lớp GameLogic là một thành phần SingletonComponent, và quá trình thông báo được diễn ra bằng MessagingSystem 

  ```c++
  void Start() {
   GameLogic.Instance.RegisterUpdateableObject(this);
  }
  void OnDestroy() {
   if (GameLogic.Instance.IsAlive) {
   GameLogic.Instance.DeregisterUpdateableObject(this);
   }
  }
  ```

+ Cài đặt thông báo này tại hàm *Start()* là tối ưu nhất bởi ta có thể đảm bảo rằng tất phương thức *Awake()* của các thành phần đã tồn tại trước đã được gọi trước thời điểm này. Nhờ vậy công việc liên quan đến khởi tạo đối tượng sẽ được thực hiện trước bắt đầu cập nhật. 

+ Cần chú ý rằng vì hàm *Start()* đã được viết trong một lớp *MonoBehaviour* cha, thì khi định nghĩa lại hàm này trong các lớp con, nó sẽ ghi đè nội dung tại class cha. Do vậy không nên viết lại hàm *Start()* ở lớp con, hãy định nghĩa một hàm ảo *Initialize()* tại lớp cha và cho phép lớp con tự định nghĩa các hành vi khi khởi tạo. Ví dụ:

  ```c++
  void Start() {
   GameLogic.Instance.RegisterUpdateableObject(this);
   Initialize();
  }
  protected virtual void Initialize() {
   	// derived classes should override this method for initialization code,
  	and NOT reimplement Start()
  }
  ```

+ Cuối cùng cần đề cập đến vấn đề cài đặt lớp *GameLogic* như thế nào. Cách thức gần như là tương tự nhau dù ta cài đặt theo hướng *SingletonComponent* hay đơn trạng thái *MonoBehaviour*. Và để quản lý các vật thể con, ta cho chúng kế thừa một interface giống như *IUpdateable*. Và quan trọng nhất là trong hàm *Update()* của *GameLogic* phải gọi đến các hàm *OnUpdate()* của các vật thể con. 

+ Sau đây là một ví dụ cài đặt: 

  ```c++
  public class GameLogicSingletonComponent :
  SingletonComponent<GameLogicSingletonComponent> {
   public static GameLogicSingletonComponent Instance {
   	get { return ((GameLogicSingletonComponent)_Instance); }
   	set { _Instance = value; }
   }
   List<IUpdateable> _updateableObjects = new List<IUpdateable>();
   
   public void RegisterUpdateableObject(IUpdateable obj) {
       if (!_updateableObjects.Contains(obj)) {
           _updateableObjects.Add(obj);
       }
   }
   public void DeregisterUpdateableObject(IUpdateable obj) {
       if (_updateableObjects.Contains(obj)) {
       _updateableObjects.Remove(obj);
       }
   }
   void Update() {
       float dt = Time.deltaTime;
       for (int i = 0; i < _updateableObjects.Count; ++i) {
       _updateableObjects[i].OnUpdate(dt);
       }
   }
  }
  ```

+ Như vậy, thay vì cần gọi hàng ngàn phương thức thực *Update()* thì ta chỉ cần gọi nó một lần, và hàng ngàn phương thức ảo *OnUpdate()* được gọi. Cách này có thể tiết kiệm được rất nhiều chu kỳ CPU bởi bên cạnh việc tiết kiệm được các chu kì do gọi hàm ảo, chúng ta có thể tự quản lý được cách mà các vật thể được cập nhật và tránh được Native-Managed Bridge. Lớp này còn có khả năng mở rộng để cung cấp sự ưu tiên cho các hệ thống, loại bỏ các nhiệm vụ không được ưu tiên nếu nó phát hiện khung hình hiện tại đang kéo dài quá lâu và trong nhiều trường hợp khác. 

+ Hay sử dụng phương pháp này một cách thông minh. Ưu điểm nó đem lại vượt trội hơn rủi ro phải đánh đổi! 

## Tổng kết 

+ Chương này đã giới thiệu rất nhiều phương pháp cải thiện hiệu năng sử dụng kịch bản trong UnityEngine và các phương án cải thiện, xử lý khi kịch bản gây ra các vấn đề về hiệu năng. Một vài giải pháp yêu cầu sự suy tính và phân tích kỹ trước khi cài đặt bởi chúng thường đi kèm với rủi ro, làm rối mã nguồn. Luồng công việc, hiệu năng và thiết kế có mức độ quan trọng như nhau, nên trước khi thay đổi mã nguồn để cải thiện hiệu năng, cần cân nhắc kỹ lưỡng xem ta có đang hy sinh quá nhiều các yếu tố khác để làm tăng hiệu năng hay không.

+ Chương 8 - *Masterful Memory Management* trong cuốn sách này sẽ phân tích kỹ hơn một số kỹ thuật nâng cao. Nhưng trước tiên hãy bắt đầu việc nâng cao hiệu năng đồ họa bằng một vài phương pháp hỗ trợ có sẵn của Unity như sử dụng các lô tĩnh và động.

## Nội dung công việc trong nhóm 
### 1. Danh sách thành viên 
1. Lương Duy Đạt    - 19020039
2. Nguyễn Anh Đức - 19020004
3. Nguyễn Như Ngọc - 19020385
4. Nguyễn Minh Quang - 19020405
5. Đỗ Đức Tâm - 19020427
6. Bùi Chí Trung - 19020054

### 2. Phân công
1. Dịch thô: Nguyễn Minh Quang, Đỗ Đức Tâm 
2. Chỉnh sửa bản dịch: Nguyễn Anh Đức, Lương Duy Đạt 
3. Làm slide: Nguyễn Như Ngọc, Bùi Chí Trung 
4. Thuyết trình: Nguyễn Minh Quang 

 