Dame-Spiel interaktiv

Aus toolbox_interaktion
Wechseln zu: Navigation, Suche

Title.jpg

Im Rahmen der Projektarbeit Eyesis wurde an der Umsetzung eines neuen Bedienkonzepts zur Steuerung von Computer Programmen gearbeitet. Im Gegensatz zu der sonst üblichen Steuerung durch Maus und Tastatur, wurde eine unmittelbare Steuerung per Fingerzeig angestrebt. Mit Eyesis wurde nun versucht, die Vorteile der direkten Steuerung eines Touchscreens auf einen handelsüblichen PC zu übertragen ohne zusätzliche Investitionen notwendig zu machen. Die einzig notwendige Komponente neben dem PC stellt eine einfache Webcam dar. Anstatt einer drucksensitiven Oberfläche wie beim Touchpad üblich, übernimmt eine Webcam die Positionsdetektion. Bei der Verwendung einer einzelnen Kamera stellt sich allerdings das Problem der fehlenden Tiefeninformation. Es liegen lediglich zweidimensionale Daten vor. Der Tastendruck bzw. Mausklick muss dadurch anders realisiert werden. In diesem Falle durch einfaches Stillhalten des Fingers über einen vordefinierten Zeitraum. Als Testanwendung für das System, entschied man sich für die Umsetzung des bekannten Dame Brettspiels. Ein besonderes Augenmerk lag auf einem modularen Aufbau der Teilkomponenten, d.h. eine Untergliederung in logisch unabhängige Teile, welche auch getrennt voneinander erarbeitet werden können.

Eyesis ist in die drei Module Eingabe, Verarbeitung und Ausgabe unterteilt. Der Eingabeblock übernimmt sämtliche Aufgaben, welche im Rahmen der Bilderkennung und Verarbeitung anfallen, d.h. den komplette Ablauf der Fingerdetektion. Die Übersetzung dieser Eingabedaten (Fingerkoordinaten) in Spielzüge übernimmt das Damemodul. Die grafische Darstellung wiederum übernimmt der Ausgabeblock. Im Rahmen dieses Projektes wird die Ausgabe mit Hilfe der OpenGL-Grafikbibliothek realisiert. Im Folgenden werden nun die Funktionsweisen der drei genannten Module in der Reihenfolge ihrer Ausführung vorgestellt.

Bildverarbeitung

Der Image Processing Teil kann gemäß grob in folgende Bereiche gegliedert werden. Input Image, Zoom, Segmentation, Quantity Of Motion, Normalize und OSC Send. Ablauf.jpg

Input Image

Mittels eines Framegrabber Blocks wird das Webcam Signal eingelesen. Im Rahmen des Projekts wurde eine handelsübliche Philips Kamera mit einer Auflösung von 640x480 verwendet.

Zoom

Zoom.jpg

Dieser Abschnitt dient der Justierung und Kalibrierung der Bildfläche. Dies ist nötig, da die aufgenommene Fläche der Kamera nicht identisch mit der Spielfläche auf dem Monitor ist. Im Normalfall ist auf dem Kamerabild ebenfalls ein Teil des Raumhintergrunds zu sehen. Abhängig von der Entfernung der Kamera zum Bildschirm fällt dieser verschieden groß aus. Das Resultat dieser Diskrepanz zwischen Kameraaufnahme und Spielausgabe wäre ein mehr oder minder großer Versatz der extrahierten Fingerkoordinaten abhängig von der Kamerapositionierung. Daher wird der spielrelevante Bereich, also die Monitorfläche, vom Hintergrund separiert und auf die gesamte Höhe und Breite des Kamerabildes ausgedehnt. Um die Spielfläche vom Hintergrund abzuheben wird im Zuge der Kamerakalibrierung eine schwarze oder weiße Fläche auf dem Monitor eingeblendet, welche in den Ausmaßen der späteren Spielfläche entspricht. Mit Hilfe einer Binarisierungsoperation wird die Spielfläche freigestellt und deren Koordinaten an den selbst geschriebenenen Zoom Block geschickt. Basierend auf den Eingangsdaten vergrößert dieser den relevanten Teilbereich des Bildes auf die komplette Breite und Höhe des Bildes. Mittels des Lock Buttons werden die Koordinaten des Teilbereiches fixiert.

