Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

06.04.2013, 19:30

Komponentenbasierte Programmierung für Spielobjekte

Hallo zusammen,

in meinem kleinen 2D Spiel bzw. Framework möchte ich die Spielobjekte mithilfe von Komponenten realisieren. Zuerst einmal geht es mir nur um die allgemeinen, gameplay-unabhängigen Objekte, wie z.B. Sprites, Tilemaps, Lichter oder Soundquellen. Um die Gegner, Items usw. kümmere ich mich im Detail später, toll wäre aber, wenn das Komponentensystem auch schon diese Objekte berücksichtigen würde.

Ich habe mich bereits in der Umsetzung des Komponentensystems versucht. Mein erster Ansatz sieht folgendermaßen aus:

Das ist die Klasse für die Spielobjekte. Sie stellt hauptsächlich einen Container für die Komponenten dar.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
class Entity {
public:
    void transform(Transform*);     // Legt die Transformationskomponente fest.
    auto transform() -> Transform*; // Fragt die Transformationskomponente ab.
    // ...
private:
    // Die Komponenten.
    Transform *m_transform = nullptr;
    Sprite    *m_sprite = nullptr;
    Tilemap   *m_tilemap = nullptr;
    // ...
};


Und das ist die Basisklasse für die Komponenten:

C-/C++-Quelltext

1
2
3
4
5
6
7
class EntityComponent {
public:
    void owner(Entity*);     // Legt den Besitzer fest.
    auto owner() -> Entity*; // Fragt den Besitzer ab.
private:
    Entity *m_owner = nullptr;
};


Ich habe also einen sehr einfachen Ansatz gewählt. Eine Entity enthält die Pointer zu allen möglichen Komponenten als Membervariablen. Eine Komponente kann über ihren Besitzer direkt auf andere Komponenten zugreifen. Letzteres habe ich bewusst ermöglicht, damit ich kein aufwendiges Messaging-System für die Komponenten benötige. (Die Sprite-Komponente könnte ich auch mit der Tilemap-Komponente zu einer allgemeinen Graphics-Komponente zusammenfassen, aber das ist wohl eine andere Frage.)

Natürlich sehe ich auch die Schwächen dieser Umsetzung: Ich muss die Entity-Klasse für jede Komponente um einen Member und entsprechende Methoden erweitern. Für die allgemeinen Komponenten (Transform, Sprite, Tilemap ...) sehe ich darin kein allzu großes Problem, weil diese sowieso von fast allen Entities benötigt werden. Wenn ich aber an die Programmierung der Gegner, Items und anderen speziellen Spielobjekte denke, sieht es schon anders aus.

Eine weniger "statische" Implementierung des Komponentensystems könnte vielleicht Abhilfe schaffen, beispielsweise indem ich die Komponenten einfach alle in einen Vector schmeiße:

std::vector<EntityComponent*> m_components;

Das würde allerdings die Abfrage und die Kommunikation zwischen den Komponenten umständlicher machen. Entweder müsste ich dann doch ein Messaging-System hinzufügen oder bei der Abfrage einer Komponente deren Typ ermitteln, um entsprechend casten zu können, z.B. so:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
template <class TComponent>
auto Entity::findComponent() -> TComponent* {
    for (auto component : m_components) {
        auto derivedComponent = dynamic_cast<TComponent*>(component);
        if (derivedComponent != nullptr) {
            return derivedComponent;
        }
    }
    return nullptr;
}


Alternativ könnte ich wohl auch eine Methode zum Abfragen des Typen zur EntityComponent-Klasse hinzufügen und dann statisch casten, aber ich weiß nicht, ob das bei der flachen Klassenhierarchie einen nennenswerten (Geschwindigkeits-)Vorteil bringen würde.

