paint-brush
Khám phá Unity DOTS và ECS: Nó có phải là yếu tố thay đổi cuộc chơi không?từ tác giả@deniskondratev
3,433 lượt đọc
3,433 lượt đọc

Khám phá Unity DOTS và ECS: Nó có phải là yếu tố thay đổi cuộc chơi không?

từ tác giả Denis Kondratev12m2023/07/18
Read on Terminal Reader

dài quá đọc không nổi

Bài viết này đi sâu vào Ngăn xếp công nghệ hướng dữ liệu (DOTS) và Hệ thống thành phần thực thể (ECS) của Unity, giúp tối ưu hóa quá trình phát triển trò chơi thông qua kiến trúc đơn giản, trực quan. Những công nghệ này, cùng với các gói Unity bổ sung, cho phép tạo trò chơi hiệu quả, hiệu suất cao. Ưu điểm của các hệ thống này được thể hiện thông qua ví dụ về thuật toán Trò chơi Cuộc sống của Conway.
featured image - Khám phá Unity DOTS và ECS: Nó có phải là yếu tố thay đổi cuộc chơi không?
Denis Kondratev HackerNoon profile picture
0-item
1-item
2-item

Unity DOTS cho phép các nhà phát triển sử dụng toàn bộ tiềm năng của bộ xử lý hiện đại và cung cấp các trò chơi hiệu quả, được tối ưu hóa cao – và chúng tôi nghĩ rằng điều này đáng để chú ý.


Đã hơn 5 năm kể từ khi Unity lần đầu tiên công bố phát triển Ngăn xếp Công nghệ Định hướng Dữ liệu (DOTS) của họ. Giờ đây, với việc phát hành phiên bản hỗ trợ dài hạn (LTS), Unity 2023.3.0f1, cuối cùng chúng tôi cũng đã thấy bản phát hành chính thức. Nhưng tại sao Unity DOTS lại quan trọng đối với ngành phát triển trò chơi và công nghệ này mang lại những lợi thế gì?


Xin chào tất cả mọi người! Tên tôi là Denis Kondratev và tôi là Nhà phát triển Unity tại MY.GAMES. Nếu bạn háo hức muốn hiểu Unity DOTS là gì và liệu nó có đáng để khám phá hay không, thì đây là cơ hội hoàn hảo để tìm hiểu sâu về chủ đề hấp dẫn này và trong bài viết này – chúng ta sẽ làm điều đó.


Hệ thống thành phần thực thể (ECS) là gì?

Về cốt lõi, DOTS triển khai mẫu kiến trúc Hệ thống Thành phần Thực thể (ECS). Để đơn giản hóa khái niệm này, hãy mô tả nó như sau: ECS được xây dựng dựa trên ba yếu tố cơ bản: Thực thể, Thành phần và Hệ thống.


Bản thân các thực thể không có bất kỳ chức năng hoặc mô tả vốn có nào. Thay vào đó, chúng đóng vai trò là nơi chứa các Thành phần khác nhau, mang lại cho chúng các đặc điểm cụ thể cho logic trò chơi, kết xuất đối tượng, hiệu ứng âm thanh, v.v.


Đổi lại, các thành phần có nhiều loại khác nhau và chỉ lưu trữ dữ liệu mà không có khả năng xử lý độc lập của riêng chúng.


Hoàn thiện khung ECS là Hệ thống xử lý Thành phần, xử lý việc tạo và hủy Thực thể cũng như quản lý việc thêm hoặc xóa Thành phần.


Ví dụ: khi tạo trò chơi "Bắn súng không gian", sân chơi sẽ có nhiều đối tượng: tàu vũ trụ của người chơi, kẻ thù, tiểu hành tinh, chiến lợi phẩm, v.v.



Tất cả các đối tượng này được coi là thực thể theo đúng nghĩa của chúng, không có bất kỳ đặc điểm riêng biệt nào. Tuy nhiên, bằng cách gán các thành phần khác nhau cho chúng, chúng ta có thể thấm nhuần chúng với các thuộc tính duy nhất.


