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.12.2014, 19:05

Entkopplung, globale Variablen und Gottobjekte

Hi, ich bin im Moment dabei mein RPG umzudesignen, weil mir viele Sachen am Code nicht gefallen. Im Moment sind einige Sachen noch zu stark gekoppelt um sie z.B. mit UnitTests testen zu können. Hier ein Beispiel:
  • Damit meine GameObjects übersichtlich bleiben, verwende ich das Component Pattern. Meine GameObjects besitzen eine Reihe (abstrakter) Komponenten; konkret: Renderkomponente, Inputkomponente usw. - so dass jeder Aspekt (Rendering, Eingabe/KI usw.) von einer eigenständigen Klasse behandelt wird. Da z.B. das Rendering mit der Zeit immer komplizierte wurde (u.A. durch die implementierten Animationen und das Lighting), bin ich mit dieser Lösung ganz zufrieden.
  • Allerdings muss die Renderkomponente meinen ResourceCache kennen, wenn es eine neue Textur anfordern will. Um globale Variablen / Singletons zu umgehen (bisher ist der ResourceCache ein Singleton -.-), würde ich diesen der Renderkomponente im Konstruktor übergeben, so dass die Komponente autonom Ressourcen anfordern kann. Das lässt sich jetzt analog mit dem Logging fortführen; d.h. ich müsste auch meine (durch Objekte repräsentierten) Logfiles ebenfalls übergeben...
  • Die momentane Idee ist, ResourceCaches (einer für Texturen, einer für Sounds usw.) und die Logfiles (eines für Debuginfos, eine für Warnungen etc.) in einem Context zu halten. Diesen Context würde ich (in main(), da wo mein Mainloop ist) erzeugen und dann weiter an meine Objekte durchreichen - alle Caches und Logger einzeln zu übergeben ist auf Dauer schlecht, weil ich bei jeder Änderung (z.B. wenn ein Cache für Shader dazu kommt) die Konstruktoren überarbeiten muss ..
  • Das mit dem Context scheint da die schönere Lösung - allerdings habe ich aus zwei Gründen Bauchschmerzen dabei:
    1. Da der Context so viele Sachen besitzt führt das doch effektiv dazu, dass ich ein "Gottobjekt" habe oder? :dash:
    2. Dazu kommt, dass die Komponenten dann wiederum an den Context gekoppelt sind und dieser an einen Großteil des übrigen Codes .. das sind für mein Empfinden zu viele Abhängigkeiten :fie: .
Würde ich jetzt eine Abstraktion des Contexts einführen (um z.B. den TextureCache mittels Getter anzufordern), würde es zwar ein Stück weit entkoppeln .. aber irgendwie gefällt mir der Gedanke auch nicht so richtig :S

Habt ihr eine Idee?

LG Glocke

Evrey

Treue Seele

Beiträge: 245

Beruf: Weltherrscher

  • Private Nachricht senden

2

06.12.2014, 19:46

Gegen den Cache nutze ich shared_ptr. Ich rieche schon "Dem Performance, dem Designz!"-Kommentare, aber die bleiben drin, bis eine Messung an der fast fertigen Basis-Engine ergibt, dass es zu viel Leistung zieht. Texturen & Co. existieren so lange, wie irgendjemand sie verwenden möchte. Will man doch einen "Cache" haben, muss man bloß eine isolierte Komponente bauen, die ebenfalls einen shared_ptr verwaltet. Dann können die Ressourcen nicht wegsterben, bis auch der Cache die Finger von lässt.

Das einzig Persistente an Abhängigkeiten sind bei mir OpenAL-Kontexte, grundliegende Initialisierungen, und sowas, die für die restliche Engine völlig wumpe sind.

C-/C++-Quelltext

1
2
3
4
int main(int _argc, char** _argv) noexcept {
  asm volatile("lock cmpxchg8b %eax");
  return 0;
} // ::main
(Dieses kleine Biest vermochte einst x86-Prozessoren lahm zu legen.)

=> Und er blogt unter Hackish.Codes D:

3

06.12.2014, 19:51

Gegen den Cache nutze ich shared_ptr.

Wie genau würde das mein beschriebenes Design verändern? Mir fehlt gerade die Vorstellung was du konkret meinst.

Vielleicht zur Konkretisierung: Meinem ResourceCache gebe ich einen std::string und er liefert mir die zugehörige Textur die dahinter steht; der String wird als Dateiname interpretiert und der Cache lädt die Textur ggf. von der Festplatte. Ansonsten hält er alle Texturen bis zu seinem Ableben fest und liefert mir Referenzen auf diese. Da ich die Referenzen direkt an die SFML-API weitergebe - ohne sie extra noch zu speichern - und der Cache solange lebt wie alle Objekte die ihn verwenden, passt das mit der Konsistenz der Lifetime.

