Grafikmodul: Gehe ich den richtigen Weg?

7

Ich versuche, das Grafikmodul meiner Engine zu schreiben. Das heißt, dieser Teil des Codes bietet nur eine Schnittstelle, über die Bilder, Schriftarten usw. geladen und auf dem Bildschirm gezeichnet werden können. Es ist auch ein Wrapper für die Bibliothek, die ich verwende (in diesem Fall SDL).

Hier sind die Schnittstellen für meine Image, Fontund GraphicsRendererKlassen. Bitte sag mir, ob ich den richtigen Weg gehe.

Bild

class Image
{
  public:
    Image();
    Image(const Image& other);
    Image(const char* file);
    ~Image();

    bool load(const char* file);
    void free();
    bool isLoaded() const;

    Image& operator=(const Image& other);

  private:
    friend class GraphicsRenderer;
    void* data_;
};

Schriftart

class Font
{
  public:
    Font();
    Font(const Font& other);
    Font(const char* file, int ptsize);
    ~Font();

    void load(const char* file, int ptsize);
    void free();
    bool isLoaded() const;

    Font& operator=(const Font& other);

  private:
    friend class GraphicsRenderer;
    void* data_;
};

GrapphicsRenderer

class GraphicsRenderer
{
  public:
    static GraphicsRenderer* Instance();

    void blitImage(const Image& img, int x, int y);
    void blitText(const char* string, const Font& font, int x, int y);
    void render();

  protected:
    GraphicsRenderer();
    GraphicsRenderer(const GraphicsRenderer& other);
    GraphicsRenderer& operator=(const GraphicsRenderer& other);
    ~GraphicsRenderer();

  private:
    void* screen_;

    bool initialize();
    void finalize();
};

Bearbeiten: Einige Änderungen am Code und weitere Details.

Nach einigen der Diskussionen hier habe ich beschlossen, meine Verwendung void*durch so etwas zu ersetzen :

class Image
{
  private:
    struct ImageData;
    std::shared_ptr<ImageData> data_;
};

(Natürlich werde ich das Gleiche für die FontKlasse tun .)

Ich sollte auch erwähnen, dass dies nicht meine letzten, vollständigen Klassen sind. Ich zeige hier nur die Grundfunktionen (Laden und Rendern). Anstatt mir zu sagen, welche Funktionen ich Ihrer Meinung nach hinzufügen muss (rotierende Bilder, Schrägstellung, Skalierung usw.), konzentrieren Sie sich nur darauf, zu überprüfen, was ich bereits habe. Ich werde versuchen, meine Entscheidungen zu verteidigen, wenn ich kann, oder meinen Ansatz ändern, wenn ich nicht kann.

Paul Manta
quelle
Frage: Was für ein Motor ist das? Was ist der erwartete Umfang des Projekts? Ich mache mir ein wenig Sorgen, dass ich aufgrund einer falschen Vorstellung davon, was Sie mit Ihrem Projekt beabsichtigt haben, schlechte Ratschläge geben könnte.
ChrisE

Antworten:

1

Das, was mich am meisten juckt, ist der void *"Missbrauch".

Leerzeiger: Ich brauche es nicht. Auf diese Weise kann ich die Anzahl der Dateien begrenzen, die SDL.h enthalten (void * data_ ist nur eine SDL_Surface *, die in void * umgewandelt wurde).

Nun, Sie könnten die Aufnahme vermeiden (was ich übrigens genehmige), indem Sie sie an einem für Sie geeigneten Ort weiterleiten.

jv42
quelle
Nach einigen Kommentaren habe ich beschlossen, dass ich ein privates struct FontDataund std::shared_ptr<FontData>ein privates Mitglied anstelle des definieren werde void*. Dies bedeutet, dass ich auch eine Vorwärtsdeklaration vermeiden kann, was bedeutet, dass die Bibliothek in der Header-Datei nicht erwähnt wird. :)
Paul Manta
Ok, schöne Lösung.
jv42
6

Über Schnittstellen (allgemein)

Sie haben uns daher gebeten, Ihre Entwürfe für Schnittstellen zu überprüfen.