Unschön bei der obigen findComponent-Methode finde ich, dass selbst die Transform-Komponente auf diese Weise abgefragt werden müsste, obwohl ich weiß, dass praktisch alle Spielobjekte diese Komponente verwenden. Dieses Manko könnte ich lösen, indem ich die häufig genutzen Grundkomponenten von den "Spezialkomponenten" (Gegner, Items etc.) trenne: Die Grundkomponenten erhielten dann weiterhin jeweils ihre eigenen Membervariablen in der Entity-Klasse, während die Spezialkomponenten in das Komponentenarray eingefügt würden.

Viele Möglichkeiten also und ich bin mir ziemlich sicher, dass jede davon auf ihre Weise funktionieren würde. Zurzeit aber fällt mir die Entscheidung schwer. Bisher habe ich noch nicht komponentenbasiert programmiert, deswegen kann ich mich auch nicht aus Erfahrung für oder gegen eine dieser Optionen entscheiden. Ich kann nur vermuten ... und natürlich auf eure Hinweise und Vorschläge hoffen. ;)

Was meint also ihr dazu? Komponenten als Membervariablen direkt in die Entity-Klasse? Oder in ein Komponenten-Array? Oder sogar beides (Grund- und Spezialkomponenten)? Soll ich mir die Mühe machen, ein Messaging-System für die Komponenten zu entwerfen oder gehts auch ohne?

BlueCobold

Community-Fossil

Beiträge: 10 738

Beruf: Teamleiter Mobile Applikationen & Senior Software Engineer

  • Private Nachricht senden

2

06.04.2013, 19:45

Ich persönlich würde direkte Member verwenden, weil ich von solchen Finds unschön finde und sogar richtig hässlich, wenn eine Komponente zwingend erforderlich ist, obwohl die Component-List doch vorgaukelt, dass jede Komponente optional sei.
Also entweder anders lösen oder direkt Member.
Ich finde auch Component-based-Systeme nur dann sinnvoll, wenn vorher nicht klar ist, was damit mal angestellt werden soll - siehe Unity. Bei einem Spiel, wo das aber ja schon durch das Game-Design fest definiert ist, finde ich einen "optimierteren" oder direkten Umsetzungsweg deutlich angemessener.
Teamleiter von Rickety Racquet (ehemals das "Foren-Projekt") und von Marble Theory

Willkommen auf SPPRO, auch dir wird man zu Unity oder zur Unreal-Engine raten, ganz bestimmt.[/Sarkasmus]

3

06.04.2013, 23:59

Zitat von »BlueCobold«

Ich finde auch Component-based-Systeme nur dann sinnvoll, wenn vorher nicht klar ist, was damit mal angestellt werden soll - siehe Unity. Bei einem Spiel, wo das aber ja schon durch das Game-Design fest definiert ist, finde ich einen "optimierteren" oder direkten Umsetzungsweg deutlich angemessener.

Ursprünglich wollte ich auch alles über Vererbung lösen und kein Komponentensystem verwenden. Das wäre mir sogar jetzt noch die liebste Lösung. Allerdings fand ich es ziemlich schwierig, alle vorgesehenen Spielobjekte in einer Klassenhierarchie abzubilden: Sprites, Tilemaps, Lichter, Soundquellen, Partikelemitter, Triggerbereiche, Collider ... Das Hauptproblem war, dass allmählich immer mehr Memberfunktionen und -variablen, die für Interface-Klassen vorgesehen waren, in der Basisklasse der Spielobjekte gelandet sind. Beispielsweise gab es ein Drawable-Interface für alle Objekte, die gerendert werden können. Soundquellen, Triggerbereiche und Collider werden normalerweise nicht gerendert - es sei denn, der In-Game-Editor wird aktiviert, dann können selbst diese Objekte gerendert werden. Letztendlich benötigte also jedes Objekt das Drawable-Interface und eine Membervariable bool m_invisible.

