Khi nào / ở đâu để cập nhật các thành phần


10

Thay vì các công cụ trò chơi nặng kế thừa thông thường của tôi, tôi đang chơi với một cách tiếp cận dựa trên nhiều thành phần hơn. Tuy nhiên tôi có một thời gian khó khăn để biện minh nơi để các thành phần làm việc của họ.

Nói rằng tôi có một thực thể đơn giản có một danh sách các thành phần. Tất nhiên các thực thể không biết những thành phần này là gì. Có thể có một thành phần hiện diện cung cấp cho thực thể một vị trí trên màn hình, một thành phần khác có thể ở đó để vẽ thực thể trên màn hình.

Để các thành phần này hoạt động, chúng phải cập nhật mọi khung hình, cách dễ nhất để làm điều này là đi qua cây cảnh và sau đó cho từng thực thể cập nhật từng thành phần. Nhưng một số thành phần có thể cần quản lý nhiều hơn một chút. Ví dụ, một thành phần làm cho một thực thể có thể va chạm phải được quản lý bởi một cái gì đó có thể giám sát tất cả các thành phần có thể va chạm. Một thành phần làm cho một thực thể có thể vẽ được cần một người nào đó giám sát tất cả các thành phần có thể vẽ khác để tìm ra thứ tự vẽ, v.v ...

Vì vậy, câu hỏi của tôi là, tôi cập nhật các thành phần ở đâu, một cách sạch sẽ để đưa chúng đến các nhà quản lý là gì?

Tôi đã nghĩ về việc sử dụng một đối tượng trình quản lý singleton cho từng loại thành phần nhưng điều đó có những hạn chế thông thường khi sử dụng singleton, một cách để giảm bớt điều này là một chút bằng cách sử dụng phép nội xạ phụ thuộc nhưng nghe có vẻ quá mức cho vấn đề này. Tôi cũng có thể đi bộ qua cây cảnh và sau đó tập hợp các thành phần khác nhau vào danh sách bằng cách sử dụng một số kiểu mẫu quan sát nhưng điều đó có vẻ hơi lãng phí khi thực hiện mọi khung hình.


1
Bạn đang sử dụng hệ thống theo một cách nào đó?
Asakeron

Hệ thống thành phần là cách thông thường để làm điều này. Cá nhân tôi chỉ gọi cập nhật trên tất cả các thực thể, gọi cập nhật trên tất cả các thành phần và có một vài trường hợp "đặc biệt" (như trình quản lý không gian để phát hiện va chạm, là tĩnh).
tro999

Hệ thống thành phần? Tôi chưa bao giờ nghe nói về những người trước đây. Tôi sẽ bắt đầu Googling, nhưng tôi sẽ hoan nghênh mọi liên kết được đề xuất.
Roy T.

1
Hệ thống thực thể là tương lai của phát triển MMOG là một nguồn tài nguyên tuyệt vời. Và, thành thật mà nói tôi luôn bối rối bởi những tên kiến ​​trúc này. Sự khác biệt với cách tiếp cận được đề xuất là các thành phần chỉ chứa dữ liệu và hệ thống xử lý nó. Câu trả lời này cũng rất phù hợp.
Asakeron

1
Tôi đã viết một bài đăng trên blog về chủ đề này ở đây: gamedevrubberduck.wordpress.com/2012/12/26/ợi
AlexFoxGill 14/2/13

Câu trả lời:


15

Tôi sẽ đề nghị bắt đầu bằng cách đọc 3 lời nói dối lớn của Mike Acton, vì bạn đã vi phạm hai trong số đó. Tôi nghiêm túc, điều này sẽ thay đổi cách bạn thiết kế mã của mình: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html

Vậy bạn vi phạm điều gì?

Lie # 3 - Mã quan trọng hơn dữ liệu

