Hero image

Last updated May 2019, 10 min. read

Làm thế nào để thiết kế kiến ​​trúc mã khi dự án của bạn mở rộng quy mô?

Trang web này đã được dịch bằng máy để thuận tiện cho bạn. Chúng tôi không thể đảm bảo tính chính xác hoặc độ tin cậy của nội dung được dịch. Nếu bạn có thắc mắc về tính chính xác của nội dung được dịch, vui lòng tham khảo phiên bản tiếng Anh chính thức của trang web.

Những gì bạn sẽ nhận được từ trang này : Các chiến lược hiệu quả để thiết kế kiến ​​trúc mã nguồn cho một dự án đang phát triển, sao cho nó có thể mở rộng một cách gọn gàng và ít gặp sự cố hơn. Khi dự án của bạn phát triển, bạn sẽ phải liên tục chỉnh sửa và hoàn thiện thiết kế của nó. Luôn luôn tốt khi lùi lại một bước để nhìn nhận lại những thay đổi bạn đang thực hiện, chia nhỏ vấn đề thành các yếu tố nhỏ hơn để sắp xếp chúng gọn gàng, rồi sau đó ghép tất cả lại với nhau.

Bài viết này do Mikael Kalms, Giám đốc công nghệ (CTO) của hãng game Fall Damage đến từ Thụy Điển, chấp bút. Mikael có hơn 20 năm kinh nghiệm trong việc phát triển và phát hành game. Sau ngần ấy thời gian, ông vẫn vô cùng quan tâm đến cách thiết kế kiến ​​trúc mã nguồn sao cho các dự án có thể phát triển một cách an toàn và hiệu quả.

Làm thế nào để thiết kế kiến ​​trúc mã khi dự án của bạn mở rộng quy mô?

Từ đơn giản đến phức tạp

Hãy cùng xem một vài ví dụ mã lập trình từ một trò chơi kiểu Pong rất đơn giản mà nhóm của tôi đã tạo ra cho bài thuyết trình của tôi Unite Berlin . Như bạn có thể thấy từ hình ảnh phía trên, có hai thanh trượt và bốn bức tường – ở trên cùng, dưới cùng, bên trái và bên phải – một số logic trò chơi và giao diện hiển thị điểm số. Trên mái chèo cũng như trên các bức tường đều có một đoạn mã đơn giản.

Ví dụ này dựa trên một vài nguyên tắc chính:

  • Một "thứ" = một nhà lắp ghép
  • Logic tùy chỉnh cho một "đối tượng" = một MonoBehavior
  • Một ứng dụng = một khung cảnh chứa các Prefab được liên kết với nhau.

Những nguyên tắc đó có thể áp dụng cho một dự án đơn giản như thế này, nhưng chúng ta sẽ phải thay đổi cấu trúc nếu muốn dự án này phát triển. Vậy, chúng ta có thể sử dụng những chiến lược nào để tổ chức mã nguồn?

Cách thiết kế kiến ​​trúc mã khi dự án mở rộng quy mô_Tham số thành phần

Các thể hiện, đối tượng dựng sẵn và đối tượng có thể lập trình

Trước tiên, hãy làm rõ sự khác biệt giữa các thể hiện (instances), Prefabs và ScriptableObjects. Hình trên là thành phần Paddle trên GameObject Paddle của Người chơi 1, được hiển thị trong Inspector:

Ta có thể thấy có ba tham số trên đó. Tuy nhiên, không có gì trong quan điểm này cho tôi biết mã nguồn bên dưới mong đợi điều gì ở tôi.

Liệu việc thay đổi Trục đầu vào trên cần điều khiển bên trái bằng cách thay đổi trực tiếp trên đối tượng (instance) có hợp lý không, hay tôi nên làm điều đó trong Prefab? Tôi cho rằng trục đầu vào khác nhau đối với cả hai người chơi, vì vậy có lẽ cần phải thay đổi nó trên phiên bản cụ thể. Còn về thang đo tốc độ di chuyển thì sao? Tôi nên thay đổi điều đó ở phần cấu hình hay ở phần Prefab?

Hãy cùng xem đoạn mã đại diện cho thành phần Paddle.

Cách thiết kế kiến ​​trúc mã khi dự án mở rộng quy mô_Các tham số trong mã

Các tham số trong một ví dụ mã đơn giản

