Home » Tutorials » Grafik und Spiele » Vier gewinnt

Vier gewinnt

Das Spielfeld – Grafiken, Bitmaps und die Privatsphäre

Wir wollen zunächst dem Auge etwas Gutes tun und fangen deshalb damit an, das Spielfeld, also im Endeffekt ein einfaches Gitter, auf dem Bildschirm darzustellen.

Wir begeben uns im Quelltext in den Bereich der Klassen und Typendeklarationen. Spätestens hier wird sich der ein oder andere wohl fragen, was man denn in der Programmierung überhaupt unter einer Klasse versteht.

Eine Klasse ist letztlich ein Bauplan, nach dem dann Objekte „gebaut“ werden können. Diese Objekte können dann unterschiedliche Aufgaben erfüllen, je nachdem wie sie aufgebaut sind. Prozeduren und Funktionen sind neben Variablen die Bestandteile der Klassen bzw. Objekte. Sie sorgen für die Funktionalität des Ganzen. Unser Programm ist prozedural geschrieben, wodurch Klassen eine eher untergeordnete Rolle spielen, zentraler Bestandteil werden sie bei der objektorientierten Programmierung, welche ein anderes Konzept darstellt.

Man könnte das Ganze mit einem Auto vergleichen. Die Klasse ist der Bauplan, die einzelnen Objekte sind die Autos.
Innerhalb eines Autos gibt es verschiedene Funktionen. Beispielsweise sorgt das Gaspedal je nach Anstellwinkel für mehr oder weniger Luft im Brennraum. Variablen wären zum Beispiel Tankfüllung oder Reifendruck (wobei die Räder eher eine untergeordnete Klasse sind).

Sowohl Methoden als auch Attribute können in der Klasse vier verschiedene Formen der Sichtbarkeitsstufen haben:

  1. private: Diese Elemente sind nur innerhalb dieser Klasse (und in der selben Unit) verfügbar.
  2. protected: Protected wird für objektorientierte Programmierung verwendet.
  3. public: Der Zugriff von außerhalb ist möglich.
  4. published: Der Zugriff von außerhalb ist möglich, zusätzlich werden Laufzeittypinformationen (RTTI) verfügbar gemacht.

Bei Methoden ist diese Einstufung nach Verwendungszeck zu entscheiden. Sinnvollerweise sollte die Lenkung eine public Schnittstelle haben, sonst kann sie schlecht bedient werden. Bei der Steuerung der Motorelektronik ist private durchaus angemessen, da hier ein externer Zugriff zu Problemen führen kann.

Natürlich dient dieses Beispiel nur der Veranschaulichung, tatsächlich ist ein Auto ungleich komplexer und das Lenkrad keine Methode, sondern ein Plastik-Metall-Gemisch. Übertragbar ist das Beispiel am ehesten noch auf eine Simulation.

Variablen sollten in einer Klasse stets Private sein, um zu vermeiden, dass von außen interne Zustände verändert werden können, wodurch dann die Klasse nicht mehr funktioniert. Dennoch kann es vorkommen, dass manche Variablen auch von außen verändert werden müssen.
Dazu gibt es die Properties. Properties sind Konstrukte, die es erlauben, auf Felder indirekt zu zu greifen. Bei der Benutzung sieht es aus, als griffe man direkt auf das Feld zu, tatsächlich kann aber eine Methode dahinter stecken und so interne Zustände vor fehlerhaften Änderungen schützen.

Beispielsweise braucht der Tank eine solche Property, da der Sprit ja von außen kommt. Diese Funktion übernimmt gewissermaßen die Tanköffnung. Was zwischen Tanköffnung und Tank passiert, ist für den äußeren Zugriff nicht erkennbar. Möglicherweise macht die Leitung eine Spirale oder enthält einige Filter. Von außen wird nur der Sprit zugegeben und intern wird geregelt, dass dieser auch an den richtigen Ort gelangt.

Hierbei ist es auch möglich, ausschließlich das Lesen zu gestatten. Wahlweise auch nur das schreiben, auch wenn diese Möglichkeit seltener sinnvoll ist.

Nach diesem Exkurs sollte auch klar sein, dass das Zeichnen des Gitters nach Private gehört. Schließlich soll nicht jede Klasse wild drauf los Zeichnen können, wie es ihr gerade passt. Zwar gibt es in unserem Programm gewissermaßen kein „außen“, aber es ist zweckmäßig, sich gleich zu Beginn einen ordentlichen Programmierstil anzugewöhnen. Später kriegt man die Marotten schlecht wieder weg.

Deshalb schreiben wir jetzt unter Private den Verweis auf unsere Funktion bzw. Prozedur auf:

procedure GitterZeichnen(breite, hoehe, feldbreite: Integer);

Zunächst steht da, was denn kommen wird. Eine Prozedur reicht hier vollkommen aus, da wir keinen Rückgabewert benötigen. Danach kommt der Name der Prozedur und anschließend in Klammern die Parameter, die mit übergeben werden, sowie deren Typ. Auch hier sollte man sprechende Namen verwenden. Ein Name, der eine Aussage über die Aufgabe der Prozedur trifft, hilft später beim zurechtfinden im Programm.

Anschließend begeben wir uns in den Bereich zwischen Implementation und end.. Dort kommt der Quelltext der Methode hin.
Dazu muss Delphi natürlich erst einmal erfahren, um welche Methode es sich handelt. Also erst wieder Typ, Name, Parameter. Vor den Name muss aber hier noch die Klasse, in der die Methode zu finden ist.

Procedure TForm1.GitterZeichnen(breite, hoehe, feldbreite: Integer);
begin
  {hier steht dann der Quelltext}
end;

Nun wollen wir das leere Feld für unser Spiel zeichnen. Bliebe nur die Frage: wohin? Genau, auf den Bildschirm. Aber bitte nicht direkt mit Edding, das geht so schwer wieder weg.
Um das Ganze sichtbar zu machen, benötigen wir etwas, um darauf zu zeichnen.

Wir erinnern uns, dass wir auf unserem Formular eine Paintbox platziert haben. Auf dieser Komponente soll am Ende unser Gitter gezeichnet werden.

Wir könnten zwar auch direkt auf das Formular zeichnen, aber da entstehen nur Probleme mit der Positionierung und wir müssen aufpassen, dass wir nur in die Bereiche zeichnen, in denen keine anderen Komponenten liegen. Die Paintbox können wir einfach an eine Position setzten, an der gezeichnet werden soll und dann diese komplett nutzen, ohne das wir uns Gedanken machen müssen, was sichtbar ist.

Dennoch werden wir, auch wenn dies nahe liegend wäre, nicht direkt auf die Paintbox zeichnen. Dies würde nämlich bedeuten, dass eine Zeichenoperation ausgeführt wird (zum Beispiel eine Linie, oder ein Punkt) und im Anschluss der Bildschirm aktualisiert
wird. Da diese Aktualisierung nach jeder Zeichenoperation stattfindet, wird das Ganze dann etwas langsam.

Das mag bei den wenigen Linien in unserem Programm gerade noch funktionieren, zumindest wenn der PC schnell genug ist, aber spätestens wenn es einmal eine kompliziertere Grafik wird, flackert die Darstellung stark, was dem Benutzer unangenehm aufstoßen dürfte.
Deshalb werden wir zunächst nur im Speicher „zeichnen“ und im Anschluss die gesamte Grafik mit einem mal auf den Bildschirm bringen. Dazu benötigen wir eine zusätzliche Variable. Eine vom Typ TBitmap. Diese tragen wir unter private in unsere Klasse ein, noch über der Prozedur.

FGrafik: TBitmap;

Das F am Variablennamen wird bei Variablen davor gesetzt, einfach um eine Unterscheidung, zum Beispiel von lokalen Variablen oder Prozeduren, zu vereinfachen. Das F steht in diesem Fall für Field ? Feld. Diese und andere Namenskonventionen finden sich auch im Styleguide wieder.

Direkt darunter fügen wir noch zwei Variablen vom Typ Integer ein.

FLinks: Integer;
FUnten: Integer;

Diese Variablen sind der linke und der untere Rand unseres Spielfeldes. Dieses soll ja schließlich schön mittig dargestellt werden und nicht irgendwo am Rand kleben. Der Typ TBitmap ist einer, der vor der Benutzung gerne erzeugt werden will, damit auch sichergestellt ist, dass er ein Plätzchen im Speicher bekommt. Dies wird gemacht, indem man der Variable das Ergebnis des Konstruktors zuweist:

FGrafik := TBitmap.Create;

Fragt sich nur noch, wohin damit. Die Lösung liegt auf der Hand: Wir wollen ja die Bitmap haben, solange das Programm läuft, ergo scheint es sinnvoll, sie beim Starten des Programmes zu erzeugen. Deshalb schreiben wir die obige Zeile in die OnCreate
Ereignisbehandlungsroutine des Formulars.

Ganz wichtig ist jetzt, dass Variablen, die per Create erzeugt werden, auch wieder freigegeben werden müssen, sonst bleiben die im Speicher, wenn das Programm beendet ist. Zwar nimmt sich im Anschluss Windows dieser verlorenen Schäfchen an, aber im Sinne eines guten Programmierstiles sollten sie trotzdem freigegeben werden.
Deshalb schreiben wir im OnDestroy des Formulars ein:

FGrafik.Free;

Nun aber wieder zu unserem Gitter.
Unsere Prozedur bekommt die Breite und die Höhe in Feldern und die Breite eines solchen Feldes in Pixel (Bildpunkte ? http://de.wikipedia.org/wiki/Pixel). Nun muss daraus ein Gitter gezaubert werden.

Dazu im Voraus eine Frage: Was ist ein Gitter? Oder, weil die Definition des Dudens oder von sonst wo eigentlich egal ist die bessere Frage, wie würden wir auf dem Papier ein Gitter zeichnen?

Genau, wir nehmen ein Lineal (ja OK, wir sind Informatiker … frei Hand ist das Mittel der Wahl) und einen Stift und zeichnen horizontale und vertikale Linien, die sich so überschneiden, dass dazwischen rechteckige (idealerweise quadratische) Felder
entstehen.
So werden wir das jetzt auch tun, nur nehmen wir keinen Stift, sondern einen Zeichenbefehl.

Genau genommen ist es doch ein Stift, der einfach mit zwei Befehlen bewegt wird. Der eine nennt sich MoveTo(x,y: integer), und positioniert den „Stift“ an einer bestimmten Stelle, der andere, LineTo(x,y: integer), zeichnet eine Linie von der Stiftposition zu den Zielkoordinaten. Beide finden sich unter FGrafik.Canvas.

Aufgabe: Zeichne Anhand der übergebenen Parameter und mit den eben erklärten Befehlen ein Gitter.

Bitte jetzt nicht jede Linie einzeln Zeichnen, für diesen Zweck gibt es wunderschöne for-Schleifen.

procedure TForm1.GitterZeichnen(breite, hoehe, feldbreite: Integer);
var
  i: Integer;
begin
  //Darstellen des leeren Spielfeldes
  FGrafik.Canvas.Pen.Color := clBlack;
  //horizontale Linien
  for i := 0 to hoehe do
  begin
    FGrafik.Canvas.MoveTo(FLinks, FUnten - i * feldbreite);
    FGrafik.Canvas.LineTo(FLinks + breite * Feldbreite, FUnten - i * feldbreite);
  end;
  //vertikale Linien
  for i := 0 to breite do
  begin
    FGrafik.Canvas.MoveTo(FLinks + i*feldbreite, FUnten - hoehe * feldbreite);
    FGrafik.Canvas.LineTo(FLinks + i*feldbreite, FUnten);
  end;
end;

Mit Pen.Color wird die Farbe des Stiftes gesetzt. Dies ist notwendig, da es sein kann, dass die Einstellung an einer anderen Stelle geändert wurde und das gibt dann möglicherweise ein buntes Gitter, oder im schlimmsten Fall ein weißes, was blöd ist, wenn der Hintergrund weiß ist…

Die Koordinaten ergeben sich folgendermaßen: Bei den horizontalen Linien ist der linke und der rechte Rand immer gleich, nur die Position in der Höhe ändert sich. Der linke Rand ist Flinks, dort wird der Stift angesetzt und zum rechten Rand gezogen. Um diesen zu ermittelt, wird zum Anfangspunkt einfach die Breite des Spielfeldes in Pixel addiert. Spielfeldbreite enthält die Breite in Feldern und muss deshalb mit der Breite eines Feldes multipliziert werden.
Die Y-Position ergibt sich, indem von unten an nach oben immer ein Feld weiter gegangen wird. Das aktuelle Feld ist in „i“ enthalten, deshalb i*Felderbreite. Subtrahiert wird deshalb, weil die y-Koordinate ja auf dem Kopf steht und von oben nach unten geht und wir von unten nach oben zeichnen. Analog sind die vertikalen Linien aufgebaut.

Wer jetzt in der Erwartung, sein tolles Gitter zu sehen, schon F9 gedrückt hat, wird wohl enttäuscht worden sein. Das Gitter ist nicht da. Das liegt ganz einfach daran, dass unsere Prozedur ja noch nirgends aufgerufen wird.

Weil so ein leerer Bildschirm so frustrierend ist wie eine Tankstellen-Quittung, werden wir uns jetzt natürlich darum kümmern, dass wir auch etwas sehen. Das hat auch den Vorteil, dass wir das Ganze gleich überprüfen können. Möglicherweise gehen die Linien ja kreuz und quer über den Bildschirm. Mitunter können kleine Fehler lustige grafische Effekte haben.
Weniger lustig wird das Ganze, wenn man seit einer ganzen Weile sucht und letztlich feststellt, dass man zwei Variablen vertauscht hat.