LG Glocke

4

06.12.2014, 20:47

Die ResourceFactory als Singleton zu machen ist eigentlich keine allzu schlechte Sache.

Die Ressourcenverwaltung möchte ich nicht statisch machen, weil ich davon zwei Stück möchte: Das eine Exemplar beinhaltet allgemein verwendete Ressourcen (Spieleinstellungen, GUI Grafiken etc.) und der zweite alle Sitzungsressourcen (Tiles, Sprites etc.). Die Sitzungsressourcen sollen nach Spielende (wenn man ins Menü zurückkehrt) freigegeben werden, so dass sich das Spiel zwischen zwei Sitzungen speichermäßig aufräumen kann. Ansonsten würden Grafiken, die in einer früheren Sitzung gebraucht wurde und in der aktuellen Sitzung nicht gebraucht werden, bis zum Programmende im Speicher verweilen.

BlueCobold

Community-Fossil

Beiträge: 10 738

Beruf: Teamleiter Mobile Applikationen & Senior Software Engineer

  • Private Nachricht senden

5

07.12.2014, 09:04

Die ResourceFactory als Singleton zu machen ist eigentlich keine allzu schlechte Sache.
Solange es sich nicht um Logs handelt, ist das eigentlich schon eine schlechte Idee. Außer Du meinst tatsächlich nur die Factory, die Objekte nur erzeugt und nicht auch noch den entsprechenden Cache dazu, der die Objekte auch irgendwie weiterhin referenziert. Letzteres ist als Singleton eine Katastrophe - wie fast alles, was man nur aus Faulheit nicht übergeben und stattdessen lieber als eine globale Variable unter Singleton verpackt verwendet. Wieso dann nicht gleich eine globale Variable? Im Falle einer Factory wären rein statische Methoden sogar irgendwie noch sinnvoller. Denn wozu überhaupt eine Instanz auf dem Heap erzeugen für eine Factory, die man eigentlich doch wirklich statisch verwenden will?
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]

6

07.12.2014, 10:02

Oder als Context, wie du schon gesagt hast. Aber das macht es komplexer.
Sowas wie LevelContext und ApplicationContext, die jeweils das gleiche Interface implementieren. Der LevelContext sollte dann einen ApplicationContext verlangen und daran weiter delegieren können.

Hmm ich werde mal drüber nachdenken.. Aber vielleicht wäre es design-technisch klüger meine Komponenten soweit zu verschlanken, dass sie kaum Elemente des Contexts brauchen; z.B. die Renderkomponente nur vom ResourceCache& abhängig machen, d.h. diesen konkret übergeben und mehr nicht. Die Renderkomponente braucht ja z.B. keinen Zugriff auf die ObjectFactory.
Analog: Ich habe eine Komponente, die über Spieleraktionen (z.B. CAST_SPELL) informiert wird und dann ein Feuerball-GameObject erzeugt. Diese Komponente bräuchte dann im Grunde nur Zugriff auf die ObjectFactory. Dass die ObjectFactory selbst wieder den ResourceCache kennen muss (um für das Feuerball-Objekt auch die Renderkomponente zu erzeugen usw. ^^) erzeugt mir dann einen tollen Abhängigkeitsbaum xD

Vielleicht sollte ich mir diesen erstmal skizzieren und überlegen, wie ich ihn verkleinern kann :S

Solange es sich nicht um Logs handelt, ist das eigentlich schon eine schlechte Idee.

Zwecks Logs überlege ich sogar, ob ich nicht zwei globale Variablen verwende:

C-/C++-Quelltext

1
2
3
4
5
6
// hpp
extern Logger debug, warn;

// cpp
Logger debug("./debug.log");
Logger warn("./warnings.log");

Vorteil wäre, dass ich wie bei std::cout und std::cerr global darauf zugreifen kann. Meine Logger-Klasse ist auch so gebaut, dass ich sie operator<< anspreche, so dass die Verwendung im Endeffekt gleich zum Standard Output ist. Im Grunde sind die globalen std::cout etc. designtechnisch ja auch vertretbar (offensichtlich^^).
Da die Logger-Klasse selbst keine weiteren Abhängigkeiten innerhalb meines Projekts hat (nur std::ofstream innerhalb der std), sollte das die Kopplung nicht in die Höhe treiben.

Evrey

