Jakie są projekty systemu jednostek opartego na komponentach, który jest przyjazny dla użytkownika, ale nadal elastyczny?


23

Przez jakiś czas interesowałem się systemem encji opartym na komponentach i czytałem o nim niezliczone artykuły ( gry Insomiac , dość standardowy Evolve Your Hierarchy , T-Machine , Chronoclast ... żeby wymienić tylko kilka).

Wszystkie wydają się mieć strukturę na zewnątrz czegoś takiego jak:

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

A jeśli wprowadzisz pomysł udostępniania danych (jest to najlepszy projekt, jaki do tej pory widziałem, pod względem nie powielania danych wszędzie)

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

Tak, to jest bardzo wydajne. Jednak nie jest to najłatwiejszy do odczytu lub zapisu; czuje się bardzo niezręcznie i działa przeciwko tobie.

Osobiście chciałbym zrobić coś takiego:

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

Chociaż oczywiście jedynym sposobem, aby dostać się do tego rodzaju projektu, jest powrót w rodzaju hierarchii Entity-> NPC-> Enemy-> FlyingEnemy-> FlyingEnemyWithAHatOn.

Czy ktoś widział projekt tego rodzaju systemu komponentów, który jest wciąż elastyczny, ale zachowuje poziom przyjazności dla użytkownika? A jeśli chodzi o tę sprawę, udaje się obejść (prawdopodobnie najtrudniejszy problem) przechowywanie danych w dobry sposób?

Jakie są projekty systemu jednostek opartego na komponentach, który jest przyjazny dla użytkownika, ale nadal elastyczny?

Odpowiedzi:


13

Jedną z rzeczy, które robi Unity, jest zapewnienie dostępu do pomocników w obiekcie nadrzędnym w celu zapewnienia bardziej przyjaznego dla użytkownika dostępu do wspólnych komponentów.

Na przykład możesz mieć swoją pozycję zapisaną w komponencie Transform. Korzystając z tego przykładu, musiałbyś napisać coś takiego

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

Ale w Unity upraszcza się to

e.transform.position = ....;

Gdzie transformdosłownie jest tylko prosta metoda pomocnicza w GameObjectklasie bazowej ( Entityklasa w twoim przypadku), która działa

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

Unity robi także kilka innych rzeczy, takich jak ustawienie właściwości „Nazwa” na samym obiekcie gry zamiast w jego elementach potomnych.

Osobiście nie podoba mi się pomysł twojego projektu współdzielenia danych według nazwy. Uzyskiwanie dostępu do właściwości według nazwy zamiast przez zmienną i posiadanie przez użytkownika również informacji o typie, wydaje mi się bardzo podatne na błędy. Jedność polega na tym, że używają podobnych metod jakGameObject transform właściwości w Componentklasach, aby uzyskać dostęp do komponentów rodzeństwa. Tak więc dowolny dowolny komponent, który napiszesz, może po prostu to zrobić:

var myPos = this.transform.position;

Aby uzyskać dostęp do pozycji. Gdzie ta transformwłaściwość robi coś takiego

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

Jasne, to trochę bardziej gadatliwe niż zwykłe powiedzenie e.position = whatever, ale przyzwyczajasz się do tego i nie wygląda tak paskudnie, jak ogólne właściwości. I tak, musielibyście zrobić to w sposób okrężny dla komponentów klienta, ale pomysł polega na tym, że wszystkie wspólne elementy „silnika” (renderery, kolidery, źródła audio, transformacje itp.) Mają łatwe akcesoria.


Rozumiem, dlaczego system współdzielonych danych według nazw może być problemem, ale w jaki sposób mógłbym uniknąć problemu wielu komponentów potrzebujących danych (tj. W którym coś przechowuję)? Jesteś bardzo ostrożny na etapie projektowania?
Kaczka komunistyczna

Ponadto, jeśli chodzi o to, że „użytkownik musi również wiedzieć, jaki to typ”, czy nie mógłbym po prostu przesłać go do właściwości property.GetType () w metodzie get ()?
Kaczka komunistyczna

