Organizujesz system jednostek za pomocą zewnętrznych menedżerów komponentów?


13

Projektuję silnik gry do odgórnej wieloosobowej strzelanki 2D, którą chcę w rozsądny sposób wykorzystać do innych strzelanek z góry. W tej chwili myślę o tym, jak należy zaprojektować coś w tym systemie bytu. Najpierw pomyślałem o tym:

Mam klasę o nazwie EntityManager. Powinien zaimplementować metodę o nazwie Update i inną o nazwie Draw. Powodem oddzielenia logiki od renderowania jest to, że wtedy mogę pominąć metodę Draw, jeśli działam jako samodzielny serwer.

EntityManager posiada listę obiektów typu BaseEntity. Każda encja posiada listę komponentów, takich jak EntityModel (reprezentowalna postać encji), EntityNetworkInterface i EntityPhysicalBody.

EntityManager jest także właścicielem listy menedżerów komponentów, takich jak EntityRenderManager, EntityNetworkManager i EntityPhysicsManager. Każdy menedżer komponentów przechowuje odniesienia do komponentów encji. Istnieją różne powody, aby przenieść ten kod z własnej klasy jednostki i zamiast tego zrobić to zbiorowo. Na przykład do gry używam zewnętrznej biblioteki fizyki Box2D. W Box2D najpierw dodajesz ciała i kształty do świata (w tym przypadku należącego do EntityPhysicsManager) i dodajesz wywołania zwrotne kolizji (które byłyby wysyłane do samego obiektu encji w moim systemie). Następnie uruchamiasz funkcję, która symuluje wszystko w systemie. Trudno mi znaleźć lepsze rozwiązanie, niż robienie tego w takim zewnętrznym menedżerze komponentów.

Tworzenie encji odbywa się w następujący sposób: EntityManager implementuje metodę RegisterEntity (entityClass, fabryka), która rejestruje sposób tworzenia encji tej klasy. Implementuje również metodę CreateEntity (entityClass), która zwróci obiekt typu BaseEntity.

Teraz pojawia się mój problem: w jaki sposób odniesienie do komponentu byłoby zarejestrowane dla menedżerów komponentów? Nie mam pojęcia, jak odsyłam do menedżerów komponentów z fabryki / zamknięcia.


Nie wiem, czy być może ma to być system hybrydowy, ale wygląda na to, że twoi „menedżerowie” nazywają ogólnie „systemami”; tj. byt jest abstrakcyjnym identyfikatorem; składnik jest pulą danych; a „menedżer” to tak zwany „system”. Czy poprawnie tłumaczę słownictwo?
BRPocock,

Moje pytanie tutaj może być interesujące: elementy gry, menedżery gier i właściwości obiektów
George Duckett,

Spójrz na gamadu.com/artemis i sprawdź, czy ich metody odpowiadają na twoje pytanie.
Patrick Hughes,

1
Nie ma jednego sposobu zaprojektowania systemu encji, ponieważ istnieje niewielka zgoda co do jego definicji. To, co opisuje @BRPocock, a także to, czego używa Artemis, zostało szerzej opisane na tym blogu: t-machine.org/index.php/category/entity-systems wraz z wiki: entity-systems.wikidot.com
user8363

Odpowiedzi:


6

Systemy powinny przechowywać parę kluczowych wartości Entity to Component w jakimś rodzaju mapy, obiektu słownikowego lub tablicy asocjacyjnej (w zależności od używanego języka). Ponadto, kiedy tworzysz swój Obiekt Entity, nie martwię się o przechowywanie go w menedżerze, chyba że będziesz w stanie wyrejestrować go z dowolnego Systemu. Jednostka jest złożona ze składników, ale nie powinna obsługiwać żadnej z aktualizacji składników. Powinno to być obsługiwane przez systemy. Zamiast tego traktuj swoją jednostkę jako klucz, który jest mapowany na wszystkie komponenty zawarte w systemach, a także jako centrum komunikacyjne dla tych komponentów, aby ze sobą rozmawiać.

