Fragen zur Spielprogrammierung (Klassenaufbau)



  • hallo allerseits,

    um meinem ziel spiele zu programmieren näher zu kommen hab ich entschlossen mal einen breakout klon zu programmieren.

    ich hatte mir im vorfeld folgenden klassenaufbau überlegt (wer nicht lesen möchte kann sich auch das bild im anhang anschaun 😉 ): es existiert eine zentrale Game klasse, die das spiel managet. sie nutzt die klassen PlayManager (die das aktive spiel verwaltet), MenuManager (die sichs ums menu kümmert) und den EditorManager (die wie der name schon sagt einen editor bereit stellen soll)

    was mich jetzt aber zu folgenden fragen führt:

    • im endeffekt sieht meine hauptprogramm dann nur noch so aus:
    Game game( );
    while( game.IsOpened() ) {
    	game.Draw();
    	game.MoveBall();
    	game.HandleInput();
    	game.ProcessEvents();
    }
    

    was ich persönlich für ziemlich 🕶 aber zugleich sinnfrei halte bzw bei mir die frage auswirft ob das ganze wirklich sinn macht; sollte ich lieber die funktionen der game klasse nehmen und jegliche funktionionalität in die mainfunktion packen, denn schließlich wird es ja nie mehr als eine game instanz geben?!?

    • desweiteren stelle ich fest dass es situation gibt wo ich die instanz des players benötige, sie aber nicht habe, also eine referenz bzw zeiger übergeben müsste. wäre es nicht viel einfacher ein globales objekt Player zu haben?
      Das gleiche gilt auch für die text-strings der oberen zeile (siehe anhang): jeder manager kann sie auf seine art nutzen (bsp: play nutzt mitte um levelname anzuzeigen, menu für aktuellen menupunkt usw..) wäre es nicht einfacherer 3 globale textobjekte zu haben?!?
    • als letztes: ich nutze eine BaseClass um es möglich zu machen, dass jede klasse einfach und gut dargestellt seinen senf auf der konsole ablassen kann (jede klasse legt hierzu "seinen" namen fest), sodass nachher sowas bei rum kommt:
    INFO: ResourceManager: Alle Resourcen geladen
    ERR : Ball: Konnte Bildressource nicht laden!
    

    ist dieses vorgehen euer meinung nach gut/richtig, oder gibt es hier auch alternativen?

    so.. nach dem ihr euch hier durch gequält habt fänd ichs echt klasse wenn mir einige mir weiter helfen könnten und/oder mal beschreiben könnten wie sie selbst solche probleme gelöst bzw umgangen haben. natürlich dürft ihr auch verbesserungsvorschläge bezüglich des klassenmodells loslassen 😉
    wenn etwas unklar ist (was denke ich bei solchen vielen problemen leicht sein kann), dann doch einfach fragen 😉

    ich nutze SFML als framework; nur falls das in ieinen form von bedeutung sein sollte...

    thanks in advance
    tibo

    PS: bitte verzeiht die falsche verwendung der uml assoziationen; ich hab es zu spät gemerkt und keine zeit alle assoziationen neu zu machen..

    Klassendiagramm: http://a.imageshack.us/img833/8299/klassendiagramm.png
    Programmentwurf: http://a.imageshack.us/img94/3979/spielentwurf.png

    EDIT: Passt das ganze auch in das allgemeine C++ Forum?



  • Ich finde es schon relativ gut gelungen.

    Deine Main-Schleife sieht man in dieser Art auch öfters.

    Wenn du dich selbst als Anfänger/Lernenden betrachtest, würde ich dir empfehlen das exakt so umzusetzen.
    Du kannst dir vorher notieren, wo du Probleme siehst und dann setzt du es um.
    Hinterher analysierst du dann alles. In der Realität hat man auch nicht immer völlige Gestaltungsfreiheit.



  • Danke 🙂 Stimmt du hast vollkommen recht.. Es würde mich nur sehr wurmen wenn ich mir die Mühe mache alles schön so zu Programmieren und am Ende dann feststelle, dass wenn ich mich ein wenig umgehört hätte, mir ein riesiger (und unnötiger) Aufwand erspart geblieben wäre 😉
    Aber ich denk dass ich das ganze dann bald so umsetzten werde..

    EDIT: Falls jemand trotzdem noch etwas beizustreuen hätte, dann kann er das gerne tun..



  • TiBo schrieb:

    hallo im endeffekt sieht meine hauptprogramm dann nur noch so aus:

    Game game( );
    while( game.IsOpened() ) {
    	game.Draw();
    	game.MoveBall();
    	game.HandleInput();
    	game.ProcessEvents();
    }
    

    Ich persönlich mache das meist recht schematisch (ich habe so einen besseren Überblick):

    1. Eingabe
    2. Logik (unter Verwendung der Eingabe)
    3. Ausgabe (Darstellung der Logik)
    

    Aber es spielt natürlich keine Rolle, da die Schleife immer wieder läuft.

    TiBo schrieb:

    desweiteren stelle ich fest dass es situation gibt wo ich die instanz des players benötige, sie aber nicht habe, also eine referenz bzw zeiger übergeben müsste. wäre es nicht viel einfacher ein globales objekt Player zu haben?

    Ja, es wäre auf den ersten Blick einfacher. Aber sehrwahrscheinlich keine gute Entscheidung. Siehe Gründe gegen globale Variablen.

    TiBo schrieb:

    [*]als letztes: ich nutze eine BaseClass um es möglich zu machen, dass jede klasse einfach und gut dargestellt seinen senf auf der konsole ablassen kann

    Ich verstehe nicht, was du meinst. Was macht BaseClass , welche Eigenschaften der anderen Klassen vereinigt sie, um als deren Basisklasse verwendet zu werden?

    TiBo schrieb:

    natürlich dürft ihr auch verbesserungsvorschläge bezüglich des klassenmodells loslassen 😉

    Sieht eigentlich nicht schlecht aus, wie gesagt weiss ich nur noch nicht genau, was BaseClass macht. Ich würde teilweise andere Namen wählen, z.B. statt "DrawItem" vielleicht "Drawable" oder "GameObject" oder sowas. Die vielen Manager würde ich auch zu aussagekräftigeren Begriffen umbenennen. Ich habe zwar selbst auch ein paar Manager, aber wo möglich wähle ich ein anderes Wort. Was spricht z.B. gegen "Menu" und "Editor"?

    Generell ist es sicher nicht schlecht, wenn du mit der Zeit Grafik und Logik trennst. Also dass du z.B. ein reines Logik-Objekt Player hast, das Trefferpunkte, Position, Geschwindigkeit, getragene Gegenstände etc. speichert. Die Grafik (in SFML wahrscheinlich mittels sf::Sprite ) regelst du an einem anderen Ort. Das kann eine eigene Klasse sein, allerdings ist das oft Overkill. Ich habe in einem Spiel von mir eine Renderer-Klasse, die eine Draw() -Funktion für verschiedene Logik-Objekte überlädt und diese entsprechend zeichnet. Der Vorteil bei dieser Separation ist Abstraktion und Zentralisierung: Du kannst den einen Teil austauschen oder verändern, ohne den anderen anzutasten, zudem musst du beim Ändern globaler Grafik-Behandlungen nicht jede einzelne Klasse ändern. Aber das ist nur eine von vielen Möglichkeiten.

    TiBo schrieb:

    EDIT: Passt das ganze auch in das allgemeine C++ Forum?

    Designfragen generell ja. Wobei das jetzige Forum im Bezug auf Spiele sicher keine schlechte Wahl ist. Im C++-Forum findest du halt auch einige Theoretiker, die selbst noch nie Spiele programmiert haben, und vielleicht weniger pragmatische Ansätze haben. 😉



  • Nexus schrieb:

    Ja, es wäre auf den ersten Blick einfacher. Aber sehrwahrscheinlich keine gute Entscheidung. Siehe Gründe gegen globale Variablen.

    Was für Alternativen gibt es denn da? Sollte die PlayManager Klasse eine statische Funktion haben die den Zeiger auf das statische Play-Objekt zurückgibt?

    Nexus schrieb:

    Ich verstehe nicht, was du meinst. Was macht BaseClass , welche Eigenschaften der anderen Klassen vereinigt sie, um als deren Basisklasse verwendet zu werden?

    Ok.. ich poste mal die BaseClass, dann wird das hoffentlich klar 😉

    class BaseClass {
    private:
    	// Saves whether currently STAT msgs are printed
    	static bool StatusPrint;
    	// Name of class
    	string m_Name;
    protected:
    	// Set name
    	void SetName( string name );
    
    	// Writes msg an normal INFO msg to screen
    	void Debug( string msg ) const;
    	// Writes msg an STAT msg to screen; last indicates whether procedure completed
    	void Status( string msg, bool last = false ) const;
    	// Writes msg as ERR msg to cerr
    	void Error( string msg ) const;
    public:
    	BaseClass( string name = "" );
    	virtual ~BaseClass();
    };
    
    bool BaseClass::StatusPrint = false;
    
    // Set name
    void BaseClass::SetName( string name ) {
    	m_Name = name;
    }
    
    // Writes msg an normal INFO msg to screen
    void BaseClass::Debug( string msg ) const {
    	if( StatusPrint ) {
    		cout << endl;
    		StatusPrint = false;
    	}
    	cout << "INFO: ";
    	if( m_Name != "" )
    		cout << m_Name << ": ";
    	cout << msg << endl;
    }
    // Writes msg an STAT msg to screen; last indicates whether procedure completed
    void BaseClass::Status( string msg, bool last ) const {
    	cout << "STAT: ";
    	if( m_Name != "" )
    		cout << m_Name << ": ";
    	cout << msg << '\r';
    	cout.flush();
    	if( last )
    		cout << endl;
    	StatusPrint = !last;
    }
    // Writes msg as ERR msg to cerr
    void BaseClass::Error( string msg ) const {
    	if( StatusPrint ) {
    		cout << endl;
    		StatusPrint = false;
    	}
    	cerr << "ERR : ";
    	if( m_Name != "" )
    		cerr << m_Name << ": ";
    	cerr << msg << endl;
    }
    
    BaseClass::BaseClass( string name ) {
    	SetName( name );
    }
    BaseClass::~BaseClass() {
    }
    

    Diese Funktionen Debug, Status, Error werden dann von den abgeleiteten Klassen genutzt.. Sinn ist halt, dass immer auch der Klassenname ausgegeben wird.

    Nexus schrieb:

    Generell ist es sicher nicht schlecht, wenn du mit der Zeit Grafik und Logik trennst. Also dass du z.B. ein reines Logik-Objekt Player hast, das Trefferpunkte, Position, Geschwindigkeit, getragene Gegenstände etc. speichert. Die Grafik (in SFML wahrscheinlich mittels sf::Sprite ) regelst du an einem anderen Ort. Das kann eine eigene Klasse sein, allerdings ist das oft Overkill. Ich habe in einem Spiel von mir eine Renderer-Klasse, die eine Draw() -Funktion für verschiedene Logik-Objekte überlädt und diese entsprechend zeichnet. Der Vorteil bei dieser Separation ist Abstraktion und Zentralisierung: Du kannst den einen Teil austauschen oder verändern, ohne den anderen anzutasten, zudem musst du beim Ändern globaler Grafik-Behandlungen nicht jede einzelne Klasse ändern. Aber das ist nur eine von vielen Möglichkeiten.

    sf::Sprite beinhaltet schon eine Position. Wie kann ich das dann auseinanderhalten?
    Wie darf ich das mit der Renderer-Klasse verstehen?



  • TiBo schrieb:

    Ok.. ich poste mal die BaseClass, dann wird das hoffentlich klar 😉

    Ah, es geht also nur ums Loggen. Dann wähle doch auch einen entsprechenden Namen, z.B. "Logger" oder "Loggable". Aber "BaseClass" sagt so ziemlich gar nichts aus...

    TiBo schrieb:

    Sollte die PlayManager Klasse eine statische Funktion haben die den Zeiger auf das statische Play-Objekt zurückgibt?

    Wie würde sich das semantisch von globalen Variablen unterscheiden?

    void SetName( string name );
    

    Du kopierst den Parameter unnötig. Nimm Referenzen auf const . Das gilt auch für die anderen ähnlichen Fälle.

    Noch dazu ist es in meinen Augen ein Designfehler, für den Klassennamen einen Setter einzurichten, da sich der Name nicht ändert. Schon den Klassennamen pro Objekt zu speichern finde ich grenzwertig. Aber wenn du das tust, dann initialisiere ihn im Konstruktor und erlaube keine nachträgliche Änderung.

    void Debug( string msg ) const;
    

    Ich bin mir nicht sicher, ob das const hier wirklich sinnvoll ist, auch wenn das Objekt selbst nicht verändert wird. Darf ein konstantes Objekt Logs ausgeben?

    BaseClass( string name = "" );
    

    Hier legst du ein lokales Objekt an, das unmittelbar wieder zerstört wird. Die Membervariablen lässt du komplett unberührt. Verwende für Initialisierungen die Konstruktor-Initialisierungsliste (wobei diese bei leeren String nicht zwingend nötig ist, aber der Übersicht halber von Vorteil sein kann).

    virtual ~BaseClass();
    

    Halte dich an die Regel der Grossen Drei: Destruktor, Zuweisungsoperator und Kopierkonstruktor immer gemeinsam deklarieren oder gar nicht. Die letzten beiden kannst du auch verbieten, was in polymorphen Kontexten oft sinnvoll ist. Dazu machst du sie private und stellst keine Definition bereit.

    TiBo schrieb:

    sf::Sprite beinhaltet schon eine Position. Wie kann ich das dann auseinanderhalten?
    Wie darf ich das mit der Renderer-Klasse verstehen?

    Wie gesagt: "Mit der Zeit". Ich würde jetzt nicht alles überstürzen. Es ist auch gut, wenn du eine Weile verschiedene Designs ausprobierst, dann erkennst du automatisch ihre Stärken und Schwächen.

    Was ich meinte, wäre dass das Sprite (Grafik) nicht mehr in die Player -Klasse (Logik) gehören würde. Der Player hätte eigene Attribute, beispielsweise als 2D-Vektoren. Bei der Position ist der Fall ein wenig unglücklich, da du Information verdoppeln müsstest, allerdings bist du damit viel flexibler. Wenn zum Beispiel eine Spieler-Geschwindigkeit dazukommt, brauchst du ohnehin einen separaten sf::Vector2f . Wenn du plötzlich planst, statt float einen anderen Datentyp für Positionen zu verwenden, kannst du das problemlos tun. Du bist damit nicht an sf::Sprite und damit an dessen Einschränkungen gebunden. Vielleicht verwendest du stattdessen plötzlich eine andere Klasse (die beispielsweise Animationen darstellen kann), und mit getrennter Logik und Grafik wäre so ein Wechsel sehr einfach möglich.

    Was verstehst du an der Renderer-Klasse nicht? Der entscheidende Punkt ist wie erwähnt die Funktionsüberladung für unterschiedliche Logik-Klassen.



  • Sry, ich sollte nach einem anstrengenden Tag abends nicht mehr zu schnell antworten^^

    Nexus schrieb:

    Was verstehst du an der Renderer-Klasse nicht? Der entscheidende Punkt ist wie erwähnt die Funktionsüberladung für unterschiedliche Logik-Klassen.

    Die Frage ist ziemlich berechtigt.. Tut mir leid..

    // Logikobjekte
    class Player;
    class Ball;
    
    class Renderer {
        void Draw( const Player& player );
        void Draw( const Ball& ball );
    }
    

    Mhm.. das macht die Sache wirklich ziemlich einfach.. So bräuchte auch nicht jedes Objekt einen Zeiger auf das Fenster zu haben 🙂 Danke, das hilft mir sehr weiter 👍


Anmelden zum Antworten