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

30.08.2010, 21:02

Fragen zur Spielprogrammierung (Klassenaufbau)

Hallo allerseits,

um meinem Ziel Spiele zu programmieren näher zu kommen hab ich mich 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:

    C-/C++-Quelltext

    1
    2
    3
    4
    5
    6
    7
    
    Game game( );
    while( game.IsOpened() ) {
        game.Draw();
        game.MoveBall();
        game.HandleInput();
        game.ProcessEvents();
    }
    Was ich persönlich für ziemlich :cool: 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 immer 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:

    Quellcode

    1
    2
    
    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 von euch 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/Fragen 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
ITiBoI

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:

(Link)

Programmentwurf:

(Link)

2

30.08.2010, 21:28

Mhh. Das, was mir hierbei am stärksten auffällt, ist deine BaseClass. Ich persönlich finde es etwas zu viel des Guten, wenn du so eine BasisKlasse nutzt, damit jeder was auf der Konsole ausgeben kann. Erstens, weil man sowieso mit std:: auf der Konsole Dinge ausgeben kann, zweitens, weil dann jede Klasse des Spiels davon erben müsste. Ich würde an deiner Stelle eine Logfile als Singleton implementieren. Diese kannst du dann überall, wo du sie benötigst aufrufen, Dinge ausgeben und das ganze schön als .txt oder sogar als .html speichern. Das sieht nicht nur besser aus, du hast außerdem noch nach Abschluss des Spiels die Möglichkeit, die Logfile zu begutachten und evt. Fehler zu entdecken. Du musst also nicht immer die Konsole geöffnet halten.
gruß Loki

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

3

30.08.2010, 21:39

Ich würde die BaseClass auf jeden Fall loswerden. Wenn du wirklich ein Log brauchst würd ich dafür eine Log Klasse machen (oder einfach einen std::ostream nehmen) und jedem Objekt das Ausgaben ins Log machen soll eine Referenz auf ein Log mitgeben. Außerdem würd ich mir überlegen ob ein Movable wirklich ein DrawItem ist (d.h. davon erben soll) oder ob es sich dabei nicht um zwei unabhängige Konzepte handelt. Ich würde auch hinterfragen ob Ball und Player wirklich DrawItems sein und nicht besser welche haben sollten. Nachdem du sowieso nur Ball und Player haben wirst könnte man zumindest auf Movable eigentlich ganz verzichten. Welchen Zweck verfolgen die 4 Manager Klassen genau?

Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von »dot« (30.08.2010, 21:48)


4

30.08.2010, 22:25

Also.. zuerst einmal zur Loggen: Ich muss gestehen das mir der Begriff Singleton nicht geläufig ist; Werde mich mal einlesen, nach der allgemeinen Beschreibung würde ich sagen dass das auch für die Game-Class nützlich wäre..
Die Frage nun warum ich auf die BaseClass ist der Gedanke dass ich gerne den Herkunftsort der Nachricht mit geloggt hätte; also jede Klasse einen offiziellen "Namen" braucht. Und um nicht in jeder Klasse

C-/C++-Quelltext

1
2
3
4
5
6
7
class XY {
    string m_Name;
}

void XY::DoStuff {
    cout << "INFO: " << m_Name << ": Doing Stuff";
}

zu verpassen und gleichzeitig die 3 Funktionen Debug, Stat und Error jedes mal "inline" im Code zu verwenden schien es mir nach dem Klassenkonzept die einzige Möglichkeit..

@dot: Die Situation ist die Folgende: SFML stellt eine Sprite-Klasse zur Verfügung. Ihr kann ich eine Image-Instanz zuweisen sowie Position und Skalierung (letzteres jetzt unwichtig). DrawItem definiert ein Sprite sowie die für alle gültigen Resourcemanager (nicht im Diagramm) und das RenderTarget. Ebenso Abfrage-Funktionen über das Sprite (Höhe, Breite, Position). Das Movable ergänzt diese Klasse um die Variablen FrameRate und Velocity, wobei ersteres wie das RenderTarget für alle Movable Klassen gleich sind..
Und noch mal als Code:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class DrawItem : public BaseClass {
protected:
    // Drawable object
    sf::Sprite m_Sprite;
    // Global resource manager
    static const ResourceManager* m_ResManager;
    // Global render target
    static sf::RenderTarget* m_Target;
public:
    DrawItem( void );
    virtual ~DrawItem();

    // Set global resource manager
    static void SetResourceManager( const ResourceManager& resManager );
    // Returns global resource manager
    static const ResourceManager* GetResourceManager( void );
    // Set global render target
    static void SetRenderTarget( sf::RenderTarget& target );
    // Returns global render target
    static sf::RenderTarget* GetRenderTarget( void );

    // Draws sprite on render target
    virtual void Draw( void );

    // Returns information about sprite
    inline int GetWidth( void ) const;
    inline int GetHeight( void ) const;
    inline sf::Vector2f GetPosition( void ) const;
};

