System encji i rendering

11

Okey, co wiem do tej pory; Jednostka zawiera komponent (przechowywanie danych), który przechowuje takie informacje; - Tekstura / sprite - Shader - itp

A potem mam system renderujący, który rysuje to wszystko. Ale nie rozumiem, w jaki sposób należy zaprojektować renderer. Czy powinienem mieć jeden komponent dla każdego „typu wizualnego”. Jeden komponent bez modułu cieniującego, jeden z modułem cieniującym itp.?

Potrzebuję tylko trochę informacji na temat „właściwego sposobu”, aby to zrobić. Wskazówki i pułapki, na które należy uważać.

Hayer
źródło
2
Staraj się nie robić rzeczy zbyt ogólnych. Wydaje się dziwne mieć jednostkę ze składnikiem Shadera, a nie ze sprite'em, więc może Shader powinien być częścią komponentu Sprite. Oczywiście będziesz potrzebował tylko jednego systemu renderowania.
Jonathan Connell

Odpowiedzi:

8

Trudno odpowiedzieć na to pytanie, ponieważ każdy ma własne wyobrażenie o tym, jak powinien wyglądać system komponentu encji. Najlepsze, co mogę zrobić, to podzielić się z Tobą niektórymi rzeczami, które uważam za najbardziej przydatne dla mnie.

Jednostka

Podchodzę do ECS klasami tłuszczowymi, prawdopodobnie dlatego, że uważam, że ekstremalne metody programowania są wysoce nieefektywne (pod względem produktywności człowieka). W tym celu byt jest dla mnie abstrakcyjną klasą, którą odziedziczą bardziej wyspecjalizowane klasy. Jednostka ma wiele wirtualnych właściwości i prostą flagę, która mówi mi, czy ta jednostka powinna istnieć. W związku z tym pytanie dotyczące systemu renderowania Entitywygląda tak:

public abstract class Entity {
    public bool IsAlive = true;
    public virtual SpatialComponent   Spatial   { get; set; }
    public virtual ImageComponent     Image     { get; set; }
    public virtual AnimationComponent Animation { get; set; }
    public virtual InputComponent     Input     { get; set; }
}

składniki

