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

Masi

Frischling

  • »Masi« ist der Autor dieses Themas

Beiträge: 5

Beruf: Elektroniker

  • Private Nachricht senden

1

21.07.2020, 13:46

Handling von Listen und der allgemeinen Struktur von Spielen

Hallo zusammen,

ich beschäftige mich schon längere Zeit mit der Programmierung von Spielen und habe ein paar Jahre mit "Blitzmax" programmiert, das ganze allerdings sehr laienhaft nach dem Motto "Hauptsache es funktioniert". Vor kurzem habe ich mich entschlossen dann doch mal auf C++ umzusteigen und das ganze ein wenig professioneller anzugehen. Neugier wie man es denn "richtig" mach. Jetzt habe ich das Buch von Heiko Kalista C++ für Spieleprogrammierer durchgearbeiten und bin momentan an meinem Projekt am arbeiten.
So jetzt zu meiner Frage: In dem Buch wird ein kleines Spiel im Asteroids Stil programmiert ich versuche hier mal kompakt die Struktur aufzuzeigen

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
class CGame
{
    CPlayer m_Player
    list<CAsteroid> m_AsteroidList
}

class CPlayer
{
    list<CShot> m_ShotList
}


Also der Spieler enthält hier die eigenen Schüsse, die dann später auf Kollision mit den Asteroiden geprüft werden.
In meinem Spiel wird es allerdings so aussehen das es verschiedene Teams gibt und jedem Schuss und jedem Spieler(egal ob KI oder Mensch) eine Team_ID zugewiesen wird und außerdem sollen die Schüsse auch nach Ableben des Schießenden noch erhalten bleiben. Ich dachte hier dann eher an folgende Lösung.

C-/C++-Quelltext

1
2
3
4
5
6
class CGame
{
    list<CPlayer> m_PlayerList
    list<CShot> m_ShotList
    list<CSprite> m_SpriteList
}


dann müsste ich mir nur noch Gedanken machen wie der Schuss der von den Spielern erzeugt wird in die Schussliste von CGame kommt.
Außerdem ist es von Nöten die Sprites zu sortieren, weil man in meinem Spiel auch in die Tiefen gehen kann. Also müsste ich auch die Sprites in eine gemeinsame Liste machen. Bin mir jetzt halt unsicher ob es Sinn macht die Listen in CGame einzubinden oder in ein Singleton, damit ich einfacher drauf zugreifen kann.

Ich hoffe ich hab das verständlich genug erklärt und das meine mini Codeschnipsel verdeutlicht haben worauf ich raus will!

Was ist denn eure Meinung zu der Sache?

Danke schonmal im Voraus!

2

22.07.2020, 03:05

Hallo :)

ja das sind so typische Sachen, die bei der Spieleentwicklung oft ein wenig dem zuwiederlaufen wie man in allen anderen Bereichen programmiert. Zum einen wegen Performance und zum anderen weil bei Spiele es oft vorkommt, dass an den erzeugten Daten/Objekte es viele Interessenten gibt und nicht nur einen oder zwei.

Verzei mir das ich nun viel schreiben werden. Ich will dir nicht einfach nur 5 Lösungs Zeilen hin werfen und sagen 'mach es so, das ist am besten' Punkt. Sondern ich sehe in deinen Überlegungen, so viel Talent, dass ich denke es hilft dir mehr, wenn du die Zusammenhänge erkennst und zukünftig dann selber immer Zielsicher die beste Lösung finden kannst ;)



