Những thiết kế nào dành cho một hệ thống thực thể dựa trên thành phần thân thiện với người dùng nhưng vẫn linh hoạt?


23

Tôi đã quan tâm đến hệ thống thực thể dựa trên thành phần trong một thời gian và đọc vô số bài viết về nó (Các trò chơi Insomiac , Evolve Your HVELy tiêu chuẩn , T-Machine , Chronoclast ... chỉ để nêu tên một số).

Tất cả chúng dường như có một cấu trúc bên ngoài của một cái gì đó như:

Entity e = Entity.Create();
e.AddComponent(RenderComponent, ...);
//do lots of stuff
e.GetComponent<PositionComponent>(...).SetPos(4, 5, 6);

Và nếu bạn nảy ra ý tưởng về dữ liệu được chia sẻ (đây là thiết kế tốt nhất tôi từng thấy cho đến nay, về mặt không trùng lặp dữ liệu ở mọi nơi)

e.GetProperty<string>("Name").Value = "blah";

Vâng, điều này rất hiệu quả. Tuy nhiên, nó không chính xác là dễ đọc hoặc viết nhất; nó cảm thấy rất cồng kềnh và chống lại bạn.

Cá nhân tôi muốn làm một cái gì đó như:

e.SetPosition(4, 5, 6);
e.Name = "Blah";

Mặc dù tất nhiên, cách duy nhất để có được kiểu thiết kế đó là quay lại Entity-> NPC-> Enemy-> FlyingEnemy-> FlyingEnemyWithAHatOn loại phân cấp mà thiết kế này cố gắng tránh.

Có ai nhìn thấy một thiết kế cho loại hệ thống thành phần này vẫn linh hoạt, nhưng vẫn duy trì mức độ thân thiện với người dùng? Và đối với vấn đề đó, quản lý để có được xung quanh lưu trữ dữ liệu (có lẽ là vấn đề khó khăn nhất) một cách tốt?

Những thiết kế nào dành cho một hệ thống thực thể dựa trên thành phần thân thiện với người dùng nhưng vẫn linh hoạt?

Câu trả lời:


13

Một trong những điều mà Unity làm là cung cấp một số người truy cập trợ giúp trên đối tượng trò chơi mẹ để cung cấp quyền truy cập thân thiện hơn với người dùng vào các thành phần phổ biến.

Ví dụ: bạn có thể lưu vị trí của mình trong thành phần Biến đổi. Sử dụng ví dụ của bạn, bạn sẽ phải viết một cái gì đó như

e.GetComponent<Transform>().position = new Vector3( whatever );

Nhưng trong Unity, điều đó được đơn giản hóa thành

e.transform.position = ....;

Ở đâu transformlà theo nghĩa đen chỉ là một phương pháp đơn giản helper trong cơ sở GameObjectlớp ( Entitylớp trong trường hợp của bạn) mà không

Transform transform
{ 
    get { return this.GetComponent<Transform>(); }
}

Unity cũng thực hiện một số việc khác, như đặt thuộc tính "Tên" trên chính đối tượng trò chơi thay vì trong các thành phần con của nó.

Cá nhân tôi không thích ý tưởng thiết kế chia sẻ dữ liệu theo tên của bạn. Truy cập các thuộc tính theo tên thay vì theo một biến và có người dùng cũng phải biết loại nào có vẻ như thực sự dễ bị lỗi đối với tôi. Điều mà Unity làm là họ sử dụng các phương thức tương tự như thuộc GameObject transformtính trong các Componentlớp để truy cập các thành phần anh chị em. Vì vậy, bất kỳ thành phần tùy ý bạn viết có thể chỉ cần làm điều này:

var myPos = this.transform.position;

Để truy cập vị trí. Nơi mà transformtài sản làm điều gì đó như thế này

Transform transform
{
    get { return this.gameObject.GetComponent<Transform>(); }
}