Nếu chúng ta dừng lại và suy nghĩ một chút, chúng ta sẽ nhận ra rằng các tham số khác nhau đang được sử dụng theo những cách khác nhau trong chương trình của chúng ta. Chúng ta nên thay đổi InputAxisName riêng cho từng người chơi: MovementSpeedScaleFactor và PositionScale nên được chia sẻ giữa cả hai người chơi. Dưới đây là một chiến lược có thể hướng dẫn bạn trong việc sử dụng các thể hiện (instances), Prefabs và ScriptableObjects khi nào:

  • Bạn chỉ cần thứ gì đó một lần thôi sao? Tạo một Prefab, sau đó khởi tạo nó.
  • Bạn cần một thứ gì đó nhiều lần, có thể kèm theo một số chỉnh sửa cụ thể cho từng trường hợp? Sau đó, bạn có thể tạo một Prefab, khởi tạo nó và ghi đè một số thiết lập.
  • Bạn có muốn đảm bảo cài đặt giống nhau trên nhiều phiên bản khác nhau không? Sau đó, tạo một ScriptableObject và lấy dữ liệu từ đó.

Hãy xem cách chúng ta sử dụng ScriptableObjects với thành phần Paddle trong ví dụ mã tiếp theo.

Cách thiết kế kiến ​​trúc mã khi dự án mở rộng quy mô_sử dụng ScriptableObjects

Sử dụng ScriptableObjects

Vì chúng ta đã chuyển các thiết lập này sang một ScriptableObject thuộc loại PaddleData, nên chúng ta chỉ cần một tham chiếu đến PaddleData đó trong Paddle Component của mình. Kết quả cuối cùng mà chúng ta nhận được trong Inspector là hai mục: một PaddleData và hai thể hiện của Paddle. Bạn vẫn có thể thay đổi tên trục và gói cài đặt dùng chung mà mỗi cần điều khiển riêng lẻ trỏ đến. Cấu trúc mới cho phép bạn dễ dàng hiểu được mục đích đằng sau các thiết lập khác nhau.

Cách thiết kế kiến ​​trúc mã khi dự án mở rộng quy mô_nguyên tắc trách nhiệm duy nhất

Chia nhỏ các MonoBehaviors lớn

Nếu đây là một trò chơi đang trong quá trình phát triển thực tế, bạn sẽ thấy các MonoBehavior riêng lẻ ngày càng lớn hơn. Hãy xem chúng ta có thể phân chia chúng như thế nào bằng cách áp dụng nguyên tắc Trách nhiệm Đơn nhất, quy định rằng mỗi lớp chỉ nên xử lý một việc duy nhất. Nếu áp dụng đúng cách, bạn sẽ có thể trả lời ngắn gọn các câu hỏi, “một lớp cụ thể làm gì?” cũng như “nó không làm gì?” Điều này giúp mọi lập trình viên trong nhóm của bạn dễ dàng hiểu được chức năng của từng lớp riêng lẻ. Đây là nguyên tắc bạn có thể áp dụng cho bất kỳ cơ sở mã nào, bất kể quy mô. Hãy xem một ví dụ đơn giản như trong hình ảnh phía trên.

Đây là mã lệnh dành cho một quả bóng. Thoạt nhìn thì không có gì đặc biệt, nhưng khi quan sát kỹ hơn, ta thấy quả bóng có một vận tốc được cả người thiết kế sử dụng để xác định vectơ vận tốc ban đầu của quả bóng, và phần mềm mô phỏng vật lý tự chế sử dụng để theo dõi vận tốc hiện tại của quả bóng.

Chúng ta đang sử dụng lại cùng một biến cho hai mục đích hơi khác nhau. Ngay khi quả bóng bắt đầu chuyển động, thông tin về vận tốc ban đầu sẽ bị mất.

Mô phỏng vật lý tự chế này không chỉ bao gồm chuyển động trong FixedUpdate(); nó còn bao gồm cả phản ứng khi quả bóng va vào tường.

Nằm sâu bên trong hàm gọi lại OnTriggerEnter() là một thao tác Destroy(). Đó là nơi mà logic kích hoạt tự xóa GameObject của chính nó. Trong các cơ sở mã nguồn lớn, việc cho phép các thực thể tự xóa chính chúng là rất hiếm; thay vào đó, xu hướng là để chủ sở hữu xóa những thứ mà họ sở hữu.