Để chứng minh, xét rằng tất cả các đối tượng này đều có vị trí trên sân thi đấu, chúng ta có thể tạo một thành phần vị trí chứa tọa độ của đối tượng. Hơn nữa, đối với tàu vũ trụ, kẻ thù và tiểu hành tinh của người chơi, chúng tôi có thể kết hợp các thành phần sức khỏe; hệ thống chịu trách nhiệm xử lý các xung đột đối tượng sẽ chi phối tình trạng của các thực thể này. Ngoài ra, chúng ta có thể gắn một thành phần loại kẻ thù vào kẻ thù, cho phép hệ thống kiểm soát của kẻ thù chi phối hành vi của chúng dựa trên loại được chỉ định.


Trong khi lời giải thích này cung cấp một cái nhìn tổng quan đơn giản, thô sơ, thì thực tế có phần phức tạp hơn. Tuy nhiên, tôi tin tưởng rằng khái niệm cơ bản của ECS là rõ ràng. Ngoài ra, chúng ta hãy đi sâu vào những lợi thế của phương pháp này.

Lợi ích của Hệ thống thành phần thực thể

Một trong những ưu điểm chính của phương pháp Hệ thống Thành phần Thực thể (ECS) là thiết kế kiến trúc mà nó thúc đẩy. Lập trình hướng đối tượng (OOP) mang một di sản quan trọng với các mẫu như kế thừa và đóng gói, và ngay cả những lập trình viên có kinh nghiệm cũng có thể mắc lỗi kiến trúc trong quá trình phát triển, dẫn đến việc tái cấu trúc hoặc rối logic trong các dự án dài hạn.


Ngược lại, ECS cung cấp một kiến trúc đơn giản và trực quan. Mọi thứ tự nhiên rơi vào các thành phần và hệ thống biệt lập, giúp dễ hiểu và phát triển hơn bằng cách sử dụng phương pháp này; ngay cả những nhà phát triển mới làm quen cũng nhanh chóng nắm bắt cách tiếp cận này với các lỗi tối thiểu.


ECS tuân theo cách tiếp cận tổng hợp, trong đó các thành phần và hệ thống hành vi riêng biệt được tạo ra thay vì các hệ thống phân cấp kế thừa phức tạp. Có thể dễ dàng thêm hoặc bớt các thành phần và hệ thống này, cho phép thay đổi linh hoạt đối với các đặc điểm và hành vi của thực thể – cách tiếp cận này giúp tăng cường đáng kể khả năng sử dụng lại mã.


Một ưu điểm quan trọng khác của ECS là tối ưu hóa hiệu suất. Trong ECS, dữ liệu được lưu trữ trong bộ nhớ theo cách liền kề và được tối ưu hóa, với các loại dữ liệu giống hệt nhau được đặt gần nhau. Điều này tối ưu hóa quyền truy cập dữ liệu, giảm lỗi bộ nhớ cache và cải thiện các kiểu truy cập bộ nhớ. Hơn nữa, các hệ thống bao gồm các khối dữ liệu riêng biệt dễ dàng song song hóa giữa các quy trình khác nhau, dẫn đến hiệu suất tăng vượt trội so với các phương pháp truyền thống.

Khám phá các gói của Unity DOTS

Unity DOTS bao gồm một tập hợp các công nghệ do Unity Technologies cung cấp để triển khai khái niệm ECS trong Unity. Nó bao gồm một số gói được thiết kế để nâng cao các khía cạnh khác nhau của quá trình phát triển trò chơi; bây giờ chúng ta hãy bao gồm một vài trong số đó.


Cốt lõi của DOTS là gói Thực thể , tạo điều kiện chuyển đổi từ MonoBehaviours và GameObject quen thuộc sang cách tiếp cận Hệ thống Thành phần Thực thể. Gói này tạo thành nền tảng của sự phát triển dựa trên DOTS.


Gói Vật lý Unity giới thiệu một cách tiếp cận mới để xử lý vật lý trong trò chơi, đạt được tốc độ vượt trội thông qua tính toán song song.