Segmentation

Für die Fingererkennung ist der Segmentation-Bereich verantwortlich. Das entscheidende Segmentierungsmerkmal sind dabei die Farbinformationen der einzelnen Bildpunkte. Die Verwendung eines Handschuhes, oder in diesem Falle eines roten fingerhutartigem Überzuges, erleichtert die Farbsegmentierung wesentlich. Statt die Farberkennung direkt im RGB-Modus zu vollziehen, wird das Videosignal erst in den HSV-Farbraum konvertiert und dort anschließend verarbeitet. Der HSV Raum (LINK) setzt sich aus den drei Kanälen Farbton, Sättigung und Hellwert zusammen. Dadurch lässt sich mit den nachfolgenden Abfragekriterien eine Farbsegmentierung wesentlich genauer vollführen, als es im RGB Bereich möglich gewesen wäre. Durch das Setzten von geeigneten Schwellwerten können die gewünschten Bildanteile, hier der rote Fingerhut, extrahiert werden. Das Ergebnis der anschließenden Verundung dieser drei parallelel Abfragen ist in Abbildung X dargestellt. Für die weiteren Berechnungen werden lediglich die Schwerpunktkoordinaten des weißen Bereiches verwendet. Farbton.jpg

Farbton Extraktion

sättigung.jpg

Sättigung Extraktion

Hellwert.jpg

Hellwert Extraktion

Segmentation.jpg

Ergebnis der Verundung

Quantity Of Motion

Zur Steuerung der Spielsteine wurde folgendes Konzept benutzt. Sobald der Benutzer seinen Finger für eine bestimmte Zeitdauer ruhig hält (und nur dann), werden die Koordinaten an die Spielengine gesendet. Damit signalisiert der Spieler, dass er eine Aktion starten will. In diesem Fall also eine Spielfigur, auf die er zeigt, bewegen. Zum Feststellen der Bewegung, bzw der Abweichung der Schwerpunktkoordinaten über einen bestimmten Zeitraum wurde ein eigener Block Quantitiy of Motion geschrieben, welcher prüft ob der Finger für eine bestimmte Dauer ruhig gehalten wurde.

Qom.jpg

Normalize

Die bisher berechneten Koordinaten wären, falls sie in ihrer aktuellen Form an das Spiel übertragen würden, völlig unbrauchbar. Schuld daran sind die abweichenden Bildauflösungen von Kamera und Spielfläche auf dem Monitor. Um also unabhängig von der verwendeten Kamera- bzw. Bildschirmauflösung zu sein, bedarf es eines weiteren Schrittes – Normierung auf 1, d.h. alle übergebenen Werte liegen in dem Intervall [0;1]. Somit spiegelt der normierte Wert einen prozentualen Anteil an der Gesamtweite oder Höhe der Aufnahmeauflösung wider. Dies kann durch Division der X- bzw. Y-Koordinate durch die Kamerabreite oder -Höhe. Dies wurde folgendermaßen realisiert. Die dafür nötigen Werte zur Kameraauflösung werden über die zwei getDescription-Blöcke bezogen.

Normalize.jpg

OSC Send

Damespiel

Die Spielregeln

Da Dame ein sehr altes Spiel ist, sind im Laufe der Zeit unterschiedliche Spielregeln und Spielformen entstanden. Für dieses Projekt musste man sich vorerst für die Umsetzung einer Variante entscheiden. Im Folgenden werden diese Regeln aufgezählt aber nicht erläutert, da davon ausgegangen wird, dass das Spiel jedem bekannt ist.

  • Spielfeldgröße - 8 mal 8 Felder im Schachbrettmuster
  • Zwei Spieler mit je 12 Steinen
  • Es wird abwechselnd gespielt bis einer der Spieler keinen Spielzug mehr machen kann
  • Die Steine ziehen in diagonaler Richtung, aber nur vorwärts
  • Gegnerische Steine müssen übersprungen werden, sofern das dahinter liegende Feld frei ist
  • Wenn das Zielfeld eines Sprungs auf ein Feld führt, von dem aus ein weiterer Stein übersprungen werden kann, so wird der Sprung fortgesetzt
  • Alle übersprungenen Steine werden vom Brett genommen
  • Erreicht ein Spielstein die gegnerische Grundlinie, wird er zur Dame befördert
  • Eine Dame darf nun auch rückwärts ziehen und springen