Nehmen wir deine Idee mit m_ShotList und m_SpriteList in CGame. Eigentlich eine gute Idee. Doch CGame (dein GameController) wird dadurch mit etwas beauftragt, was eigentlich nicht seine Aufgabe ist. Alisa, räume dein Zimmer und deine Schminksachen auf, und wenn du schon dabei bist, sortiere bitte auch gleich meine Stifte bei meinem Schreibtisch und zwar mag ich es farbig Sortiert von hell nach dunkel, aber blau trotzdem als erstes weil den brauche ich meistens.
Kann man machen, aber 'weils nicht sein Ding ist', wird er diese Aufgabe niemals optimal lösen. Und wenn du schon ShotList und SpritList in ihm hast, dann werden zukünftig bei neuen Dingen sich erstrecht Lösungen aufdrängen, die dann auch wieder in CGame landen... Plötzlich hast du dann ein CGame welches irgendwie alles kann, aber alles was du dort aufrufst musst du mit vielen Funktionen und vielen verschiedenen Funktionsparametern, ganz genau 'erklären'.
Es wird mit der Zeit in deinem Spiel Aufgaben geben die wirklich zu CGame passen und dort rein sollten. Das wird dazu führen, das CGame immer mehr Bereiche bekommt um den es sich kümmern muss. Es wird unübersichtlich werden. Also solltest du dir immer genau überlegen, was wirklich dort rein passt. Sonst wird es zur Gottklasse/Gottobjekt.
Bei ganz kleinen Programmen, kann man sowas mal machen. Denn es ist die schnellste (und sogar übersichtlichste Lösung) wenn es etwas wirklich kleines ist. Maximal 'Taschenrechner der nur Grundrechenarten kann'. Sobald es aber auch nur ein bisschen größer wird, sollte man sowas lieber vermeiden.

Grundsatz: Jede Klasse sollte möglichst nur für eine Sache zuständig sein. Seine Domain.



Ok, du könntest nun eine Zwischenschicht bauen. Zwischen CGame und den Restlichen Dingen die im Spiel ablaufen. z.B. SceneController. Und SceneController kann dann ShotList und SpriteList tragen. Dazu dann noch paar Verwaltungsfunktionen, wie 'Spieler in Liste hinzufügen' und '..aus Liste löschen' und '...such/gib mir Spieler X aus der Liste' ....
Aber spätestens hier sollte es dann aufhören. In Liste rein/raus/suchen .. also die 3 Grundfunktionen, die sind gerade noch mit schmerz Ok. Aber alle weiteren Funktionen, sollten dann die Listen-Objekte selber können.
Was ich meine Ist: SceneController::AddShot(#) ... und CShot::CalcDamage()
Damit wäre CGame entlastet, und du hätest trotzdem noch eine (nicht zusehr überfüllte) Klasse die deine Anlaufstelle ist für die Verwaltungssachen. Aber du kannst dir schon selber ausrechnen, wenn man da nicht aufpasst und sich bei jedem neuen Ding überlegt ob es wirklich in SceneController rein gehört oder besser woanders hin... dann könnte es passieren das SceneController mit der Zeit zu mächtig/gross wird. Das Vieze-Gottobjekt.



Warum ? nun also nicht sowas wie: CManagerShotList , CManagerPlayerList und CManagerSpriteList
CManagerPlayList wäre dann eine Art Wraper um die m_Player. Es wäre sowas wie eine Spezialisierte 'List' Klasse.
So könnte dann CManagerPlayList funktionen haben für add/remove/get .. aber auch sowas wie GetPlayerMitBestenPunktestand() oder GetAllPlayersMitMehrAls0Lebenspunkte()
Dinge die zum selben Bereich gehören, wären zusammengefasst. Und dennoch wären Dinge die nicht zusammen gehören, getrennt.

Anfangs wird du zwar viele extrem kleine Klassen haben (die jeweils nur vielleicht 3 bis 5 Zeilen groß sind) und deswegen wird es dir unüberichtlicher vorkommen. Das ist genau der Grund warum bei absoluten Mini-Projekten so ein 'aufteilen' sogar unübersichtlicher sein kann als die Gottklasse die bei dem Mini-Projekt noch nicht Zeit hate um zu groß zu werden.
Doch wenn du mit der Zeit dann immer mehr die Details deines Spiels programmierst, werden auch diese kleinen Klassen viel mehr Funktionen bekommen. Und spätestens dann wird es so viel Übersichtlicher und viel leichter zu programmieren sein, als die Gottklasse.

Als Zwischenschritt bietet sich hier dann noch an, das du zwar deine Listen auf 3 Klassen aufteilst. Aber diese 3 Klassen evtl. nicht von anfang an auf 3 Quellcode Dateien verteils, sondern vielleicht ersteinmal in eine einzige Quellcodedatei. Ist zwar eigentlich Programmiertechnisch nicht so sauber und in ein paar Wochen wirst du vermutlich von alleine es dann auch auf mehere Dateien aufteilen wollen. Doch gerade wenn man vom Gefühl her sich an diese 'vielen kleinen Klassen' noch nicht gewöhnt hat (man fühlt sich unwohl, es kommt einen sehr unübersichtlich vor), dann ist das eine gute möglichkeit.



Nun deine Idee mit Singleton. Übrigens guter Gedanke ;)
Der Singleton ist eigentlich davon nun Unabhängig. Er hat nicht wirklich damit zu tun, ob du nun alles in CGame rein haust, oder irgendwie anders aufteilst.
Das eine ist 'was/wo' du die Listen ablegst und wo du die Funktionen der Listenverwaltung rein schreibst.
Das andere (Singleton) betrifft hingegen 'wie' wird zugegriffen?
Also wie greift ein Objekt eines bestimmten Typs, auf Objekte/Listen einer ganz anderen/fremden Sachen zu? Der Singleton dient der Domain-übergreifenden Verknüpfung.