Ngoài ra, gói Havok Vật lý cho Unity cho phép tích hợp với công cụ Vật lý Havok hiện đại. Công cụ này cung cấp khả năng phát hiện va chạm và mô phỏng vật lý hiệu suất cao, hỗ trợ các trò chơi phổ biến như The Legend of Zelda: Breath of the Wild, Doom Eternal, Death Stranding, Mortal Kombat 11, v.v.


Death Stranding, giống như nhiều trò chơi điện tử khác, sử dụng công cụ Vật lý Havok phổ biến.


Gói Entities Graphics tập trung vào kết xuất trong DOTS. Nó cho phép thu thập dữ liệu kết xuất hiệu quả và hoạt động liền mạch với các quy trình kết xuất hiện có như Quy trình kết xuất chung (URP) hoặc Quy trình kết xuất độ nét cao (HDRP).


Một điều nữa, Unity cũng đã tích cực phát triển một công nghệ mạng có tên là Netcode. Nó bao gồm các gói như Unity Transport để phát triển trò chơi nhiều người chơi ở mức độ thấp, Netcode cho GameObjects cho các phương pháp truyền thống và gói Unity Netcode for Entities đáng chú ý, phù hợp với các nguyên tắc DOTS. Các gói này tương đối mới và sẽ tiếp tục phát triển trong tương lai.

Nâng cao hiệu suất trong Unity DOTS và hơn thế nữa

Một số công nghệ liên quan chặt chẽ đến DOTS có thể được sử dụng trong khuôn khổ DOTS và hơn thế nữa. Gói Hệ thống Công việc cung cấp một cách thuận tiện để viết mã với các tính toán song song. Nó xoay quanh việc chia công việc thành các phần nhỏ gọi là công việc, thực hiện tính toán trên dữ liệu của chính chúng. Hệ thống Công việc phân phối đồng đều các công việc này trên các luồng để thực thi hiệu quả.


Để đảm bảo an toàn cho mã, Hệ thống công việc hỗ trợ xử lý các loại dữ liệu có thể xóa được. Các loại dữ liệu có thể chia sẻ dữ liệu có cùng biểu diễn trong bộ nhớ được quản lý và không được quản lý, đồng thời không yêu cầu chuyển đổi khi chuyển giữa mã được quản lý và không được quản lý. Ví dụ về các loại blittable bao gồm byte, sbyte, short, ushort, int, uint, long, ulong, float, double, IntPtr và UIntPtr. Các mảng một chiều của các kiểu nguyên thủy có thể đọc được và các cấu trúc chứa các loại có thể đọc được độc quyền cũng được coi là có thể đọc được.


Tuy nhiên, các loại chứa một mảng có thể thay đổi gồm các loại có thể đọc được không được coi là có thể đọc được. Để giải quyết hạn chế này, Unity đã phát triển gói Bộ sưu tập , gói này cung cấp một tập hợp các cấu trúc dữ liệu không được quản lý để sử dụng trong các công việc. Các bộ sưu tập này được cấu trúc và lưu trữ dữ liệu trong bộ nhớ không được quản lý bằng cơ chế Unity. Nhà phát triển có trách nhiệm phân bổ các bộ sưu tập này bằng phương thức Disposal().


Một gói quan trọng khác là Trình biên dịch Burst , có thể được sử dụng với Hệ thống công việc để tạo mã được tối ưu hóa cao. Mặc dù nó đi kèm với một số giới hạn sử dụng mã nhất định, trình biên dịch Burst cung cấp một hiệu suất tăng đáng kể.

Đo lường hiệu suất với Job System và Burst Compile

Như đã đề cập, Job System và Burst Compiler không phải là thành phần trực tiếp của DOTS nhưng cung cấp hỗ trợ có giá trị trong việc lập trình tính toán song song hiệu quả và nhanh chóng. Hãy kiểm tra khả năng của họ bằng một ví dụ thực tế: triển khai Thuật toán Trò chơi Cuộc sống của Conway . Trong thuật toán này, một trường được chia thành các ô, mỗi ô có thể còn sống hoặc đã chết. Trong mỗi lượt, chúng tôi kiểm tra số lượng hàng xóm trực tiếp cho mỗi ô và trạng thái của chúng được cập nhật theo các quy tắc cụ thể.