Bạn nói về tiêm phụ thuộc, có thể hữu ích trong một số trường hợp (và chỉ một số) nhưng phải luôn rung chuông cảnh báo lớn nếu bạn sử dụng nó, đặc biệt là trong phát triển trò chơi! Tại sao? Bởi vì nó thường là một sự trừu tượng không cần thiết. Và trừu tượng ở những nơi sai lầm là khủng khiếp. Vì vậy, bạn có một trò chơi. Trò chơi có người quản lý cho các thành phần khác nhau. Các thành phần đều được xác định. Vì vậy, tạo một lớp ở đâu đó trong mã vòng lặp trò chơi chính của bạn "có" các trình quản lý. Giống:

private CollissionManager _collissionManager;
private BulletManager _bulletManager;

Cung cấp cho nó một số hàm getter để nhận từng lớp trình quản lý (getBONSManager ()). Có thể bản thân lớp này là Singleton hoặc có thể truy cập được từ một người (dù sao bạn cũng có thể có một đơn vị Game trung tâm ở đâu đó). Không có gì sai với dữ liệu và hành vi được mã hóa cứng được xác định rõ.

Không tạo Trình quản lý cho phép bạn đăng ký Người quản lý bằng khóa, có thể được truy xuất bằng khóa đó bởi các lớp khác muốn sử dụng trình quản lý. Đó là một hệ thống tuyệt vời và rất linh hoạt, nhưng nơi nói về một trò chơi ở đây. Bạn biết chính xác những hệ thống trong trò chơi. Tại sao lại giả vờ như bạn không? Bởi vì đây là một hệ thống dành cho những người nghĩ rằng mã quan trọng hơn dữ liệu. Họ sẽ nói "Mã rất linh hoạt, dữ liệu chỉ cần điền vào". Nhưng mã chỉ là dữ liệu. Hệ thống tôi mô tả dễ dàng hơn, đáng tin cậy hơn, dễ bảo trì hơn và linh hoạt hơn rất nhiều (ví dụ: nếu hành vi của một người quản lý khác với người quản lý khác, bạn chỉ phải thay đổi một vài dòng thay vì làm lại toàn bộ hệ thống)

Lie # 2 - Mã nên được thiết kế xung quanh một mô hình của thế giới

Vì vậy, bạn có một thực thể trong thế giới trò chơi. Các thực thể có một số thành phần xác định hành vi của nó. Vì vậy, bạn tạo một lớp Thực thể với một danh sách các đối tượng Thành phần và hàm Update () gọi hàm Update () của mỗi Thành phần. Đúng?

Không :) Đó là thiết kế xung quanh một mô hình của thế giới: bạn có viên đạn trong trò chơi của mình, vì vậy bạn thêm một lớp Bullet. Sau đó, bạn cập nhật từng Bullet và chuyển sang cái tiếp theo. Điều này sẽ hoàn toàn giết chết hiệu suất của bạn và nó cung cấp cho bạn một cơ sở mã hóa khủng khiếp với mã trùng lặp ở khắp mọi nơi và không có cấu trúc logic của mã tương tự. (Kiểm tra câu trả lời của tôi ở đây để được giải thích chi tiết hơn về lý do tại sao thiết kế OO truyền thống bị mất hoặc tìm kiếm Thiết kế hướng dữ liệu)

Chúng ta hãy xem xét tình huống mà không có sự thiên vị OO của chúng tôi. Chúng tôi muốn những điều sau đây, không hơn không kém (xin lưu ý rằng không có yêu cầu để tạo một lớp cho thực thể hoặc đối tượng):

  • Bạn có một loạt các thực thể
  • Các thực thể bao gồm một số thành phần xác định hành vi của thực thể
  • Bạn muốn cập nhật từng thành phần trong trò chơi từng khung hình, tốt nhất là theo cách được kiểm soát
  • Khác với việc xác định các thành phần là thuộc về nhau, bản thân thực thể không cần phải làm gì. Đó là một liên kết / ID cho một vài thành phần.