Wielką częścią modeli Entity-Component-System jest to, że można dość łatwo wdrożyć możliwość przekazywania komunikatów z jednego komponentu do reszty komponentów encji. Pozwala to komponentowi na rozmowę z innym komponentem bez faktycznej wiedzy o tym, kim jest ten komponent i jak obsługiwać komponent, który zmienia. Zamiast tego przekazuje komunikat i pozwala komponentowi się zmienić (jeśli istnieje)

Na przykład, System Pozycji nie miałby w nim dużo kodu, jedynie śledziłby Obiekty Istoty odwzorowane na ich Komponenty Pozycji. Ale gdy pozycja się zmienia, mogą wysłać wiadomość do zaangażowanej Jednostki, która z kolei jest przekazywana wszystkim komponentom tej jednostki. Z jakiejkolwiek przyczyny zmienia się pozycja? System pozycji wysyła Entity wiadomość z informacją, że pozycja uległa zmianie, a gdzieś element renderujący obraz tego podmiotu otrzymuje ten komunikat i aktualizuje, gdzie się następnie narysuje.

I odwrotnie, system fizyki musi wiedzieć, co robią wszystkie jego obiekty; Musi być w stanie zobaczyć wszystkie obiekty świata w celu przetestowania kolizji. Kiedy dochodzi do kolizji, aktualizuje komponent kierunku encji, wysyłając do encji coś w rodzaju „komunikatu zmiany kierunku” zamiast odwoływać się bezpośrednio do komponentu encji. To uniezależnia menedżera od konieczności zmiany kierunków za pomocą komunikatu zamiast polegania na określonym elemencie (którego może wcale nie być, w takim przypadku wiadomość po prostu głucha zamiast jakiegoś błędu) występujące, ponieważ oczekiwany obiekt był nieobecny).

Zauważysz ogromną przewagę, ponieważ wspomniałeś, że masz interfejs sieciowy. Komponent sieciowy nasłuchi wszystkich nadchodzących wiadomości, o których wszyscy inni powinni wiedzieć. Uwielbia plotki. Następnie, gdy system sieciowy się aktualizuje, komponenty sieciowe wysyłają te wiadomości do innych systemów sieciowych na innych komputerach klienckich, które następnie wysyłają te wiadomości do wszystkich innych komponentów w celu aktualizacji pozycji odtwarzacza itp. Może być potrzebna specjalna logika, aby tylko niektóre podmioty mogły wysyłać wiadomości przez sieć, ale to jest piękno Systemu, możesz po prostu kontrolować tę logikę, rejestrując odpowiednie rzeczy.

W skrócie:

Podmiot to kompozycja składników, które mogą odbierać wiadomości. Jednostka może odbierać wiadomości, delegując je do wszystkich swoich składników, aby je zaktualizować. (Wiadomość ze zmienioną pozycją, Kierunek zmiany prędkości itp.) To jak centralna skrzynka pocztowa, w której wszystkie elementy mogą słyszeć od siebie zamiast rozmawiać bezpośrednio ze sobą.

Komponent to niewielka część encji, która przechowuje pewien stan encji. Są one w stanie analizować niektóre wiadomości i wyrzucać inne. Na przykład „komponent kierunku” będzie dbał tylko o „komunikaty zmiany kierunku”, ale nie „komunikaty zmiany pozycji”. Składniki aktualizują swój stan na podstawie komunikatów, a następnie aktualizują stany innych składników, wysyłając wiadomości ze swojego systemu.

System zarządza wszystkimi komponentami określonego typu i jest odpowiedzialny za aktualizację wymienionych komponentów w każdej ramce, a także za wysyłanie wiadomości z zarządzanych przez nich komponentów do encji, do których należą komponenty

Systemy mogą być w stanie równolegle aktualizować wszystkie swoje komponenty i przechowywać wszystkie wiadomości w miarę ich przemieszczania. Następnie, po zakończeniu wykonywania wszystkich metod aktualizacji systemów, należy poprosić każdy system o wysłanie wiadomości w określonej kolejności. Najpierw kontrole, następnie fizyka, następnie kierunek, pozycja, rendering itp. To ma znaczenie, w jakiej kolejności są wysyłane, ponieważ zmiana kierunku fizyki zawsze powinna wyważyć zmianę kierunku opartą na kontroli.