Sie haben uns keine Schnittstellen gegeben, Sie haben uns vollständige Klassenerklärungen gegeben. Wenn dies Schnittstellen wären, würde ich etwas erwarten wie:

virtual bool load file(const char* file) = 0;

Das ist in C ++ eine Schnittstelle. Ich kann es in einer Unterklasse überschreiben, die Funktionalität implementiert (tatsächlich muss ich!). Wenn Sie eine Schnittstelle schreiben, erzwingen Sie Richtlinien, und wie oben beschrieben tun Sie dies.

Die Hälfte der Beschwerden über die Verwendung von void * in den anderen Antworten wäre vermieden worden, wenn Sie nur die Schnittstellenfunktionen verfügbar gemacht und die Mitgliedsvariablen ausgeblendet hätten (wie sie sein sollten, in einer Schnittstellenklasse ).

Rawr.

Auf Schnittstellen (deine)

Bild: Kopieren

Sie haben einen Kopierkonstruktor und einen Gleichheitsoperator. Das Problem, das ich hier sehe, ist, dass es keine gute Möglichkeit gibt, den Benutzer daran zu hindern, alberne Fremdkopien von Bildern zu erstellen.

Für Sie ist die Verwendung von SDL_surfaces ein großes Problem . Ohne beleidigend zu sein, bin ich bereit zu wetten, dass Sie nicht darüber nachgedacht haben, was passiert, wenn Sie ein Bild freigeben, das ein Duplikat eines anderen Bildes ist. Ich bin weiterhin bereit zu wetten, dass Sie nicht geplant haben, die SDL_surface vollständig zu kopieren. In dem oben genannten Fall werden Sie wahrscheinlich ein Bild freigeben, und dann werden Ihre anderen Kopien davon explodieren und jeden töten, den Sie lieben .

Lösung: KEINE KOPIEN. Tu es nicht, erlaube es nicht. Verwenden Sie eine Factory- oder eine C-Ladefunktion, um neue Instanzen eines Images zu erstellen, und verwenden Sie diese, anstatt das Kopieren zuzulassen oder Zuweisungen gleichzusetzen. Alternativ können Sie vollständig herausfinden, wie Sie ein SDL_image tief kopieren (nicht sehr schwer, aber ärgerlich).

Bildbearbeitung

Wie ändere ich Ihre Bilder, nachdem ich sie geladen habe? Laut Ihrer Schnittstelle nicht. Sind Sie sicher, dass dies eine gute Idee ist? Wie finde ich die Bittiefe eines Bildes heraus? Seine Höhe? Breite? Farbraum?

Schriftart

Wie zeichne ich mit dieser Schriftart? Wie bekomme ich den Namen? Wie verhindere ich die Kopierprobleme, über die ich mich oben beschwert habe? Wie stelle ich die Farbe ein? Kerning? Was ist mit der Unicode-Unterstützung?

Renderer: Allgemein

Ich stelle also fest, dass Sie einige Funktionen von blit * () und eine Funktion von render () haben. Dies scheint zu implizieren, dass Benutzer in der Lage sein sollen, eine Reihe von Blitting-Vorgängen in die Warteschlange zu stellen und sie dann alle mit dem Aufruf render () auf einmal auf den Bildschirm zu spülen.

Das ist gut; Genau so geht auch die Motorentechnologie meiner Gruppe damit um. :) :)

Die Verwendung eines Singletons ist hier akzeptabel, vor allem, weil Sie dem Renderer anscheinend die vollständige Kontrolle über das Zeichnen geben möchten. Wenn es nur eine Instanz davon gibt (wie es wahrscheinlich sein sollte), wird dies nichts schaden. Nicht was wir tun, aber hey, es ist Geschmackssache.

Es gibt jedoch ein paar große Probleme, die ich hier sehe.

Renderer: Transformationen

Sie scheinen nur in 2D zu arbeiten. Das ist gut. ABER...

Wie gehen Sie mit Dingen wie dem Drehen eines Bildes beim Zeichnen um? Skalieren? Sie benötigen volle Unterstützung für sogenannte affine Transformationen . Auf diese Weise können Sie Bilder auf hübsche Art und Weise leicht drehen, skalieren, übersetzen, verzerren und auf andere Weise frobieren.