Đây là cơ hội để chia nhỏ mọi thứ thành các phần nhỏ hơn. Các lớp học này bao gồm nhiều loại trách nhiệm khác nhau – lập trình logic trò chơi, xử lý dữ liệu đầu vào, mô phỏng vật lý, thuyết trình và nhiều hơn nữa.

Dưới đây là các cách để tạo những chi tiết nhỏ hơn đó:

  • Logic tổng thể của trò chơi, xử lý đầu vào, mô phỏng vật lý và hiển thị có thể nằm trong MonoBehaviors, ScriptableObjects hoặc các lớp C# thô.
  • Để hiển thị các tham số trong Inspector, có thể sử dụng MonoBehaviors hoặc ScriptableObjects.
  • Các trình xử lý sự kiện của engine và việc quản lý vòng đời của một GameObject cần phải nằm trong MonoBehaviors.

Tôi nghĩ rằng đối với nhiều trò chơi, việc tận dụng tối đa mã từ MonoBehaviors là điều đáng giá. Một cách để làm điều đó là sử dụng ScriptableObjects, và đã có một số tài liệu tham khảo tuyệt vời về phương pháp này.

Cách thiết kế kiến ​​trúc mã khi dự án của bạn mở rộng quy mô_chuyển đổi Monobehaviors sang các lớp C#

Từ MonoBehaviors đến các lớp C# thông thường

Việc chuyển MonoBehaviors sang các lớp C# thông thường là một phương pháp khác cần xem xét, nhưng lợi ích của việc này là gì?

Các lớp C# thông thường có các công cụ ngôn ngữ tốt hơn so với các đối tượng của Unity trong việc chia nhỏ mã thành các khối nhỏ, có thể kết hợp với nhau. Và, mã C# thông thường có thể được chia sẻ với các cơ sở mã .NET gốc bên ngoài Unity.

Mặt khác, nếu bạn sử dụng các lớp C# thông thường, thì trình soạn thảo sẽ không hiểu các đối tượng đó và không thể hiển thị chúng một cách tự nhiên trong Inspector, v.v.

Với phương pháp này, bạn cần phân chia logic theo loại trách nhiệm. Nếu quay lại ví dụ về quả bóng, chúng ta đã chuyển mô phỏng vật lý đơn giản vào một lớp C# mà chúng ta gọi là BallSimulation. Nhiệm vụ duy nhất của nó là tích hợp các định luật vật lý và phản ứng mỗi khi quả bóng va chạm với vật thể nào đó.

Tuy nhiên, liệu việc mô phỏng quả bóng đưa ra quyết định dựa trên những gì nó thực sự va phải có hợp lý không? Nghe có vẻ giống logic trò chơi hơn. Tóm lại, chúng ta có một phần logic điều khiển quá trình mô phỏng theo một số cách, và kết quả của quá trình mô phỏng đó được đưa trở lại MonoBehavior.

Nếu nhìn vào phiên bản được tổ chức lại ở trên, một thay đổi đáng kể mà chúng ta thấy là thao tác Destroy() không còn bị ẩn sâu nhiều lớp nữa. Hiện tại, chỉ còn lại một vài lĩnh vực trách nhiệm rõ ràng trong MoneBehavior.

Chúng ta có thể làm thêm nhiều việc nữa về vấn đề này. Nếu bạn xem xét logic cập nhật vị trí trong FixedUpdate(), ta có thể thấy rằng mã cần truyền vào một vị trí và sau đó trả về một vị trí mới từ đó. Mô phỏng quả bóng thực chất không nắm giữ vị trí của quả bóng; nó chạy một chu kỳ mô phỏng dựa trên vị trí của quả bóng được cung cấp, và sau đó trả về kết quả.

Cách thiết kế kiến ​​trúc mã khi dự án mở rộng quy mô bằng cách sử dụng giao diện

Sử dụng giao diện

Nếu chúng ta sử dụng giao diện, có lẽ chúng ta có thể chia sẻ một phần của MonoBehavior hình cầu đó với mô phỏng, chỉ những phần mà nó cần (xem hình trên).

Chúng ta hãy xem lại đoạn mã. Lớp Ball triển khai một giao diện đơn giản. Lớp LocalPositionAdapter cho phép chuyển tham chiếu đến đối tượng Ball cho một lớp khác. Chúng ta không truyền toàn bộ đối tượng Ball, mà chỉ truyền phần LocalPositionAdapter của nó.