1
Jakiego rodzaju dane globalne (w zakresie encji) wypychasz do tych współużytkowanych repozytoriów danych? Wszelkie dane obiektów gry powinny być łatwo dostępne za pośrednictwem klas „silnika” (transformacja, zderzacze, renderery itp.). Jeśli wprowadzasz zmiany w logice gry na podstawie tych „współdzielonych danych”, to o wiele bezpieczniej jest mieć jedną klasę i faktycznie upewnić się, że komponent znajduje się na tym obiekcie gry, niż tylko ślepo pytać, czy istnieje jakaś nazwana zmienna. Tak, powinieneś sobie z tym poradzić na etapie projektowania. Zawsze możesz stworzyć komponent, który po prostu przechowuje dane, jeśli naprawdę tego potrzebujesz.
Tetrad

@Tetrad - Czy możesz krótko omówić działanie proponowanej metody GetComponent <Transform>? Czy przejdzie przez wszystkie Składniki GameObject i zwróci pierwszy Składnik typu T? A może dzieje się tam coś jeszcze?
Michael

@ Michael, które by działało. Możesz po prostu sprawić, że to osoba dzwoniąca będzie buforować to lokalnie w celu poprawy wydajności.
Tetrad

3

Sugerowałbym jakąś klasę interfejsu dla twoich obiektów Entity byłby miły. Może to zrobić obsługę błędów z sprawdzaniem, aby upewnić się, że encja zawiera również składnik o odpowiedniej wartości w jednym miejscu, abyś nie musiał tego robić wszędzie, gdy uzyskujesz dostęp do wartości. Alternatywnie, większość projektów, które kiedykolwiek zrobiłem z systemem opartym na komponentach, zajmuję się komponentami bezpośrednio, prosząc na przykład o ich komponent pozycyjny, a następnie bezpośrednio uzyskując / aktualizując właściwości tego komponentu.

Bardzo podstawowa na wysokim poziomie, klasa bierze pod uwagę byt i zapewnia łatwy w użyciu interfejs do podstawowych części składowych w następujący sposób:

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;
        }
    }
}

Jeśli założę, że będę musiał obsługiwać wyjątek NoPositionComponentException lub coś takiego, jaka jest przewaga tego nad umieszczeniem tego wszystkiego w klasie Entity?
Kaczka komunistyczna

Ponieważ nie jestem pewien, co zawiera twoja klasa jednostek, nie mogę powiedzieć, że jest to zaleta, czy nie. Ja osobiście opowiadam się za systemami składowymi, w których nie ma jednostki bazowej, aby uniknąć pytania „co w niej należy”. Jednak uważam taką klasę interfejsu za dobry argument za jej istnieniem.
James

Widzę, jak to jest łatwiejsze (nie muszę pytać o pozycję za pomocą GetProperty), ale nie widzę zalet tego sposobu zamiast umieszczania go w samej klasie Entity.
Kaczka komunistyczna

2

W Pythonie możesz przechwycić część „setPosition” e.SetPosition(4,5,6)poprzez zadeklarowanie __getattr__funkcji na Entity. Ta funkcja może iterować po komponentach, znajdować odpowiednią metodę lub właściwość i zwracać ją, aby wywołanie funkcji lub przypisanie trafiło we właściwe miejsce. Być może C # ma podobny system, ale może nie być to możliwe, ponieważ jest statycznie wpisany - prawdopodobnie nie może się skompilować, e.setPositionchyba że e gdzieś ustawił pozycjęPozycja.

Być może możesz również zmusić wszystkie swoje jednostki do implementacji odpowiednich interfejsów dla wszystkich komponentów, które kiedykolwiek możesz do nich dodać, za pomocą metod pośredniczących, które zgłaszają wyjątek. Następnie wywołując addComponent, po prostu przekierowujesz funkcje encji dla interfejsu tego komponentu do komponentu. Trochę niezręcznie.

Ale może najłatwiej byłoby przeciążać operatora [] na swojej klasy Entity szukać załączonego komponenty na obecność ważnej własności i wrócić, że tak może być przypisany do tak: e["Name"] = "blah". Każdy komponent będzie musiał zaimplementować własną GetPropertyByName, a Entity wywoła każdy z nich po kolei, dopóki nie znajdzie komponentu odpowiedzialnego za daną właściwość.