Đây là cách triển khai thuật toán này bằng cách sử dụng phương pháp truyền thống:


 private void SimulateStep() { Profiler.BeginSample(nameof(SimulateStep)); for (var i = 0; i < width; i++) { for (var j = 0; j < height; j++) { var aliveNeighbours = CountAliveNeighbours(i, j); var index = i * height + j; var isAlive = aliveNeighbours switch { 2 => _cellStates[index], 3 => true, _ => false }; _tempResults[index] = isAlive; } } _tempResults.CopyTo(_cellStates); Profiler.EndSample(); } private int CountAliveNeighbours(int x, int y) { var count = 0; for (var i = x - 1; i <= x + 1; i++) { if (i < 0 || i >= width) continue; for (var j = y - 1; j <= y + 1; j++) { if (j < 0 || j >= height) continue; if (_cellStates[i * width + j]) { count++; } } } return count; }


Tôi đã thêm các điểm đánh dấu vào Profiler để đo thời gian tính toán. Trạng thái của các ô được lưu trữ trong một mảng một chiều có tên _cellStates . Ban đầu, chúng tôi ghi các kết quả tạm thời vào _tempResults và sau đó sao chép chúng trở lại _cellStates sau khi hoàn thành các phép tính. Cách tiếp cận này là cần thiết vì việc ghi trực tiếp kết quả cuối cùng vào _cellStates sẽ ảnh hưởng đến các tính toán tiếp theo.


Tôi đã tạo một trường gồm 1000x1000 ô và chạy chương trình để đo hiệu suất. Đây là kết quả:



Như đã thấy từ kết quả, các phép tính mất 380 ms.


Bây giờ hãy áp dụng Job System và Burst Compiler để cải thiện hiệu suất. Đầu tiên, chúng ta sẽ tạo Công việc chịu trách nhiệm thực hiện thuật toán Conway's Game of Life.


 public struct SimulationJob : IJobParallelFor { public int Width; public int Height; [ReadOnly] public NativeArray<bool> CellStates; [WriteOnly] public NativeArray<bool> TempResults; public void Execute(int index) { var i = index / Height; var j = index % Height; var aliveNeighbours = CountAliveNeighbours(i, j); var isAlive = aliveNeighbours switch { 2 => CellStates[index], 3 => true, _ => false }; TempResults[index] = isAlive; } private int CountAliveNeighbours(int x, int y) { var count = 0; for (var i = x - 1; i <= x + 1; i++) { if (i < 0 || i >= Width) continue; for (var j = y - 1; j <= y + 1; j++) { if (j < 0 || j >= Height) continue; if (CellStates[i * Width + j]) { count++; } } } return count; } }


Tôi đã gán thuộc tính [ReadOnly] cho trường CellStates , cho phép truy cập không hạn chế vào tất cả các giá trị của mảng từ bất kỳ luồng nào. Tuy nhiên, đối với trường TempResults có thuộc tính [WriteOnly] , việc ghi chỉ có thể được thực hiện thông qua chỉ mục được chỉ định trong phương thức Execute(int index) . Cố gắng ghi một giá trị vào một chỉ mục khác sẽ tạo ra một cảnh báo. Điều này đảm bảo an toàn dữ liệu khi làm việc ở chế độ đa luồng.


Bây giờ, từ mã thông thường, hãy khởi chạy Công việc của chúng ta:


 private void SimulateStepWithJob() { Profiler.BeginSample(nameof(SimulateStepWithJob)); var job = new SimulationJob { Width = width, Height = height, CellStates = _cellStates, TempResults = _tempResults }; var jobHandler = job.Schedule(width * height, 4); jobHandler.Complete(); job.TempResults.CopyTo(_cellStates); Profiler.EndSample(); }


Sau khi sao chép tất cả các dữ liệu cần thiết, chúng tôi lên lịch thực hiện công việc bằng phương thức Schedule() . Điều quan trọng cần lưu ý là việc lập lịch trình này không thực hiện các phép tính ngay lập tức: các hành động này được bắt đầu từ luồng chính và quá trình thực thi diễn ra thông qua các công nhân được phân bổ giữa các luồng khác nhau. Để đợi công việc hoàn thành, chúng ta sử dụng jobHandler.Complete() . Chỉ sau đó, chúng ta mới có thể sao chép kết quả thu được trở lại _cellStates .