Mam nadzieję że to pomoże. To piekielnie wzorzec projektowy, ale jest absurdalnie potężny, jeśli zostanie wykonany poprawnie.


0

Używam podobnego systemu w silniku, a sposób, w jaki to zrobiłem, polega na tym, że każda jednostka zawiera listę komponentów. Z EntityManager mogę zapytać każdą z Encji i zobaczyć, które zawierają dany Komponent. Przykład:

class Component
{
    private uint ID;
    // etc...
}

class Entity
{
    List<Component> Components;
    // etc...
    public bool Contains(Type type)
    {
        foreach(Component comp in Components)
        {
            if(typeof(comp) == type)
                return true;
        }
        return false;
    }
}

class EntityManager
{
    List<Entity> Entities;
    // etc...
    public List<Entity> GetEntitiesOfType(Type type)
    {
        List<Entity> results = new List<Entity>();
        foreach(Entity entity in Entities)
        {
            if(entity.Contains(type))
                results.Add(entity);
        }
        return results;
    }
}

Oczywiście nie jest to dokładny kod (w rzeczywistości potrzebujesz funkcji szablonu do sprawdzania różnych typów komponentów, zamiast używania typeof), ale koncepcja istnieje. Następnie możesz wziąć te wyniki, odnieść się do poszukiwanego komponentu i zarejestrować go w fabryce. Zapobiega to bezpośredniemu sprzężeniu między komponentami i ich menedżerami.


3
Zastrzegający emptor: w momencie, gdy twoja Istota zawiera dane, jest to obiekt, a nie byt… Traci się większość zalet paralelizacji (sic?) W tej strukturze. „Czyste” systemy E / C / S są relacyjne, a nie obiektowe… Nie znaczy to, że niekoniecznie jest „złe” w niektórych przypadkach, ale z pewnością „łamie model relacyjny”
BRPocock

2
Nie jestem pewien czy cię rozumiem. Rozumiem (i proszę o poprawienie, jeśli się mylę), że podstawowy System Elementów Entity ma klasę Entity, która zawiera Komponenty i może mieć identyfikator, nazwę lub jakiś identyfikator. Myślę, że możemy mieć nieporozumienie w tym, co rozumiem przez „typ” Istoty. Kiedy mówię „encja”, mam na myśli typy komponentów. Innymi słowy, encja jest typem „duszka”, jeśli zawiera komponent duszka.
Mike Cluck,

1
W klasycznym systemie jednostki / części, jednostka jest zwykle atomów: np typedef long long int Entity; Komponent to rekord (może być zaimplementowany jako klasa obiektu lub po prostu a struct), który zawiera odwołanie do encji, do której jest dołączony; a System byłby metodą lub zbiorem metod. Model ECS nie jest zbyt kompatybilny z modelem OOP, chociaż komponent może być (głównie) obiektem tylko danych, a system to obiekt typu singleton zawierający tylko kod, którego stan żyje w komponentach ... chociaż systemy „hybrydowe” są bardziej powszechne niż te „czyste”, tracą wiele wrodzonych korzyści.
BRPocock,

2
@BRPocock re „czyste” systemy encji. Myślę, że istota jako obiekt jest w porządku, nie musi to być prosty identyfikator. Jedną z nich jest szeregowa reprezentacja, innym jest układ w pamięci obiektu / koncepcji / bytu. Tak długo, jak możesz utrzymywać sterowane danymi, nie powinieneś być związany z kodem innym niż idiomatyczny tylko dlatego, że jest to „czysty” sposób.
Raine

1
@BRPocock jest to uczciwe ostrzeżenie, ale w przypadku systemów jednostek podobnych do „maszyny typu t”. Rozumiem dlaczego, ale nie są to jedyne sposoby modelowania jednostek opartych na komponentach. aktorzy są interesującą alternatywą. Coraz bardziej doceniam je, szczególnie dla podmiotów czysto logicznych.
Raine