Myślałem o użyciu indeksatorów .. to jest naprawdę miłe. Zaczekam chwilę, aby zobaczyć, co jeszcze się pojawi, ale skręcam w kierunku mieszanki tego, zwykłej GetComponent () i niektórych właściwości, takich jak @James sugerowanych dla typowych komponentów.
Kaczka komunistyczna

Być może brakuje mi tego, co tu powiedziane, ale wydaje się, że jest to raczej zamiennik e.GetProperty <typ> („NameOfProperty”). Cokolwiek z e [„NameOfProperty”]. Cokolwiek?
James

@James Jest to opakowanie (podobnie jak twój projekt) .. z wyjątkiem tego, że jest bardziej dynamiczne - robi to automatycznie i jest czystsze niż GetPropertymetoda .. Jedynym minusem jest manipulowanie ciągiem i porównywanie. Ale to kwestia sporna.
Kaczka komunistyczna

Ach, moje nieporozumienie. Zakładałem, że GetProperty była częścią bytu (i tak zrobiłbym to, co opisano powyżej), a nie metodą C #.
James

2

Aby rozwinąć odpowiedź Kylotana, jeśli używasz C # 4.0, możesz statycznie wpisać zmienną, aby być dynamiczną .

Jeśli odziedziczysz po System.Dynamic.DynamicObject, możesz zastąpić TryGetMember i TrySetMember (spośród wielu metod wirtualnych), aby przechwycić nazwy komponentów i zwrócić żądany komponent.

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

Przez lata pisałem trochę o systemach bytów, ale zakładam, że nie mogę konkurować z gigantami. Niektóre notatki ES (nieco przestarzałe i niekoniecznie odzwierciedlają moje obecne rozumienie systemów bytów, ale warto je przeczytać, imho).


Dzięki za to, pomoże :) Czy dynamiczny sposób nie miałby takiego samego efektu jak rzutowanie na property.GetType ()?
Kaczka komunistyczna

W pewnym momencie nastąpi rzutowanie, ponieważ TryGetMember ma objectparametr wyjściowy - ale wszystko to zostanie rozwiązane w czasie wykonywania. Myślę, że to świetny sposób na utrzymanie płynnego interfejsu np. Entity.TryGetComponent (nazwa_komputera, składnik wyjściowy). Nie jestem jednak pewien, czy zrozumiałem pytanie :)
Raine

Pytanie brzmi: mógłbym używać wszędzie typów dynamicznych lub zwracającej (AFAICS) właściwości (property.GetType ()).
Kaczka komunistyczna

1

Widzę duże zainteresowanie tutaj w ES na C #. Sprawdź mój port doskonałej implementacji ES Artemis:

https://github.com/thelinuxlich/artemis_CSharp

Ponadto, przykładowa gra na początek, jak to działa (za pomocą XNA 4): https://github.com/thelinuxlich/starwarrior_CSharp

Sugestie są mile widziane!


Z pewnością ładnie wygląda. Osobiście nie jestem fanem idei tak wielu EntityProcessingSystems - w kodzie, jak to działa pod względem użytkowania?
Kaczka komunistyczna

Każdy system jest aspektem przetwarzającym jego zależne komponenty. Zobacz to, aby uzyskać pomysł: github.com/thelinuxlich/starwarrior_CSharp/tree/master/…
thelinuxlich

0

Zdecydowałem się użyć doskonałego systemu odbicia C #. Oparłem go na ogólnym systemie komponentów oraz mieszance odpowiedzi tutaj.

Komponenty są dodawane bardzo łatwo:

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

I można zapytać o nazwę, typ lub (jeśli takie jak Zdrowie i Pozycja) o właściwości:

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

I usunięty:

e.RemoveComponent<HealthComponent>();

Ponownie dostęp do danych mają zwykle właściwości:

e.MaxHP

Który jest przechowywany po stronie komponentu; mniejszy narzut, jeśli nie ma 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;
}

Dużej ilości pisania pomaga Intellisense w VS, ale w większości przypadków mało jest pisania - tego szukałem.

Za kulisami przechowuję Dictionary<Type, Component> i Componentszastępuję ToStringmetodę sprawdzania nazw. Mój oryginalny projekt używał przede wszystkim ciągów nazw i rzeczy, ale stał się świnią do pracy (och, muszę rzucić wszędzie, a także brak łatwo dostępnych właściwości).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.