Và hãy nhìn vào tình hình. Hệ thống thành phần của bạn sẽ cập nhật hành vi của mọi đối tượng trong trò chơi mọi khung hình. Đây chắc chắn là một hệ thống quan trọng của động cơ của bạn. Hiệu suất là quan trọng ở đây!

Nếu bạn quen thuộc với kiến ​​trúc máy tính hoặc Thiết kế hướng dữ liệu, bạn sẽ biết cách đạt được hiệu suất tốt nhất: bộ nhớ được đóng gói chặt chẽ và bằng cách nhóm thực thi mã. Nếu bạn thực thi các đoạn mã A, B và C như thế này: ABCABCABC, bạn sẽ không có được hiệu suất như khi bạn thực thi nó như thế này: AAABBBCCC. Điều này không chỉ vì bộ đệm dữ liệu hướng dẫn và dữ liệu sẽ được sử dụng hiệu quả hơn mà còn bởi vì nếu bạn thực hiện tất cả các chữ "A" sau nhau, có rất nhiều chỗ để tối ưu hóa: xóa mã trùng lặp, tính toán trước dữ liệu được sử dụng bởi tất cả "A", v.v.

Vì vậy, nếu chúng ta muốn cập nhật tất cả các thành phần, chúng ta đừng biến chúng thành các lớp / đối tượng với chức năng cập nhật. Chúng ta đừng gọi chức năng cập nhật đó cho từng thành phần trong mỗi thực thể. Đó là giải pháp "ABCABCABC". Chúng ta hãy nhóm tất cả các cập nhật thành phần giống hệt nhau. Sau đó, chúng ta có thể cập nhật tất cả các thành phần A, tiếp theo là B, v.v. Chúng ta cần gì để thực hiện điều này?

Đầu tiên, chúng ta cần Quản lý thành phần. Đối với mọi loại thành phần trong trò chơi, chúng tôi cần một lớp người quản lý. Nó có chức năng cập nhật sẽ cập nhật tất cả các thành phần của loại đó. Nó có một hàm tạo sẽ thêm một thành phần mới của loại đó và một hàm loại bỏ sẽ phá hủy thành phần được chỉ định. Có thể có các hàm trợ giúp khác để nhận và đặt dữ liệu cụ thể cho thành phần đó (ví dụ: đặt mô hình 3D cho Thành phần mô hình). Lưu ý rằng người quản lý theo một cách nào đó là một hộp đen với thế giới bên ngoài. Chúng tôi không biết làm thế nào dữ liệu của từng thành phần được lưu trữ. Chúng tôi không biết làm thế nào mỗi thành phần được cập nhật. Chúng tôi không quan tâm, miễn là các thành phần hoạt động như họ cần.

Tiếp theo chúng ta cần một thực thể. Bạn có thể biến nó thành một lớp học, nhưng điều đó hầu như không cần thiết. Một thực thể có thể không có gì nhiều hơn một ID số nguyên duy nhất hoặc một chuỗi được băm (cũng là một số nguyên). Khi bạn tạo một thành phần cho Thực thể, bạn chuyển ID làm đối số cho Trình quản lý. Khi bạn muốn xóa thành phần, bạn lại chuyển ID. Có thể có một số lợi thế khi thêm một chút dữ liệu vào Thực thể thay vì chỉ làm ID, nhưng chúng sẽ chỉ là các hàm trợ giúp vì như tôi đã liệt kê trong các yêu cầu, tất cả hành vi thực thể được xác định bởi chính các thành phần. Đó là động cơ của bạn, vì vậy hãy làm những gì có ý nghĩa với bạn.