Die Klasse Cplayer

class Cplayer
{
 public:
//Konstanten
char name[20]; //Name des Spielers
char figure; //Spielerzeichen ’x’ oder ’o’ 
char direction; //Bewegungsrichtung 1 oder -1
char last_row; //Enthält die Feldnummer für die letzte Reihe (0 oder 7) Variablen
char can_move; //Zustandsvariable ob Spieler eine Figur bewegen kann
char can_drop; //Zustandsvariable ob Spieler eine Figur schmeißen kann
char wanna_move; //Spieler möchte seine Figur ziehen
char wanna_drop; //Spieler möchte eine gegnerische Figur schmeißen
char num_of_fig; //Anzahl der im Spiel verbliebenen Figuren
//Funktionen
bool IsMyFigure(char Figure); //Prüft ob eine Figur des Spielers vorliegt
bool IsChecker(char *position, char board[][8]); //Prüft ob eine Figur bereit Dame ist
bool IsEnemy(char *koord, char Brett[][8]); //Prüft ob eine Figur zum Gegner gehört
bool CanMove(char *koord, char Brett[][8]); //Prüft ob eine Figur ziehen kann
bool CanDrop(char *koord, char Brett[][8]); //Prüft ob eine Figur eine gegnerische schmeissen kann
bool CheckSituation(char Brett[][8]); //Prüft die Situation für den Spieler
bool validDirection(char *move_from, char *move_to, char Brett[][8]); //Prüft ob der Zug in eine für diesen Spieler gültige Richtung geht
void Drop(char *von, char *nach, char Brett[][8], Cplayer &Gegner); //Schmeißt die Figur eines Gegners
void Move(char *von, char *nach, char Brett[][8]); //Zieht eine Figur
bool Checker(char *move_to, char board[][8]); //Macht eine Spielfigur zur Dame wenn sie in der letzten Reihe ankommt
};

Das Spielfeld

Um den aktuellen Spielstand zu speichern, wird sinnvollerweise ein zweidimensionales Array der Größe 8 mal 8 verwendet, da es dem wirklichen Spielfeld am nächsten kommt. Die Zeilen und Spalten beginnen mit 0 und enden folglich mit 7. Damit der Quellcode lesbar bleibt, wurden bestimmte Zeichen verwendet, um dieses Array zu füllen. Um ein unbesetztes Feld darzustellen, wird eine Konstante namens LEER verwendet, welcher das ASCII-Zeichen ’ ’ (blank) zugewiesen wird.

const char LEER = ’ ’;

Die Spielfiguren werden durch die Zeichen ’x’ und ’o’ repräsentiert. Im Falle einer Dame, werden einfach die jeweiligen Großbuchstaben verwendet.

Spielablauf und Funktionen

Gespielt wird, solange es keinen Gewinner gibt. Das gesamte Spiel wird von einer while- Schleife umhüllt, deren Argument die Variable no_winner ist, welche daher mit true initialisiert wird und auf false gesetzt wird, sobald ein Gewinner feststeht. Dies ist der Fall, wenn einer der Spieler keinen Spielzug mehr machen kann, also wenn er keine Figuren mehr hat oder wenn alle eigenen Spielsteine vom gegnerischen Spieler blockiert werden. Um eine solche Situation festzustellen, wurde die Funktion checkSituation() geschrieben. Diese Funktion prüft mit Hilfe der Funktionen CanMove() und CanDrop() für jeden Spielstein eines Spielers, ob dieser mindestens einen Spielzug durchführen kann und setzt entsprechend die boolschen Variablen can_move und can_drop auf true. checkSituation() liefert das Ergebnis der ODER-Verknüpfung von can_move und can_drop. Ist dieses Ergebnis false wird no_winner auf false gesetzt, und das Spiel ist beendet. Wenn der Rückgabewert von checkSituation() true ist, so muss der Spieler, der am Zug ist, die Spielfigur wählen, die er bewegen möchte. Nun wird die Schnittstellenfunktion getFieldCoord() aufgerufen, welche die Spielfeldkoordinaten von Eyesweb, via UDP/IP, empfängt. Eyesweb sendet einen String, welcher die auf 1 normierten Koordinaten des Fingers, bezogen auf den Arbeitsraum der Kamera, enthält. Beispiel:

/eyesweb/fingercoord ",ff" 0.738 0.692

Diese Koordinaten müssen aus dem String gefiltert werden. Hierzu durchsucht man den String nach der Unterzeichenfolge „0.“. Die folgenden Ziffern bis zum nächsten „0.“ ist die X-Koordinate. Das zweite „0.“ und die nachfolgenden Ziffern entsprechen der Y-Koordinate.

X = 0.738, Y = 0.692

Da das Spielbrett in einem 8x8-Felder-Array hinterlegt ist, müssen diese X-Y- Koordinaten einem Wertepaar zugeordnet werden, welches dem am Bildschirm berührten Feld entspricht. Nachdem das Spielfeld herausgefiltert wurde, muss geprüft werden, ob dort ein gültiger Spielstein steht. Ist das Feld leer oder von einem gegnerischen Stein besetzt, ist die Wahl ungültig und muss wiederholt werden. Da der Spieler verpflichtet ist, einen gegnerischen Stein vom Feld zu nehmen, wenn er die Möglichkeit hat (can_drop == true), muss nun geprüft werden, ob der zum Zug ausgewählte Spielstein sich in einer solchen Situation befindet. Hierzu wird zusätlich nur für diesen Stein noch einmal die Funktion CanDrop() aufgerufen. Ist der Rückgabewert false, muss auch hier die Eingabe wiederholt werden. Wenn can_drop gleich false ist, muss can_move true sein. Und auch hier muss mittels CanMove() geprüft werden, ob der ausgewählte Spielstein in der Lage ist, einen Zug durchzuführen. Wenn die Wahl des Spielsteins nicht mehr wiederholt werden muss, ist der nächste Schritt das Zielfeld zu wählen. Mit getFieldCoord() wird also wieder per UDP ein String von EyesWeb empfangen. Mit den Funktionen IsEmpty() und validDirection() wird überprüft, ob das Zielfeld leer ist und ob der Spieler seine Figur in die richtige Richtung zieht. Auch die Zugweite spielt eine Rolle. Je nachdem, ob gezogen oder geworfen wird, muss die Zugweite eins oder zwei betragen. Desweiteren muss beim Überspringen eines gegnerischen Spielsteins auch ein solcher vorhanden sein. Fällt eine dieser zahlreichen Überprüfungen negativ aus, muss die Eingabe des Zielfeldes wiederholt werden. Nun wird eine der Funktionen Drop() oder Move() aufgerufen. Anschließend wird mit der Funktion Checker() geprüft, ob der bewegte Spielstein sich an der Grundlinie des Gegners befindet, wodurch der Spielstein zur Dame befördert wird. Wurde eine Spielfigur des Gegner entfernt, wird wieder die CanDrop() Funktion aufgerufen, da Mehrfachsprünge möglich sind. In diesem Fall steigt man wieder bei der Wahl eines Zielfeldes ein. Zuletzt findet ein Spielerwechsel statt und der Ablauf beginnt von vorne.

Grafik

Die Grafische Ausgabe wurde, wie bereits erwähnt, mit OpenGL realisiert.

Zur Darstellung mit OpenGL wird eine Endlosschleife verwendet, womit die Art der Programmierung von üblichen Programmen abweicht. Der Hintergrund dieses Systems ist ständige neuzeichnen der Anzeige, um auch bei unverändertem Inhalt nach einer Interaktion durch den Benutzer, wie beispielsweise das Ziehen des Anzeigenfensters, die Grafik weiter anzuzeigen. Sie wird also laufend neu gezeichnet. Das Schema eines OpenGL-Programms lässt sich wie folgt darstellen:

Ogl struktur.jpg

Um die Verwendung von OpenGL einfacher zu gestalten, und ebenso Ein- wie Ausgaben leichter erstellen zu können, wurde das Toolkit GLUT verwendet, welches ebendiese Funktionen bereitstellt. Weiter wurden verschiedene Bibliotheken eingebunden, die die Verarbeitung von Dateien im PNG-Format erleichtern, sowie andere Funktionalitäten zur Bearbeitung von Bildern zur Verfügung stellen. Diese werden jedoch hier nicht genutzt.