Im Grunde ist es oft Geschmackssache. Man kann jedem Objekt das Zugang zu einen der woanders gepflegten Listen (oder Managerklassen) braucht, entweder direkt eine Referenz Variable mitgeben welche auf die 'Globale Liste'/Managerklasse zeig oder eben Singleton's machen.

Die Referenz Variable wäre eher in Richtung ordentlicher Objektorientiertheit. Hat aber mehrere kleinere Nachteile:
- Wenn du viele Objekte mit der Zeit hast die eine Referenzvariable auf etwas anderes halten, wird der Speicherzugriff evtl. relevant. Der Speicherverbrauch ansich wäre dabei noch nicht einmal das Problem (der wäre sehr klein). Jedoch erhöht das die Speicherfragmentierung. Wenn man sowas ganz viel macht, dann bekommt man irgendwann Mikrorukler, oder komische ganz kurze Hänger, bei z.B. Level-Neustart.
- Wenn ein bestimmtes Objekt/Klasse nun nicht nur einen Verweis auf eine andere Liste/Klasse braucht, sondern sagen wir auf alle 3 (CManagerShotList , CManagerPlayerList und CManagerSpriteList), dann wird alleine diese wachsende Liste der Membervariablen in dem Objekt, schon bisschen unübersichtlicher.
- Und diese Referenzvariablen müssen auch beim erzeugen der Objekte, z.B. dem Kontruktor mit übergeben werden. Und dort wiederum musst du paar Zeilen programmieren für das zuweisen der Variablen...
Und der Aufwand dann bei jeder Klasse, die interesse an einer der anderen ManagerKlassen (oder anderen Listen) hat...

Gibt noch mehr sehr kleine Nachteile (die erst dann eine Rolle speielen, wenn man viele Dinge miteinander verknüpfen muss). Und auch diese drei genanten Nachteile sind nun nicht so schlimme Nachteile. Doch es wird wohl klar, das es nicht unbedingt für alle Probleme sooo die super Lösung sein ist.
Und hier kann man halt evtl. dann auch einfach Singletons verwenden. Denn diese haben genau diese Nachteile eben nicht. Sondern im Gegeteil, haben sie hier sogar ihre großen Stärken.
Bei Singletons solltest du aber immer im Hinterkopf behalten, sie dürfen/sollten nur dann verwendet werden, wenn wirklich ganz sicher niemals mehr als eine Instanz gebraucht wird!