Dies muss (irgendwie) sowohl für Text als auch für Bilder unterstützt werden.

Renderer: Färben und Mischen

Ich möchte in der Lage sein, Farben auf meine Bilder zu mischen und die Farben für meinen Text festzulegen. Sie sollten dies aussetzen.

Ich möchte auch in der Lage sein, Dinge wie das Mischen von Bildern beim Blitzen zu tun, damit ich Dinge wie durchscheinende Geister oder Rauch oder Feuer tun kann.

So sparen Sie sich die Mühe

Verwenden Sie SFML . Es hat ein besseres Design für so ziemlich alles über SDL. Sie haben bereits getan, was Sie hier versuchen. Schauen Sie sich zumindest an, wie sie ihre Schnittstellen spezifiziert haben und wie sie ihre Klassenhierarchie entworfen haben.

Beachten Sie auch, dass sie sich mit dem Problem der Transformationen befassen, auf das ich bereits in ihrer Drawable-Klasse hingewiesen habe. Und färben. Und mischen.

Sie haben gute Tutorials und Dokumentationen, daher lohnt es sich möglicherweise, ein wenig damit herumzuspielen und ein Gefühl dafür zu bekommen, was Ihr Code leisten kann.

Viel Glück!

ChrisE
quelle
1
Es gibt andere stilistische Entscheidungen, die Sie getroffen haben (load () -Methoden, void *, Verwendung von Singletons usw.), die offen gesagt völlig von der mangelnden Funktionalität Ihrer vorgeschlagenen Schnittstelle überschattet werden. Sie sollten nicht das, was Software - Entwicklung tropes sollten Sie verwenden , wenn Sie Ihre Software sein verschwenden Zeit sich Gedanken über Engineering hier so verdammt gebrochen ist es meine Augen bluten macht.
ChrisE
1
Sie haben einen guten Start hingelegt. Sie benötigen lediglich etwas mehr Erfahrung, um zu erkennen, welche Funktionen Ihnen fehlen. Schauen Sie sich SFML an, erstellen Sie lustige kleine Apps und kehren Sie dann zu diesem Thema zurück, wenn Sie eine klarere Vorstellung davon haben, welche Funktionen Sie bereitstellen müssen. Mach dir keine Sorgen, wie hässlich die Klempnerarbeiten sind, bis du alles hast, was du sonst brauchst.
ChrisE
1
Schnittstelle: Ja, es ist eine Schnittstelle. Es ist die API-Schnittstelle, nicht die Schnittstelle der virtuellen Klasse. Leerzeiger: Bereits aussortiert. Siehe meine Kommentare zur Antwort von jv42. | Bildkopie: Sie machen diese Annahme basierend auf was? | Manipulation: Viele Details der Klassen, die ich noch nicht implementiert habe oder die ich nur als Ablenkung für die Frage angesehen habe. Außerdem scheinen Sie nach Dingen wie Bildrotation, Abrufen des Namens der Schriftart usw. zu fragen. Ich werde diese implementieren, wenn und falls dies erforderlich ist, nicht jetzt. [Fortsetzung]
Paul Manta
1
[Forts.] SFML: In der Tat habe ich diese Bibliothek gerade entdeckt und sie gefällt mir viel besser. | Fazit: Was ich hier gezeigt habe, ist nur der Kern (Laden und Rendern). Andere Details hielt ich für unwichtig. Außerdem werde ich keine Funktionen (Rotation, Schrägstellung, Skalierung) implementieren, es sei denn, ich stelle später fest, dass ich sie benötige. Das wird sowieso einfach sein.
Paul Manta
1
+1 für SFML. Es wird die meisten dieser Schnittstellen mit seiner API loswerden. : P
Die kommunistische Ente
0

Ich bin ein bisschen besorgt über die "Lade" -Methode, die gegen das Prinzip der Einzelverantwortung verstößt (übrigens, für die kommunistische Ente, ich denke, deshalb verwendet er ein const char *, weil die SDL-Ladebildfunktion codiert ist nimm in C keinen std :: string). Es sollte aus mindestens zwei Gründen kein Job einer Image-Klasse sein, sich selbst zu laden:

  • Wenn Sie Ihre Speichernutzung optimieren möchten, benötigen Sie spezielle Klassen zum Laden von Ressourcen.
  • Mit einer speziellen Klasse können Sie Ausnahmen einfacher behandeln.