BallLogic cũng cần thông báo cho Ball khi đến lúc phải phá hủy GameObject. Thay vì trả về một cờ, Ball có thể cung cấp một ủy quyền cho BallLogic. Đó chính là chức năng của dòng được đánh dấu cuối cùng trong phiên bản được sắp xếp lại. Điều này mang lại cho chúng ta một thiết kế gọn gàng: có rất nhiều logic lặp đi lặp lại, nhưng mỗi lớp đều có một mục đích được xác định rõ ràng.

Bằng cách áp dụng những nguyên tắc này, bạn có thể duy trì cấu trúc tốt cho một dự án cá nhân.

Cách thiết kế kiến ​​trúc mã khi dự án của bạn mở rộng quy mô_kiến trúc phần mềm

Kiến trúc phần mềm

Hãy cùng xem xét các giải pháp kiến ​​trúc phần mềm cho các dự án có quy mô lớn hơn một chút. Nếu lấy ví dụ về trò chơi Ball, một khi chúng ta bắt đầu đưa thêm các lớp cụ thể hơn vào mã nguồn – BallLogic, BallSimulation, v.v. – thì chúng ta sẽ có thể xây dựng một hệ thống phân cấp:

Các MonoBehaviour phải biết về mọi thứ khác vì chúng chỉ đơn thuần là gói gọn tất cả các logic khác, nhưng các thành phần mô phỏng của trò chơi không nhất thiết phải biết về cách thức hoạt động của logic đó. Họ chỉ đang chạy một mô phỏng. Đôi khi, logic gửi tín hiệu đến mô phỏng, và mô phỏng sẽ phản hồi tương ứng.

Việc xử lý dữ liệu đầu vào ở một nơi riêng biệt, độc lập sẽ mang lại nhiều lợi ích. Đó là nơi các sự kiện đầu vào được tạo ra và sau đó được đưa vào hệ thống logic. Những gì xảy ra tiếp theo đều phụ thuộc vào mô phỏng.

Phương pháp này hoạt động tốt cho cả việc nhập liệu và mô phỏng. Tuy nhiên, bạn có thể sẽ gặp vấn đề với bất cứ điều gì liên quan đến phần trình bày, ví dụ như logic tạo ra hiệu ứng đặc biệt, cập nhật bộ đếm điểm số, v.v.

Logic và cách trình bày

Bài thuyết trình cần nắm được tình hình hoạt động của các hệ thống khác nhưng không cần phải có quyền truy cập đầy đủ vào tất cả các hệ thống đó. Nếu có thể, hãy tách biệt phần logic và phần trình bày. Hãy cố gắng đạt đến điểm mà bạn có thể chạy mã nguồn của mình ở hai chế độ: chỉ logic và logic kèm theo hiển thị.

Đôi khi bạn cần kết nối logic và phần trình bày sao cho phần trình bày được cập nhật đúng lúc. Tuy nhiên, mục tiêu vẫn là chỉ cung cấp bản trình bày với những thông tin cần thiết để hiển thị chính xác, và không hơn thế nữa. Bằng cách này, bạn sẽ có được một ranh giới tự nhiên giữa hai phần, giúp giảm độ phức tạp tổng thể của trò chơi mà bạn đang xây dựng.

Các lớp chỉ chứa dữ liệu và các lớp hỗ trợ

Đôi khi, việc có một lớp chỉ chứa dữ liệu mà không cần tích hợp tất cả logic và các thao tác có thể thực hiện với dữ liệu đó vào cùng một lớp là hoàn toàn ổn.

Cũng nên tạo các lớp không sở hữu bất kỳ dữ liệu nào nhưng chứa các hàm có mục đích thao tác với các đối tượng được truyền vào.

Phương pháp tĩnh

Ưu điểm của phương thức tĩnh là, nếu bạn giả định rằng nó không tác động đến bất kỳ biến toàn cục nào, thì bạn có thể xác định phạm vi ảnh hưởng tiềm tàng của phương thức chỉ bằng cách xem xét các tham số được truyền vào khi gọi phương thức. Bạn hoàn toàn không cần xem xét cách triển khai phương pháp đó.

