Datenhaltung mit gamedata.class.php 15
In meinem Artikel über das Zeitgeist Gamesystem-Modul habe ich etwas mehr über die Klasse zur Datenhaltung (gamedata.class.php) geschrieben. Diese ist in Zeitgeist angelehnt an die Datenhaltung eines Entity-Systems. In den Kommentaren wurde mir von Gameplorer folgende Frage zu dem Prinzip gestellt:
Wie gut eignet sich das tatsächlich für Browsergames? Durch die component_data_N – Tabellen ist es ja nicht mehr möglich, effektiv mit Joins zu arbeiten. Dadurch muss ich aber pro Entity zig Queries absetzen (jede component_data – Tabelle) was auf die Performance schlägt. Für z.B. einen Kampf müssten dann auch noch mehrere Entities geladen werden. Das klingt für mich nach einem absoluten NoGo in einem Browsergame.
Mir kam die Artikelreihe daher eher vor, als wäre ES für Clientbasierte-Spiele ausgelegt.
Das ist eine gute Frage, da sie auf einem verbreiten Mißverständnis beruht wie Entity Systeme funktionieren. Es werden (wenn richtig implementiert) kaum Joins benötigt, da keine Notwendigkeit besteht die kompletten Entites zu laden (geschweige denn mehrere). Das Missverständnis beruht wohl auf dem Versuch sich Entity Systeme wie klassisch objektorientierte Systeme vorzustellen, in denen das System über die Objektklassen immer (mehr oder weniger direkten) Zugriff auf alle Daten des Objekts hat. Mir ging es nicht anders und ich brauchte einige Anläufe, um mich davon zu lösen.
Hier also mein Versuch zu erklären, wie ich Entity Systeme verstehe und wie sie in Zeitgeist implementiert wurden.
Ein simples Weltraum-Spiel
Da man anhand von Beispielen immer besser erklären kann, nehmen wir einmal ein einfaches Weltraum-Spiel mit folgenden Eigenschaften:
- Ein Spieler hat ein oder mehrere Raumschiffe
- Es gibt 3 Raumschifftypen: leicht, mittel, schwer
- Ein Raumschiff hat eine gewisse Panzerung, entsprechend den Typen: leicht, mittel und schwer (vergleichbar mit Lebenspunkten: wenig, mittel und viel)
- Ein Raumschiff hat eine festgelegte Waffe, entsprechend den Typen: leicht, mittel und schwer (vergleichbar Angriffskraft: wenig, mittel und viel)
- Ein Raumschiff fliegt durch das Universum
- Ein Raumschiff kann gegen ein anderes Raumschiff kämpfen
- Ein Raumschiff kann in Raumstationen fliegen, in denen es sich reparieren lassen kann
- Angedockte Raumschiffe können nicht angegriffen werden
Was würde für so ein Spiel also an Daten benötigt? Zunächst einmal die Raumschiff-Daten: Welcher Spieler besitzt welches Raumschiff? An welcher Position befindet sich das Raumschiff? Wie viel Waffenschaden und Panzerung hat es noch und so weiter. Dazu kommen noch die Daten für die Objekte im Raum: Raumstationen und deren Position.
Um die Unterschiede klar zu machen folgt nun eine beispielhafte objektorientierte und komponentenbasierte Umsetzung. Beide Varianten kann (und sollte) man sicherlich auch anders implementieren, aber dies soll nur als Veranschaulichung dienen.
Objektorientierter Ansatz
Der objektorientierte Ansatz sieht die Welt durch eine Ansammlung von Objektklassen, welche jeweils einen Typ von Gegenstand in der Welt repräsentieren. Deren Methoden sind die Aktionen, welche dieser Typ Gegenstand durchführen kann.
In unserem Beispiel gäbe es üblicherweise eine Objektklasse für Raumschiffe. Wie definiert hat ein Raumschiff einen Besitzer, eine Panzerung und Waffe, sowie eine Position im Raum. Außerdem kann es gerade fliegen oder an einer Raumstation angedockt sein. Daneben gäbe es wahrscheinlich eine Klasse für Raumstationen, die jedoch nichts weiter tun, als auf einer Position zu verharren.
Die Daten werden in Tabellen gespeichert, welche mehr oder minder den Objektstrukturen ähneln. In der Realität mag es mehr Streuung durch Normalisierung geben, aber im Großen und Ganzen sähen die Tabellen für unser Beispiel so aus:
- Tabelle 1: Raumschiff (Besitzer, Panzerung, Waffenschaden, Position, Angedockt)
- Tabelle 2: Raumstationen (Position)
Spieler sind im Besitz eines Raumschiffs, sobald dieses in der Tabelle Raumschiff eingetragen ist.
Die eigentlichen Raumschiffe der Spieler und Raumstationen in der Spielwelt wären Instanzen ihrer entsprechenden Objektklassen. Das System initialisiert die Instanz eines spezifischen Raumschiffs mit den entsprechenden Daten aus der Tabelle.
Neben den Daten verfügt eine Objektklasse über Methoden, welche die Daten der jeweiligen Instanz (und damit des Raumschiffs bzw. der Raumstation) verändern. Als Beispiel für die Raumschiffklasse:
- Raumschiff.bewegen(x,y,z): verändert die Position des Raumschiffs
- Raumschiff.angreifen(RaumschiffID): greift das Raumschiff mit der gegebenen ID an
- Raumschiff.andocken(RaumstationID): dockt das Raumschiff an die Raumstation mit der gegebenen ID an
- ..
So weit die kleine Wiederholung von objektorientierter Programmierung.
Komponentenbasierte Datenhaltung
Der komponentenbasierte Ansatz sieht die Welt durch eine Sammlung von Komponenten und Entitäten. Zunächst einmal die Erklärung, was diese eigentlich sind:
- Eine Komponente beschreibt eine Eigenschaft, die ein Gegenstand haben kann
- Eine Entität repräsentiert einen dedizierten Gegenstand in der Welt
- Eine Entität besteht aus einer oder mehreren Komponenten
An dem Beispiel können diese Definitionen vielleicht besser veranschaulicht werden. Mit folgenden Komponenten (Eigenschaften) können alle Gegenstände unserer fiktiven Spielwelt abgebildet werden (in Klammern steht jeweils der Wert, mit dem eine Eigenschaft bemessen werden kann):
- Komponente 1: Ein Gegenstand kann eine Waffe haben (Angriffsschaden)
- Komponente 2: Ein Gegenstand kann gepanzert sein (Panzerungswert)
- Komponente 3: Ein Gegenstand kann eine Position haben (x/y/z)
- Komponente 4: Ein Gegenstand kann einen Besitzer haben (SpielerID)
- Komponente 5: Ein Gegenstand kann beweglich sein (j/n)
- Komponente 6: Ein Gegenstand kann angreifbar sein (j/n)
- Komponente 7: Ein Gegenstand kann andere Gegenstände reparieren (j/n)
Nehmen wir nun einmal irgendein Raumschiff. Dieses hätte folgende Komponenten: Waffe, Panzerung, Position, Besitzer, istBeweglich, istAngreifbar. Eine Raumstation hingegen hätte nur folgende Komponenten: Position, kannReparieren. Diese Typen von Entities sind Assemblages - eigentlich nur Sammlungen von Komponenten, die einen bestimmten Typ von Gegenstand ausmachen.
Der nächste Schritt ist leicht – jede Komponente bekommt eine Tabelle in der Datenbank. Dazu noch eine für die Entities, sowie eine Tabelle, um die Verbindung zwischen einer Entity und ihren Komponenten herzustellen:
- Tabelle 1: Entities(ID)
- Tabelle 2: Entities_zu_Komponenten(Entity, Komponente)
- Tabelle 3: Komponente 1 – Waffe (Angriffsschaden)
- Tabelle 4: Komponente 2 – Panzerung (Panzerungswert)
- Tabelle 5: Komponente 3 – Position (x/y/z)
- Tabelle 6: Komponente 4 – Besitzer (Spieler_ID)
- Tabelle 7: Komponente 5 – istBeweglich (j/n)
- Tabelle 8: Komponente 6 – istAngreifbar (j/n)
- Tabelle 8: Komponente 7 – kannReparieren (j/n)
Die Assemblages würden so aussehen:
- Assemblage 1: Raumschiff (Waffe, Panzerung, Position, Besitzer, istBeweglich, istAngreifbar)
- Assemblage 2: Raumstation (Position, kannReparieren)
Damit wurden nur Daten beschrieben – nirgendwo steckt ein Stück Interaktion. Im Gegensatz zum objektorientierten Ansatz, bei dem die Interaktion in den Methoden der Gegenstände selbst steckt, ist eine Entität oder eine Komponente völlig losgelöst von Methoden oder überhaupt Code. Wo also kommt die Interaktivität her?
Entity Systeme
Die Interaktivität kommt von den sogenannten Systemen. Objektorientierung geht davon aus, dass eine Methode für eine Objektklasse sich von den Methoden anderer Objektklassen unterscheidet. Das Komponentenmodell geht im Gegensatz dazu davon aus, dass die Interaktion die auf eine Komponente einwirkt unabhängig vom Gegenstands immer gleich ist. Ein Beispiel wäre die Komponente “Position”. Es ist unerheblich welcher Gegenstand eine Position hat, das Prinzip “Bewegung” ist für alle Gegenstände gleich: die Position ändert sich. Damit hat sich der Gegenstand bewegt. Dies gilt für Raumschiffe ebenso wie für Rennautos, Flugzeuge oder Fußbälle.
Die Interaktion für Komponenten wird also in einzelnen, in sich abgeschlossenen Systemen gekapselt, die nur ihre jeweils relevante Komponente(n) bearbeiten. Alles andere interessiert sie nicht. Um einen Gegenstand zu bewegen ist es unerheblich wem er gehört oder wie er bewaffnet oder gepanzert ist. Das heißt nicht, dass diese Informationen in diesem Moment gänzlich unwichtig sind, aber dafür ist ein anderes System zuständig. Und deshalb laufen alle Systeme möglichst parallel.
Auswirkung 1: Komponentensicht = Flexibilität
In jedem Browsergame findet der Großteil der Entwicklung nach dem Launch statt. Bugfixing macht davon nur einen kleinen Teil aus. Früher oder später muss man am Balancing nachdrehen und in mehr oder weniger regelmäßigen Abständen wird es Erweiterungen geben: neue Einheiten, neue Regeln, neue Features, neue Dinge zu entdecken.
Nehmen wir unser Beispiel. Angenommen Spieler sollen auch Raumstationen besitzen können. Andere Spieler können diese natürlich zerstören. In einem objektorientierten Umfeld würde ich jetzt anfangen die Klasse der Raumstation zu erweitern. Bei einem Komponentensystem würde ich die Komponente “Besitzer” und “istAngreifbar” der Assemblage der Raumstation hinzufügen. Done. Keine Codeänderung nötig.
Oder fügen wir die Raumschiffklasse “Trägerschiff” hinzu. Es soll eine Art großes Raumschiff sein, das wie eine bewegliche Raumstation agiert und andere Raumschiffe daran andocken können. Also, ich erstelle eine neue Klasse dafür und fange an zu programmieren. Oder aber ich erstelle eine neue Assemblage und füge die entsprechenden Komponenten hinzu (was Datenbankeinträge sind!).
Und jetzt stellen wir uns einen einfachen Einheiten-Editor vor, der einfach nur Assemblages erstellen und mit Komponenten verknüpfen kann.
Wenn ich denn tatsächlich mal eine neue Eigenschaft brauche, muss ich natürlich das entsprechende System entwickeln. Allerdings steht die Eigenschaft damit automatisch auch allen anderen Assemblages zur Verfügung.
Zugegeben, mit Mehrfachvererbung oder Interfaces kann man auch in klassisch objektorientierten Umgebungen so eine Flexibilität versuchen, aber ich würde es nicht empfehlen. Früher oder später läuft alles auf massive Superklassen hinaus, die nicht mehr pflegbar sind.
Auswirkung 2: Kleine Systeme = Wartbarkeit
Wenn jedes System nur eine (oder wenige) Komponente(n) bearbeitet, sind diese per Definition relativ überschaubar. Anstatt über die Klassen verteilt habe ich auch nur an einem dedizierten Punkt meine Logik: in den Systeme selbst. Ich muss auch nicht in jeder Klasse immer wieder die gleiche oder ähnliche Logik unterbringen oder mich mit Vererbung und Interfaces herumschlagen, um den Code aus den Klassen herauszuhalten. Ich habe einen zentralen Punkt, an dem ein abgegrenztes Set von Daten geändert wird.
Durch diese Abgrenzung sind die Systeme außerdem großartig testbar.
Auswirkung 3: Spezialisierte Systeme = Performance
Nehmen wir als Beispiel an in der Queue des Eventhandlers liegen Events zur Bewegung von Schiffen. Es existiert ein System für “Bewegung”, dessen Aufgabe die Bearbeitung dieser Events ist. Es schaut also in der Event-Tabelle nach: was muss bewegt werden und wohin?
Klassisch objektorientiert wäre das “was” die ID eines Raumschiffs, aus dem dann die Raumschiff-Instanz erzeugt wird. Instinktiv mag man dem “was” in einem Entity System die ID der Entity zuweisen. In diesem Fall müsste das System jedoch erst einmal die Entity-ID finden, dann die Zwischentabelle befragen wo in der Komponententabelle “Position” denn der dazugehörige Eintrag liegt. Und da sind wir wieder bei der eigentlichen Frage von Gameplorer: “Sind diese Joins nicht unperformant?” Antwort: ja, aber warum sollte ich sie überhaupt machen? Wieso wird nicht einfach die ID des Komponenteneintrags als “was” genutzt? Sobald das passiert ist reduziert sich die Aufgabe des Systems auf einfache Updates.
Und noch besser: wer sagt, dass ein System in meiner Applikation leben muss? Letztendlich fragt es die Datenbank ab (Event-Tabelle), um mit den Ergebnissen eine andere Tabelle zu aktualisieren (Conponent_Data_Position). Das alles kann man bequem in einer Stored Procedure erledigen.
Auswirkung 4: Parallele Systeme = Skalierung
Angenommen ich habe alle Systeme ausgelagert. Weiter angenommen ich merke, dass ein System viele Ressourcen frisst, wohingegen andere recht genügsam sind. Was hintert mich daran das anspruchsvolle System einfach auf einen eigenen Server zu verlegen? Solange es Zugang zur Datenbank hat ist alles fein.
Was jetzt noch bleibt
Neben den Systemen bleibt noch der Logik-Server. Dies ist der Haupt-Gameserver, den die Clients ansprechen. Etwas muss ja auch dafür sorgen, dass sich Nutzer anmelden können, die Spielregeln implementiert sind und der Eventhandler befüllt wird. Dadurch dass aber alle Vorgänge innerhalb der Spielwelt nun losgelöst sind beschränkt sich dieser aber auch auf diese Aufgaben, wird deutlich schlanker und dadurch auch performanter.
So, der Artikel is jetzt lang genug. Ich hoffe ich habe euch meine Vorstellung von Entity Systemen etwas näher erklären können. Wie schon erwähnt gibt es viele Arten von Entity Systemen. Es würde mich freuen von euch zu hören, falls ihr eine Meinung zu dem Modell habt.
Hallo Dirk,
klasse Beitrag! Ich glaube, ich habe meinen Denkfehler gefunden. Ich spiele das die Tage mal konzeptionell durch und melde mich nochmal mit einem ausführlicheren Kommentar. Geholfen hat dein Artikel aber auf jeden Fall. Danke.
Gruß
Dennis
Das hört sich eigentlich sehr gut an und ich höre aus immer mehr Richtungen davon. Der Haken, den ich bei der ganzen Sache sehe, ist, dass, wenn man viele Objekte gleichzeitig und mit vielen Komponenten haben will, man sehr – eben unperformante – große Joins benutzen muss…
Die JOINs (oder vielmehr viele SELECTs) kommen eigentlich nur zustande, wenn man die Entities mit all ihren Komponenten laden will. Das ist allerdings im Backend kaum der Fall, da die Systeme jeweils auf den Komponenten arbeiten, nicht auf den Entities.
Im Frontend kann das passieren (“Gib mir mal alle Raumschiffe von Spieler X”). Aber auch dafür gibt es entsprechende Optimierungen, zum Beispiel Views, die bestimmte Informationen aggregieren, die das Frontend immer braucht.
Meine Empfehlung wäre: nimm dir die Implemetation und fang an ein Beispiel zu aufzusetzen. Mit der Zeit kommen dann auch die Aha-Erlebnisse.
Hi Dirk, am Wochenende hatte ich ein wenig Zeit, das mal gedanklich durchzuspielen.
> Es werden kaum Joins benötigt, da keine Notwendigkeit besteht die kompletten Entites zu laden.
meine Zweifel bezogen sich auf das Frontend, weniger auf die einzelnen Subsysteme. Ähnlich wie Marc hier schreibt, benötigen wir durchaus Entities mit vielen Komponenten zeitgleich, meistens in den Übersichten über z.B. die Flotte, in der die Position, Schilde, Angriffskraft etc. mehr oder weniger tabellarisch dargestellt werden.
Aber mit Joins und Views kann hier doch gearbeitet werden. Im ersten Schritt klang das für mich, als wäre die einzige Lösung jede component_data einzeln abzufragen. Da hatte ich irgendwo einen Denkfehler.
Ich habe aber auch ein paar neue Fragen mitgebracht:
1. Woher weißt du denn, welche Entities Raumschiffe sind (z.B. für eine Übersicht im Frontend)? Die Tabelle “entities” soll ausschließlich eine ID enthalten. D.h., dass ich die Info ob es ein Raumschiff ist noch irgendwo anders verwalten muss. Ist das eine eigene Komponente “istRaumschiff”? Dann würde ein Fantasy-Rollenspiel tausende Tabellen brauchen, für “istSchwert”, “istAxt”, “istKaninchen”, “istEdlerPrinz”.
würde ich eine Typ-Spalte in der entity-Tabelle sehen oder zum jeweiligen Assemblage joinen und die Info dorthin packen. Aber ersteres soll man nicht und die Verbindung zum Assemblage vermisse ich aktuell noch gänzlich.
Ist die Lösung die Typen zu gruppieren und Komponenten wie “istKreatur” und “istGegenstand” mit einer z.B. enum-Spalte “Typ” anzulegen?
Intuitiv und objektorientiert wie ich bin
2. Die Ja/Nein – Komponenten können doch so implementiert werden, dass es keine Value Spalte gibt und dann implizit ein “Ja” angenommen wird, wenn ein Eintrag vorhanden ist. Spricht da aus deiner Erfahrung was dagegen?
3. Die Struktur der Assemblage sind mir noch unklar. Damit ein Game-Editor funktioniert, sind die ja datenbankseitig gespeichert und nicht direkt im Code. Sind das einfach nur 2 Tabellen “assemblages” (id, name) und “assemblage_components” (id, assemblage_id, component_id)?
3.1. Mir fehlt da dann der Weg, von einer Entity zu ihrer Assemblage, um z.B. auf den Namen zu kommen => eine dritte Tabelle “assemblages_entities” (id, assemblage_id, entity_id)? Klingt irgendwie unhandlich.
3.2. Und was ist mit Vorbelegungen für z.B. die Angriffskraft? Kommen wir da nicht an den Punkt, wo wir auch “assemblage_component_data” – Tabellen bräuchten?
Was ist, wenn ein Kaninchen zwischen 70 und 100 Lebenspunkten haben kann, was erst beim Instanzieren festgelegt wird?
Die letzten Fragen gehen schon sehr ins Detail, aber konzeptionell ecke ich da an. Vor allem das “Best Practice” aus deiner Erfahrung interessiert mich dabei.
Viele Grüße
Dennis
Hi Dennis,
1) Um zwischen einer Komponente und einem Entity-Attribut zu unterscheiden muss man nur die Frage stellen: “Wirkt ein System auf darauf ein?”. Lautet die Antwort “Ja” ist es eine Komponente. Bei “Nein” ist es ein Attribut. Es wird wohl kein System geben, was auf eine Axt einwirkt und sie “axtiger” macht oder eine Entity, die sowohl Axt, als auch Schwert ist (kein Kommentar über Hellebarden, bitte). Entsprechend würde ich Axt, Schwert, etc. als Typ einer Waffe definieren, eventuell als Komponente “istWaffe” mit dem Typ als ein Attribut und eventuell noch Schaden etc., aber das hängt in dem Moment wieder stark vom Spiel an sich ab.
2) Kommt darauf an. Wenn es Flags sind, dann spricht etwas dagegen. Wenn sie eine permanente Eigenschaft darstellen, dann nein. In dem Artikelbeispiel gibt es eine Komponente “istAngreifbar” und “istBeweglich”. Beide ändern sich für ein Raumschiff in dem Moment, wo es angedockt ist, demnach würden implizite Komponenten nicht funktionieren.
3) Korrekt. Siehe Beispielimplementation in Zeitgeist.
3.1) Wozu solltest du ihn brauchen? Wenn du Entites gruppieren willst, mache das über eine Komponente oder ein Attribut.
3.2) Entweder in einer Tabelle, in Konfigurationen oder im Code, je nachdem was du vorhast und wie dein Editor / deine Pipeline aussieht.
Gibt es eigentlich Onlinedemos von deinen Spielen, die du mit der Engine gemach hast ? Du hast in irgendeinen Beitrag geschrieben, dass bereits drei Spiele entwickelt wurden.
Grüße
Horst
Hallo Horst,
nein, es gibt keine öffentlichen Online-Demos von Spielen. 2 Spiele haben es nicht über die Beta-Phase hinaus geschafft. Ebenso eine Applikation (http://www.taskkun.de), die es auch wohl nicht in den Live-Betrieb schaffen wird. Technisch gesehen sind die Anwendungen fertig, aber die Konzepte funktionierten nicht so, wie ich sie mir erhofft hatte. An dem dritten Spiel schreibe ich gerade, welches auch in diesem Jahr noch veröffentlicht wird (so der Plan).
Ich bin aber gerade dabei ein Demo-Spiel zu schreiben, welches mit den Framework-Beispielen ins SVN gehängt wird.
Wenn das stimmt, was die Blogeinträge vermuten lassen, ist das ein geniales Konzept.
Hi Quu! Es stimmt und inzwischen funktionieren viele Spiele und insbesondere MMOs nach diesem Prinzip. Wäre auch schön, wenn es mein Konzept wäre, aber ich habe es nur nach Anleitung angepasst und implementiert.
Hat es irgendeine besondere bewanniss damit, dass nichts mehr passiert?
Der übliche: andere Sideprojects haben sich gerade in den Vordergrund gedrängt und beanspruchen mehr Zeit. Einen Hinweis gibt es auf meinem privatem Blog. Den anderen Hinweis gibt es in absehbarer Zeit hier zu sehen. Hat allerdings alles nur am Rande mit Gaming zu tun, in so fern habe ich hier nicht gepostet.
[...] Technischer Leiter bei Deck13) über komponentenbasierte Systeme (bzw. Entity-Systeme). Da meine Artikel zu dem Thema bei euch gut ankamen könnte dieser für euch ebenfalls interessant [...]
[...] bewusst ist können meine Arbeitskollegen / unsere Kunden / meine Familie gerne lesen, wie ich mir komponentenbasierte Datenhaltung in PBBGs vorstelle - es wird sie aber wahrscheinlich nicht besonders [...]
Dirk, entschuldige die späte Antwort. Ich wollte es praktisch ausprobieren, kam aber bis heute nicht dazu. Zwar ist mir der theoretische Ansatz größtenteils klar, aber ein ES konkret zu implementieren ist ja nochmal ein anderer Punkt. Falls ich doch irgendwann zu einem Test komme, melde ich mich aber mit Sicherheit mit weiteren Fragen.
Du bloggst ja hoffentlich darüber, wenn dein neues Spiel online geht?
Grüße
Dennis
Ein kleines PS zu 3.1:
Bei meinen Gedankenspielen bin ich davon ausgegangen, Entities dynamisch zu instanzieren. Wüßte ich die assemblage_id, könnte ich generisch alle Daten der Komponenten laden. Das würde bei Oberflächen (Übersichten von Raumschiffen o.ä.) weniger Implementierungsaufwand bedeuten, da eine Load-Methode direkt aus dem Framework alle Daten zu Entities laden kann.
Aber im Endeffekt macht es wohl wenig Sinn, da die Oberfläche ohnehin spezifisch angepasst werden muss. Da kommt es auf die Lade-Methode auch nicht mehr an. Und die Systeme laden ja ohnehin nur das, was sie benötigen und brauchen damit ebenfalls keinen generischen Lademechanismus.
Kommt das hin?