Als Beispiel: Bei einem 'TastaturManager' wirst du vermutlich niemals einen Grund finden, warum du zwei verschiedene TastaturManager gleichzeitig brauchen könntest. Auch bei CManagerShotList , CManagerPlayerList und CManagerSpriteList vermutlich nicht.
Aber bei z.B. InventarList wird es vielleicht schon Gefährlich. Ist und bleibt es garantiert ein Singleplayerspiel, kann man aus InventarList auch einen Singleton machen (einen Manager sozusagen). Doch er wird zum Problem wenn aus dem anfänglich gedachten Singleplayer, dann ein kleiner Mehrspieler wird... wie in deinem Fall? z.B. ein Hotseat Mehrspieler, also ein Mehrspieler bei dem 2 Leute an einem Bildschirm sitzen und gleichzeitig z.B. die Tastatur nutzen, einer links AWSD und einer rechts Pfeil rauf/runter/links/rechts. Dann hast du plötzlich mehrere Spieler die jeweils ein eigenes Inventar brauchen. Dann passt die Lösung mit dem 'einen einzigen InventarList Singleton' nicht mehr.

Singleton sind halt immer Global. Das kann gut sein. Genau deswegen nimmt man sie ja auch. Wenn man es bewust überlegt verwendet. Aber sie sind eigentlich ein Hack des Objektorientierten Konzepts!
Sie haben Vorteile, und oft überwiegen sie auch. Gerade bei der Spieleprogrammierung finde ich sind sie für bestimmte Probleme, oft die mit Abstand beste Lösung. Deswegen verwende ich sie dort gerne. Ausserhalb der Spieleprogrammierung verwende ich sie aber fast nie. Liegt an der Konstelation: Viele Dinge der einen Sache, die interesse haben/zugang brauchen zu anderen Klassen (Domains, Themenbereiche). Und zugleich von diesen Domain/Themenbereichen auch garantiert nur eine einzige Instanz gebraucht wird.



Bei der Programmierung ganz allgemein, gibt es oft nicht die beste Lösung für einen ganz bestimmten Fall. Und manchmal kann es sogar Situationen geben, wo man eben gegen die Objektorientiertheit absichtlich verstößt. Oder sogar absichtlich Gottobjekte baut. Das ist auch OK. Aber mann sollte sich immer vorher genau darüber gedanken machen und überlegen welche Lösungen es gibt, welche Nachteile/Vorteile für das aktuelle Problem überwiegen, und sich dann ganz bewust entscheiden. Dann ist es auch absolut OK. Egal welche Lösung es dann wird. Zur Katastrophe wird es immer nur, wenn man ohne die Bewuste entscheidung ('hat sich durch Zufall irgendwie so ergeben') total vom sauberen Standard abweicht.
Von daher Masi, absolut super von dir! Du hast sofort etwas in deinem Code bemerkt und dir angefangen gedanken zu machen, noch bevor 'es sich Zufällig so ergeben hat und dann mit der Zeit zur Katastrophe ausgewuchert ist'. In dir steckt wohl sehr viel Talent und das richtige Gespür, für einen super Programmierer/Entwickler.

Jonathan

Community-Fossil

  • Private Nachricht senden

3

22.07.2020, 10:09

Ich würde von Singletons abraten. Es sind halt globale Objekte die garantieren, dass es nur genau eine Instanz von ihnen geben kann, aber in der Praxis habe ich diese Garantie noch absolut nie gebraucht. Man will vielleicht oft nur ein Objekt haben, aber es würde halt nicht unbedingt etwas schlimmes passieren, wenn man mehrere hätte, und man bezahlt damit halt mit einem irgendwie hässlichen Interface.

Im obigen Beispiel spricht für mich wenig dagegen, wenn jedes Spielobjekt einen Rückzeiger auf das Game-Objekt hat. Das kostet nicht wirklich viel und es ist immer klar, was im Code passiert. Ich hatte mal den Fall, das derartige Zeiger über mehrere Ebenen runtergereicht werden müssen, was zu einer Menge zusätzlichen Code führt und globale Objekte attraktiv erscheinen lässt. Aber meines Erachtens nach ist der Hauptnachteil globaler Objekte nicht, dass man sie von überall aus Versehen ändern kann, sondern dass die Initialisierungsreihenfolge (und die Zerstörungsreihenfolge) ein echtes Problem werden kann, wenn man mehrere Klassen hat, die irgendwie voneinander abhängen. Und das sind Fehler, die hässlich zu debuggen sind.
Lieber dumm fragen, als dumm bleiben!

