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

09.07.2016, 23:54

(C#) ECS - Auf welcher Weise Componenten speichern

Hallo,
ich bin gerade dabei, ein ECS zu entwickeln und bin auf ein fundamentales Problem gestoßen; wie ich denn die Components abspeichern soll.

Meine erste Idee war, dass ich eine Klasse Entity habe und die Components über ein dictonary<Type, IComponent> abspeicher. Es hat sich aber herausgestellt, dass diese Variante zu langsam und cache-unfriendly war.
Was ich dann versucht habe, ein großen Array zu nehmen, dort die Components abspeicher und dann in der Klasse Entity über ein dictonary<Type, index> die Indices der Components speicher, in der Hoffnung, dass es cache-friendly werden würde. War leider nichts.

Ich hab mich dann noch ein wenig belesen und festgestellt, dass sich zwei Varianten durchgesetzt haben.
Beide haben gemeinsam, dass Entity eine bloße ID ist. Die Systeme prüfen dann auf eine Signatur oder Bitmask, wofür ein enum für die Components angelegt wird.

Die erste Variante hat ein ComponentManager, der alle Components als Array verwalten. Hier wird dann die Entity-ID als Index verwendet.
Ich verstehe nicht, wie ich daraus dann die Components ableiten kann. Muss ich dann jedes Component-Array nach der ID absuchen?
Das würde auch bedeuten, dass ich für die Erstellung eines Components die Component selber erstellen, das Enum und den ComponentManager änder müsste. Gäbe es nicht eine bessere Lösung?

Die zweite Variante hat ein Mega-array, das alle Entitys und Components speichert.
Wie das verwaltet werden soll, bleibt mir schleierhaft.

Ich will mich daher mehr auf die erste Variante vertiefen, und hoffe, dass jemand sie wiedererkennt und mir genauer erklären kann. Insbesondere, wie ich nun die Components speichern soll.
Falls jemand die zweite noch erkennt und weiß, wie sie funktioniert, wäre ich für eine Erklärung sehr dankbar.

Gruß CroBrox

Legend

Alter Hase

Beiträge: 731

Beruf: Softwareentwickler

  • Private Nachricht senden

2

10.07.2016, 11:58

Nur zur Sicherheit: Ich hoffe du hast deine Entities als struct und nicht als class deklariert, oder?
Sonst wäre es kein Wunder, dass deine Implementation mit Arrays nicht Cache-freundlicher geworden ist.
"Wir müssen uns auf unsere Kernkompetenzen konzentrieren!" - "Juhu, wir machen eine Farm auf!"

Netzwerkbibliothek von mir, C#, LGPL: https://sourceforge.net/projects/statetransmitt/

TrommlBomml

Community-Fossil

Beiträge: 2 117

Wohnort: Berlin

Beruf: Software-Entwickler

  • Private Nachricht senden

3

10.07.2016, 12:33

Ich glaube C# ist nicht wirklich gut geeignet, um DOD-Architekturen zu implementiere, da ist C++ und andere etwas mehr low-level Sprachen besser. Man hat halt keine Kontrolle, wie man die Daten übergibt. Structs werden immer kopiert und Klassen immer referenziert. Ich finde es auch schlecht, wenn die Entities immer kopiert werden, vor allem, wenn man sie dann wirklich referenzieren will muss man immer ref benutzen, bäh.

Mein Tipp ist eher: Wie lange dauert es denn, deine Entities zu speichern? Für kleinere bis mittlerle Spiele sollte Cachefreundlichkeit nicht wirklich eine Rolle spielen denke ich.

Um vielleicht auch deine Frage mit dem "wie"-Speichern: Meine aktuelle Implementierung in einem Spiel funktioniert so, dass ich json benutze. Jedes Entity hat ein Array an Komponenten. Jede Komponente hat eine String-Id und eine Liste von Key-Value pairs (string, string), um seine Daten zu serialisieren. Das Laden der Komponente kann eine Factory machen und das klappt hervorragend. Ich lade mehrere tausend Entities innerhalb von einer halben Sekunde.

4

10.07.2016, 20:04

Ich glaube C# ist nicht wirklich gut geeignet, um DOD-Architekturen zu implementiere, da ist C++ und andere etwas mehr low-level Sprachen besser. Man hat halt keine Kontrolle, wie man die Daten übergibt. Structs werden immer kopiert und Klassen immer referenziert. Ich finde es auch schlecht, wenn die Entities immer kopiert werden, vor allem, wenn man sie dann wirklich referenzieren will muss man immer ref benutzen, bäh.

Mein Tipp ist eher: Wie lange dauert es denn, deine Entities zu speichern? Für kleinere bis mittlerle Spiele sollte Cachefreundlichkeit nicht wirklich eine Rolle spielen denke ich.

Um vielleicht auch deine Frage mit dem "wie"-Speichern: Meine aktuelle Implementierung in einem Spiel funktioniert so, dass ich json benutze. Jedes Entity hat ein Array an Komponenten. Jede Komponente hat eine String-Id und eine Liste von Key-Value pairs (string, string), um seine Daten zu serialisieren. Das Laden der Komponente kann eine Factory machen und das klappt hervorragend. Ich lade mehrere tausend Entities innerhalb von einer halben Sekunde.


Ich bin auch der Meinung, dass C++ besser ist. Allerdings arbeite ich nicht alleine und wegen ein paar anderen Sachen musste mich den anderen beugen.
Wie auch immer, ich hatte die selbe Idee, Entity-Components über JSON zu laden/speichern. Mein Problem liegt aber noch ein paar Schritte davor.

ATM sieht es bei mir so aus, dass ich ein EntityManager und ComponentManager habe. Der EntityManager erstellt die Entity-IDs. Der ComponentManager hält eine Liste von verschiedenen Components als Liste. Zusätzlich habe ich ein Enum-Flag mit den Components für die Signatur und ein Enum mit Componets, die als Index eingesetzt werden.
Hier mal ein Img zur Verständlichkeit:


Hier noch einmal der ungefähre Code:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
[Flags]
enum ComponentSign {
    HealthComponent = 0,
    EnergyComponent = 1 << 0,
    PositionComponent = 1 << 1,
    // ...
}

enum ComponentId {
    HealthComponent = 0,
    EnergyComponent,
    PositionComponent,
    // ...
}

interface IComponent {
    // marker
}

class ComponentManager {

    List<List<IComponent>> comps = new List<List<IComponent>>();
    List<IComponent> pos = new List<Position>();
    List<IComponent> health = new List<Health>();
    List<IComponent> energy = new List<Energy>();
    // ...

    public void Init() {
        comps.Insert(pos, ComponentId.PositionComponent);
        comps.Insert(health, ComponentId.HealthComponent);
        comps.Insert(energy, ComponentId.EnergyComponent);
        // ...
    }

    public void AddComponent(uint entityID, ComponentId cid, IComponent c) {
        var desiredCompList = comps[cid];       // get the desired component list
        desiredCompList.Insert(entityID, c);    // insert component based on entity id to desired component list
        // ...
    }

    public T GetComponent<T>(uint entityID, ComponentId cid) where T : IComponent {
        // ...
        return comps[cid][entityID] as T;
    }

    // ...

}


Performace am Anfang ist besser, liegt an den geminderten Cache-misses.
Je öfter ich die Elemente raus hole und rein lege, fällt die Performance. Liegt wahrscheinlich an den "Lücken" im Array, die wieder cache-unfriendly sind.
Gesamt betrachtet, ist die Performance besser, aber der Arbeitsaufwand ist recht hoch. Da bin ich am Überlegen, ob ich nicht doch beim Dictonary bleibe.

Vielleicht hat jemand eine bessere Idee.

TrommlBomml

Community-Fossil

Beiträge: 2 117

Wohnort: Berlin

Beruf: Software-Entwickler

  • Private Nachricht senden

5

10.07.2016, 21:05

Frage ist, wie viel besser und hast du schlimme FPS-Drops/Ruckler? Ich würde erstmal tippen, dass es dann doch eher am regelmäßigen Remove/Insert liegt. Da werden ja intern Arrays benutzt, und dann müssen da ja immer die elemente kopiert werden. Vielleicht solltest du dir für diese Fälle eine einfache, eigene List-Klasse bauen, die intern ein Array hält. Wenn jetzt ein Element entfernt wird, wird es einfach mit dem letzten aktiven getauscht und das gelöschte genullt oder nur als deleted markiert und gehalten wird.

EDIT: Ich bin mir nicht sicher, ob du das mit den Cache misses richtig verstanden hast und was das im Spielekontext bedeutet: Cache misses hast du immer, irgendwann muss die CPU halt aus dem RAM nachladen. Allerdings bei cachefreundlichen Strukturen möchtest du für eine große Anzahl an Daten, die du in kurzer Zeit lesen möchtest, möglichst in nacheinander liegenden Speicher haben. Dazu spielt auch noch die Größe eines Elements eine Rolle. Wenn du pro Objekt sehr viele Daten hast, passen deutlich weniger Objekte in den Cache bei selber Objektanzahl, und das ist ja glaube auch noch CPU-Abhängig. Damit ist an einigen Stellen der Nutzen für DoD meiner Meinung nach gar nicht gerechtfertigt.

Bitte korrigiert mich, wenn hier jemand noch was dazu zu sagen hat. Ist ja schon ein spannendes Thema ;)

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »TrommlBomml« (10.07.2016, 21:11)