Mit Komponenten lässt sich das etwas eleganter lösen, indem die Grafik-Komponente bei Bedarf einfach hinzugefügt oder entfernt wird (bzw. an-/ausgeschaltet). Praktisch an einem Komponentensystem ist auch, dass komplexe Objekte relativ einfach aus den Grundkomponenten zusammengebaut werden können. Zum Beispiel eine Fackel: Sprite + Licht + Partikelemitter + Sound.

Ich stimme dir aber zu, dass der direkte Weg hier wohl der angemessenere wäre. Ich werde noch mal darüber nachgrübeln. Vielleicht bekomme ich es ja doch noch über Vererbung geregelt.

BlueCobold

Community-Fossil

Beiträge: 10 738

Beruf: Teamleiter Mobile Applikationen & Senior Software Engineer

  • Private Nachricht senden

4

07.04.2013, 08:48

Ich sehe nicht, wo sich Vererbung und Komponenten für Trigger oder ähnliches widersprechen. Bei Rickety Racquet erben die Spiel-Objekte von einem ganzen Haufen Interfaces, bzw. abstrakter Klassen und weder brauchten wir ein m_invisible, noch etwas anderes, damit eine oder mehrere der ererbten Eigenschaften optional ist.
Nur weil ein Objekt ein Drawable ist, heißt es doch nicht, dass es immer etwas zu zeichnen haben muss. Ein Auto ist schließlich auch ein Drivable und muss nicht die ganze Zeit gefahren werden.
Teamleiter von Rickety Racquet (ehemals das "Foren-Projekt") und von Marble Theory

Willkommen auf SPPRO, auch dir wird man zu Unity oder zur Unreal-Engine raten, ganz bestimmt.[/Sarkasmus]

5

14.04.2013, 16:04

Jetzt habe ich mich noch mal drangesetzt und konnte das Ganze letztendlich doch noch ziemlich problemlos über Vererbung lösen. Ich schätze, ich habe mich vorher u.a. zu sehr mit der Unterscheidung zwischen Editor- und Spielmodus aufgehalten. Ich bin z.B. davon ausgegangen, dass Objekte wie Soundquellen normalerweise (d.h. im Spielmodus) nicht gerendert werden und deswegen auch keine Drawables sein dürften. Aber wie du, BlueCobold, treffend angemerkt hast, muss ein Drawable natürlich nicht gerendert werden.

Ich habe mich nun darum bemüht, die Spiellogik möglichst geschickt von der Renderlogik bzw. der Repräsentation der Spielwelt zu trennen. Die Spielobjekte werden jetzt von einem simplen Szenengraphen abgebildet und eine Klasse namens SceneNode stellt die Basisklasse für alle spezialisierten Knoten dieses Graphen dar (SpriteNode, TilemapNode usw.). Die SceneNode-Klasse selbst wird außerdem als allgemeiner Gruppenknoten benutzt.

Ich weiß noch nicht, ob ich die Verbindung zwischen den Szenenknoten und der Spiellogik über Vererbung oder Aggregation herstelle, aber vorerst möchte ich sowieso erst mal das Rendering einigermaßen sicher auf die Beine stellen. Dann bin ich zufrieden.

Danke für deinen Input!

buggypixels

Treue Seele

Beiträge: 125

Wohnort: Meerbusch

Beruf: Programmierer

  • Private Nachricht senden

6

18.04.2013, 11:03

Ein richtiges ECS auf die Beine zu stellen, ist leider etwas komplex. In Deinem Beispiel oben fehlen noch ein paar Zutaten und darum scheint es Dir vielleicht noch nicht so recht passend.
Ich habe auch etwas Zeit gebraucht, bis ich endlich ein lauffendes (und performantes) ECS zusammen hatte.
Ich schreibe einfach noch mal kurz, wie es von den meisten umgesetzt ist.
Also ein Entity ist nur eine ID. In manchen Implementierung hängt an dem Entity einer Liste an Komponenten. Ist aber unnötig meiner Meinung nach. Dann die Komponente selber sind nur Daten. Keine Logik. In C entspricht das einem struct wie

