Tuy nhiên, có hợp lý không khi tạo các ứng dụng sử dụng kiến trúc Thành phần-Thực thể phổ biến trong các công cụ trò chơi?
Đối với tôi, hoàn toàn. Tôi làm việc trong FX trực quan và nghiên cứu rất nhiều hệ thống trong lĩnh vực này, kiến trúc của họ (bao gồm CAD / CAM), khao khát SDK và bất kỳ bài báo nào sẽ cho tôi cảm nhận về ưu và nhược điểm của các quyết định kiến trúc dường như vô tận có thể được thực hiện, với ngay cả những người tinh tế nhất không phải lúc nào cũng tạo ra một tác động tinh tế.
VFX khá giống với các trò chơi ở chỗ có một khái niệm trung tâm của một "cảnh", với các khung nhìn hiển thị kết quả được hiển thị. Cũng có xu hướng xử lý vòng lặp trung tâm diễn ra liên tục xoay quanh cảnh này trong bối cảnh hoạt hình, trong đó có thể xảy ra vật lý, các hạt phát sinh hạt, lưới được hoạt hình và kết xuất, hoạt hình chuyển động, v.v. tất cả cho người dùng ở cuối
Một khái niệm tương tự khác với các công cụ trò chơi ít phức tạp nhất là nhu cầu về khía cạnh "nhà thiết kế" nơi các nhà thiết kế có thể thiết kế linh hoạt các cảnh, bao gồm khả năng thực hiện một số chương trình nhẹ của riêng họ (tập lệnh và nút).
Tôi thấy rằng, trong nhiều năm qua, ECS đã phù hợp nhất. Tất nhiên điều đó không bao giờ hoàn toàn ly dị với sự chủ quan, nhưng tôi sẽ nói rằng nó xuất hiện mạnh mẽ để đưa ra ít vấn đề nhất. Nó đã giải quyết được rất nhiều vấn đề lớn mà chúng tôi luôn phải vật lộn, trong khi chỉ trả lại cho chúng tôi một vài vấn đề nhỏ mới.
OOP truyền thống
Các cách tiếp cận OOP truyền thống hơn có thể thực sự mạnh mẽ khi bạn nắm vững các yêu cầu thiết kế trước nhưng không phải là các yêu cầu thực hiện. Cho dù thông qua cách tiếp cận đa giao diện phẳng hơn hoặc cách tiếp cận ABC phân cấp lồng nhau hơn, nó có xu hướng củng cố thiết kế và làm cho nó khó thay đổi hơn trong khi thực hiện thay đổi dễ dàng và an toàn hơn. Luôn có nhu cầu về sự không ổn định trong bất kỳ sản phẩm nào đi qua một phiên bản duy nhất, vì vậy các phương pháp OOP có xu hướng làm lệch tính ổn định (khó thay đổi và thiếu lý do thay đổi) đối với mức độ thiết kế và không ổn định (dễ thay đổi và lý do thay đổi) đến mức thực hiện.
Tuy nhiên, chống lại sự phát triển của các yêu cầu cuối người dùng, cả thiết kế và triển khai có thể cần phải thường xuyên thay đổi. Bạn có thể tìm thấy thứ gì đó kỳ lạ như nhu cầu mạnh mẽ của người dùng đối với sinh vật tương tự cần cả thực vật và động vật cùng một lúc, làm mất hiệu lực hoàn toàn toàn bộ mô hình khái niệm bạn đã xây dựng. Các cách tiếp cận hướng đối tượng thông thường không bảo vệ bạn ở đây và đôi khi có thể thực hiện những thay đổi không thể định nghĩa, phá vỡ khái niệm như vậy thậm chí còn khó hơn. Khi các khu vực rất quan trọng về hiệu suất có liên quan, lý do cho thiết kế thay đổi nhiều hơn nữa.
Kết hợp nhiều giao diện chi tiết để tạo thành giao diện phù hợp của một đối tượng có thể giúp ích rất nhiều trong việc ổn định mã máy khách, nhưng nó không giúp ổn định các kiểu con đôi khi có thể làm giảm số lượng phụ thuộc của máy khách. Ví dụ, bạn có thể có một giao diện được sử dụng bởi một phần trong hệ thống của bạn, nhưng với hàng ngàn kiểu con khác nhau thực hiện giao diện đó. Trong trường hợp đó, việc duy trì các kiểu con phức tạp (phức tạp vì chúng có quá nhiều trách nhiệm giao diện khác nhau phải hoàn thành) có thể trở thành cơn ác mộng thay vì mã sử dụng chúng thông qua giao diện. OOP có xu hướng chuyển độ phức tạp sang cấp đối tượng, trong khi ECS chuyển nó sang cấp độ máy khách ("hệ thống") và điều đó có thể lý tưởng khi có rất ít hệ thống ngoại trừ toàn bộ "đối tượng" ("thực thể").
Một lớp cũng sở hữu dữ liệu riêng tư và do đó có thể tự duy trì bất biến. Tuy nhiên, có những bất biến "thô" thực sự vẫn có thể khó duy trì khi các đối tượng tương tác với nhau. Đối với một hệ thống phức tạp, toàn bộ ở trạng thái hợp lệ thường cần xem xét một biểu đồ phức tạp của các đối tượng, ngay cả khi các bất biến riêng lẻ của chúng được duy trì đúng cách. Các cách tiếp cận kiểu OOP truyền thống có thể giúp duy trì các bất biến dạng hạt, nhưng thực sự có thể gây khó khăn cho việc duy trì các bất biến rộng, thô nếu các đối tượng tập trung vào các khía cạnh thiếu niên của hệ thống.
Đó là nơi mà các cách tiếp cận hoặc biến thể ECS xây dựng khối lego có thể rất hữu ích. Ngoài ra, với các hệ thống có thiết kế thô hơn so với vật thể thông thường, việc duy trì các loại bất biến thô đó trong tầm nhìn của hệ thống sẽ trở nên dễ dàng hơn. Rất nhiều tương tác đối tượng tuổi teen biến thành một hệ thống lớn tập trung vào một nhiệm vụ rộng lớn thay vì các đối tượng nhỏ tuổi tập trung vào các nhiệm vụ nhỏ tuổi với biểu đồ phụ thuộc sẽ bao phủ một km giấy.
Tuy nhiên, tôi đã phải nhìn ra ngoài lĩnh vực của mình, trong ngành công nghiệp game, để tìm hiểu về ECS, mặc dù tôi luôn là một trong những người có tư duy định hướng dữ liệu. Ngoài ra, thật vui, tôi gần như tự mình tìm đến ECS chỉ cần lặp đi lặp lại và cố gắng đưa ra những thiết kế tốt hơn. Mặc dù vậy, tôi đã không thực hiện được và đã bỏ lỡ một chi tiết rất quan trọng, đó là việc chính thức hóa phần "hệ thống" và nghiền nát các thành phần cho đến dữ liệu thô.
Tôi sẽ cố gắng xem xét cách tôi kết thúc việc giải quyết trên ECS và cách nó kết thúc giải quyết tất cả các vấn đề với các lần lặp thiết kế trước đó. Tôi nghĩ rằng sẽ giúp làm nổi bật chính xác lý do tại sao câu trả lời ở đây có thể là "có" rất mạnh, rằng ECS có khả năng áp dụng vượt xa ngành công nghiệp game.
Kiến trúc Brute Force thập niên 1980
Kiến trúc đầu tiên tôi làm trong ngành VFX có một di sản dài đã trải qua một thập kỷ kể từ khi tôi gia nhập công ty. Đó là sự thô bạo của C mã hóa mọi cách (không phải là một sự nghiêng về C, như tôi yêu C, nhưng cách nó được sử dụng ở đây thực sự thô thiển). Một lát cắt thu nhỏ và quá đơn giản giống như các phụ thuộc như thế này:
Và đây là một sơ đồ cực kỳ đơn giản của một phần nhỏ của hệ thống. Mỗi khách hàng này trong sơ đồ ("Kết xuất", "Vật lý", "Chuyển động") sẽ nhận được một số đối tượng "chung" thông qua đó họ sẽ kiểm tra trường loại, như vậy:
void transform(struct Object* obj, const float mat[16])
{
switch (obj->type)
{
case camera:
// cast to camera and do something with camera fields
break;
case light:
// cast to light and do something with light fields
break;
...
}
}
Tất nhiên với mã xấu hơn và phức tạp hơn đáng kể so với mã này. Thông thường các chức năng bổ sung sẽ được gọi từ các trường hợp chuyển đổi này sẽ thực hiện chuyển đổi lặp đi lặp lại nhiều lần. Biểu đồ này và mã gần như có thể trông như ECS-lite, nhưng không có sự khác biệt thực thể thành phần mạnh ( " là đối tượng này một máy ảnh?", Không phải 'không đối tượng này cung cấp chuyển động?'), Và không chính thức hóa 'hệ thống' ( chỉ là một loạt các chức năng lồng nhau đi khắp nơi và trộn lẫn các trách nhiệm). Trong trường hợp đó, mọi thứ đều phức tạp, bất kỳ chức năng nào cũng có khả năng xảy ra thảm họa đang chờ xảy ra.
Quy trình thử nghiệm của chúng tôi ở đây thường phải kiểm tra những thứ như lưới tách biệt với các loại vật phẩm khác, ngay cả khi điều tương tự xảy ra với cả hai, vì bản chất vũ phu của mã hóa ở đây (thường đi kèm với nhiều bản sao và dán) thường được thực hiện rất có thể là những gì khác chính là logic tương tự có thể thất bại từ loại này sang loại khác. Cố gắng mở rộng hệ thống để xử lý các loại mặt hàng mới là khá vô vọng, mặc dù có nhu cầu cuối cùng của người dùng được thể hiện mạnh mẽ, vì quá khó khăn khi chúng tôi phải vật lộn rất nhiều chỉ để xử lý các loại mặt hàng hiện có.
Một số ưu điểm:
- Uhh ... tôi không có kinh nghiệm về kỹ thuật, tôi đoán vậy? Hệ thống này không yêu cầu bất kỳ kiến thức nào về các khái niệm cơ bản như đa hình, nó hoàn toàn là vũ lực, vì vậy tôi đoán ngay cả một người mới bắt đầu cũng có thể hiểu được một số mã ngay cả khi một chuyên gia gỡ lỗi có thể duy trì nó.
Một số nhược điểm:
- Cơn ác mộng bảo trì. Đội ngũ tiếp thị của chúng tôi thực sự cảm thấy cần phải tự hào về việc chúng tôi đã sửa hơn 2000 lỗi duy nhất trong một chu kỳ 3 năm. Đối với tôi đó là điều đáng xấu hổ về việc chúng tôi có quá nhiều lỗi ngay từ đầu và quá trình đó có lẽ vẫn chỉ sửa được khoảng 10% tổng số lỗi đang phát triển về số lượng.
- Về giải pháp không linh hoạt nhất có thể.
Kiến trúc COM những năm 1990
Hầu hết ngành công nghiệp VFX sử dụng phong cách kiến trúc này từ những gì tôi đã thu thập được, đọc tài liệu về các quyết định thiết kế của họ và liếc nhìn bộ công cụ phát triển phần mềm của họ.
Nó có thể không chính xác là COM ở cấp độ ABI (một số kiến trúc này chỉ có thể có các plugin được viết bằng cùng một trình biên dịch), nhưng chia sẻ rất nhiều đặc điểm tương tự với các truy vấn giao diện được thực hiện trên các đối tượng để xem các thành phần nào hỗ trợ giao diện của chúng.
Với cách tiếp cận này, transform
chức năng tương tự ở trên giống với hình thức này:
void transform(Object obj, const Matrix& mat)
{
// Wrapper that performs an interface query to see if the
// object implements the IMotion interface.
MotionRef motion(obj);
// If the object supported the IMotion interface:
if (motion.valid())
{
// Transform the item through the IMotion interface.
motion->transform(mat);
...
}
}
Đây là cách tiếp cận nhóm mới của cơ sở mã cũ đã giải quyết, để cuối cùng tái cấu trúc hướng tới. Và đó là một sự cải tiến đáng kể so với bản gốc về tính linh hoạt và khả năng bảo trì, nhưng vẫn còn một số vấn đề tôi sẽ đề cập trong phần tiếp theo.
Một số ưu điểm:
- Kịch tính linh hoạt / mở rộng / duy trì hơn nhiều so với giải pháp vũ lực trước đây.
- Thúc đẩy sự phù hợp mạnh mẽ với nhiều nguyên tắc của RẮN bằng cách làm cho mọi giao diện hoàn toàn trừu tượng (không trạng thái, không thực hiện, chỉ có giao diện thuần túy).
Một số nhược điểm:
- Rất nhiều nồi hơi. Các thành phần của chúng tôi phải được xuất bản thông qua sổ đăng ký để khởi tạo các đối tượng, các giao diện mà chúng hỗ trợ yêu cầu cả kế thừa ("triển khai" trong Java) và cung cấp một số mã để chỉ ra giao diện nào có sẵn trong truy vấn.
- Quảng cáo logic trùng lặp ở khắp mọi nơi là kết quả của các giao diện thuần túy. Ví dụ, tất cả các thành phần được triển khai
IMotion
sẽ luôn có cùng trạng thái và thực hiện chính xác cho tất cả các chức năng. Để giảm thiểu điều này, chúng tôi sẽ bắt đầu tập trung hóa các lớp cơ sở và chức năng của trình trợ giúp trong toàn hệ thống cho những thứ có xu hướng được triển khai một cách dự phòng theo cùng một cách cho cùng một giao diện, và có thể có nhiều kế thừa diễn ra sau mui xe, nhưng nó khá đẹp lộn xộn dưới mui xe mặc dù mã máy khách đã dễ dàng.
- Không hiệu quả: các phiên vtune thường
QueryInterface
hiển thị chức năng cơ bản hầu như luôn hiển thị dưới dạng điểm nóng từ giữa đến trên và đôi khi là điểm nóng số 1. Để giảm thiểu điều đó, chúng tôi sẽ làm những việc như hiển thị các phần của bộ đệm cơ sở mã một danh sách các đối tượng đã biết để hỗ trợIRenderable
, nhưng điều đó đã leo thang đáng kể sự phức tạp và chi phí bảo trì. Tương tự như vậy, điều này khó đo lường hơn nhưng chúng tôi nhận thấy một số sự chậm lại nhất định so với mã hóa kiểu C mà chúng tôi đang làm trước đây khi mọi giao diện đơn lẻ đều yêu cầu một công văn động. Những điều như dự đoán sai về chi nhánh và các rào cản tối ưu hóa rất khó đo lường bên ngoài một khía cạnh nhỏ của mã, nhưng người dùng thường chỉ nhận thấy khả năng phản hồi của giao diện người dùng và những thứ như thế trở nên tồi tệ hơn bằng cách so sánh các phiên bản phần mềm mới hơn và mới hơn bên cho các khu vực nơi độ phức tạp thuật toán không thay đổi, chỉ các hằng số.
- Vẫn còn khó để lý giải về tính đúng đắn ở cấp độ hệ thống rộng hơn. Mặc dù nó dễ dàng hơn đáng kể so với cách tiếp cận trước đó, nhưng vẫn khó có thể nắm bắt được các tương tác phức tạp giữa các đối tượng trong toàn hệ thống này, đặc biệt là với một số tối ưu hóa bắt đầu trở nên cần thiết để chống lại nó.
- Chúng tôi đã gặp sự cố khi giao diện của chúng tôi chính xác. Mặc dù có thể chỉ có một vị trí rộng trong hệ thống sử dụng giao diện, các yêu cầu cuối của người dùng sẽ thay đổi so với các phiên bản và cuối cùng chúng tôi sẽ phải thực hiện thay đổi xếp tầng cho tất cả các lớp thực hiện giao diện để phù hợp với chức năng mới được thêm vào giao diện, ví dụ, trừ khi có một số lớp cơ sở trừu tượng đã tập trung logic dưới mui xe (một số trong số này sẽ xuất hiện ở giữa các thay đổi tầng này với hy vọng không lặp đi lặp lại nhiều lần).
Phản ứng thực dụng: Thành phần
Một trong những điều chúng tôi đã nhận thấy trước đây (hoặc ít nhất là tôi) đã gây ra vấn đề là IMotion
có thể được thực hiện bởi 100 lớp khác nhau nhưng với cùng một triển khai và trạng thái chính xác liên quan. Hơn nữa, nó sẽ chỉ được sử dụng bởi một số ít các hệ thống như kết xuất, chuyển động có khung và vật lý.
Vì vậy, trong trường hợp như vậy, chúng ta có thể có mối quan hệ 3 trên 1 giữa các hệ thống sử dụng giao diện với giao diện và mối quan hệ 100 trên 1 giữa các kiểu con thực hiện giao diện với giao diện.
Sự phức tạp và bảo trì sau đó sẽ bị sai lệch nghiêm trọng đối với việc triển khai và bảo trì 100 kiểu con, thay vì 3 hệ thống máy khách phụ thuộc vào IMotion
. Điều này đã chuyển tất cả các khó khăn bảo trì của chúng tôi sang việc bảo trì 100 loại phụ này, chứ không phải 3 địa điểm sử dụng giao diện. Cập nhật 3 vị trí trong mã với một vài hoặc không có "khớp nối tiếp xúc gián tiếp" (như phụ thuộc vào nó nhưng gián tiếp thông qua giao diện, không phụ thuộc trực tiếp), không có vấn đề gì lớn: cập nhật 100 vị trí phụ với một khối "khớp nối gián tiếp" , vấn đề khá lớn *.
* Tôi nhận ra rằng thật kỳ quặc và sai lầm khi định nghĩa "khớp nối tràn đầy" theo nghĩa này từ góc độ thực hiện, tôi chỉ không tìm thấy cách nào tốt hơn để mô tả sự phức tạp bảo trì liên quan khi cả giao diện và triển khai tương ứng của một trăm kiểu con phải thay đổi.
Vì vậy, tôi đã phải thúc đẩy mạnh mẽ nhưng tôi đề nghị rằng chúng tôi cố gắng trở nên thực dụng hơn một chút và thư giãn toàn bộ ý tưởng "giao diện thuần túy". Nó không có ý nghĩa với tôi để làm cho một cái gì đó như IMotion
hoàn toàn trừu tượng và không trạng thái trừ khi chúng ta thấy một lợi ích cho nó có nhiều triển khai phong phú. Trong trường hợp của chúng tôi, IMotion
để có nhiều triển khai phong phú thực sự sẽ biến thành một cơn ác mộng bảo trì, vì chúng tôi không muốn sự đa dạng. Thay vào đó, chúng tôi đã cố gắng thực hiện một triển khai chuyển động duy nhất thực sự tốt để thay đổi các yêu cầu của khách hàng và thường làm việc xung quanh ý tưởng giao diện thuần túy rất nhiều khi cố gắng buộc mọi người thực hiện IMotion
sử dụng cùng một triển khai và trạng thái liên quan để chúng tôi không ' mục tiêu trùng lặp.
Do đó, các giao diện trở nên giống như rộng hơn Behaviors
liên quan đến một thực thể. IMotion
đơn giản sẽ trở thành một Motion
"thành phần" (Tôi đã thay đổi cách chúng ta định nghĩa "thành phần" từ COM thành một nơi gần với định nghĩa thông thường hơn, về một mảnh tạo thành một thực thể "hoàn chỉnh").
Thay vì điều này:
class IMotion
{
public:
virtual ~IMotion() {}
virtual void transform(const Matrix& mat) = 0;
...
};
Chúng tôi đã phát triển nó thành một cái gì đó giống như thế này:
class Motion
{
public:
void transform(const Matrix& mat)
{
...
}
...
private:
Matrix transformation;
...
};
Đây là một sự vi phạm trắng trợn nguyên tắc đảo ngược phụ thuộc để bắt đầu chuyển từ trừu tượng trở lại cụ thể, nhưng với tôi mức độ trừu tượng như vậy chỉ hữu ích nếu chúng ta có thể thấy trước một nhu cầu thực sự trong một tương lai, vượt quá sự nghi ngờ hợp lý và không thực hiện các tình huống "nếu như" lố bịch tách rời hoàn toàn khỏi trải nghiệm người dùng (có thể sẽ yêu cầu thay đổi thiết kế bằng mọi cách), vì sự linh hoạt như vậy.
Vì vậy, chúng tôi bắt đầu phát triển để thiết kế này. QueryInterface
ngày càng trở nên giống như QueryBehavior
. Hơn nữa, nó bắt đầu dường như vô nghĩa khi sử dụng thừa kế ở đây. Chúng tôi sử dụng thành phần thay thế. Các đối tượng biến thành một tập hợp các thành phần mà tính khả dụng của chúng có thể được truy vấn và đưa vào khi chạy.
Một số ưu điểm:
- Việc bảo trì vẫn còn dễ dàng hơn nhiều trong trường hợp của chúng tôi so với hệ thống kiểu COM giao diện thuần túy trước đây. Những bất ngờ không lường trước như thay đổi yêu cầu hoặc khiếu nại trong quy trình làm việc có thể được điều chỉnh dễ dàng hơn với một
Motion
triển khai rất trung tâm và rõ ràng , ví dụ, và không phân tán trên một trăm tiểu loại.
- Đã đưa ra một mức độ linh hoạt hoàn toàn mới của loại mà chúng ta thực sự cần. Trong hệ thống trước đây của chúng tôi, do mô hình thừa kế có mối quan hệ tĩnh, chúng tôi chỉ có thể xác định hiệu quả các thực thể mới tại thời gian biên dịch trong C ++. Chúng tôi không thể làm điều đó từ ngôn ngữ kịch bản, ví dụ: Với cách tiếp cận sáng tác, chúng tôi có thể kết hợp các thực thể mới một cách nhanh chóng khi chạy bằng cách chỉ gắn các thành phần vào chúng và thêm chúng vào danh sách. Một "thực thể" biến thành một khung vẽ trống mà trên đó chúng ta có thể ghép một ảnh ghép của bất cứ thứ gì chúng ta cần khi đang di chuyển, với các hệ thống có liên quan sẽ tự động nhận ra và xử lý các thực thể này.
Một số nhược điểm:
- Chúng tôi vẫn gặp khó khăn trong bộ phận hiệu quả và khả năng bảo trì trong các lĩnh vực quan trọng về hiệu suất. Mỗi hệ thống vẫn sẽ muốn lưu trữ các thành phần của các thực thể đã cung cấp các hành vi này để tránh lặp đi lặp lại tất cả chúng và kiểm tra những gì có sẵn. Mỗi hệ thống yêu cầu hiệu năng sẽ làm điều này hơi khác nhau một chút và dễ bị một bộ lỗi khác nhau khi không cập nhật danh sách được lưu trong bộ nhớ cache này và có thể là một cấu trúc dữ liệu (nếu một số hình thức tìm kiếm có liên quan như loại bỏ bực bội hoặc raytracing) trên một số sự kiện thay đổi cảnh tối nghĩa, ví dụ
- Vẫn còn một điều gì đó vụng về và phức tạp mà tôi không thể đặt ngón tay lên liên quan đến tất cả những vật thể đơn giản, nhỏ bé này. Chúng tôi vẫn sinh ra rất nhiều sự kiện để xử lý các tương tác giữa các đối tượng "hành vi" này đôi khi cần thiết và kết quả là mã rất phi tập trung. Mỗi đối tượng nhỏ đều dễ dàng kiểm tra tính chính xác và, được thực hiện riêng lẻ, thường là hoàn toàn chính xác. Tuy nhiên, vẫn có cảm giác như chúng tôi đang cố gắng duy trì một hệ sinh thái rộng lớn bao gồm những ngôi làng nhỏ và cố gắng suy luận về những gì tất cả họ làm riêng lẻ và cộng lại để tạo nên một tổng thể. Codebase thập niên 80 theo phong cách C có cảm giác như một siêu anh hùng sử thi, đông dân, chắc chắn là một cơn ác mộng bảo trì,
- Mất tính linh hoạt với sự thiếu trừu tượng nhưng trong một lĩnh vực mà chúng ta chưa bao giờ thực sự gặp phải một nhu cầu thực sự cho nó, vì vậy hầu như không phải là một con lừa thực tế (mặc dù chắc chắn ít nhất là một lý thuyết).
- Duy trì khả năng tương thích ABI luôn khó khăn và điều này làm cho nó khó hơn bằng cách yêu cầu dữ liệu ổn định và không chỉ giao diện ổn định liên quan đến "hành vi". Tuy nhiên, chúng ta có thể dễ dàng thêm các hành vi mới và chỉ đơn giản là không tán thành các hành vi hiện có nếu cần thay đổi trạng thái, và điều đó dễ dàng hơn nhiều so với thực hiện các backflips bên dưới các giao diện ở cấp độ phụ để xử lý các lo ngại về phiên bản.
Một hiện tượng xảy ra là, vì chúng ta mất đi sự trừu tượng trên các thành phần hành vi này, chúng ta có nhiều hơn chúng. Ví dụ: thay vì một IRenderable
thành phần trừu tượng , chúng tôi sẽ đính kèm một đối tượng bằng một thành phần cụ thể Mesh
hoặc PointSprites
thành phần. Hệ thống kết xuất sẽ biết cách kết xuất Mesh
và PointSprites
các thành phần và sẽ tìm các thực thể cung cấp các thành phần đó và vẽ chúng. Vào những thời điểm khác, chúng tôi có các kết xuất linh tinh như thế SceneLabel
mà chúng tôi phát hiện ra chúng tôi cần trong nhận thức muộn, và vì vậy chúng tôi sẽ đính kèm một SceneLabel
trong những trường hợp đó cho các thực thể có liên quan (có thể ngoài a Mesh
). Việc triển khai hệ thống kết xuất sau đó sẽ được cập nhật để biết cách kết xuất các thực thể đã cung cấp các thực thể đó và đó là một thay đổi khá dễ thực hiện.
Trong trường hợp này, một thực thể bao gồm các thành phần sau đó cũng có thể được sử dụng làm thành phần cho thực thể khác. Chúng tôi sẽ xây dựng mọi thứ theo cách đó bằng cách nối các khối lego.
ECS: Hệ thống và thành phần dữ liệu thô
Hệ thống cuối cùng đó là theo như tôi tự làm, và chúng tôi vẫn đang làm hỏng nó bằng COM. Cảm giác như nó muốn trở thành một hệ thống thành phần thực thể nhưng lúc đó tôi không quen với nó. Tôi đã nhìn xung quanh các ví dụ kiểu COM đã bão hòa lĩnh vực của tôi, khi tôi nên nhìn vào các công cụ trò chơi AAA để tìm cảm hứng kiến trúc. Cuối cùng tôi đã bắt đầu làm điều đó.
Những gì tôi đã thiếu là một số ý tưởng chính:
- Việc chính thức hóa "hệ thống" để xử lý "các thành phần".
- "Thành phần" là dữ liệu thô thay vì các đối tượng hành vi được kết hợp thành một đối tượng lớn hơn.
- Các thực thể không có gì khác hơn một ID nghiêm ngặt liên quan đến một bộ sưu tập các thành phần.
Cuối cùng tôi đã rời công ty đó và bắt đầu làm việc trên một ECS với tư cách là một người độc lập (vẫn làm việc với nó trong khi rút hết tiền tiết kiệm của mình) và đó là hệ thống dễ quản lý nhất từ trước đến nay.
Điều tôi nhận thấy với phương pháp ECS là nó đã giải quyết được những vấn đề tôi vẫn đang phải vật lộn ở trên. Quan trọng nhất với tôi, có cảm giác như chúng tôi đang quản lý những "thành phố" có kích thước khỏe mạnh thay vì những ngôi làng nhỏ tuổi teen với những tương tác phức tạp. Không khó để duy trì như một "megalopolis" nguyên khối, dân số quá lớn để quản lý hiệu quả, nhưng không hỗn loạn như một thế giới đầy những ngôi làng nhỏ bé tương tác với nhau khi chỉ nghĩ về các tuyến đường thương mại ở đó giữa chúng tạo thành một đồ thị ác mộng. ECS chắt lọc tất cả sự phức tạp đối với các "hệ thống" cồng kềnh, giống như một hệ thống kết xuất, một "thành phố" có kích thước khỏe mạnh nhưng không phải là "megalopolis đông dân".
Các thành phần trở thành dữ liệu thô lúc đầu cảm thấy thực sự kỳ lạ với tôi, vì nó phá vỡ cả nguyên tắc che giấu thông tin cơ bản của OOP. Đó là một thách thức đối với một trong những giá trị lớn nhất mà tôi yêu thích về OOP, đó là khả năng duy trì bất biến cần phải đóng gói và che giấu thông tin. Nhưng nó bắt đầu trở thành một vấn đề không đáng lo ngại vì nó nhanh chóng trở nên rõ ràng những gì đang xảy ra chỉ với hàng tá hệ thống rộng lớn biến đổi dữ liệu đó thay vì logic như vậy được phân tán trên hàng trăm đến hàng nghìn kiểu con thực hiện kết hợp giao diện. Tôi có xu hướng nghĩ về nó giống như vẫn theo kiểu OOP ngoại trừ trải ra nơi các hệ thống đang cung cấp chức năng và triển khai truy cập dữ liệu, các thành phần đang cung cấp dữ liệu và các thực thể đang cung cấp các thành phần.
Nó trở nên dễ dàng hơn , ngược lại bằng trực giác, để lý giải về các tác dụng phụ do hệ thống gây ra khi chỉ có một số ít các hệ thống cồng kềnh chuyển đổi dữ liệu theo các đường rộng. Hệ thống trở nên "phẳng" hơn rất nhiều, các ngăn xếp cuộc gọi của tôi trở nên nông hơn bao giờ hết cho mỗi luồng. Tôi có thể nghĩ về hệ thống ở cấp độ giám sát đó và không gặp phải những bất ngờ kỳ lạ.
Tương tự, nó làm cho ngay cả các khu vực quan trọng về hiệu suất trở nên đơn giản đối với việc loại bỏ các truy vấn đó. Do ý tưởng về "Hệ thống" trở nên rất chính thức, một hệ thống có thể đăng ký các thành phần mà nó quan tâm và chỉ cần đưa ra một danh sách lưu trữ các thực thể đáp ứng các tiêu chí đó. Mỗi cá nhân không phải quản lý tối ưu hóa bộ đệm, nó trở thành tập trung vào một nơi duy nhất.
Một số ưu điểm:
- Dường như chỉ giải quyết hầu hết mọi vấn đề kiến trúc lớn mà tôi gặp phải trong sự nghiệp mà không bao giờ cảm thấy bị mắc kẹt trong một góc thiết kế khi gặp phải những nhu cầu không lường trước được.
Một số nhược điểm:
- Thỉnh thoảng tôi vẫn gặp khó khăn trong đầu, và đó không phải là mô hình trưởng thành hay vững chắc nhất ngay cả trong ngành công nghiệp game, nơi mọi người tranh luận về chính xác ý nghĩa và cách làm. Đó chắc chắn không phải là điều tôi có thể làm với đội ngũ cũ mà tôi đã làm việc, bao gồm các thành viên gắn bó sâu sắc với lối tư duy kiểu COM hoặc lối tư duy kiểu C thập niên 1980 của cơ sở mã gốc. Đôi khi tôi cảm thấy bối rối giống như cách mô hình hóa các mối quan hệ kiểu đồ thị giữa các thành phần, nhưng tôi luôn tìm thấy một giải pháp không trở nên khủng khiếp sau này khi tôi có thể tạo ra một thành phần phụ thuộc vào một thành phần khác ("chuyển động này thành phần phụ thuộc vào thành phần này với tư cách là cha mẹ và hệ thống sẽ sử dụng ghi nhớ để tránh lặp lại các phép tính chuyển động đệ quy tương tự ", vd)
- ABI vẫn còn khó khăn, nhưng cho đến nay tôi thậm chí còn mạo hiểm nói rằng nó dễ hơn phương pháp giao diện thuần túy. Đó là một sự thay đổi trong suy nghĩ: sự ổn định dữ liệu trở thành trọng tâm duy nhất của ABI, thay vì ổn định giao diện và theo một số cách, nó dễ dàng đạt được sự ổn định dữ liệu hơn sự ổn định của giao diện (ví dụ: không có sự thay đổi nào chỉ vì nó cần một tham số mới. Đó là những thứ xảy ra bên trong việc triển khai hệ thống thô mà không phá vỡ ABI).
Tuy nhiên, có hợp lý không khi tạo các ứng dụng sử dụng kiến trúc Thành phần-Thực thể phổ biến trong các công cụ trò chơi?
Vì vậy, dù sao, tôi sẽ nói hoàn toàn "có", với ví dụ VFX cá nhân của tôi là một ứng cử viên mạnh. Nhưng điều đó vẫn khá giống với nhu cầu chơi game.
Tôi đã không đặt nó để thực hành ở các khu vực xa hơn hoàn toàn tách rời khỏi mối quan tâm của các công cụ trò chơi (VFX khá giống nhau), nhưng dường như nhiều khu vực khác là ứng cử viên tốt cho phương pháp ECS. Thậm chí có thể một hệ thống GUI sẽ phù hợp với một người, nhưng tôi vẫn sử dụng cách tiếp cận OOP hơn ở đó (nhưng không có sự kế thừa sâu sắc không giống như Qt, ví dụ).
Đó là lãnh thổ chưa được khám phá rộng rãi, nhưng nó có vẻ phù hợp với tôi bất cứ khi nào các thực thể của bạn có thể bao gồm một sự kết hợp phong phú của "đặc điểm" (và chính xác là những đặc điểm nào mà chúng cung cấp có thể thay đổi) và nơi bạn có một số ít khái quát hệ thống xử lý các thực thể có những đặc điểm cần thiết.
Nó trở thành một sự thay thế rất thực tế trong những trường hợp đó cho bất kỳ kịch bản nào mà bạn có thể bị cám dỗ sử dụng một cái gì đó như đa thừa kế hoặc mô phỏng khái niệm (mixins, ví dụ) chỉ để tạo ra hàng trăm hoặc nhiều combo trong hệ thống phân cấp thừa kế sâu hoặc hàng trăm combo của các lớp trong một hệ thống phân cấp phẳng thực hiện một tổ hợp giao diện cụ thể, nhưng trong đó các hệ thống của bạn có số lượng ít (hàng chục, ví dụ).
Trong những trường hợp đó, độ phức tạp của codebase bắt đầu cảm thấy tỷ lệ thuận hơn với số lượng hệ thống thay vì số lượng kết hợp loại, vì mỗi loại bây giờ chỉ là một thành phần cấu thành thực thể không có gì nhiều hơn dữ liệu thô. Các hệ thống GUI tự nhiên phù hợp với các loại thông số kỹ thuật này, nơi chúng có thể có hàng trăm loại tiện ích có thể kết hợp từ các loại cơ sở hoặc giao diện khác, nhưng chỉ một số ít hệ thống để xử lý chúng (hệ thống bố trí, hệ thống kết xuất, v.v.). Nếu một hệ thống GUI sử dụng ECS, có lẽ sẽ dễ dàng hơn rất nhiều để lý giải về tính đúng đắn của hệ thống khi tất cả các chức năng được cung cấp bởi một số các hệ thống này thay vì hàng trăm loại đối tượng khác nhau với các giao diện hoặc lớp cơ sở được kế thừa. Nếu một hệ thống GUI được sử dụng ECS, các widget sẽ không có chức năng, chỉ có dữ liệu. Chỉ một số ít các hệ thống xử lý các thực thể widget sẽ có chức năng. Làm thế nào các sự kiện quá mức cho một widget sẽ được xử lý vượt quá tôi, nhưng chỉ dựa trên kinh nghiệm hạn chế của tôi cho đến nay, tôi đã không tìm thấy một trường hợp mà loại logic đó không thể được chuyển tập trung vào một hệ thống nhất định theo cách mà nhận thức muộn màng, mang lại một giải pháp thanh lịch hơn nhiều mà tôi từng mong đợi.
Tôi rất thích nhìn thấy nó được sử dụng trong nhiều lĩnh vực, vì nó là cứu cánh trong tôi. Tất nhiên là không phù hợp nếu thiết kế của bạn không bị hỏng theo cách này, từ các thực thể tổng hợp các thành phần đến các hệ thống thô xử lý các thành phần đó, nhưng nếu chúng phù hợp một cách tự nhiên với kiểu máy này, thì đó là điều tuyệt vời nhất tôi từng gặp .