6

10.07.2016, 22:01

Frage ist, wie viel besser und hast du schlimme FPS-Drops/Ruckler? Ich würde erstmal tippen, dass es dann doch eher am regelmäßigen Remove/Insert liegt. Da werden ja intern Arrays benutzt, und dann müssen da ja immer die elemente kopiert werden. Vielleicht solltest du dir für diese Fälle eine einfache, eigene List-Klasse bauen, die intern ein Array hält. Wenn jetzt ein Element entfernt wird, wird es einfach mit dem letzten aktiven getauscht und das gelöschte genullt oder nur als deleted markiert und gehalten wird.


Hickups habe ich keine, außer ich lege viele neue Components an. Das liegt wahrscheinlich an dem Kopieren, wie du schon beschrieben hast. Eine eigene Liste anzulegen, bzw. die Components zu reusen ist eine gute Idee. Die Umsetzung ist nur ein wenig schwierig, da ich an dieser Stelle nicht iterieren möchte.


EDIT: Ich bin mir nicht sicher, ob du das mit den Cache misses richtig verstanden hast und was das im Spielekontext bedeutet: Cache misses hast du immer, irgendwann muss die CPU halt aus dem RAM nachladen. Allerdings bei cachefreundlichen Strukturen möchtest du für eine große Anzahl an Daten, die du in kurzer Zeit lesen möchtest, möglichst in nacheinander liegenden Speicher haben. Dazu spielt auch noch die Größe eines Elements eine Rolle. Wenn du pro Objekt sehr viele Daten hast, passen deutlich weniger Objekte in den Cache bei selber Objektanzahl, und das ist ja glaube auch noch CPU-Abhängig. Damit ist an einigen Stellen der Nutzen für DoD meiner Meinung nach gar nicht gerechtfertigt.