C-/C++-Quelltext

1
2
3
struct Position : public Copmonent {
  float x,y,z;
}

Dann kommt das System. Hier steckt die Logik drin. Also ein MotionSystem würde eine Position und eine Velocity Komponente verknüpfen für ein Entity und dann ihre Sachen berechnen. Dann kommen wir nämlich zum letzten Baustein. Manche nennen es Node oder manche Data oder wie auch immer. Das ist jedenfalls eine Sammlung an Komponenten für ein System.
Beispiel:

C-/C++-Quelltext

1
2
3
4
struct MotionData {
  Entity entity;
  Position* position;
  velocity* velocity;


Das MotionSystem hat eine Liste an MotionData. Bei einem Update Lauf von MotionSystem nimmt er dann einfach alle MotionData und aktualisiert sie.

Das sind die 4 Kernpunkte in einem ECS.

Der schwierige Part ist jetzt, wie enstehen diese Data Objekte und wie landen sie bei dem passenden System. Hier habe ich mir ordentlich einen abgebrochen.
Aber im Prinzip ist es so, dass man zuerst ein Entity mit allen Komponenten anlegt (ein bisschen Pseudo Code von meinem System):

C-/C++-Quelltext

1
2
3
4
Entity e = engine.create();
engine.assign(e,new Position(200,200));
engine.assign(e,new Velocity(200,0));
engine.add(e);

In der add Method wird die Liste aller Komponenten für ein Entity an alle System gereicht. Das MotionSystem z.B. schaut dann, ob eine Velocity und eine Position Komponte vorhanden sind. Falls ja generiert es eine MotionData und fügt die seiner Liste hinzu.
Ich hoffe, ich konnte mich etwas verständlich ausdrücken :)
Aber Vorsicht: Es ist nicht jedermanns Geschmack und es gibt viele Meinungen dazu. Ob es etwas für dich ist, mußt du selber entscheiden. Der Einstieg ist etwas komplex, weil es auch eine andere Denke ist.

7

20.04.2013, 01:38

Danke für das Beispiel, buggypixels. Über die Trennung von Systemen und Komponenten hatte ich auch schon mal im Zusammenhang mit dem Artemis Framework gelesen. Dort gibt es aber - soweit ich das verstanden habe - keine Unterscheidung zwischen einzelnen Komponenten und Komponentensammlungen (Position <-> MotionData in deinem Beispiel). Andere Implementierungen wiederum speichern die Daten anscheinend nicht direkt in den Komponenten, sondern in "Property-Objekten", die global für alle Komponenten einer Entity verfügbar sind. In dem Bereich scheint es also viele verschiedene Herangehensweisen zu geben. :)

Den von dir angesprochenen Ansatz finde ich jedenfalls sehr interessant. Da die Komponenten dank der Systeme nicht mehr miteinander kommunizieren müssen, entfällt auch das Nachrichtensystem.

Zitat von »buggypixels«

Aber Vorsicht: Es ist nicht jedermanns Geschmack und es gibt viele Meinungen dazu. Ob es etwas für dich ist, mußt du selber entscheiden. Der Einstieg ist etwas komplex, weil es auch eine andere Denke ist.

Ja, in der Tat muss man in vielerlei Hinsicht umdenken. Beispielsweise muss man wohl, um den Typen einer Entity zu bestimmen, auf Ducktyping zurückgreifen, weil die Entities keine unterscheidbaren Typen besitzen. Polymorphes Verhalten für Entities stelle ich mir auch nicht so einfach vor, da die Entities lediglich Datencontainer darstellen und die Logik allein in den Systemen/Managern steckt.

Für mein aktuelles Projekt behalte ich die Vererbung bei. Danach werde ich mir aber mit Sicherheit noch mal die Komponenten zu Gemüte führen und mich an diesen Thread erinnern. Thanksy thanks!

Werbeanzeige