Raveline
quelle
Laden: Nein, es sollte ein Job der Image-Klasse sein, um sich selbst zu laden, genauso wie es der Job von std :: ifstream ist, um sich selbst zu laden. Dies ist nicht der Ort für die Mem-Verwaltung: Sie wird in einer separaten ResourceManager-Klasse durchgeführt. | Strings: Es ist nicht schwer zu konvertierenstd::string zu char*, so dass nicht der Grund ist. Der Grund ist einfach, dass std::stringdies keinen Vorteil bringen würde.
Paul Manta
-1

Singleton = BAD, sofort entfernen. Schriftarten und Bilder sollten keine free()Funktion haben, das sollte die Aufgabe des Destruktors sein. Die GraphicsRenderersollten die nicht anbietenBlit Funktionen, sollten Sie objektorientierte Klassen bieten , die eine Position für jedes Ergebnis liefern wird, und die tatsächliche Rendering automatisch vom GraphicsRenderer verwaltet. Verwenden Sie zur Kapselung die Vererbung, nicht PIMPL, und verwenden Sie auf keinen Fall eine Leere *. Verwenden Sie einen stark typisierten undurchsichtigen Zeiger.

Hier sind einige Auszüge aus meinem eigenen Design, obwohl ich das Umschalten zur Kompilierungszeit und nicht die Vererbung zur Laufzeit verwendet habe.

class D3D9Render 
{
public:
    std::shared_ptr<D3D9Font> CreateFont();
};
class D3D9Font 
{
public:
    // PUBLIC INTERFACE ---------------------------------------------------
    std::unique_ptr<D3D9Text> CreateText();

    int Height();
    D3D9Font* Height(char newheight);
    D3D9Font(D3D9Render& ref);

    int Width();
    D3D9Font* Width(char newwidth);
    int Weight();
    D3D9Font* Weight(short newweight);
    bool Italic();
    D3D9Font* Italic(bool newitalic);
    string Font();
    D3D9Font* Font(string str);
    D3D9Font* CommitChanges();
};

class D3D9Text 
{
public:

    // PUBLIC INTERFACE ---------------------------------------------------
    D3D9Text* Text(string str);
    D3D9Text* PositionSizeX(short newx, short newxsize);
    D3D9Text* PositionSizeY(short newy, short newysize);
    D3D9Text* Font(std::shared_ptr<D3D9Font> ref);
    D3D9Text* Colour(unsigned int newcolour);
    string Text();
    int PositionX();
    int PositionY();
    int SizeX();
    int SizeY();
    std::shared_ptr<D3D9Font> Font();
    unsigned int Colour();
    D3D9Text* CommitChanges();
};

Hier wird der Speicher für Sie verwaltet - alle Eigentumsrechte werden durch intelligentes Zeigen verwaltet, und die Schnittstelle ist vollständig objektorientiert.