class Movable : public DrawItem {
protected:
    // Global frame rate
    static const int* m_FrameRate;
    // Velocity of object
    float m_Velocity;
public:
    Movable(void );
    virtual ~Movable( );

    // Set global frame rate
    static void SetFrameRate( const int& frameRate );

    // Returns velocity
    float GetVelocity( void ) const;
    // Change velocity
    void SetVelocity( float velocity );
    void ChangeVelocity( float percentage );
};


Zu den Manager Klassen: LevelManager ist nicht die gleiche Ebene / das selbe wie die anderen.
Meine Überlegung war: Das Programm kann 3 "Zustände" haben: Es wird gespielt, im Menu herumgesucht oder mit dem Editor neue Level erstellt werden. Nun könnte ich in der GameKlasse beim Zeichnen oder InputVerarbeiten immer schön if clauses machen und dann den jeweiligen Code reinschreiben (oder in Funktionen packen), allerdings wird dann alles ganz schnell sehr groß (so meine Befürchtung).
Und damit das gar nicht erst passiert pack ich jeweils die "zusammengehörigen" funktionen in eine klasse. Denn was ich nicht möchte ist ein Lernprojekt wo ich nachher da steh und sag.. So.. fertig.. alles läuft "iwie", aber durch den Funktionenwust blick ich nicht mehr durch ?( Und spätestens wenn ich dann was erweitern/ändern möchte steh ich da ;(

5

30.08.2010, 22:51

Nein man... also ich finde deinen Entwurf echt gut!

Das dein Spiel verschiedene Modi hat, finde ich gut. Ist auch mein Plan bei meinem nächsten Projekt, habe nämlich nun gemerkt dass das nicht so das Gelbe vom Ei ist, wie ich das andauernd mache (if(menu)....)...

Was ich nur nicht verstehe, oder bin ich etwas zu kleinlich?
Du sagst zum Logging "so kann bei einem fehler der Klassenname ausgegeben werden".
Ich sehe aber niergends diese Variable in deiner Klasse..... würde mich nur interessieren wie du das löst.

6

30.08.2010, 23:03

Ok.. ich poste mal die BaseClass, dann wird das hoffentlich alles klar ;)

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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();
};

Was dann in der Implementation so aussieht:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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..

7

31.08.2010, 20:31

Gäbe es denn eine Möglichkeit dies als Singleton zu implementieren? Ich weiß nämlich nicht wie ich den Namen der Klasse "ins Spiel" bringen soll..
Bis jetzt habe ich verstanden, dass es sich dabei um die OOP-Alternative zu globalen Variablen handelt. Stimmt es, dass ich nun zu dem Grundgerüst (das man überall findet) jetzt seine Funktionen hinzufügt, oder habe ich das falsch verstanden?

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Singleton {
   public:
     static Singleton* Get();

   protected:
     Singleton() {}

   private:
     static Singleton *instanz;
};

Singleton* Singleton::instanz = 0;

Singleton* Singleton::Get()
{
  if( instanz == 0 )
    instanz = new Singleton();
  return instanz;
}


EDIT: Hier wird noch gesagt das noch der Kopierkonstruktor fehlt. Warum man allerdings einen "Wächter" und nicht den normalen Destruktor benutzen kann ist mir nicht ganz klar..

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »ITiBoI« (31.08.2010, 20:38)


dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

8

31.08.2010, 20:43

Singleton ist ein Antipattern den man leider viel zu oft sieht. Der Grund dafür ist praktisch immer Faulheit. Ich sehe hier keinen Grund für den Einsatz eines Singleton und würde dringend davon abraten. Merk dir dazu vielleich folgende einfache Faustregel: Wann immer du denkst dass ein Singleton eine gute Lösung wäre liegst du falsch. ;)

9

31.08.2010, 22:21

Was für Alternativen gibt es denn noch?
Globale Variablen scheiden aus --> gleiche wie Singleton
Was mir noch einfällt wäre jedes Mal eine Referenz zu übergeben (was ich ja eigt vermeiden wollte); oder in der Klasse eine statische Referenz definieren, die dann einmal für alle Klassen/Funktionen genutzt wird?
Gäbe es noch andere Möglichkeiten?

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

10

31.08.2010, 22:27

Was mir noch einfällt wäre jedes Mal eine Referenz zu übergeben (was ich ja eigt vermeiden wollte)

Warum willst du das vermeiden?

Werbeanzeige