Treue Seele

Beiträge: 245

Beruf: Weltherrscher

  • Private Nachricht senden

7

07.12.2014, 11:46

Ich bin mir nicht sicher, ob das eine dufte Idee ist, Logger nach Kategorien zu trennen. Ein Error kann durch falsche Daten ausgelöst werden, über die schon bei einer Warning-Nachricht gewarnt wurde, deren Entstehung man hätte bei einer Debug-Nachricht nachvollziehen können. Habe ich eine Log-Datei für alles (oder von mir aus eine pro Programmausführung), sehe ich potenzielle Abhängigkeiten und Querverweise unmittelbar übereinander.

Zitat

Wie genau würde das mein beschriebenes Design verändern? Mir fehlt gerade die Vorstellung was du konkret meinst.
Na, jeder der z.B. eine Textur verwendet, besitzt sie im Prinzip. (Erstmal. Vielleicht baue ich das ja noch um, weil mir ein Manager dann doch lieber wird, oder so.) Dadurch wird die Textur entfernt, sobald niemand sie mehr haben will. Ein Ressourcen-Cache ist dazu da, Ressourcen am Leben zu erhalten, auch wenn niemand sie mehr haben will, für den Fall, dass später jemand doch wieder gern die Ressource hätte. Damit ließe sich ein Cache ganz einfach basteln, indem noch jemand einen shared_ptr greift und der RefCount dadurch nicht auf 0 fallen kann.

C-/C++-Quelltext

1
2
3
4
int main(int _argc, char** _argv) noexcept {
  asm volatile("lock cmpxchg8b %eax");
  return 0;
} // ::main
(Dieses kleine Biest vermochte einst x86-Prozessoren lahm zu legen.)

=> Und er blogt unter Hackish.Codes D:

Beiträge: 1 223

Wohnort: Deutschland Bayern

Beruf: Schüler

  • Private Nachricht senden

8

07.12.2014, 12:00

Ich persönlich habe den praktischen Sinn von Singletons nie so wirklich verstanden.
Wenn ich wirklich mal etwas global zugreifbar haben will, dann nehme ich eine globale Variable.
Ähnlich wie BlueCobold vorgeschlagen hat und Glocke für Logging vor hat.

Auch meine "Factories" sind in der Regel einfach globale Variablen. Manchmal habe ich dafür sogar noch für jeden Typ eine globale Variable mit weiteren (statischen und konstanten) Informationen den ich dann meistens "Descriptor" nenne. (Darein kann zum Beispiel der Name der Klasse kommen oder zum Beispiel bei meinen Klassen um Grafiken zu laden die typischen Dateiendungen des Dateityps.)

9

07.12.2014, 12:07

Ich bin mir nicht sicher, ob das eine dufte Idee ist, Logger nach Kategorien zu trennen. Ein Error kann durch falsche Daten ausgelöst werden, über die schon bei einer Warning-Nachricht gewarnt wurde, deren Entstehung man hätte bei einer Debug-Nachricht nachvollziehen können. Habe ich eine Log-Datei für alles (oder von mir aus eine pro Programmausführung), sehe ich potenzielle Abhängigkeiten und Querverweise unmittelbar übereinander.

Interessanter Punkt :thumbup:

Na, jeder der z.B. eine Textur verwendet, besitzt sie im Prinzip. (Erstmal. Vielleicht baue ich das ja noch um, weil mir ein Manager dann doch lieber wird, oder so.) Dadurch wird die Textur entfernt, sobald niemand sie mehr haben will. Ein Ressourcen-Cache ist dazu da, Ressourcen am Leben zu erhalten, auch wenn niemand sie mehr haben will, für den Fall, dass später jemand doch wieder gern die Ressource hätte. Damit ließe sich ein Cache ganz einfach basteln, indem noch jemand einen shared_ptr greift und der RefCount dadurch nicht auf 0 fallen kann.

Okay, so hatte ich es vermutet. Aber wie gibst du der Komponente dann seine shared_ptr, bzw. wie holt sie sich die? Irgendwer muss die Ressource ja laden und als shared_ptr zur Verfügung stellen (also der RessourceCache/Manager). Wie greifst du auf ihn zu - übergibst du ihn direkt?

BlueCobold

Community-Fossil

Beiträge: 10 738

Beruf: Teamleiter Mobile Applikationen & Senior Software Engineer

  • Private Nachricht senden

10

07.12.2014, 18:32

Man kann sich drüber streiten, aber eine Lösung wäre schlecht (Singleton) und die andere gut.
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]

Werbeanzeige