Hãy đo tốc độ:



Tốc độ thực thi đã tăng gần gấp 10 lần và thời gian thực hiện hiện là khoảng 42 mili giây. Trong cửa sổ Profiler, chúng ta có thể thấy rằng khối lượng công việc được phân bổ cho 17 công nhân. Con số này ít hơn một chút so với số luồng bộ xử lý trong môi trường thử nghiệm, đó là Intel® Core™ i9-10900 với 10 lõi và 20 luồng. Mặc dù kết quả có thể khác nhau trên các bộ xử lý có ít lõi hơn, nhưng chúng tôi có thể đảm bảo sử dụng hết công suất của bộ xử lý.


Nhưng đó không phải là tất cả – đã đến lúc sử dụng Trình biên dịch Burst, cung cấp khả năng tối ưu hóa mã đáng kể nhưng đi kèm với một số hạn chế nhất định. Để bật Trình biên dịch Burst, chỉ cần thêm thuộc tính [BurstCompile] vào SimulationJob .


 [BurstCompile] public struct SimulationJob : IJobParallelFor { ... }


Hãy đo lại:



Kết quả vượt xa cả những kỳ vọng lạc quan nhất: tốc độ đã tăng gần 200 lần so với kết quả ban đầu. Bây giờ, thời gian tính toán cho 1 triệu ô không quá 2 ms. Trong Profiler, các phần được thực thi bởi mã được biên dịch bằng Trình biên dịch Burst được đánh dấu bằng màu xanh lục.

Phần kết luận

Mặc dù việc sử dụng tính toán đa luồng có thể không phải lúc nào cũng cần thiết và việc sử dụng Burst Compiler có thể không phải lúc nào cũng thực hiện được, nhưng chúng ta có thể quan sát xu hướng chung trong việc phát triển bộ xử lý theo hướng kiến trúc đa lõi. Điều này có nghĩa là chúng ta nên chuẩn bị để khai thác toàn bộ sức mạnh của chúng. ECS, và cụ thể là Unity DOTS, hoàn toàn phù hợp với mô hình này.


Tôi tin rằng Unity DOTS ít nhất cũng xứng đáng được chú ý. Mặc dù có thể không phải là giải pháp tốt nhất cho mọi trường hợp, nhưng ECS có thể chứng minh giá trị của mình trong nhiều trò chơi.


Khung Unity DOTS, với cách tiếp cận đa luồng và hướng dữ liệu, mang lại tiềm năng to lớn để tối ưu hóa hiệu suất trong các trò chơi Unity. Bằng cách áp dụng kiến trúc Hệ thống thành phần thực thể và tận dụng các công nghệ như Hệ thống công việc và Trình biên dịch liên tục, các nhà phát triển có thể mở khóa các cấp hiệu suất và khả năng mở rộng mới.


Khi quá trình phát triển trò chơi tiếp tục phát triển và phần cứng tiến bộ, việc sử dụng Unity DOTS ngày càng trở nên có giá trị. Nó trao quyền cho các nhà phát triển khai thác toàn bộ tiềm năng của bộ xử lý hiện đại và cung cấp các trò chơi được tối ưu hóa và hiệu quả cao. Mặc dù Unity DOTS có thể không phải là giải pháp lý tưởng cho mọi dự án, nhưng nó chắc chắn mang lại nhiều hứa hẹn cho những người tìm kiếm khả năng mở rộng và phát triển dựa trên hiệu suất.


Unity DOTS là một khung mạnh mẽ có thể mang lại lợi ích đáng kể cho các nhà phát triển trò chơi bằng cách nâng cao hiệu suất, cho phép tính toán song song và nắm bắt tương lai của xử lý đa lõi. Thật đáng để khám phá và xem xét việc áp dụng nó để tận dụng tối đa phần cứng hiện đại và tối ưu hóa hiệu suất của trò chơi Unity.