Dungeon-CampusMinden / Dungeon

The "Dungeon" is a tool to gamify classroom content and integrate it into a 2D Rogue-Like role-playing game.
MIT License
15 stars 36 forks source link

[DUNGEON] `TaskContentComponent` kann nicht von Items im Inventar implementiert werden #903

Closed AMatutat closed 11 months ago

AMatutat commented 1 year ago

Wir nutzen das TaskContentComponent, um Entitäten mit den Task und den TaskContent (also z.B. Lösungen) zu verknüpfen.

Für die meisten unserer Aufgabentypen und Szenarien nutzen wir Items, um diese als Lösungsoption darzustellen. Solange ein Item sich im Level befindet, ist es eine Entität, und wir können es auch mit dem TaskContentComponent mit einem TaskContent verknüpfen.

Sobald das Item vom Spieler aufgesammelt wird, wird die Item-Entität jedoch zerstört, und nur noch das ItemData im InventoryComponent des Helden gespeichert. => Wir verlieren das TaskContentComponent und damit die Verbindung zum TaskContent und Task.

Selbst wenn aus dem ItemData im InventoryComponent eine neue Entität gemacht wird (z.B. wenn man das Item auf den Boden wirft), ist diese Entität eine neue Entität und daher dem DSL und dem TaskContentComponent nicht bekannt.