Chắc chắn, nó dài dòng hơn một chút so với chỉ nói e.position = whatever, nhưng bạn đã quen với nó, và nó không có vẻ khó chịu như các thuộc tính chung. Và vâng, bạn sẽ phải thực hiện theo cách vòng cho các thành phần máy khách của mình, nhưng ý tưởng là tất cả các thành phần "động cơ" chung của bạn (trình kết xuất, trình thu thập, nguồn âm thanh, biến đổi, v.v.) đều có bộ truy cập dễ dàng.


Tôi có thể thấy lý do tại sao hệ thống chia sẻ dữ liệu theo tên có thể là một vấn đề, nhưng làm thế nào khác tôi có thể tránh được vấn đề nhiều thành phần cần dữ liệu (tức là tôi sẽ lưu trữ cái gì)? Chỉ cần rất cẩn thận ở giai đoạn thiết kế?
Vịt Cộng sản

Ngoài ra, về mặt 'có người dùng cũng phải biết nó thuộc loại nào', tôi không thể chuyển nó sang property.GetType () trong phương thức get ()?
Vịt Cộng sản

1
Loại dữ liệu toàn cầu (trong phạm vi thực thể) nào bạn đang đẩy đến các kho dữ liệu được chia sẻ này? Bất kỳ loại dữ liệu đối tượng trò chơi nào cũng có thể dễ dàng truy cập thông qua các lớp "công cụ" của bạn (biến đổi, trình thu thập, trình kết xuất, v.v.). Nếu bạn đang thực hiện thay đổi logic trò chơi dựa trên "dữ liệu được chia sẻ" này, thì sẽ an toàn hơn rất nhiều khi có một lớp sở hữu nó và thực sự đảm bảo rằng thành phần đó nằm trên đối tượng trò chơi đó hơn là chỉ hỏi một cách mù quáng xem có tồn tại một biến có tên nào đó không. Vì vậy, có, bạn nên xử lý trong giai đoạn thiết kế. Bạn luôn có thể tạo một thành phần chỉ lưu trữ dữ liệu nếu bạn thực sự cần nó.
Tết

@Tetrad - Bạn có thể giải thích ngắn gọn về cách phương thức GetComponent <Transform> được đề xuất của bạn sẽ hoạt động không? Nó sẽ lặp qua tất cả các Thành phần của GameObject và trả về Thành phần đầu tiên của loại T? Hay là một cái gì đó khác đang xảy ra ở đó?
Michael

@Michael sẽ làm việc. Bạn chỉ có thể làm cho người gọi có trách nhiệm lưu trữ bộ đệm đó cục bộ như một cải tiến hiệu suất.
Tetrad

3

Tôi sẽ đề xuất một số loại lớp Giao diện cho các đối tượng Thực thể của bạn sẽ tốt. Nó có thể xử lý lỗi bằng cách kiểm tra để đảm bảo một thực thể có chứa một thành phần có giá trị phù hợp ở một vị trí để bạn không phải thực hiện ở mọi nơi khi bạn truy cập các giá trị. Cách khác, hầu hết các thiết kế tôi đã từng thực hiện với một hệ thống dựa trên thành phần, tôi trực tiếp xử lý các thành phần, yêu cầu, ví dụ, thành phần vị trí của chúng và sau đó truy cập / cập nhật các thuộc tính của thành phần đó trực tiếp.

Rất cơ bản ở mức cao, lớp nhận thực thể được đề cập và cung cấp giao diện dễ sử dụng cho các phần thành phần cơ bản như sau:

public class EntityInterface
{
    private Entity entity { get; set };
    public EntityInterface(Entity e)
    {
        entity = e;
    }

    public Vector3 Position
    {
        get
        {
            return entity.GetProperty<Vector3>("Position");
        }
        set
        {
            entity.GetProperty<Vector3>("Position") = value;
        }
    }
}

Nếu tôi cho rằng tôi chỉ phải xử lý NoPocationComponentException hoặc một cái gì đó, thì lợi thế của việc này là gì khi chỉ đưa tất cả những thứ đó vào lớp Thực thể?
Vịt Cộng sản