Składniki są „głupie”, ponieważ nic nie robią ani nic nie wiedzą . Nie mają żadnych odniesień do innych komponentów i zazwyczaj nie mają żadnych funkcji (pracuję w języku C #, więc używam właściwości do obsługi funkcji pobierających / ustawiających - jeśli mają funkcje, opierają się na odzyskiwaniu przechowywanych danych).

Systemy

Systemy są mniej „głupie”, ale wciąż są głupimi automatami. Nie mają one kontekstu dla całego systemu, nie mają odniesień do innych systemów i nie przechowują żadnych danych poza kilkoma buforami, które mogą być potrzebne do ich indywidualnego przetwarzania. W zależności od systemu może mieć specjalistyczną metodę Updatelub Drawmetodę, aw niektórych przypadkach obie te metody.

Interfejsy

Interfejsy są kluczową strukturą w moim systemie. Służą do określania, co Systemmoże przetwarzać, a co Entitypotrafi. Interfejsami istotnymi do renderowania są: IRenderablei IAnimatable.

Interfejsy po prostu informują system, które komponenty są dostępne. Na przykład system renderujący musi znać obwiednię elementu i rysowany obraz. W moim przypadku byłoby to SpatialComponenti ImageComponent. Wygląda to tak:

public interface IRenderable {
    SpatialComponent Component { get; }
    ImageComponent   Image     { get; }
}

RenderingSystem

Jak więc system renderujący rysuje byt? To jest naprawdę dość proste, więc pokażę ci tylko uproszczoną klasę, aby dać ci pomysł:

public class RenderSystem {
    private SpriteBatch batch;
    public RenderSystem(SpriteBatch batch) {
        this.batch = batch;
    }
    public void Draw(List<IRenderable> list) {
        foreach(IRenderable obj in list) {
            this.batch.draw(
                obj.Image.Texture,
                obj.Spatial.Position,
                obj.Image.Source,
                Color.White);
        }
    }
}

Patrząc na klasę, system renderowania nawet nie wie, co to Entityjest. Wszystko, o czym wie IRenderable, to po prostu podano listę do narysowania.

Jak to wszystko działa

Pomoże to również zrozumieć, w jaki sposób tworzę nowe obiekty gry i jak je karmię do systemów.

Tworzenie jednostek

Wszystkie obiekty gry dziedziczą od Entity oraz wszelkie odpowiednie interfejsy opisujące możliwości tego obiektu gry. Prawie wszystko, co jest animowane na ekranie, wygląda następująco:

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

Karmienie systemów

Trzymam listę wszystkich bytów, które istnieją w świecie gry, na jednej liście o nazwie List<Entity> gameObjects. Następnie każdą ramkę przeglądam tę listę i kopiuję odwołania do obiektów do większej liczby list na podstawie typu interfejsu, takiego jak List<IRenderable> renderableObjectsi List<IAnimatable> animatableObjects. W ten sposób, jeśli różne systemy muszą przetwarzać ten sam byt, mogą to zrobić. Następnie po prostu przekazuję te listy każdemu z systemów Updatelub Drawmetod i pozwalam systemom wykonywać swoją pracę.

Animacja

Możesz być ciekawy, jak działa system animacji. W moim przypadku możesz zobaczyć interfejs IAnimatable:

public interface IAnimatable {
    public AnimationComponent Animation { get; }
    public ImageComponent Image         { get; set; }
}

Kluczową rzeczą, na którą należy zwrócić uwagę, jest to, że ImageComponentaspekt IAnimatableinterfejsu nie jest tylko do odczytu; ma setera .

Jak można się domyślać, komponent animacji po prostu przechowuje dane dotyczące animacji; lista ramek (które są składnikami obrazu), bieżąca ramka, liczba ramek na sekundę do narysowania, czas, jaki upłynął od przyrostu ostatniej klatki, oraz inne opcje.

System animacji korzysta z systemu renderowania i relacji komponentu obrazu. Po prostu zmienia komponent obrazu elementu, zwiększając klatkę animacji. W ten sposób animacja jest renderowana pośrednio przez system renderowania.

Szyfrować
źródło
Powinienem chyba zauważyć, że tak naprawdę nie wiem, czy jest to nawet bliskie temu, co ludzie nazywają systemem elementów składowych bytu . Próbując wdrożyć projekt oparty na kompozycji, wpadłem w ten wzorzec.
Cypher
Ciekawy! Nie przepadam za abstrakcyjną klasą dla twojej Entity, ale interfejs IRenderable to dobry pomysł!
Jonathan Connell,
5

Zobacz tę odpowiedź, aby zobaczyć rodzaj systemu, o którym mówię.

Komponent powinien zawierać szczegóły dotyczące tego, co narysować i jak go narysować. System renderowania weźmie te szczegóły i narysuje obiekt w sposób określony przez komponent. Tylko jeśli użyjesz znacznie różnych technologii rysowania, będziesz mieć osobne komponenty dla osobnych stylów.

MichaelHouse
źródło
3

Kluczowym powodem rozdzielenia logiki na komponenty jest utworzenie zestawu danych, które po połączeniu w całość dają użyteczne zachowanie wielokrotnego użytku. Na przykład rozdzielenie jednostki na element PhysicsComponent i RenderComponent ma sens, ponieważ prawdopodobne jest, że nie wszystkie jednostki będą miały fizykę, a niektóre jednostki mogą nie mieć Sprite.

Aby odpowiedzieć na twoje pytanie, musisz spojrzeć na swoją architekturę i zadać sobie dwa pytania:

  1. Czy ma sens posiadanie modułu cieniującego bez tekstury?
  2. Czy oddzielenie Shadera od Texture pozwoli mi uniknąć duplikacji kodu?

Przy dzieleniu komponentu ważne jest, aby zadać to pytanie, jeśli odpowiedź na 1. brzmi „tak”, prawdopodobnie masz dobrego kandydata do utworzenia dwóch oddzielnych komponentów, jednego z modułem cieniującym i jednego z teksturą. Odpowiedź na 2. jest zwykle tak dla komponentów takich jak Pozycja, w których wiele elementów może używać pozycji.

Na przykład, zarówno fizyka, jak i dźwięk mogą używać tej samej pozycji, zamiast obu komponentów przechowujących zduplikowane pozycje refaktoryzujesz je do jednego PositionComponent i wymagać, aby podmioty korzystające z PhysicsComponent / AudioComponent również miały PositionComponent.

W oparciu o informacje, które nam przekazałeś, nie wydaje się, aby Twój RenderComponent był dobrym kandydatem do podziału na TextureComponent i ShaderComponent, ponieważ shader są całkowicie zależne od Texture i nic więcej.

Zakładając, że używasz czegoś podobnego do T-Machine: Entity Systems przykładowa implementacja RenderComponent & RenderSystem w C ++ wyglądałaby mniej więcej tak:

struct RenderComponent {
    Texture* textureData;
    Shader* shaderData;
};

class RenderSystem {
    public:
        RenderSystem(EntityManager& manager) :
            m_manager(manager) {
            // Initialize Window, rendering context, etc...
        }

        void update() {
            // Get all the entities with RenderComponent
            std::vector<RenderComponent>& components = m_manager.getComponents<RenderComponent>();

            for(auto component = components.begin(); entity != components.end(); ++components) {
                // Do something with the texture
                doSomethingWithTexture(component->textureData);

                // Do something with the shader if it's not null
                if(component->shaderData != nullptr) {
                    doSomethingWithShader(component->shaderData);
                }
            }
        }
    private:
        EntityManager& m_manager;
}
Jake Woods
źródło
To całkowicie źle. Cały sens komponentów polega na oddzieleniu ich od bytów, a nie zmuszaniu systemów renderujących do przeszukiwania bytów, by je znaleźć. Systemy renderujące powinny w pełni kontrolować własne dane. PS Nie umieszczaj std :: vector (szczególnie z danymi instancji) w pętlach, to okropne (wolne) C ++.
snake5
@ snake5 masz rację w obu kwestiach. Napisałem kod od czubka głowy i wystąpiły pewne problemy, dziękuję za wskazanie ich. Poprawiłem ten kod, aby był wolniejszy i poprawnie używał idiomów systemu encji.
Jake Woods
2
@ snake5 Nie przeliczasz danych w każdej ramce, getComponents zwraca już znany wektor m_manager i zmienia się tylko podczas dodawania / usuwania komponentów. Jest to zaletą, gdy masz system, który chce korzystać z wielu komponentów tego samego obiektu, na przykład PhysicsSystem, który chce używać PositionComponent i PhysicsComponent. Inne systemy prawdopodobnie będą chciały pozycji, a posiadając PositionComponent nie będziesz mieć zduplikowanych danych. Przede wszystkim rozwiązuje problem komunikacji komponentów.
Jake Woods
5
@ snake5 Pytanie nie dotyczy tego, jak należy ułożyć system EC ani jak działa. Pytanie dotyczy konfiguracji systemu renderowania. Istnieje wiele sposobów na zbudowanie systemu EC, nie daj się złapać problemom z wydajnością jednego nad drugim tutaj. OP prawdopodobnie używa zupełnie innej struktury EC niż którakolwiek z twoich odpowiedzi. Kod podany w tej odpowiedzi ma na celu lepsze pokazanie przykładu, a nie krytykę za jego wydajność. Gdyby pytanie dotyczyło wydajności, być może uczyniłoby to odpowiedź „nieużyteczną”, ale nie jest.
MichaelHouse
2
Wolę projekt przedstawiony w tej odpowiedzi niż w Cyphers. Jest bardzo podobny do tego, którego używam. Mniejsze komponenty są lepsze imo, nawet jeśli mają tylko jedną lub dwie zmienne. Powinny one zdefiniować aspekt bytu, taki jak mój „Damagable” komponent miałby 2, może 4 zmienne (maksimum i prąd dla każdego zdrowia i zbroi). Te komentarze stają się długie, przejdźmy do czatu, jeśli chcesz omówić więcej.
John McDonald
2

Pitfall # 1: przesadny kod. Zastanów się, czy naprawdę potrzebujesz każdej rzeczy, którą wdrożysz, ponieważ będziesz musiał z tym żyć przez dłuższy czas.

Pitfall # 2: zbyt wiele obiektów. Nie użyłbym systemu z zbyt dużą liczbą obiektów (po jednym dla każdego typu, podtypu itp.), Ponieważ utrudnia to automatyczne przetwarzanie. Moim zdaniem o wiele przyjemniej jest kontrolować każdy obiekt określonym zestawem funkcji (w przeciwieństwie do jednego elementu). Na przykład tworzenie komponentów dla każdego bitu danych zawartych w renderowaniu (komponent tekstury, komponent modułu cieniującego) jest zbyt podzielony - zwykle i tak trzeba mieć wszystkie te komponenty razem, prawda?

Pitfall # 3: zbyt ścisła kontrola zewnętrzna. Wolę zmieniać nazwy na obiekty cieniujące / tekstury, ponieważ obiekty mogą się zmieniać za pomocą renderera / typu tekstury / formatu cieniującego / cokolwiek innego. Nazwy są prostymi identyfikatorami - to renderer decyduje, co z nich zrobić. Pewnego dnia możesz chcieć mieć materiały zamiast zwykłych shaderów (np. Dodaj shadery, tekstury i tryby mieszania z danych). Dzięki interfejsowi tekstowemu znacznie łatwiej jest to zaimplementować.

Jeśli chodzi o renderer, może to być prosty interfejs, który tworzy / niszczy / utrzymuje / renderuje obiekty utworzone przez komponenty. Najbardziej prymitywną reprezentacją może być coś takiego:

class Renderer {
    function Draw() { ... }
    function AddSprite( ... ) { ... return sprite; }
    function RemoveSprite( sprite ) { ... }
    ...
};

Umożliwiłoby to zarządzanie tymi obiektami ze składników i utrzymanie ich na tyle daleko, aby można je było renderować w dowolny sposób.

snake5
źródło