0

1) Do metody Factory należy przekazać odwołanie do EntityManager, który ją wywołał (użyję C # jako przykładu):

delegate BaseEntity EntityFactory(EntityManager manager);

2) Niech CreateEntity otrzyma również identyfikator (np. Ciąg, liczbę całkowitą, zależy od Ciebie) oprócz klasy / typu encji i automatycznie zarejestruje utworzoną encję w Słowniku, używając tego identyfikatora jako klucza:

class EntityManager
{
    // Rest of class omitted

    BaseEntity CreateEntity(string id, Type entityClass)
    {
        BaseEntity entity = factories[entityClass](this);
        registry.Add(id, entity);
        return entity;
    }

    Dictionary<Id, BaseEntity> registry;
}

3) Dodaj moduł Getter do EntityManager, aby uzyskać dowolną jednostkę według identyfikatora:

class EntityManager
{
    // Rest of class omitted

    BaseEntity GetEntity(string id)
    {
        return registry[id];
    }
}

I to wszystko, czego potrzebujesz, aby odwołać się do dowolnego ComponentManager z poziomu metody Factory. Na przykład:

BaseEntity CreateSomeSortOfEntity(EntityManager manager)
{
    // Create and configure entity
    BaseEntity entity = new BaseEntity();
    RenderComponent renderComponent = new RenderComponent();
    entity.AddComponent(renderComponent);

    // Get a reference to the render manager and register component
    RenderEntityManager renderer = manager.GetEntity("RenderEntityManager") as RenderEntityManager;
    if(renderer != null)
        renderer.Register(renderComponent)

    return entity;
}

Oprócz Id możesz także użyć jakiejś właściwości Type (niestandardowe wyliczenie lub po prostu polegać na systemie typów języka) i utworzyć moduł pobierający, który zwraca wszystkie wartości BaseEntities określonego typu.


1
Nie po to, by być pedantycznym, ale znowu… w systemie czysto Istotowym (relacyjnym) byty nie mają żadnego rodzaju, poza tym, że są im nadawane na podstawie ich składników…
BRPocock

@BRPocock: Czy możesz stworzyć przykład zgodny z czystą cnotą?
Zolomon,

1
@Raine Być może nie mam doświadczenia z pierwszej ręki, ale to właśnie przeczytałem. Istnieją optymalizacje, które można wdrożyć, aby skrócić czas wyszukiwania komponentów według identyfikatora. Jeśli chodzi o spójność pamięci podręcznej, myślę, że ma to sens, ponieważ przechowujesz w pamięci dane tego samego typu w sposób ciągły, szczególnie gdy komponenty są lekkie lub proste. Czytałem, że brak jednej pamięci podręcznej na PS3 może być tak samo drogi jak tysiąc instrukcji procesora, a takie podejście do ciągłego przechowywania danych podobnego typu jest bardzo popularną techniką optymalizacji we współczesnym tworzeniu gier.
David Gouveia,

2
W odniesieniu: „czysty” system encji: Identyfikator jednostki jest zazwyczaj podobny do typedef unsigned long long int EntityID;:; Idealne jest to, że każdy system może żyć na osobnym procesorze lub hoście i wymaga tylko pobierania komponentów, które są odpowiednie dla / aktywnego w tym systemie. W przypadku obiektu Entity może być konieczne utworzenie instancji tego samego obiektu Entity na każdym hoście, co utrudnia skalowanie. Model czysto byt-komponent-system dzieli przetwarzanie na węzły (procesy, procesory lub hosty) według systemu, a nie według jednostki, zazwyczaj.
BRPocock,

1
@DavidGouveia wspomniał o „optymalizacjach… wyszukiwanie podmiotów według ID”. W rzeczywistości (kilka) systemów, które zaimplementowałem w ten sposób, zazwyczaj tego nie robią. Częściej wybieraj Komponenty według jakiegoś wzorca wskazującego, że są one interesujące dla konkretnego Systemu, używając Encji (ID) tylko w połączeniach międzyskładnikowych.
BRPocock,
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.