Initialisierung

Hier werden grundlegende Einstellungen vorgenommen. Beispielsweise die Größe des Fensters, der Texturmodus und andere.

// Initialize	GLUT
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize(400, 400); //Set the window size
glEnable(GL_TEXTURE_2D);
// Create	the	window
glutCreateWindow("GUI");
initRendering(); //Initialize rendering

Die aufgerufenen Funktionen sind sämtlich von OpenGL oder dem GLUT Toolkit bereitgestellt.

GLUT_DOUBLE:

Hier wird OpenGL mitgeteilt, die Darstellung doppelt zu puffern. D.h. in der Zeichenfunktion wird in den inaktiven Puffer gechrieben. Um diesen dann darzustellen, muss die Funktion glutSwapBuffers() augerufen werden, welche die beiden Puffer tauscht.

GLUT_RGB:

Der Farbmodus wird auf RGB gesetzt. Weitere Einstellungen wie Einstellungen bezüglich Alpha-Kanälen und Antialiasing werden beim Laden der entsprechenden Texturen vorgenommen, da verschiedene Formate verwendet werden, und die Texturen deshalb nicht gleich behandelt werden können.

GLUT_DEPTH:

GLUT_DEPTH aktiviert den „Tiefenpuffer“ (depth buffer), der eine Auswertung der Z-Achse in Blickrichtung ermöglicht. Dies wird benötigt, um den 3D-Raum bei der Konvertierung in ein zweidimensionales Bild, tiefenrichtig darzustellen. Das bedeutet konkret, dass näher an der Kamera liegende Pixel, darunter liegende Pixel überschreiben.

GL_TEXTURE_2D:

Der Texturmodus für 2D-Texturen wird eingeschaltet. Für die einfach Ebenendarstellung des Damespiels sind diese ausreichend.

Texturen laden und Funktionen setzen

// Textur
loadTextures();

Ruft die Funktion zum Laden der Texturen auf. Im folgenden ein Auszug aus dieser Funktion:

Image* image =NULL;
glEnable(GL_TEXTURE_2D);
// Brett Texturen
textureID[0] = *loadBMP("white.bmp");
textureID[1] = *loadBMP("black.bmp");
// Figuren Texturen
texture_o = getalphatexture("player1.png");
texture_O = getalphatexture("player1d.png");

Die Klasse Image

// Represents	an	image
class Image
{
 public:
  Image(char* ps, int w, int h);
  ~Image()
 {
  delete[] pixels;
 }
 char* pixels;
 int width;
 int height;
 float alpha;
};

Es werden verschiedene Texturarten geladen. Zum einen im BMP-Format, zum anderen PNG-Dateien mit deren Alpha-Kanälen. Die Texturen im BMP-Format werden für die Schachbretttexturen geladen, und ein Zeiger auf diese in einem Array abgelegt. Die PNG-Texturen werden für die Spielerfiguren verwendet. OpenGL erlaubt nur bestimmte Objekte mit Texturen zu belegen. Beim zeichnen eines Objekts muss zuvor die Art der Verbindung der einzelnen Punkte festgelegt werden. Im Modus GL_POLYGON können mehrer Punkte angegeben werden, die anschließend miteinander verbunden werden. Objekte die in diesem Modus erstellt wurden, können jedoch nicht mit Texturen belegt werden. Die naheliegende Idee, eine Viereckige Textur mit Alpha-Kanälen zu laden, wurde trotz erhöhtem Aufwand deshalb verwendet.

Figur textur 3.jpg

Die GD library

Einen weiterführenden und vor allem vereinfachten Zugriff auf die Bildbearbeitungsfunktionen von OpenGL bietet die GD library. Eigentlich erlaubt die GD-Bibliothek ein sehr viel größeres Spektrum als nur das Laden einer Grafik, dennoch erfüllt sie den gewünschten Zweck, andere brauchbare Bibliotheken waren nicht zu finden. Die Figurobjekte werden jetzt als Rechtecke erzeugt, und anschließend mit einer aus png Dateien erzeugten Textur belegt. Die Texturdatei enthält Pixel, die als durchsichtig definiert sind. Dadurch wird ebenfalls eine als Kreis wahrgenommene Figur dargestellt