Masi

Frischling

  • »Masi« ist der Autor dieses Themas

Beiträge: 5

Beruf: Elektroniker

  • Private Nachricht senden

4

22.07.2020, 10:19

Hallo Audori,

erstmal vielen Dank dafür, das du dir die Zeit genommen hast um mir so eine ausführliche Antwort zu verfassen!

Das ich mir so viele Gedanken über die Struktur machen kommt daher, das ich ja kein kompletter Neuling bin und schon auf die harte Tour erfahren musste was passiert wenn man unüberlegt einfach drauf los programmiert.
Es ist wie du es sagst, bei kleineren Projekten fällt sowas weniger ins Gewicht, aber bei größeren Projekten verliert man einfach die Übersicht und einem vergeht die Lust daran den Code noch zu erweitern.
viel learning by doing und trial and error :).

Also ich werde vermutlich jetzt so vorgehen, das ich mir für die genannten Listen ein Manager als Singleton anlegen werde.
Aber falls ich mich jetzt doch für die Manager variante entscheiden würde, wie würde das dann aussehen? so in etwa?

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CGame
{
    CManagerShotList m_ManagerShotList;
    
    Run();//GameLoop
}

class CPlayer
{
    CManagerShotList* m_ManagerShotList;

    Init(CManagerShotList* ManagerShotList)
    {
        m_ManagerShotList = ManagerShotList;
    }

}


Also dann würde Quasi der Manager in meinem GameController abgelegt sein und dann würde z.B. beim Initialisieren vom Spieler ein Zeiger auf den Manager an das Player Objekt gegeben und dort gespeichert werden, dann könnte der Spieler ohne Probleme neue Shot Objekte erstellen.

Ich werde vermutlich noch den ganzen Tag darüber nachdenken bevor ich meine finale Entscheidung treffe :D

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »Masi« (22.07.2020, 10:33)


Jonathan

Community-Fossil

  • Private Nachricht senden

5

22.07.2020, 10:29

Also der Codeentwurf sieht eigentlich ok aus. Anmerkungen:

- Statt Zeiger kannst du Referenzen verwenden. Die garantieren, dass sie initialisiert sind und sind ggf. besser zu lesen (nicht nur wegen . vs -> sondern auch weil man weiß dass Referenzen nicht NULL sind und damit keine optionalen Parameter sind)
- Vielleicht sollte der Zeiger (die Referenz) direkt auf das Game Objekt zeigen. Es gibt ja später vielleicht noch mehr Manager als nur den Shot-Manager
- Überlege dir statt std::list lieber std::vector zu benutzen: https://isocpp.org/blog/2014/06/stroustrup-lists
Lieber dumm fragen, als dumm bleiben!

Masi

Frischling

  • »Masi« ist der Autor dieses Themas

Beiträge: 5

Beruf: Elektroniker

  • Private Nachricht senden

6

22.07.2020, 12:55

Ok Alles klar! Das mit den Vektoren werde ich mir mal anschauen.

Also zum Verständnis, eine Referenz bietet sich hier an weil die Schussliste
Nicht optional ist ansonsten sollte ich Zeiger verwenden, oder?

Sonst stellt sich mir die Frage wann ich überhaupt Zeiger verwenden sollte.

Hat mir alles schon viel weiter geholfen!
Kann es kaum erwarten weiter zu machen :)

Jonathan

Community-Fossil

  • Private Nachricht senden

7

23.07.2020, 00:04

Sonst stellt sich mir die Frage wann ich überhaupt Zeiger verwenden sollte.

Beispielsweise wenn der Spieler einen Gegner anvisieren kann um in seine Richtung zu zielen. Du würdest dann einen Zeiger auf den Gegner speichern der sowohl Null sein kann, wenn gerade niemand in der Zielerfassung ist, als auch jederzeit geändert werden kann, wenn ein neuer Gegner anvisiert wird. Beides ist so mit Referenzen nicht möglich.
Lieber dumm fragen, als dumm bleiben!

Werbeanzeige