Những gì chúng ta cần là một Trình quản lý thực thể. Lớp này sẽ tạo ID duy nhất nếu bạn sử dụng giải pháp chỉ ID hoặc nó có thể được sử dụng để tạo / quản lý các đối tượng Thực thể. Nó cũng có thể giữ một danh sách tất cả các thực thể trong trò chơi nếu bạn cần điều đó. Trình quản lý thực thể có thể là lớp trung tâm của hệ thống thành phần của bạn, lưu trữ các tham chiếu đến tất cả các Trình quản lý thành phần trong trò chơi của bạn và gọi các chức năng cập nhật của chúng theo đúng thứ tự. Bằng cách đó, tất cả các vòng lặp trò chơi phải làm là gọi EntityManager.update () và toàn bộ hệ thống được tách biệt với phần còn lại của động cơ của bạn.

Đó là chế độ xem mắt chim, chúng ta hãy xem cách các nhà quản lý thành phần làm việc. Đây là những gì bạn cần:

  • Tạo dữ liệu thành phần khi tạo (entityID) được gọi
  • Xóa dữ liệu thành phần khi remove (entityID) được gọi
  • Cập nhật tất cả dữ liệu thành phần (áp dụng) khi update () được gọi (nghĩa là không phải tất cả các thành phần cần cập nhật từng khung)

Cái cuối cùng là nơi bạn xác định hành vi / logic thành phần và phụ thuộc hoàn toàn vào loại thành phần bạn đang viết. AnimationComponent sẽ cập nhật dữ liệu Hoạt hình dựa trên khung hình. DragableComponent sẽ chỉ cập nhật một thành phần đang được chuột kéo. ChemistryComponent sẽ cập nhật dữ liệu trong hệ thống vật lý. Tuy nhiên, vì bạn cập nhật tất cả các thành phần cùng loại trong một lần, bạn có thể thực hiện một số tối ưu hóa không thể có khi mỗi thành phần là một đối tượng riêng biệt với chức năng cập nhật có thể được gọi bất cứ lúc nào.

Lưu ý rằng tôi vẫn chưa bao giờ kêu gọi tạo lớp XxxComponent để giữ dữ liệu thành phần. Tùy bạn. Bạn có thích Thiết kế hướng dữ liệu? Sau đó cấu trúc dữ liệu trong các mảng riêng cho từng biến. Bạn có thích Thiết kế hướng đối tượng? (Tôi không khuyến nghị điều đó, nó vẫn sẽ giết chết hiệu suất của bạn ở nhiều nơi) Sau đó, tạo một đối tượng XxxComponent sẽ giữ dữ liệu của từng thành phần.

Điều tuyệt vời về các nhà quản lý là đóng gói. Bây giờ đóng gói là một trong những triết lý bị lạm dụng khủng khiếp nhất trong thế giới lập trình. Đây là cách nó nên được sử dụng. Chỉ người quản lý biết dữ liệu thành phần nào được lưu trữ ở đâu, cách thức hoạt động của logic thành phần. Có một vài chức năng để lấy / đặt dữ liệu nhưng đó là nó. Bạn có thể viết lại toàn bộ trình quản lý và các lớp bên dưới của nó và nếu bạn không thay đổi giao diện chung, thậm chí không có ai thông báo. Thay đổi động cơ vật lý? Chỉ cần viết lại ChemistryComponentManager và bạn đã hoàn thành.

Sau đó, có một điều cuối cùng: giao tiếp và chia sẻ dữ liệu giữa các thành phần. Bây giờ điều này là khó khăn và không có giải pháp một kích cỡ phù hợp cho tất cả. Bạn có thể tạo các hàm get / set trong các trình quản lý để cho phép, ví dụ thành phần va chạm để lấy vị trí từ thành phần vị trí (ví dụ: PositionManager.getP vị trí (entityID)). Bạn có thể sử dụng một hệ thống sự kiện. Bạn có thể lưu trữ một số dữ liệu được chia sẻ trong thực thể (giải pháp xấu nhất theo ý kiến ​​của tôi). Bạn có thể sử dụng (cái này thường được sử dụng) một hệ thống nhắn tin. Hoặc sử dụng kết hợp nhiều hệ thống! Tôi không có thời gian hoặc kinh nghiệm để đi vào từng hệ thống này, nhưng google và tìm kiếm stackoverflow là bạn của bạn.