Không chắc chắn những gì lớp thực thể của bạn thực sự chứa Tôi không thể nói có lợi thế hay không. Cá nhân tôi ủng hộ các hệ thống thành phần trong đó không có thực thể cơ bản nào nói đơn giản để tránh câu hỏi 'À cái gì thuộc về nó'. Tuy nhiên tôi sẽ coi một lớp giao diện như thế này là một đối số tốt cho nó hiện có.
James

Tôi có thể thấy cách này dễ dàng hơn (tôi không phải truy vấn vị trí thông qua GetProperty), nhưng tôi không thấy lợi thế của cách này thay vì đặt nó vào chính lớp Thực thể.
Vịt Cộng sản

2

Trong Python, bạn có thể chặn phần 'setPocation' e.SetPosition(4,5,6)thông qua khai báo một __getattr__hàm trên Entity. Hàm này có thể lặp qua các thành phần và tìm ra phương thức hoặc thuộc tính thích hợp và trả về giá trị đó, để lệnh gọi hoặc gán hàm đi đúng nơi. Có lẽ C # có một hệ thống tương tự, nhưng nó có thể không thực hiện được do nó được gõ tĩnh - có lẽ nó không thể biên dịch e.setPositiontrừ khi e có setPocation trong giao diện ở đâu đó.

Bạn cũng có thể làm cho tất cả các thực thể của mình triển khai các giao diện có liên quan cho tất cả các thành phần bạn có thể thêm vào chúng, với các phương thức còn sơ khai đưa ra một ngoại lệ. Sau đó, khi bạn gọi addComponent, bạn chỉ cần chuyển hướng các chức năng của thực thể cho giao diện của thành phần đó đến thành phần. Một chút khó khăn mặc dù.

Nhưng có lẽ dễ nhất là quá tải toán tử [] trên lớp Thực thể của bạn để tìm kiếm các thành phần đính kèm cho sự hiện diện của một thuộc tính hợp lệ và trả lại, vì vậy nó có thể được gán cho như vậy : e["Name"] = "blah". Mỗi thành phần sẽ cần triển khai GetPropertyByName của riêng mình và Thực thể gọi lần lượt từng thành phần cho đến khi tìm thấy thành phần chịu trách nhiệm về thuộc tính được đề cập.


Tôi đã nghĩ đến việc sử dụng bộ chỉ mục .. điều này thực sự tốt đẹp. Tôi sẽ chờ một chút để xem những gì khác xuất hiện, nhưng tôi đang hướng tới sự pha trộn của điều này, GetComponent () thông thường và một số thuộc tính như @James được đề xuất cho các thành phần phổ biến.
Vịt Cộng sản

Tôi có thể thiếu những gì đang được nói ở đây nhưng điều này dường như thay thế cho e.GetProperty <type> ("NameOfProperty"). Dù thế nào với e ["NameOfProperty"]. Sao cũng được ??
James

@James Đó là một trình bao bọc cho nó (Giống như thiết kế của bạn) .. ngoại trừ nó năng động hơn - nó tự động và sạch hơn phương GetPropertythức..Chỉ là nhược điểm là thao tác và so sánh chuỗi. Nhưng đó là một điểm moot.
Vịt Cộng sản

Ah, hiểu lầm của tôi ở đó. Tôi giả sử GetProperty là một phần của thực thể (Và do đó sẽ làm những gì được mô tả ở trên) chứ không phải phương thức C #.
James

2

Để mở rộng câu trả lời của Kylotan, nếu bạn đang sử dụng C # 4.0, bạn có thể nhập tĩnh một biến thành động .

Nếu bạn kế thừa từ System.Docate.DocateObject, bạn có thể ghi đè TryGetMember và TrySetMember (trong số nhiều phương thức ảo) để chặn tên thành phần và trả về thành phần được yêu cầu.