void circle(float xmp, float ymp, float zmp, float r)
{
 float z=zmp;
 // Aus Vierecken wg. Texturen
 glBegin(GL_QUADS);
 glTexCoord2f(0.0f, 0.0f);
 glVertex3f(xmp-r,ymp-r,z);
 glTexCoord2f(1.0f, 0.0f);
 glVertex3f(xmp+r,ymp-r,z);
 glTexCoord2f(1.0f, 1.0f);
 glVertex3f(xmp+r,ymp+r,z);
 glTexCoord2f(0.0f, 1.0f);
 glVertex3f(xmp-r,ymp+r,z);
 glEnd ();
}

Zeichenfunktion

Die Zeichenfunktion wird, wie in der Grafik dargestellt, regelmäßig aufgerufen. Nach dem Durchlaufen der Zeichenfunktion, wird glutSwapBuffers() aufgerufen, wie bereits erklärt. Da die eigentliche Zeichenfunktion sehr umfangreich ist, soll hier nur ein Auszug gezeigt werden.

void DrawScene(void)
{
 //	comm ();
 // Board fuellen
 int row, col;
 float xmp,ymp;
 const GLfloat z = -240.0f; // -200 -> Ohne Rand
 int x=-FieldSizeX*4, y=FieldSizeY*4; //um halbe Spielbrettgroeße
 // verschoben	damit	mittig
 const	int	width=FieldSizeX ,	height=FieldSizeY;
 // schwarz und weiss
 GLfloat ChessColors[2][4] = {{0.0f,0.0f,0.0f,1.0f},{1.0f,1.0f,1.0f,1.0f}};
 //Clear information from last draw
 glClearColor(0.0f , 0.0f, 0.0f, 1);// Bildschirm löschen mit
 // eingestellter	Farbe	--	RGBA
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 glMatrixMode(GL_MODELVIEW);
 glLoadIdentity ();
 glDisable(GL_BLEND);

Die Parameter des Befehls glClear legen fest, dass farbiges Schreiben aktiviert wurde, ausserdem wird der Tiefenpuffer aktiviert (s.o.). Der Aufruf glMatrixMode bezieht sich in erster Linie auf die folgenden Matrixoperationen. Je nach gewähltem Modus, wird ein Stack von Matrizen angelegt, durch die ein Objekt Transformiert werden kann. Je nach Anwendungsfall sind hierbei verschiedene Modi vorzuziehen. Um eine Tiefendarstellung zu erreichen, verwendet man den Modus GL_PROJECTION, welcher 2 Matrizen bereitstellt. Die Darstellung erreicht man dann über den Aufruf von glOrtho() mit passenden Parametern. Der Modus GL_MODELVIEW bietet 32 Matrizen. Dieser Modus bietet sich insbesondere an, wenn mehrfach gleiche Objekte an verschiedenen Stellen erzeugt werden sollen. Das Ausgangsobjekt befindet sich in der sogenannten Identity Matrix. Per glLoadIdentity() kann man zu dieser Matrix zurückkehren. Ein Aufruf von glPushMatrix() erzeugt eine Kopie der Identity Matrix, die man daraufhin modifizieren kann, wie z.B. Transversal verschieben. Mit glPopMatrix() wird diese wieder aus dem Stack entfernt. In dieser Abfolge kann man beliebig viele Objekte erstellen, während der Matrix-Stack diese Operationen CPU- und Speicherschonend durchführen lösst. Weitere Modi sind GL_TEXTURE sowie GL_COLOR, die hier jedoch keine Anwendung fanden. Der Aufruf glDisable(GL_BLEND) beendet den Blending-Modus (s.o.).

OpenGL Mainloop

Die Hauptfunktion long WINAPI ogl_main(void) beinhaltet auch den Aufruf der Hauptschleife. Im Gesamten hat die Funktion diesen Inhalt.

long WINAPI ogl\_main(void)
{
 int ArrayEntry;
 // Initialize	GLUT
 glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glEnable(GL_TEXTURE_2D);
glShadeModel(GL_SMOOTH);
// Create	the	window
glutCreateWindow("GUI");
initRendering(); //Initialize rendering
// Textur
loadTextures ();
// Testfiguren setzen
int outer,inner;
for(outer=0;outer <8;outer++)
{
 for(inner=0;inner <8;inner++)
 {
  board[inner][outer]=’␣’;
 }
}
//Set	handler	functions for drawing, keypresses, and window resizes
glutDisplayFunc(DrawScene);
glutKeyboardFunc(handleKeypress);
glutReshapeFunc(handleResize);
glutMouseFunc(handleMouse);
glutTimerFunc(1000, handleTimer, 1000); //alle 1000 ms neu zeichnen
glutMainLoop(); //Start the main loop. glutMainLoop doesn’t return.
return 0; //This line is never reached
}

Kalibrierung

Beim Start des Programms wird ein dunkles Rechteck angezeigt, um der Grafikverarbeitung durch Eyesweb die Eckdaten des Bretts mitzuteilen. Die Kalibrierung wird mit der ESC-Taste ein- und ausgeschaltet. Ähnlich dem Prozess des Damespiels, läuft der Kommunikationsprozess im Hintergrund. Dieser sendet die erhaltenen Daten an die GUI Hauptfunktion, welche im nächsten Aktualisierungsdurchlauf das neue Spielbrett zeichnet.

Multithreading

Um das Spielbrett untereinander austauschen zu können, ist irgendeine Art der Kommunikation zwischen der Grafikausgabe sowie dem Damespiel an sich (der Engine) nötig. Ausserdem muss zwischen der Engine und dem Bildverarbeitungsmodul eine Kommunikation stattfinden. Da die Bildverarbeitung stark an das Programm Eyesweb gebunden ist, ist dessen Einbindung unmöglich. Anders jedoch bei den Modulen Grafik und Damespiel. Beide sind in der Programmiersprache C verfasst. Deshalb wurden beide in ein Projekt der Entwicklungsumgebung Microsoft Visual Studio 2005 eingebunden. Ein erster Versuch beide Programme miteinander zu verbinden, führte nicht zum gewünschten Ergebnis.

Die Architektur

Es wurde die Initialisierungsfunktion für OpenGL als Hauptfunktion gewählt, und vor einem Aufruf der Zeichenfunktion das Damespiel gestartet. Nach einigen Anpassungschwierigkeiten konnte die Grundlegende Funktionalität eingebaut werden. Insbesondere da auch das Damespiel als Stand-Alone-Modul ausgelegt wurde und deshalb eine „Hauptschleife“ beinhaltet, wurde dieser Umbau eine Zeitaufwändige Variante. Nachdem in der OpenGL-Initialisierung das Damespiel aufgerufen werden konnte, stellte sich heraus, dass diese Architektur aufgrund der OpenGL Hauptschleife nicht funktionsfähig ist. Die langen Pausen zwischen zwei Zeichenvorgängen, durch den Aufruf des Damespiels, ließen die Grafik nahezu funktionslos erscheinen. Als einzige, wiederum sehr aufwändige und Zeitintensive Variante, bot sich die Realisierung einer Multiprozessvariante an. Diese Variante ist auch jetzt implementiert.

Prozesse

Als einzelne Prozesse werden die Grafik als auch das Damespiel aufgerufen. Zur Verwaltung der Prozesse wird die Windows API verwendet. Hierzu wird die Bibliothek kernel32.lib bzw. kernel32.dll eingebunden, sowie die Headerdatei windows.h. Die Funktionen sind wie folgt definiert.

long WINAPI ogl_main(void);
long WINAPI dame_main(int argc, char *argv[]);

Die Verwaltung der Prozesse findet in der Hauptfunktion statt.

int main(int argc, char **argv)
{
 // init glut, geht nicht über prozess
 glutInit(&argc, argv);
 DWORD	dwMainThreadId	= GetCurrentThreadId ();
 // Critical Sections
 hMutex = CreateMutex(NULL,FALSE,_T("Boardmutex"));
 // fire up threads
 HANDLE	hThread [2];
 DWORD	dwThreadId [2];
hThread[0] = CreateThread(
  (SECURITY_ATTRIBUTES*) 0,0,
  (LPTHREAD_START_ROUTINE)ogl_main ,
  (void*)0,0, &dwThreadId[0]);

 hThread[1] = CreateThread(
  (SECURITY_ATTRIBUTES*) 0,0,
  (LPTHREAD_START_ROUTINE)dame_main ,
  (void*)0,0, &dwThreadId[1]);

 hThread[2] = CreateThread(
  (SECURITY_ATTRIBUTES*) 0, 0,
  (LPTHREAD_START_ROUTINE)comm,
  (void*)0, 0,
  &dwThreadId[2]);

 WaitForMultipleObjects( 2, hThread, true, INFINITE);
 CloseHandle(hThread[0]);
 CloseHandle(hThread[1]);
 CloseHandle(hThread[2]);
 CloseHandle(hMutex);
 CloseHandle(hMutex_comm);
 CloseHandle(hMutex_selections);
 return 0;
}

Der Aufruf glutInit() musste aufgrund der geforderten Übergabe von den Originalparametern argc und argv in die Hauptfunktion ausgelagert werden.

Mutex

Der Name Mutex kommt aus dem Englischen und steht für mutual exclusion. Der Begriff Wechselseitiger Ausschluss bzw. Mutex bezeichnet eine Gruppe von Verfahren, mit denen das Problem des kritischen Abschnitts gelöst wird. Mutex-Verfahren verhindern, dass nebenläufige Prozesse bzw. Threads gleichzeitig oder zeitlich verschränkt gemeinsam genutzte Datenstrukturen unkoordiniert verändern, wodurch die Datenstrukturen in einen inkonsistenten Zustand geraten können, auch wenn die Aktionen jedes einzelnen Prozesses/Threads für sich betrachtet konsistenzerhaltend sind. Mutex-Verfahren koordinieren den zeitlichen Ablauf nebenläufiger Prozesse/Threads derart, dass andere Prozesse/Threads von der Ausführung kritischer Abschnitte ausgeschlossen sind, wenn sich bereits ein Prozess/Thread im kritischen Abschnitt befindet (die Datenstruktur verändert). Mutex-Verfahren sind der Klasse der Verfahren zur Prozess- oder Thread- Synchronisation zugeordnet und sind von zentraler Bedeutung für jegliche Systeme nebenläufig ablaufender Prozesse/Threads mit modifizierendem Zugriff auf gemeinsam genutzte Daten/Datenstrukturen, so z.B. auch für Client/Server-Systeme mit unabhängigen Client-Prozessen/-Threads, die gleichzeitig bzw. zeitlich verschränkt auf einen Datenbank-Server zugreifen. Die hierzu benötigten Funktionen werden ebenfalls durch die WinAPI bereitgestellt.

Die Architektur wurde so gewählt, dass sowohl das Damespiel als auch das Grafikprogramm einen eigenen Speicher für das Array des Spielbretts verwenden. In der Datei sections.cpp wird diese Speicherverwaltung vorgenommen.

#include "sections.h"
BOARD_TYPEDEF getNewBoard()
{
 BOARD_TYPEDEF temp;
 WaitForSingleObject(hMutex, INFINITE);
 temp = Board;
 ReleaseMutex(hMutex);
 return temp;
}

void	sendNewBoard(BOARD_TYPEDEF newBoard ,BOARD_TYPEDEF target)
{
 printf("sendNewBoard-Entry\n");
 WaitForSingleObject(hMutex, INFINITE);
 target = newBoard;
 ReleaseMutex(hMutex);
 printf("sendNewBoard-Released\n");
}

Der in der Hauptfunktion erzeugte Mutex kontrolliert hier den Zugriff auf den während des Durchlaufs verwendeten Speicher. Nachdem eine Änderung vorgenommen wurde, speichert das Damespiel (als Thread) den neuen Array in dem Spielfeld-Array der Grafik, indem die Funktion sendNewBoard() aufgerufen wird. Weiter bietet die Funktion getNewBoard() dem Grafik Thread die Möglichkeit, das Spielfeld Array neu zu laden. Diese Variante erwies sich als zuverlässig, und ausreichend schnell. Deshalb ist diese die jetzt verwendete Methode. Beide Threads erzeugen jeweils ein Fenster. Eine Anpassung der angezeigten Grafiken ist durch einfache Änderung der entsprechenden Grafikdateien möglich. Ein Auszug aus diesen, um den Aufbau der Anzeige nachvollziehen zu können.

Darstellung.jpg