Tôi thấy câu trả lời này rất thú vị. Chỉ cần một câu hỏi (hy vọng bạn hoặc ai đó có thể trả lời tôi). Làm thế nào để bạn quản lý để loại bỏ thực thể trên hệ thống dựa trên thành phần DOD? Ngay cả Artemis cũng sử dụng Entity như một lớp học, tôi không chắc điều đó rất tinh ranh.
Wolfrevo Kcats

1
Bạn có ý nghĩa gì khi loại bỏ nó? Bạn có nghĩa là một hệ thống thực thể không có lớp Thực thể? Lý do Artemis có một Thực thể là vì trong Artemis, lớp Thực thể quản lý các thành phần của chính nó. Trong hệ thống tôi đã đề xuất, các lớp ElementManager quản lý các thành phần. Vì vậy, thay vì cần một lớp Thực thể, bạn chỉ có thể có một ID số nguyên duy nhất. Vì vậy, giả sử bạn có thực thể 254, có thành phần vị trí. Khi bạn muốn thay đổi vị trí, bạn có thể gọi PositionCompMgr.setP vị trí (int id, Vector3 newPos), với 254 là tham số id.
Mart

Nhưng làm thế nào để bạn quản lý ID? Điều gì xảy ra nếu bạn muốn xóa một thành phần khỏi một thực thể để gán nó sau này cho một thành phần khác? Điều gì nếu bạn muốn xóa một thực thể và thêm một thực thể mới? Điều gì xảy ra nếu bạn muốn một thành phần được chia sẻ giữa hai hoặc nhiều thực thể? Tôi thực sự quan tâm đến điều này.
Wolfrevo Kcats

1
EntityManager có thể được sử dụng để đưa ra ID mới. Nó cũng có thể được sử dụng để tạo các thực thể hoàn chỉnh dựa trên các mẫu được xác định trước (ví dụ: tạo "EnemyNinja" để tạo ID mới và tạo tất cả các thành phần tạo nên một ninja kẻ thù như có thể kết xuất, cắt dán, AI, có thể là một thành phần nào đó để chiến đấu cận chiến , Vân vân). Nó cũng có thể có chức năng removeEntity tự động gọi tất cả các chức năng loại bỏ ElementManager. Trình quản lý thành phần có thể kiểm tra xem nó có dữ liệu thành phần cho Thực thể đã cho hay không và nếu có, hãy xóa dữ liệu đó.
Mart

1
Di chuyển một thành phần từ thực thể này sang thực thể khác? Chỉ cần thêm một hàm exchangeComponentOwner (int oldEntity, int newEntity) cho mỗi Thành phần. Dữ liệu là tất cả có trong ElementManager, tất cả những gì bạn cần là một chức năng để thay đổi chủ sở hữu của nó. Mỗi Thành phần quản lý sẽ có một cái gì đó giống như một chỉ mục hoặc bản đồ để lưu trữ dữ liệu thuộc về ID thực thể nào. Chỉ cần thay đổi ID thực thể từ ID cũ sang ID mới. Tôi không chắc việc chia sẻ các thành phần có dễ dàng trong hệ thống mà tôi đã nghĩ ra không, nhưng nó khó đến mức nào? Thay vì một liên kết ID thực thể <-> Thành phần dữ liệu trong bảng chỉ mục có nhiều liên kết.
Mart

3

Để các thành phần này hoạt động, chúng phải cập nhật mọi khung hình, cách dễ nhất để làm điều này là đi qua cây cảnh và sau đó cho từng thực thể cập nhật từng thành phần.