Cách tiếp cận này liên quan đến lĩnh vực lập trình hàm. Khối cấu trúc cốt lõi ở đây là: bạn gửi một tham số đến một hàm, và hàm đó trả về một kết quả hoặc có thể sửa đổi một trong các tham số đầu ra. Hãy thử cách tiếp cận này; bạn có thể thấy rằng mình sẽ gặp ít lỗi hơn so với khi sử dụng lập trình hướng đối tượng truyền thống.

Tách rời các đối tượng của bạn

Bạn cũng có thể tách rời các đối tượng bằng cách chèn logic kết nối giữa chúng. Nếu chúng ta lấy ví dụ trò chơi kiểu Pong một lần nữa: logic của quả bóng và cách hiển thị điểm số sẽ tương tác với nhau như thế nào? Liệu logic xử lý bóng có ảnh hưởng đến việc hiển thị tỷ số khi có sự cố xảy ra liên quan đến quả bóng không? Liệu logic tính điểm có truy vấn logic về quả bóng không? Họ sẽ cần phải nói chuyện với nhau bằng cách nào đó.

Bạn có thể tạo một đối tượng bộ đệm với mục đích duy nhất là cung cấp vùng lưu trữ nơi logic có thể ghi dữ liệu và phần trình bày có thể đọc dữ liệu. Hoặc, bạn có thể đặt một hàng đợi ở giữa chúng, để hệ thống logic có thể đưa các phần tử vào hàng đợi và phần trình bày sẽ đọc những gì đến từ hàng đợi.

Một cách hay để tách biệt logic khỏi phần trình bày khi trò chơi của bạn phát triển là sử dụng một hệ thống truyền tải thông điệp. Nguyên tắc cốt lõi của việc nhắn tin là cả người nhận lẫn người gửi đều không biết về bên kia, nhưng cả hai đều biết về hệ thống/đường truyền thông điệp. Vì vậy, phần trình bày điểm số cần được hệ thống nhắn tin thông báo về bất kỳ sự kiện nào làm thay đổi điểm số. Hệ thống logic của trò chơi sau đó sẽ gửi các sự kiện đến hệ thống nhắn tin để báo hiệu sự thay đổi điểm số của người chơi. Một điểm khởi đầu tốt nếu bạn muốn tách rời các hệ thống là sử dụng UnityEvents - hoặc tự viết; sau đó bạn có thể có các bus riêng biệt cho các mục đích khác nhau.

Cảnh đang tải

Hãy ngừng sử dụng LoadSceneMode.Single và thay thế bằng LoadSceneMode.Additive.

Hãy sử dụng các lệnh giải phóng rõ ràng khi bạn muốn giải phóng một cảnh – sớm muộn gì bạn cũng cần giữ lại một vài đối tượng trong quá trình chuyển đổi cảnh.

Hãy ngừng sử dụng DontDestroyOnLoad nữa. Nó khiến bạn mất quyền kiểm soát vòng đời của một vật thể. Thực tế, nếu bạn đang tải các đối tượng bằng LoadSceneMode.Additive, thì bạn sẽ không cần sử dụng DontDestroyOnLoad. Thay vào đó, hãy đặt những vật dụng có tuổi thọ cao của bạn vào một khung cảnh đặc biệt dành cho những vật dụng có tuổi thọ cao.

Tắt máy một cách sạch sẽ và có kiểm soát.

Một lời khuyên hữu ích khác mà tôi đã áp dụng trong mọi trò chơi mình từng tham gia là hỗ trợ việc tắt máy một cách an toàn và có kiểm soát.

Hãy đảm bảo ứng dụng của bạn có khả năng giải phóng gần như toàn bộ tài nguyên trước khi kết thúc. Nếu có thể, không nên gán giá trị cho bất kỳ biến toàn cục nào và không nên đánh dấu bất GameObjects bằng thuộc tính DontDestroyOnLoad.

Khi bạn có một trình tự cụ thể để tắt các chương trình, bạn sẽ dễ dàng phát hiện lỗi và tìm ra sự lãng phí tài nguyên hơn. Điều này cũng sẽ giúp Unity Editor của bạn ở trạng thái tốt khi bạn thoát khỏi chế độ Chơi. Unity không thực hiện tải lại toàn bộ miền khi thoát khỏi chế độ chơi. Nếu bạn tắt máy đúng cách, khả năng trình chỉnh sửa hoặc bất kỳ loại kịch bản chế độ chỉnh sửa nào hiển thị các hành vi bất thường sau khi bạn chạy trò chơi trong trình chỉnh sửa sẽ giảm đi.