dynamic entity = EntityRegistry.Entities[1000];
entity.MyComponent.MyValue = 100;

Tôi đã viết một chút về các hệ thống thực thể trong những năm qua, nhưng tôi cho rằng tôi không thể cạnh tranh với những người khổng lồ. Một số ghi chú ES (hơi lỗi thời và không nhất thiết phản ánh sự hiểu biết hiện tại của tôi về các hệ thống thực thể, nhưng nó đáng để đọc, imho).


Cảm ơn vì điều đó, sẽ giúp :) Tuy nhiên, cách thức năng động có cùng loại hiệu ứng như chỉ truyền vào property.GetType () không?
Vịt Cộng sản

Sẽ có một diễn viên tại một số điểm, vì TryGetMember có một objecttham số out - nhưng tất cả điều này sẽ được giải quyết trong thời gian chạy. Tôi nghĩ rằng đó là một cách tuyệt vời để giữ một giao diện trôi chảy trên ví dụ như entity.TryGetComponent (compName, out thành phần). Tôi không chắc là tôi đã hiểu câu hỏi, tuy nhiên :)
Raine

Câu hỏi là tôi có thể sử dụng các loại động ở mọi nơi hoặc (AFAICS) trả về (property.GetType ()).
Vịt Cộng sản

1

Tôi thấy rất nhiều quan tâm ở đây về ES trên C #. Kiểm tra xem cổng của tôi về triển khai ES xuất sắc:

https://github.com/thelinuxlich/artemis_CSharp

Ngoài ra, một trò chơi ví dụ để giúp bạn bắt đầu về cách thức hoạt động (sử dụng XNA 4): https://github.com/thelinuxlich/starwar Warrior_CSharp

Đề nghị được chào đón!


Chắc chắn trông rất đẹp. Cá nhân tôi không phải là người hâm mộ ý tưởng của rất nhiều EntityProcessingSystems - trong mã, làm thế nào để giải quyết vấn đề sử dụng?
Vịt Cộng sản

Mỗi hệ thống là một khía cạnh xử lý các thành phần phụ thuộc của nó. Xem điều này để có được ý tưởng: github.com/thelinuxlich/starwar
Warrior_CSharp/tree/master/ mẹo

0

Tôi quyết định sử dụng hệ thống phản chiếu tuyệt vời của C #. Tôi dựa trên hệ thống thành phần chung, cộng với một loạt các câu trả lời ở đây.

Các thành phần được thêm vào rất dễ dàng:

Entity e = new Entity(); //No templates/archetypes yet.
e.AddComponent(new HealthComponent(100));

Và có thể được truy vấn theo tên, loại hoặc (nếu phổ biến như Sức khỏe và Vị trí) theo thuộc tính:

e["Health"] //This, unfortunately, is a bit slower since it requires a dict search
e.GetComponent<HealthComponent>()
e.Health

Và xóa:

e.RemoveComponent<HealthComponent>();

Dữ liệu, một lần nữa, được truy cập phổ biến bởi các thuộc tính:

e.MaxHP

Được lưu trữ phía thành phần; ít chi phí hơn nếu không có HP:

public int? MaxHP
{
get
{

    return this.Health.MaxHP; //Error handling returns null if health isn't here. Excluded for clarity
}
set
{
this.Health.MaxHP = value;
}

Số lượng gõ lớn được hỗ trợ bởi Intellisense trong VS, nhưng đối với hầu hết các phần có rất ít gõ - điều tôi đang tìm kiếm.

Đằng sau hậu trường, tôi đang lưu trữ một Dictionary<Type, Component>và có Componentsghi đè ToStringphương thức để kiểm tra tên. Thiết kế ban đầu của tôi sử dụng chủ yếu các chuỗi cho tên và mọi thứ, nhưng nó đã trở thành một con lợn để làm việc (ồ, tôi phải đi khắp nơi cũng như thiếu các thuộc tính dễ truy cập).

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.