Entity System und Rendering

10

Okey, was ich bisher weiß; Die Entität enthält eine Komponente (Datenspeicherung), die Informationen wie enthält; - Textur / Sprite - Shader - etc.

Und dann habe ich ein Renderer-System, das all dies zeichnet. Was ich aber nicht verstehe, ist, wie der Renderer gestaltet werden soll. Sollte ich eine Komponente für jeden "visuellen Typ" haben? Eine Komponente ohne Shader, eine mit Shader usw.?

Benötigen Sie nur einige Eingaben, was der "richtige Weg" ist, um dies zu tun. Tipps und Fallstricke, auf die Sie achten sollten.

hayer
quelle
2
Versuchen Sie, die Dinge nicht zu allgemein zu gestalten. Es erscheint seltsam, eine Entität mit einer Shader-Komponente und nicht mit einer Sprite-Komponente zu haben. Daher sollte der Shader möglicherweise Teil der Sprite-Komponente sein. Natürlich benötigen Sie dann nur ein Rendering-System.
Jonathan Connell

Antworten:

7

Diese Frage ist schwer zu beantworten, da jeder seine eigene Vorstellung davon hat, wie ein Entitätskomponentensystem aufgebaut sein sollte. Das Beste, was ich tun kann, ist, Ihnen einige der Dinge mitzuteilen, die sich für mich als am nützlichsten erwiesen haben.

Entität

Ich verfolge den Fat-Class-Ansatz für ECS, wahrscheinlich weil ich extreme Programmiermethoden als äußerst ineffizient empfinde (in Bezug auf die menschliche Produktivität). Zu diesem Zweck ist eine Entität für mich eine abstrakte Klasse, die von spezialisierteren Klassen geerbt wird. Die Entität verfügt über eine Reihe virtueller Eigenschaften und ein einfaches Flag, das angibt, ob diese Entität vorhanden sein soll oder nicht. In Bezug auf Ihre Frage zu einem Render-System Entitysieht das also so aus:

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

Komponenten