Giảm đau bằng cách hợp nhất các tập tin cảnh.

Bạn có thể thực hiện việc này bằng cách sử dụng hệ thống quản lý phiên bản như Git, Perforce hoặc Plastic. Lưu trữ tất cả tài sản dưới dạng văn bản và di chuyển các đối tượng ra khỏi tệp cảnh bằng cách biến chúng thành Prefab. Cuối cùng, hãy chia các tệp cảnh thành nhiều cảnh nhỏ hơn, nhưng cần lưu ý rằng điều này có thể yêu cầu thêm công cụ hỗ trợ.

Tự động hóa quy trình cho việc kiểm thử mã.

Nếu nhóm của bạn sắp có từ 10 người trở lên, bạn sẽ cần phải thực hiện một số công việc về tự động hóa quy trình.

Là một lập trình viên sáng tạo, bạn muốn thực hiện những công việc độc đáo, tỉ mỉ và để lại càng nhiều phần lặp đi lặp lại càng tốt cho tự động hóa.

Hãy bắt đầu bằng cách viết các bài kiểm thử cho mã của bạn. Cụ thể, nếu bạn chuyển các thành phần từ MonoBehaviours sang các lớp thông thường, thì việc sử dụng một framework kiểm thử đơn vị để xây dựng các bài kiểm thử đơn vị cho cả logic và mô phỏng sẽ rất đơn giản. Phương pháp này không phải lúc nào cũng hiệu quả, nhưng nó thường giúp các lập trình viên khác dễ dàng truy cập mã nguồn của bạn hơn sau này.

Tự động hóa quy trình cho việc kiểm thử nội dung

Việc kiểm thử không chỉ đơn thuần là kiểm thử mã nguồn. Bạn cũng cần kiểm tra nội dung của mình. Nếu nhóm của bạn có những người sáng tạo nội dung, tất cả mọi người sẽ làm việc hiệu quả hơn nếu họ có một phương pháp chuẩn hóa để nhanh chóng xác nhận nội dung mà họ tạo.

Các logic kiểm thử – chẳng hạn như xác thực một Prefab hoặc xác thực một số dữ liệu mà người dùng đã nhập thông qua trình chỉnh sửa tùy chỉnh – cần phải dễ dàng truy cập đối với người tạo nội dung. Nếu họ chỉ cần nhấp vào một nút trong trình chỉnh sửa và nhận được xác nhận nhanh chóng, họ sẽ sớm nhận ra rằng điều này giúp tiết kiệm thời gian.

Bước tiếp theo là thiết lập Unity Test Runner để bạn có thể tự động kiểm tra lại mọi thứ một cách thường xuyên. Bạn muốn thiết lập nó như một phần của hệ thống xây dựng, để nó cũng chạy tất cả các bài kiểm tra của bạn. Một cách làm tốt là thiết lập thông báo , để khi có sự cố xảy ra, các thành viên trong nhóm của bạn sẽ nhận được thông báo qua Slack hoặc email.

Tạo các lượt chơi tự động

Chơi tự động bao gồm việc tạo ra một trí tuệ nhân tạo (AI) có thể chơi trò chơi của bạn và ghi lại các lỗi. Nói một cách đơn giản, bất kỳ lỗi nào mà AI của bạn tìm thấy đều là một lỗi mà bạn không cần phải tốn thời gian tìm kiếm!

Trong trường hợp của chúng tôi, chúng tôi đã thiết lập khoảng 10 máy khách trò chơi trên cùng một máy, với cài đặt đồ họa thấp nhất, và cho phép tất cả chúng chạy. Chúng tôi đã chứng kiến ​​cảnh chúng rơi xuống và sau đó xem nhật ký sự cố. Mỗi lần một trong những máy khách này gặp sự cố là chúng tôi tiết kiệm được thời gian vì không phải tự chơi game hoặc nhờ người khác chơi để tìm lỗi. Điều đó có nghĩa là khi chúng tôi thực sự chơi thử trò chơi cùng với những người chơi khác, chúng tôi có thể tập trung vào việc trò chơi có thú vị hay không, các lỗi hình ảnh nằm ở đâu, v.v.