Đây là cách tiếp cận ngây thơ điển hình để cập nhật thành phần (và không có gì sai với việc nó ngây thơ, nếu nó hiệu quả với bạn). Một trong những vấn đề lớn mà bạn thực sự gặp phải - bạn đang vận hành thông qua giao diện của thành phần (ví dụ IComponent) để bạn không biết gì về những gì bạn vừa cập nhật. Bạn có thể cũng không biết gì về thứ tự các thành phần trong thực thể, vì vậy

  1. bạn có thể thường xuyên cập nhật các thành phần của các loại khác nhau (về cơ bản là tham chiếu mã kém)
  2. hệ thống này không cho vay chính xác các bản cập nhật đồng thời vì bạn không có khả năng xác định các phụ thuộc dữ liệu và do đó tách các bản cập nhật thành các nhóm đối tượng không liên quan cục bộ.

Tôi đã nghĩ về việc sử dụng một đối tượng trình quản lý singleton cho từng loại thành phần nhưng điều đó có những hạn chế thông thường khi sử dụng singleton, một cách để giảm bớt điều này là một chút bằng cách sử dụng phép nội xạ phụ thuộc nhưng nghe có vẻ quá mức cho vấn đề này.

Một singleton không thực sự cần thiết ở đây, và vì vậy bạn nên tránh nó bởi vì nó mang những nhược điểm bạn đã đề cập. Việc tiêm phụ thuộc không quá mức cần thiết - cốt lõi của khái niệm là bạn truyền những thứ mà một đối tượng cần cho đối tượng đó, lý tưởng nhất là trong hàm tạo. Bạn không cần khung DI nặng (như Ninject ) cho việc này - chỉ cần chuyển một tham số phụ cho nhà xây dựng ở đâu đó.

Trình kết xuất là một hệ thống cơ bản và nó có thể hỗ trợ tạo và quản lý vòng đời của một loạt các đối tượng có thể kết xuất tương ứng với những thứ trực quan trong trò chơi của bạn (có khả năng là mô hình hoặc mô hình). Tương tự, một động cơ vật lý có khả năng kiểm soát trọn đời đối với những thứ đại diện cho các thực thể có thể di chuyển xung quanh trong mô phỏng vật lý (cơ thể cứng nhắc). Mỗi hệ thống có liên quan nên sở hữu, trong một số khả năng, các đối tượng đó và chịu trách nhiệm cập nhật chúng.

Các thành phần bạn sử dụng trong hệ thống thành phần thực thể trò chơi của bạn chỉ nên là các trình bao bọc xung quanh các phiên bản từ các hệ thống cấp thấp hơn đó - thành phần vị trí của bạn chỉ có thể bao bọc một cơ thể cứng nhắc, thành phần hình ảnh của bạn chỉ bao bọc một mô hình hoặc mô hình có thể kết xuất, et cetera.

Sau đó, chính hệ thống sở hữu các đối tượng cấp thấp hơn chịu trách nhiệm cập nhật chúng và có thể thực hiện hàng loạt và theo cách cho phép nó đa luồng cập nhật nếu thích hợp. Vòng lặp trò chơi chính của bạn kiểm soát thứ tự thô mà các hệ thống đó cập nhật (vật lý trước, sau đó là trình kết xuất hoặc bất cứ thứ gì). Nếu bạn có một hệ thống con không có quyền kiểm soát trọn đời hoặc cập nhật đối với các phiên bản mà nó cung cấp, bạn có thể xây dựng một trình bao bọc đơn giản để xử lý cập nhật tất cả các thành phần có liên quan đến hệ thống đó và quyết định nơi đặt nó cập nhật liên quan đến phần còn lại của các bản cập nhật hệ thống của bạn (điều này thường xảy ra, tôi thấy, với các thành phần "script").

Cách tiếp cận này đôi khi được gọi là phương pháp tiếp cận thành phần bên ngoài , nếu bạn đang tìm kiếm chi tiết hơn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.