Komponenten sind insofern "dumm", als sie nichts tun oder wissen . Sie haben keine Verweise auf andere Komponenten und normalerweise keine Funktionen (ich arbeite in C #, daher verwende ich Eigenschaften, um Getter / Setter zu behandeln - wenn sie Funktionen haben, basieren sie auf dem Abrufen von Daten, die sie enthalten).

Systeme

Systeme sind weniger "dumm", aber immer noch dumme Automaten. Sie haben keinen Kontext des Gesamtsystems, keine Verweise auf andere Systeme und enthalten keine Daten außer einigen Puffern, die sie möglicherweise für ihre individuelle Verarbeitung benötigen. Je nach System kann es eine spezielle Methode Updateoder DrawMethode oder in einigen Fällen beides geben.

Schnittstellen

Schnittstellen sind eine Schlüsselstruktur in meinem System. Sie werden verwendet, um zu definieren, was eine SystemDose verarbeiten kann und wozu eine Entityfähig ist. Die für das Rendern relevanten Schnittstellen sind: IRenderableund IAnimatable.

Die Schnittstellen teilen dem System einfach mit, welche Komponenten verfügbar sind. Beispielsweise muss das Rendering-System den Begrenzungsrahmen der Entität und das zu zeichnende Bild kennen. In meinem Fall wäre das das SpatialComponentund das ImageComponent. So sieht es aus:

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

Das RenderingSystem

Wie zeichnet das Rendering-System eine Entität? Es ist eigentlich ganz einfach, also zeige ich Ihnen nur die abgespeckte Klasse, um Ihnen eine Idee zu geben:

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

Wenn man sich die Klasse ansieht, weiß das Render-System nicht einmal, was ein Entityist. Alles, was es weiß, ist IRenderableund es wird einfach eine Liste von ihnen zum Zeichnen gegeben.

Wie alles funktioniert

Es kann hilfreich sein, auch zu verstehen, wie ich neue Spielobjekte erstelle und wie ich sie den Systemen zuführe.

Entitäten erstellen

Alle Spielobjekte erben von Entity und alle anwendbaren Schnittstellen, die beschreiben, was dieses Spielobjekt tun kann. Fast alles, was auf dem Bildschirm animiert wird, sieht folgendermaßen aus:

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

Fütterung der Systeme

Ich führe eine Liste aller Entitäten, die in der Spielwelt existieren, in einer einzigen Liste mit dem Namen List<Entity> gameObjects. In jedem Frame durchsuche ich dann diese Liste und kopiere Objektreferenzen in weitere Listen, die auf dem Schnittstellentyp basieren, wie z. B. List<IRenderable> renderableObjectsund List<IAnimatable> animatableObjects. Auf diese Weise können verschiedene Systeme, die dieselbe Entität verarbeiten müssen, dies tun. Dann übergebe ich diese Listen einfach jedem der Systeme Updateoder DrawMethoden und lasse die Systeme ihre Arbeit machen.

Animation

Sie könnten neugierig sein, wie das Animationssystem funktioniert. In meinem Fall möchten Sie möglicherweise die IAnimatable-Oberfläche sehen:

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

Das Wichtigste dabei ist, dass der ImageComponentAspekt der IAnimatableBenutzeroberfläche nicht schreibgeschützt ist. es hat einen Setter .

Wie Sie vielleicht erraten haben, enthält die Animationskomponente nur Daten über die Animation. Eine Liste der Bilder (die Bildkomponenten sind), das aktuelle Bild, die Anzahl der zu zeichnenden Bilder pro Sekunde, die seit dem letzten Bildinkrement verstrichene Zeit und andere Optionen.

Das Animationssystem nutzt die Beziehung zwischen Rendering-System und Bildkomponente. Es ändert einfach die Bildkomponente der Entität, während der Rahmen der Animation erhöht wird. Auf diese Weise wird die Animation indirekt vom Rendering-System gerendert.

Chiffre
quelle
Ich sollte wahrscheinlich beachten, dass ich nicht wirklich weiß, ob dies auch nur annähernd dem entspricht, was Leute ein Entity-Component-System nennen . Bei meinem Versuch, ein kompositionsbasiertes Design zu implementieren, fiel ich in dieses Muster.
Cypher
Interessant! Ich bin nicht besonders an der abstrakten Klasse für Ihre Entität interessiert, aber die IRenderable-Oberfläche ist eine gute Idee!
Jonathan Connell
5

Sehen Sie sich diese Antwort an, um zu sehen, um welches System es sich handelt.

Die Komponente sollte die Details enthalten, was und wie gezeichnet werden soll. Das Rendering-System nimmt diese Details und zeichnet die Entität auf die von der Komponente angegebene Weise. Nur wenn Sie erheblich unterschiedliche Zeichentechnologien verwenden würden, hätten Sie separate Komponenten für separate Stile.

MichaelHouse
quelle
3

Der Hauptgrund für die Aufteilung der Logik in Komponenten besteht darin, einen Datensatz zu erstellen, der in einer Entität ein nützliches, wiederverwendbares Verhalten erzeugt. Das Trennen einer Entität in eine PhysicsComponent und eine RenderComponent ist beispielsweise sinnvoll, da wahrscheinlich nicht alle Entitäten über Physik verfügen und einige Entitäten möglicherweise nicht über Sprite verfügen.

Um Ihre Frage zu beantworten, müssen Sie sich Ihre Architektur ansehen und sich zwei Fragen stellen:

  1. Ist es sinnvoll, einen Shader ohne Textur zu haben?
  2. Kann ich durch das Trennen von Shader und Texture Code-Duplikationen vermeiden?

Wenn Sie eine Komponente aufteilen, ist es wichtig, diese Frage zu stellen. Wenn die Antwort auf 1. Ja lautet, haben Sie wahrscheinlich einen guten Kandidaten für die Erstellung von zwei separaten Komponenten, eine mit einem Shader und eine mit Textur. Die Antwort auf 2. lautet normalerweise Ja für Komponenten wie Position, bei denen mehrere Komponenten die Position verwenden können.

Beispielsweise können sowohl Physik als auch Audio dieselbe Position verwenden, anstatt dass beide Komponenten doppelte Positionen speichern, die Sie in eine PositionComponent umgestalten, und erfordern, dass Entitäten, die PhysicsComponent / AudioComponent verwenden, auch eine PositionComponent haben.

Basierend auf den Informationen, die Sie uns gegeben haben, scheint Ihre RenderComponent kein guter Kandidat für die Aufteilung in eine TextureComponent und eine ShaderComponent zu sein, da Shader vollständig von Texture und nichts anderem abhängig sind.

Angenommen, Sie verwenden etwas Ähnliches wie T-Machine: Entity Systems. Eine Beispielimplementierung einer RenderComponent & RenderSystem in C ++ würde ungefähr so ​​aussehen:

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
quelle
Das ist völlig falsch. Der Sinn von Komponenten besteht darin, sie von Entitäten zu trennen und Render-Systeme nicht dazu zu bringen, Entitäten zu durchsuchen, um sie zu finden. Render-Systeme sollten ihre eigenen Daten vollständig kontrollieren. PS Setzen Sie std :: vector (insbesondere mit Instanzdaten) nicht in Schleifen ein, das ist schreckliches (langsames) C ++.
Schlange5
@ snake5 du bist in beiden Punkten korrekt. Ich habe den Code oben auf meinem Kopf eingegeben und es gab einige Probleme, danke, dass Sie darauf hingewiesen haben. Ich habe den betroffenen Code so korrigiert, dass er weniger langsam ist und die Entitätssystem-Idiome korrekt verwendet.
Jake Woods
2
@ snake5 Sie berechnen Daten nicht in jedem Frame neu. GetComponents gibt einen Vektor von m_manager zurück, der bereits bekannt ist und sich nur ändert, wenn Sie Komponenten hinzufügen / entfernen. Dies ist von Vorteil, wenn Sie ein System haben, das mehrere Komponenten derselben Entität verwenden möchte, z. B. ein PhysicsSystem, das PositionComponent und PhysicsComponent verwenden möchte. Andere Systeme möchten wahrscheinlich die Position und mit einer PositionComponent haben Sie keine doppelten Daten. In erster Linie wird das Problem der Kommunikation von Komponenten gelöst.
Jake Woods
5
@ snake5 Die Frage ist nicht, wie das EC-System aufgebaut sein soll oder wie es funktioniert. Die Frage betrifft das Einrichten des Render-Systems. Es gibt mehrere Möglichkeiten, ein EC-System zu strukturieren. Lassen Sie sich hier nicht von den Leistungsproblemen übereinander einfangen. Das OP verwendet wahrscheinlich eine völlig andere EC-Struktur als jede Ihrer Antworten. Der in dieser Antwort bereitgestellte Code soll das Beispiel nur besser zeigen und nicht für seine Leistung kritisiert werden. Wenn die Frage nach der Leistung wäre, würde dies die Antwort vielleicht "nicht nützlich" machen, aber es ist nicht so.
MichaelHouse
2
Ich bevorzuge das in dieser Antwort dargelegte Design viel mehr als in Cyphers. Es ist dem sehr ähnlich, den ich benutze. Kleinere Komponenten sind imo besser, auch wenn sie nur eine oder zwei Variablen haben. Sie sollten einen Aspekt einer Entität definieren, so wie meine "Damagable" -Komponente 2, vielleicht 4 Variablen haben würde (max und aktuell für jede Gesundheit und Rüstung). Diese Kommentare werden lang. Lassen Sie uns zum Chatten übergehen, wenn Sie mehr diskutieren möchten.
John McDonald
2

Fallstrick Nr. 1: Überentwickelter Code. Überlegen Sie, ob Sie wirklich alles brauchen, was Sie implementieren, weil Sie einige Zeit damit leben müssen.

Fallstrick Nr. 2: zu viele Objekte. Ich würde kein System mit zu vielen Objekten verwenden (eines für jeden Typ, Subtyp und was auch immer), da dies die automatisierte Verarbeitung nur erschwert. Meiner Meinung nach ist es viel schöner, wenn jedes Objekt einen bestimmten Funktionsumfang steuert (im Gegensatz zu einem Funktionsumfang). Zum Beispiel ist das Erstellen von Komponenten für jedes im Rendering enthaltene Datenbit (Texturkomponente, Shader-Komponente) zu geteilt - normalerweise müssten Sie sowieso alle diese Komponenten zusammen haben, würden Sie nicht zustimmen?

Fallstrick Nr. 3: zu strenge externe Kontrolle. Ändern Sie lieber Namen als Shader- / Texturobjekte, da sich Objekte mit Renderer / Textur-Typ / Shader-Format / was auch immer ändern können. Namen sind einfache Bezeichner - es liegt am Renderer, zu entscheiden, was daraus gemacht werden soll. Eines Tages möchten Sie vielleicht Materialien anstelle von einfachen Shadern haben (fügen Sie beispielsweise Shader, Texturen und Mischmodi aus Daten hinzu). Mit einer textbasierten Oberfläche ist es viel einfacher, dies zu implementieren.

Der Renderer kann eine einfache Schnittstelle sein, die von Komponenten erstellte Objekte erstellt / zerstört / verwaltet / rendert. Die primitivste Darstellung davon könnte ungefähr so ​​sein:

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

Auf diese Weise können Sie diese Objekte von Ihren Komponenten aus verwalten und so weit halten, dass Sie sie nach Ihren Wünschen rendern können.

Schlange5
quelle