Auf Anhieb sehe ich zwei Optionen, die beide nicht sonderlich schön sind:

  1. Das InventoryComponent speichert nicht ItemData, sondern Entitäten mit einem ItemComponent. Dann könnten wir das Task-Item unverändert im Inventar ablegen und dort das TaskContentComponent auslesen. Dann speichern wir aber wieder Referenzen auf Entitäten in anderen Entitäten, das ist weder ECS noch besonders schön und stellt viele konzeptionelle Probleme auf, die wir eigentlich mit den Konzept beheben wollten (siehe auch #447).
  2. Das TaskContentComponent müsste irgendwie in das ItemData-Konstrukt eingefügt werden, um so auch die Verbindung zwischen TaskContent und ItemData zu haben. Das geht aber irgendwie in die Richtung, dass ein non-Entity-Konstrukt ein Component implementiert, was nicht gerade ECS ist.

Im Gespräch mit @malt-r kam auch die Idee auf, die Funktionalität von TaskContentComponent nicht als Component umzusetzen (wie konkret wussten wir noch nicht), aber das sei auf der Seite der DSL mit erheblichem Aufwand verbunden und sollte vermieden werden.

cagix commented 1 year ago

Wir rennen scheinbar andauernd in solche oder ähnliche Situationen, und wieder ist es die Doppel-Verzeigerung der Strukturen.

Das Problem hier ist, dass wir einen Task haben und TaskContent, wobei wir letzteres einem Item umhängen. Beim Aufsammeln bleibt nur der Kern des Items übrig (ItemData).

Ich sehe eigentlich nur einen Weg:

(1) Das TaskContent ist ein Untertyp von ItemData, d.h. beim Aufsammeln wandert das TaskContent ins Inventar. (2) Es muss irgendwo eine Stelle ("Task-Verwaltung") geben, wo man die TaskContent-Objekte "registriert" und dabei mit einem Task verknüpft. Über diese Stelle könnte man später nach dem Aufsammeln des Items den zu dem aufgesammelten TaskContent gehörigen Task wieder herausfinden. (3) Das Handling (Abfragen der TaskContent-Components, Anstoßen der Auswerte-Routine, ...) sollte in einem System stattfinden (TaskSystem?). (4) Das Game könnte analog zur Verwaltung der Entitäten auch die Tasks verwalten. Vielleicht will man die Task-Verwaltung aber auch beim Petri-Netz ansiedeln, welches dann aber wieder bei Game aufgehängt sein sollte.

AMatutat commented 1 year ago

Wir rennen scheinbar andauernd in solche oder ähnliche Situationen, und wieder ist es die Doppel-Verzeigerung der Strukturen.

Ich weiß das dich die Doppel-Verzeigerung sehr stört, aber dieses Problem hätten wir auch mit einen einfachen Zeiger.

Edit: Ich hab dir dafür mal #905 angelegt und direkt mal eine erste Idee zum auflösen mit reingepackt. Ist für mich aber grade wirklich ein later.

Das Problem hier ist, dass wir einen Task haben und TaskContent, wobei wir letzteres einem Item umhängen. Beim Aufsammeln bleibt nur der Kern des Items übrig (ItemData).

Ich sehe eigentlich nur einen Weg:

(1) Das TaskContent ist ein Untertyp von ItemData, d.h. beim Aufsammeln wandert das TaskContent ins Inventar.

Das würde ich gerne vermeiden. Ich würde hier ansonsten eine "Code-Abhängigkeit" schaffen. Das game-Projekt/Framework müsste dann Code aus dem dungeon-Projekt verwenden, und das wollten wir eigentlich vermeiden, damit das Framework auch alleine kompilieren kann. Eine Möglichkeit wäre, ein Interface-Konstrukt zu erstellen, und ItemData speichert dann eine Referenz auf eine Interface-Instanz, während TaskContent das Interface implementiert. Allerdings ist das auch nicht die ideale Lösung.

(2) Es muss irgendwo eine Stelle ("Task-Verwaltung") geben, wo man die TaskContent-Objekte "registriert" und dabei mit einem Task verknüpft. Über diese Stelle könnte man später nach dem Aufsammeln des Items den zu dem aufgesammelten TaskContent gehörigen Task wieder herausfinden.

Vielleicht habe ich grade einen hänger, aber ich glaube das brauchen wir nicht, wofür denn?

Es muss irgendwo eine Stelle ("Task-Verwaltung") geben, wo man die TaskContent-Objekte "registriert" und dabei mit einem Task verknüpft.

Klar, das ist ja der Task selbst der die TaskContent speichert.

Über diese Stelle könnte man später nach dem Aufsammeln des Items den zu dem aufgesammelten TaskContent gehörigen Task wieder herausfinden.

Wenn das Item auf dem Boden liegt (also eine Entität ist), dann hat es auch das TaskContentComponent, darüber komm ich an den TaskContent und den Task. Und wenn ich mir (wie ich oben beschrie eigentlich vermeiden möchte), den TaskContent mit in die itemData schreibe, dann komm ich von der DSL-Funktion die in die Kiste guckt ob das richtige Item drinne liegt auch vom itemData´ ->TaskContent`.

Ich sehe jetzt keinen Bedarf für irgendeine weitere zentrale Verwaltungsstelle, aber vielleicht hab ich dich auch miss verstanden.

(3) Das Handling (Abfragen der TaskContent-Components, Anstoßen der Auswerte-Routine, ...) sollte in einem System stattfinden (TaskSystem?).

Das wird nicht möglich sein. Die Frage, wie die Auswertungs-Routine aussieht und wann diese ausgelöst wird, ist so stark von dem Szenario abhängig (welches rein in der DSL definiert ist), dass wir dafür kein allgemeingültiges System bauen können. Die Auswertungs-Routine wird über Callbacks implementiert, welche in den jeweiligen Events hinterlegt werden (Interaktion, Kollision, etc.). Die Bewertung ist nämlich eine Reaktion auf ein bestimmtes Event, da ist das nur konsequent. Darum haben wir uns damals auch für das Strategy/Callback-Pattern-Konstrukt entschieden, so kann die DSL relativ einfach in die Game-Loop integriert werden. Der Ansatz wurde bis jetzt auch so verfolgt und umgesetzt.

Edit: Das hatten wir auch hier festgehalten. EInmal etwas unkonkret unter Ziele und etwas konkreter unter Event-Handler und DSL-Callbacks

(4) Das Game könnte analog zur Verwaltung der Entitäten auch die Tasks verwalten. Vielleicht will man die Task-Verwaltung aber auch beim Petri-Netz ansiedeln, welches dann aber wieder bei Game aufgehängt sein sollte.

Die Tasks werden im Petri-Netz liegen, dort sollen ja auch (aktuell noch leicht magische) Steuermechanismen des Netzes liegen. Ob das Game das Netz verwaltet oder das Netz das Game verwaltet, da bin ich mir noch nicht sicher. Finde zweiteres eigentlich recht sexy, weiß nur noch nicht, wie man das richtig umsetzt.

cagix commented 1 year ago

Wir rennen scheinbar andauernd in solche oder ähnliche Situationen, und wieder ist es die Doppel-Verzeigerung der Strukturen.

Ich weiß das dich die Doppel-Verzeigerung sehr stört, aber dieses Problem hätten wir auch mit einen einfachen Zeiger.

Edit: Ich hab dir dafür mal #905 angelegt und direkt mal eine erste Idee zum auflösen mit reingepackt. Ist für mich aber grade wirklich ein later.

Das Problem hier ist, dass wir einen Task haben und TaskContent, wobei wir letzteres einem Item umhängen. Beim Aufsammeln bleibt nur der Kern des Items übrig (ItemData). Ich sehe eigentlich nur einen Weg: (1) Das TaskContent ist ein Untertyp von ItemData, d.h. beim Aufsammeln wandert das TaskContent ins Inventar.

Das würde ich gerne vermeiden. Ich würde hier ansonsten eine "Code-Abhängigkeit" schaffen. Das game-Projekt/Framework müsste dann Code aus dem dungeon-Projekt verwenden, und das wollten wir eigentlich vermeiden, damit das Framework auch alleine kompilieren kann.

Hmmm? Das ist doch genau anders herum? Das dungeon-Projekt würde Code aus game referenzieren?

(2) Es muss irgendwo eine Stelle ("Task-Verwaltung") geben, wo man die TaskContent-Objekte "registriert" und dabei mit einem Task verknüpft. Über diese Stelle könnte man später nach dem Aufsammeln des Items den zu dem aufgesammelten TaskContent gehörigen Task wieder herausfinden.

Vielleicht habe ich grade einen hänger, aber ich glaube das brauchen wir nicht, wofür denn?

Naja, irgendwer muss mir doch sagen, wo mein TaskContent hingehört? Wenn ich nur die TaskComponent in der Hand habe?

Es muss irgendwo eine Stelle ("Task-Verwaltung") geben, wo man die TaskContent-Objekte "registriert" und dabei mit einem Task verknüpft.

Klar, das ist ja der Task selbst der die TaskContent speichert.

Jaaaa. Aber wenn ich später in einer Entität nur noch die Component, also den TaskContent habe, möchte ich doch immer noch herausfinden, wo diese Component hingehört?

(3) Das Handling (Abfragen der TaskContent-Components, Anstoßen der Auswerte-Routine, ...) sollte in einem System stattfinden (TaskSystem?).

Das wird nicht möglich sein. Die Frage, wie die Auswertungs-Routine aussieht und wann diese ausgelöst wird, ist so stark von dem Szenario abhängig (welches rein in der DSL definiert ist), dass wir dafür kein allgemeingültiges System bauen können. Die Auswertungs-Routine wird über Callbacks implementiert, welche in den jeweiligen Events hinterlegt werden (Interaktion, Kollision, etc.). Die Bewertung ist nämlich eine Reaktion auf ein bestimmtes Event, da ist das nur konsequent. Darum haben wir uns damals auch für das Strategy/Callback-Pattern-Konstrukt entschieden, so kann die DSL relativ einfach in die Game-Loop integriert werden. Der Ansatz wurde bis jetzt auch so verfolgt und umgesetzt.

Hmmm? Was hat das eine mit dem anderen zu tun? Irgendwer muss doch die Callbacks anstoßen? Und bei uns (ECS) sind das Systeme, die die Eigenschaften von Entitäten auswerten und dann Dinge anstoßen?

(4) Das Game könnte analog zur Verwaltung der Entitäten auch die Tasks verwalten. Vielleicht will man die Task-Verwaltung aber auch beim Petri-Netz ansiedeln, welches dann aber wieder bei Game aufgehängt sein sollte.

Die Tasks werden im Petri-Netz liegen, dort sollen ja auch (aktuell noch leicht magische) Steuermechanismen des Netzes liegen. Ob das Game das Netz verwaltet oder das Netz das Game verwaltet, da bin ich mir noch nicht sicher. Finde zweiteres eigentlich recht sexy, weiß nur noch nicht, wie man das richtig umsetzt.

Dann wird es wohl mal Zeit für ein Konzept, oder? Aktuell ist das Game die zentrale Drehscheibe, die alles steuert. Jetzt haben wir daneben irgendwie noch die DSL bzw. den "Interpreter", der aber nach meinem Verständnis bisher eher eine Art Konfigurationsmaschine ist. Und dann kommt noch das Petri-Netz. Wer steuert jetzt das Spiel und wie bleibt das Spiel unabhängig vom Dungeon-Code, damit es weiter in PM einsetzbar ist?

AMatutat commented 1 year ago

Hmmm? Das ist doch genau anders herum? Das dungeon-Projekt würde Code aus game referenzieren?

ItemData liegt in game und wenn du jetzt TaskContent aus dungeon in ItemData speicherst, dann musst du dir das in ItemData importieren.

Hmmm? Was hat das eine mit dem anderen zu tun? Irgendwer muss doch die Callbacks anstoßen? Und bei uns (ECS) sind das Systeme, die die Eigenschaften von Entitäten auswerten und dann Dinge anstoßen?

Ja, aber die Callbacks sind reaktionen auf ein Event, z.B eine Kollision. Was das Callbkack macht (also ob du da jetzt das Monster schupst oder eine Frage auswertest) ist dem ECS und den KollisionsSystem herzlich egal.

Du hattest aber explizit von einem TaskSystem gesprochen welches die Bewertung anstößt, dass passt da nicht rein. Vielleicht reden wir auch aneinander vorbei.

Dann wird es wohl mal Zeit für ein Konzept, oder? Aktuell ist das Game die zentrale Drehscheibe, die alles steuert. Jetzt haben wir daneben irgendwie noch die DSL bzw. den "Interpreter", der aber nach meinem Verständnis bisher eher eine Art Konfigurationsmaschine ist. Und dann kommt noch das Petri-Netz. Wer steuert jetzt das Spiel und wie bleibt das Spiel unabhängig vom Dungeon-Code, damit es weiter in PM einsetzbar ist?

Yep. Für mich steht Fest: Das Framework wird vom Game als zentrale Einheit gesteuert und das soll auch so bleiben (dann ist das in PM auch ohne dungeon nutzbar), Die Frage ist jetzt, ordnet sich das Netz dem Game unter (lässt sich also auch irgendwie mit steuern) oder liegt es über den Game und zieht heimlich im Hintergrund die Fäden. Das fände ich eigentlich ganz sexy, aber auch nicht Teil dieses Issues.

906

cagix commented 1 year ago

ItemData liegt in game und wenn du jetzt TaskContent aus dungeon in ItemData speicherst, dann musst du dir das in ItemData importieren.

Ich habe nie "speichern" gesagt (hoffe ich). Das eine soll ein Untertyp des anderen sein und quasi stattdessen genutzt werden?!

AMatutat commented 1 year ago

ItemData liegt in game und wenn du jetzt TaskContent aus dungeon in ItemData speicherst, dann musst du dir das in ItemData importieren.

Ich habe nie "speichern" gesagt (hoffe ich). Das eine soll ein Untertyp des anderen sein und quasi stattdessen genutzt werden?!

Du hast recht

(1) Das TaskContent ist ein Untertyp von ItemData, d.h. beim Aufsammeln wandert das TaskContent ins Inventar.

ich hab das nur falsch gelesen/interpretiert.

Jetzt muss ich neu grübeln. Was ist dann mit TaskContent der kein Item ist?

AMatutat commented 1 year ago

@malt-r ich habe mal versucht, unsere Idee zu modellieren und habe dabei ein paar Kleinigkeiten gerade ausgebessert.

Das Ergebniss:

questItem

Wie besprochen speichert das InventoryComponent die Items nicht mehr direkt, sondern eine Instanz der Klasse Inventory. Die Klasse Inventory entspricht der Implementierung des aktuellen InventoryComponent und dient als Standard für das Framework.

Im Dungeon erstellen wir uns ein spezielles Inventar, das QuestInventory, welches dann von Quest-Entitäten (Held, Kisten) im InventoryComponent gespeichert wird.

Um das klar hervorzuheben: Das QuestInventory leitet von Inventory ab und kann daher auch normal die itemData speichern.

Das QuestInventory fügt dem Inventar jetzt noch Funktionen hinzu, um Entitäten hinzuzufügen. In den jeweiligen Funktionen muss dann vorher überprüft werden, ob die Entität auch das ItemComponent hat, also ein Item ist. Um das hier auch nochmal aufzuschreiben: Dass das QuestInventory Entitäten "speichert", ist ein Bug, der langfristig behoben werden sollte. Wir sollten auch nochmal die anderen Möglichkeiten untersuchen, bevor wir das so umsetzen.

Ich würde beide Sets, also das mit dem ItemData und das mit den Entitäten getrennt speichern (also wenn ich #add(Entity) aufrufe, packe ich das nur in das Entity-Set und hole mir nicht die ItemData, um das auch im anderen Set zu speichern). Dadurch erspare ich mir komplexe Zuordnungsversuche.

Die belegten Inventarplätze würden sich dann aus inventory.size() + itemEntity.size() ergeben. Die Funktion Inventory#item würde sowohl den Inhalt aus inventory zurückgeben als auch die itemData aus den Entitäten rausholen und mit übergeben. Dadurch gibt die Funktion wirklich alle Items zurück, die im Inventar sind (besonders wichtig für die GUI, damit wirklich alle Items gezeichnet werden und mir der Inventory-Typ egal ist).

Die Funktion QuestInventory#itemEntity ist dann "für die DSL" da. Dort werden nur Entitäten zurückgeholt. Diese Entitäten haben dann im Falle von QuestItems auch das TaskContentComponent.

Vermutlich müssen die anderen Funktionen auch überschrieben werden, damit das alles so funktioniert.

Wir müssen dann noch die Default-Methoden für Items erweitern.

Normale Item-Entitäten (das sind Entitäten mit ItemComponent) werden beim Aufsammeln zerstört, und das ItemData wird ins Inventar gelegt.

Im Falle von QuestItems (das sind Entitäten mit ItemComponent und TaskContentComponent) muss das anders implementiert werden. Die QuestItem-Entität würde das TaskContentComponent und ItemComponent behalten und dann ins Inventar gelegt werden. Ich bin mir noch nicht sicher, ob die Entität aus der Game-Entity-Liste entfernt werden muss (ich tendiere zu ja).

Vorteil an dieser Variante:

Nachteil:

Edit: Mal ne dumme Idee, was wäre denn wenn ich anstellen eines Set von Entitäten eine Map <itemData,TaskContent> speicher und wenn die Methode QuestInventory#itemEntity aufgerufen wird, erzeuge ich aus der Map einfach neue Entitäten mit einen neuen TaskContentComponent und konfiguriere das.

Anmerkung: Dafür dürften Entiäten sich nicht mehr automatisch im Game registrieren, da sonst auch die "Helper-Entities" registiert werden. Aber dafür macht sich @cagix schon länger stark, von daher sollte das passen.

malt-r commented 1 year ago

Das klingt für mich (ohne tief in den Dungeon-Interna drin zu stecken) erstmal gut. Dann könnte sich die DSL vollkommen auf die Behandlung von Entitäten beschränken und muss keine widersprüchliche Semantik für zwei Arten von Items (einmal als Entity, einmal nur als ItemData, wenn ein Item in einem Inventar liegt) implementieren. Die Methode QuestInventory::itemEntity dann als Schnittstelle zur DSL zu verwenden, erscheint mit auch sinnvoll.

Mal ne dumme Idee, was wäre denn wenn ich anstellen eines Set von Entitäten eine Map <itemData,TaskContent> speicher und wenn die Methode QuestInventory#itemEntity aufgerufen wird, erzeuge ich aus der Map einfach neue Entitäten mit einen neuen TaskContentComponent und konfiguriere das.

Hierzu kann ich nicht so viel sagen (ich weiß auch nicht, ob ich das muss), für die DSL macht es vermutlich keinen Unterschied, ob die Entitäten schon vorher bestehen oder on-the-fly erstellt werden.

cagix commented 1 year ago

@AMatutat Mal ins unreine gedacht: Was würde passieren, wenn es nur einen Typ Inventory gibt und wo die Schnittstelle auf Entitäten arbeitet, also add(item: Entity) bzw. remove(item: Entity)?

"Normale" Items haben intern ein ItemData, was sich das Inventory bei add holen könnte bzw. aus dem es bei remove wieder eine Entität macht.

Quest-Items haben dann ein anderes BlaData, womit das Inventory ganz analog umgehen kann.

Damit das klappt, müsste es nur einen gemeinsamen Obertyp (Interface) für ItemData und BlaData geben?


Edit: Mit Abstand betrachet: Warum machen wir uns das eigentlich so kompliziert? Das ItemData wird dann wieder in einer passenden Component gespeichert ... Warum holt sich das Inventory nicht gleich die Component aus der Entität und speichert das bzw. macht daraus wieder die passende Entität beim "Rausgeben"? Dann wäre auch der Unterschied "Item" und "Quest" polymorph auf JVM-Ebene.

AMatutat commented 1 year ago

@AMatutat Mal ins unreine gedacht: Was würde passieren, wenn es nur einen Typ Inventory gibt und wo die Schnittstelle auf Entitäten arbeitet, also add(item: Entity) bzw. remove(item: Entity)?

Auf die Idee bin ich eben auch gekommen, aber ich bin noch unentschlossen. Aktuell muss sich die Item-Entity erstmal selbst auseinanderbauen (ItemData#onCollect) oder zusammenbauen (ItemData#onDrop). Das sind Bi/Tri-Consumer mit einer default Implementierung als Vorgabe. Das erlaubt theoretisch den items beim aufheben noch was machen zu lassen (Spieler vergiften, oderso). Hat aber eine gewisse Komplexität und ist erstmal nicht so Verständlich.

"Normale" Items haben intern ein ItemData, was sich das Inventory bei add holen könnte bzw. aus dem es bei remove wieder eine Entität macht.

Quest-Items haben dann ein anderes BlaData, womit das Inventory ganz analog umgehen kann.

Damit das klappt, müsste es nur einen gemeinsamen Obertyp (Interface) für ItemData und BlaData geben?

Klingt auf den ersten Blick gut, aber auf DSL Ebene müsste man dann Sonderverhalten implementieren, oder @malt-r ? Man müsste beim Betrachten in der DSL dann erst gucken ob das ItemData-Objekt in der Kiste ein QuestItemData ist, wenn ja kann man auf das TaskContent-Objekt zugreifen.

Edit: Mit Abstand betrachet: Warum machen wir uns das eigentlich so kompliziert? Das ItemData wird dann wieder in einer passenden Component gespeichert ... Warum holt sich das Inventory nicht gleich die Component aus der Entität und speichert das bzw. macht daraus wieder die passende Entität beim "Rausgeben"? Dann wäre auch der Unterschied "Item" und "Quest" polymorph auf JVM-Ebene.

Wenn ich das richtig vestehe, dann willst du das ItemComponent im Inventar speichern. Components können ohne Entität nicht existieren und Items im Inventar sind keine Entitäten.

cagix commented 1 year ago

@AMatutat Mal ins unreine gedacht: Was würde passieren, wenn es nur einen Typ Inventory gibt und wo die Schnittstelle auf Entitäten arbeitet, also add(item: Entity) bzw. remove(item: Entity)?

Auf die Idee bin ich eben auch gekommen, aber ich bin noch unentschlossen. Aktuell muss sich die Item-Entity erstmal selbst auseinanderbauen (ItemData#onCollect) oder zusammenbauen (ItemData#onDrop). Das sind Bi/Tri-Consumer mit einer default Implementierung als Vorgabe. Das erlaubt theoretisch den items beim aufheben noch was machen zu lassen (Spieler vergiften, oderso). Hat aber eine gewisse Komplexität und ist erstmal nicht so Verständlich.

Wann werden diese Aktionen denn ausgeführt? Es würde Dich doch niemand hindern, diese Aktion beim add ins Inventory von just diesem ausführen zu lassen und sich hinterher das Data-Objekt aus der Entität rauszufischen?

"Normale" Items haben intern ein ItemData, was sich das Inventory bei add holen könnte bzw. aus dem es bei remove wieder eine Entität macht. Quest-Items haben dann ein anderes BlaData, womit das Inventory ganz analog umgehen kann. Damit das klappt, müsste es nur einen gemeinsamen Obertyp (Interface) für ItemData und BlaData geben?

Klingt auf den ersten Blick gut, aber auf DSL Ebene müsste man dann Sonderverhalten implementieren, oder @malt-r ? Man müsste beim Betrachten in der DSL dann erst gucken ob das ItemData-Objekt in der Kiste ein QuestItemData ist, wenn ja kann man auf das TaskContent-Objekt zugreifen.

Das meinte ich mit "polymorphen Handling". Sollte eigentlich auf der Seite von Inventory oder Entity erschlagbar sein, ohne dass sich die DSL darum kümmern muss. Stichwort: Überladene Methoden.

Edit: Mit Abstand betrachet: Warum machen wir uns das eigentlich so kompliziert? Das ItemData wird dann wieder in einer passenden Component gespeichert ... Warum holt sich das Inventory nicht gleich die Component aus der Entität und speichert das bzw. macht daraus wieder die passende Entität beim "Rausgeben"? Dann wäre auch der Unterschied "Item" und "Quest" polymorph auf JVM-Ebene.

Wenn ich das richtig vestehe, dann willst du das ItemComponent im Inventar speichern. Components können ohne Entität nicht existieren und Items im Inventar sind keine Entitäten.

Merkst Du nicht, wie Du Dir selbst die Sachen verkomplizierst? Wenn Du Dich mal von diesem Dogma der Doppel-Verkettung löst und die Beziehungen zw. Game und Entität und Component als Baum betrachtest, wird irgendwie vieles deutlich einfacher :)

AMatutat commented 1 year ago

Jaaaa, das ist das alte Dogma. Aber wenn Du Dich mal davon löst und das wirklich als Baum betrachtest, wird irgendwie vieles deutlich einfacher :)

Dann könnte ich doch gleich die Entity im Inventar speichern und sparr mir das geraffel, oder überseh ich grade etwas? Der Baum-Gedanke bringt mich grade etwas aus dem Konzept

AMatutat commented 1 year ago

@AMatutat Mal ins unreine gedacht: Was würde passieren, wenn es nur einen Typ Inventory gibt und wo die Schnittstelle auf Entitäten arbeitet, also add(item: Entity) bzw. remove(item: Entity)?

Auf die Idee bin ich eben auch gekommen, aber ich bin noch unentschlossen. Aktuell muss sich die Item-Entity erstmal selbst auseinanderbauen (ItemData#onCollect) oder zusammenbauen (ItemData#onDrop). Das sind Bi/Tri-Consumer mit einer default Implementierung als Vorgabe. Das erlaubt theoretisch den items beim aufheben noch was machen zu lassen (Spieler vergiften, oderso). Hat aber eine gewisse Komplexität und ist erstmal nicht so Verständlich.

Wann werden diese Aktionen denn ausgeführt? Es würde Dich doch niemand hindern, diese Aktion beim add ins Inventory von just diesem ausführen zu lassen und sich hinterher das Data-Objekt aus der Entität rauszufischen?

Ich hab mir die onCollect jetzt nochmal angeguckt.
Die Funktion ist der Callback für das InteractionComponent und implementiert (zumindest in der bereitgestellten Default-Implementierung) das grundsätzliche Aufheben und Einstecken Verhalten. Der Name onCollect ist eigentlich unpassend, es müsst ehrcollectOnInteraction sein. Das auseinanderbauen der Entität könnte m.M tatsächlich im Inventar passieren.

cagix commented 1 year ago

Jaaaa, das ist das alte Dogma. Aber wenn Du Dich mal davon löst und das wirklich als Baum betrachtest, wird irgendwie vieles deutlich einfacher :)

Dann könnte ich doch gleich die Entity im Inventar speichern und sparr mir das geraffel, oder überseh ich grade etwas? Der Baum-Gedanke bringt mich grade etwas aus dem Konzept

Ja, nein - Entitäten sollen ja nur an einer Stelle verwaltet/referenziert werden. Aber Components sind ja nur Datencontainer - warum sollte es nicht auch Components ohne Entität geben bzw. erst später einer Entität zugeordnet werden?

AMatutat commented 1 year ago

"Normale" Items haben intern ein ItemData, was sich das Inventory bei add holen könnte bzw. aus dem es bei remove wieder eine Entität macht. Quest-Items haben dann ein anderes BlaData, womit das Inventory ganz analog umgehen kann. Damit das klappt, müsste es nur einen gemeinsamen Obertyp (Interface) für ItemData und BlaData geben?

Klingt auf den ersten Blick gut, aber auf DSL Ebene müsste man dann Sonderverhalten implementieren, oder @malt-r ? Man müsste beim Betrachten in der DSL dann erst gucken ob das ItemData-Objekt in der Kiste ein QuestItemData ist, wenn ja kann man auf das TaskContent-Objekt zugreifen.

Das meinte ich mit "polymorphen Handling". Sollte eigentlich auf der Seite von Inventory oder Entity erschlagbar sein, ohne dass sich die DSL darum kümmern muss. Stichwort: Überladene Methoden.

@malt-r und ich haben heute über einen allgemeinen Ablauf von Entity nach TaskContent gesprochen. @malt-r möchte diesen immer gleich gestalten, damit das Handling innerhalb der DSL (vom Lehrenden) für solche Sachen immer identisch ist (also egal, ob er gerade ein Item anschaut oder einen Zauberer). Wir sind da bei Entity -> TaskContentComponent -> TaskContent gelandet.

Für mich/das Dungeon bedeutet das, egal wie. Sobald die DSL in die Kiste gucken und die Items anschauen will, muss ich eine Entität mit einem TaskContentComponent bereithalten.

Jetzt kommt meine zweite Bedingung: Ich möchte keinen Code vom dungeon in game haben. Mein InventoryComponent kann TaskContent, TaskContentComponent oder das BlaData daher nicht direkt kennen.

Anmerkung: Mein Kopf ist grade etwas in alle Richtungen gelaufen, also vielleicht löse ich grade ein Problem ohne das klar ist wo das Problem liegt:

Wenn mein Obertyp IItemData eine Entity toEntity Methode hat, könnte das default ItemData einfach ein 0815 WorldItem erzeugen und zurückgeben. QuestData würde der Entity zusätzlich ein TaskContentComponent verpassen.

Meinem Inventar ist das im konkreten egal und kann mit beiden Typen gleich umgehen. In der DSL müsste @malt-r bzw. der Lehrende für jedes IItemData in der Kiste #toEntity aufrufen und dann (über das ggf. vorhandene TaskContentComponent) auf das TaskContent Objekt zugreifen.

Das find ich irgendwie unschön. In der DSL sollten nur Items mit dem TaskContentComponent ankommen.

AMatutat commented 1 year ago

Jaaaa, das ist das alte Dogma. Aber wenn Du Dich mal davon löst und das wirklich als Baum betrachtest, wird irgendwie vieles deutlich einfacher :)

Dann könnte ich doch gleich die Entity im Inventar speichern und sparr mir das geraffel, oder überseh ich grade etwas? Der Baum-Gedanke bringt mich grade etwas aus dem Konzept

Ja, nein - Entitäten sollen ja nur an einer Stelle verwaltet/referenziert werden. Aber Components sind ja nur Datencontainer - warum sollte es nicht auch Components ohne Entität geben bzw. erst später einer Entität zugeordnet werden?

Wenn wir das erlauben, haben wir viele Probleme gelöst Ja. Bevor ich da aber was zu sage, will ich mich erstmal schlau machen.

Oder? Das Inventory speichert dann ItemComponent, aber davon hab ich noch kein TaskComponent. Ich glaub ich hänge gedanklich irgendwo fest.

AMatutat commented 1 year ago

Ok je mehr ich drüber nachdenke, desto weniger Vorteile sehe ich durch das speichern vom ItemComponent im Inventar. Wenn ich damit nicht die Referenz auf die Entität "mit speicher" (ohne doppelte Zeiger komm ich da schließlich nicht dran), dann kann ich mir weiterhin das ItemData Objekt speichern und habe genau den selben Status.

cagix commented 1 year ago

@malt-r und ich haben heute über einen allgemeinen Ablauf von Entity nach TaskContent gesprochen. @malt-r möchte diesen immer gleich gestalten, damit das Handling innerhalb der DSL (vom Lehrenden) für solche Sachen immer identisch ist (also egal, ob er gerade ein Item anschaut oder einen Zauberer).

@AMatutat @malt-r Wenn ich mir das als Lehrender anschaue, möchte ich eigentlich gar nichts von dem internen Aufbau des Spiels wissen. Ich möchte meine Fragen stellen und die Antworten bewerten können. Dafür muss die DSL mir geeignete Mittel zur Verfügung stellen, und der Interpreter/Compiler muss den Rest dann intern hinkriegen. Ich will mich doch gar nicht mit Entitäten und Components und TaskContentComponent und TaskContent und so einem Kram auf DSL-Ebene rumschlagen? Dann bin ich doch sofort wieder nah am Java-Code und dann stellt sich die Frage, warum ich das nicht gleich direkt in Java mache?!

AMatutat commented 1 year ago

@malt-r und ich haben heute über einen allgemeinen Ablauf von Entity nach TaskContent gesprochen. @malt-r möchte diesen immer gleich gestalten, damit das Handling innerhalb der DSL (vom Lehrenden) für solche Sachen immer identisch ist (also egal, ob er gerade ein Item anschaut oder einen Zauberer).

@AMatutat @malt-r Wenn ich mir das als Lehrender anschaue, möchte ich eigentlich gar nichts von dem internen Aufbau des Spiels wissen. Ich möchte meine Fragen stellen und die Antworten bewerten können. Dafür muss die DSL mir geeignete Mittel zur Verfügung stellen, und der Interpreter/Compiler muss den Rest dann intern hinkriegen. Ich will mich doch gar nicht mit Entitäten und Components und TaskContentComponent und TaskContent und so einem Kram auf DSL-Ebene rumschlagen? Dann bin ich doch sofort wieder nah am Java-Code und dann stellt sich die Frage, warum ich das nicht gleich direkt in Java mache?!

Will jetzt nichts falsches sagen, aber ist hier nicht genau der Gedanke der native-lib der DSL. Wenn ich will kann ich alles im detail konfigurieren, ja fast schon programmieren. Auf dieser eben sollte sich trotzdem alles sinnvoll und konstant verhalten. Und dann kommt da noch die hübsche Wrapper-Schicht drüber, damit du dir den ganzen kram nicht angucken musst.

malt-r commented 1 year ago

@AMatutat @malt-r Wenn ich mir das als Lehrender anschaue, möchte ich eigentlich gar nichts von dem internen Aufbau des Spiels wissen. Ich möchte meine Fragen stellen und die Antworten bewerten können. Dafür muss die DSL mir geeignete Mittel zur Verfügung stellen, und der Interpreter/Compiler muss den Rest dann intern hinkriegen. Ich will mich doch gar nicht mit Entitäten und Components und TaskContentComponent und TaskContent und so einem Kram auf DSL-Ebene rumschlagen? Dann bin ich doch sofort wieder nah am Java-Code und dann stellt sich die Frage, warum ich das nicht gleich direkt in Java mache?!

Wir haben wirklich schon häufig darüber gesprochen, dass es möglich sein sollte, die Szenarien in der DSL zu definieren, aber das nicht zwingend nötig ist, da das in einer "Standardbibliothek" gekapselt wird. Kernbegriff ist hier "Taskbuilder-Methode" #197, die dafür zuständig sein soll, das Szenario zu definieren und die in der DSL definierten Entitätstypen/Prototypen (die dann eben auch vorgeben, welche Komponenten enthalten sind) und Event-Handler Funktionen zu verknüpfen.

Wie wir dann von der DSL-Definition zum konkreten Szenario kommen wollen, ist auch schon länger hier dokumentiert (Kasten 3 zeigt da unter anderem diese "Standard-Bibliothek" die im Wesentlichen aus DSL-Definitionen besteht).

Dass es in der einfachsten Form einer DSL-Definition ausreichen sollte, nur die Aufgaben und die Art der Bewertung zu definieren, stimmt. In dem Fall kommt die Definition des Szenarios mit allen Event-Handler-Funktionen usw. die nötig sind, um die Verbindung zwischen Aufgabendefinition und konkretem Szenario herzustellen aus besagter Standardbibliothek (also DSL-Definitionen, welche Default-Implementierungen für Bewertungsfunktionen etc. enthalten). Wie z.B. die Bewertungsfunktion mit der Szenario-Definition zusammen hängt ist hier schon länger dokumentiert.

Falls wir die Möglichkeit schaffen wollen, auch eigene Szenarien per DSL-Eingabe definieren zu lassen (und da haben wir uns relativ früh im Projekt für entschieden, das #197 Issue ist schon etwas älter), dann bedeutet das in der Konsequenz auch, dass man über die DSL eine gewisse Konfigurierbarkeit der im Szenario enthaltenen Entitäten/Komponenten bietet. Dazu gehört dann bspw. in der Event-Handler Funktion einer Truhe in der DSL über alle enthaltenen Items zu iterieren und zu überprüfen, ob die Items enthalten sind, die laut Aufgabendefinition in der Truhe erwartet werden. Wenn wir die Freiheit der Szenariodefinition per DSL bieten wollen, dann können wir das nicht einfach in eine Blackbox verpacken, die von Nutzenden nicht mehr einsehbar ist, sondern müssen das irgendwie zugänglich machen, weil diese Abläufe zu spezifisch an ein bestimmtes Szenario gekoppelt sind.

cagix commented 1 year ago

@malt-r Ist ja alles richtig. Aber ich frage mich im Moment angesichts der obigen Diskussion (Entity -> TaskContentComponent -> TaskContent), ob wir wirklich auf dem richtigen Weg sind. Vielleicht ist das auch nur der Urlaubsmodus ;)

Wir bauen Java-seitig ziemlich komplexe Strukturen auf, und die wollen wir wirklich an Lehrende weiterreichen, die ihrerseits eigentlich nicht programmieren können sollen? Und das soll dann auch noch alles in einer DSL gemacht werden (können), die dann (auf mich) wie eine Schatten-Variante des Java-Codes wirkt?

Für mich wäre es zunächst wichtig, die Aufgaben plus Bewertungen und so definieren zu können. Danach wäre es noch schön, in der DSL einen gewissen Einfluss auf das Spiel bzw. dessen Konfiguration nehmen zu können - aber dazu möchte ich mich dann eigentlich nicht mit den Strukturen des Spiels beschäftigen müssen. Das müsste also über entsprechende Primitive der DSL abgebildet werden.

malt-r commented 1 year ago

Die Argumentation, dass die DSL ein bestimmtes Feature unterstützt, ist für mich nicht direkt damit zusammenhängend, ob wir den Lehrenden zumuten, das auch zu nutzen zu müssen. Wir haben immer die Möglichkeit, die nötige DSL-Logik in Funktionen in der Standard-Bibliothek, oder nativen Funktionen zu kapseln und so irgendwie die Komplexität der nötigen Eingaben zu verringern, aber eben mit "dsl-eigenen Mitteln" und damit theoretisch im Zugriff der Nutzenden.

Ich sehe den Punkt, dass man mit dieser Argumentation auch die Verpackung von diesen Komplexitäten in den Interpreter (damit dann aber DSL-Nutzenden unzugänglich) rechtfertigen kann und darauf, wo da die richtige Grenze ist, habe ich auch keine 100%ige Antwort :)

AMatutat commented 11 months ago

Nach dem gespräch mit Malte:

QuestItem extends Item
private TaskContentComponent tcc;

QuestItem(......, TaskContentComoponent)
.....

public TaskContentComponent taskContentComponent() return tcc;
AMatutat commented 11 months ago

@Lena241 in #1047 hab ich dir einen Fix dafür auf dein Item-Rework Branch erstellt. Damit übergebe ich an dich (musst eigentlich nichts hierfür mehr machen)