Absolut richtig, so habe ich es in der Vorlesung auch aufgeschnappt. Meine Components halten nicht viele Daten, manche sind nur als "Marker" gedacht, ich vermute mal, dass sie dementsprechend nicht viel Speicher schlucken.

TrommlBomml

Community-Fossil

Beiträge: 2 117

Wohnort: Berlin

Beruf: Software-Entwickler

  • Private Nachricht senden

7

11.07.2016, 09:25

Zitat

Hickups habe ich keine, außer ich lege viele neue Components an. Das liegt wahrscheinlich an dem Kopieren, wie du schon beschrieben hast. Eine eigene Liste anzulegen, bzw. die Components zu reusen ist eine gute Idee. Die Umsetzung ist nur ein wenig schwierig, da ich an dieser Stelle nicht iterieren möchte.


Jetzt ist die Frage, was bei dir die meiste Zeit gemacht wird und was schnell sein soll: Iterieren oder einfügen/löschen. Beides geht nicht schnell, du musst dich entscheiden. Normalerweise geht man davon aus, dass einfügen/löschen eher selten ist und die meiste Zeit iteriert wird, dann würde ich mit dem Overhead leben und lieber das wegschmeissen von Speicher reduzieren. Beim einfügen kannst du viel Zeit sparen, wenn du vor dem Einfügen schonmal eine List mit initialCapacity anlegst, das macht einen deutlichen Unterschied!

C#-Quelltext

1
Absolut richtig, so habe ich es in der Vorlesung auch aufgeschnappt. Meine Components halten nicht viele Daten, manche sind nur als "Marker" gedacht, ich vermute mal, dass sie dementsprechend nicht viel Speicher schlucken.


Musst du ausgeben und dann mal rechnen. Ich hatte zu dem Thema gestern einen sehr spannenden Vortrag von einem Engine-Entwickler genau zu dem Thema: Data Oriented Design C++. Ist zwar C++, aber er hat ein paar sehr interessante Punkte zu dem Thema, die sich auch auf C# Anwenden lassen.

8

11.07.2016, 13:21


Jetzt ist die Frage, was bei dir die meiste Zeit gemacht wird und was schnell sein soll: Iterieren oder einfügen/löschen. Beides geht nicht schnell, du musst dich entscheiden. Normalerweise geht man davon aus, dass einfügen/löschen eher selten ist und die meiste Zeit iteriert wird, dann würde ich mit dem Overhead leben und lieber das wegschmeissen von Speicher reduzieren. Beim einfügen kannst du viel Zeit sparen, wenn du vor dem Einfügen schonmal eine List mit initialCapacity anlegst, das macht einen deutlichen Unterschied!

Ich werds mal so versuchen.


Musst du ausgeben und dann mal rechnen. Ich hatte zu dem Thema gestern einen sehr spannenden Vortrag von einem Engine-Entwickler genau zu dem Thema: Data Oriented Design C++. Ist zwar C++, aber er hat ein paar sehr interessante Punkte zu dem Thema, die sich auch auf C# Anwenden lassen.

Danke, schau ich mir an.

Werbeanzeige