DeadMG
quelle
2
Singleton: Singletons sind nicht schlecht, wenn sie richtig verwendet werden. Grundsätzlich hat der Renderer keinen nennenswerten Status, so dass Singleton hier in Ordnung ist (es ist jedoch komplexer). | Frei: Ausplanung wird im destructor behandelt. Die free()Funktion ist aus demselben Grund vorhanden, aus dem die ifstream::close()Funktion vorhanden ist. | Blit: Ich versuche, die Daten von dem Mechanismus zu trennen, der die Daten verwendet. | Sprites: Dies ist nur das Grafikmodul. Sprites sind in den höheren Ebenen des Motors auf ähnliche Weise wie von Ihnen beschrieben zu handhaben. [Fortsetzung]
Paul Manta
[Fortsetzung] Vererbung: Ich verstehe nicht, wie ich Vererbung hier verwenden könnte. | Leerzeiger: Warum sollte er nicht verwendet werden? Ich dachte, es ist eine gute Möglichkeit, die Anzahl der Dateien zu begrenzen, die Zugriff auf die Bibliothek eines Drittanbieters haben. data_ist nur eine SDL_Surface*Besetzung zu void*. Wann immer ich es benutze, wirke ich es zurück zu SDL_Surface*. | Blit [Forts.]: Eine andere Art, darüber nachzudenken, ist, dass sich das Bild nicht selbst zeichnet & mdash; Der Renderer zeichnet es. Das Bild existiert einfach .
Paul Manta
Danke, dass du es mir gezeigt hast std::shared_ptr. Vielen Dank auf jeden Fall nützlich sein.
Paul Manta
1
Frei : Je nach System muss das Objekt und die zugehörigen Ressourcen (z. B. eine Textur) möglicherweise in verschiedenen Threads zerstört werden, daher ist eine free()Funktion erforderlich . Man könnte auch ein benutzerdefiniertes Zuordnungsschema verwenden, das den Destruktor nicht aufruft.
Sam Hocevar
1
@DeadMG Ich bin auch verwirrt darüber, wie man die Kapselung mit Vererbung versehen kann. Ich dachte immer, der beste Weg, private Mitglieder zu verstecken, wäre, sich an den stark getippten undurchsichtigen Zeiger zu halten. Könnten Sie Ihren Erbschaftsvorschlag näher erläutern?
michael.bartnett
-1

Erste Dinge, die ich erkennen kann:

  • Singletons sind normalerweise sehr, sehr schlecht. Es ist nicht "Hm, will ich nur eines davon?" aber mehr 'Hm, wird mehr als einer von diesen das Programm brechen?'.
  • void * -Pointer sind auch ziemlich riskant. Warum brauchst du einen? Irgendwo schlechtes Design.
  • Fontund Imagescheinen eng verwandt zu sein. Vielleicht könnten Sie einen Teil der Funktionalität der Hierarchie auf a bringen Renderable.
  • Ich denke, das bin ich, aber aus irgendeinem Grund verwenden Sie const char*über std::string?
Die kommunistische Ente
quelle
Guter Ort auf der const char*, das hatte ich nicht bemerkt.
DeadMG
Leerzeiger: Ich brauche es nicht. Auf diese Weise kann ich die Anzahl der enthaltenen Dateien begrenzen SDL.h( void* data_ist nur eine SDL_Surface*Besetzung void*). | Zeichenfolgen: Habe ich einen Grund, sie nicht zu verwenden? Ich mache keine Manipulationen, ich brauche nur eine Schnur. Ich kann es std::stringaußerhalb dieser Klassen verwenden, wenn ich dies wünsche. | Singletons: Ja, mehr als ein GR wird meinen Code brechen. GR initialisiert SDL. Wenn es ein Singleton ist, muss ich mir keine Sorgen machen, wenn SDL initialisiert oder beendet wird.
Paul Manta
Der Vorteil von weniger Dateien einschließlich SDL.hist so ziemlich Null. Irgendwie hätte ich keinen Renderer, der SDL initialisiert. Ich würde das auf die Hauptmaschine oder etwas anderes abstrahieren. Was ist, wenn Sie andere Teile von SDL außer Grafiken möchten?
Die kommunistische Ente
Andere Komponenten: Ich würde sie separat in den Komponenten initialisieren, die diese Teile benötigen. | Initialisierung: Die Haupt-Engine (normalerweise GameManager) sollte sich nicht um Details wie die von mir verwendete Bibliothek kümmern. In meinem aktuellen Setup muss ich mich kein bisschen um die Initialisierung kümmern, alles geschieht automatisch (und es wird sichergestellt, dass es genau dann geschieht, wenn ich es brauche, nicht später, nicht früher). | Header: Ja, es gibt keinen wirklichen technischen Vorteil. Ich finde den Code jedoch ordentlicher, wenn mir die zugrunde liegende Bibliothek in der Klassenschnittstelle nicht zur Verfügung steht.
Paul Manta
1
Eher eine Sache, die die einzigen Punkte, die ich falsch sehen kann, mit dem Design, für das Sie Gründe haben. Ich stehe immer noch dazu, dass Singletons hier nicht benötigt werden und dass leere Zeiger durch SDL_Surface-Zeiger ersetzt werden sollten. : P
Die kommunistische Ente