überarbeitet von: Denny R. Walter auf Basis eines älteren Kapitels (vom 29.10.2000) von Dr. Erhard Henkes

Kapitel 9 - "Bewegungsabläufe"     Stand: 29.06.2006



Hallo, mein Name ist
Denny R. Walter. Herr Dr. Henkes hat mich darum gebeten, dieses Kapitel zu überarbeiten, weshalb ich es diesmal sein werde, der Ihnen die Vorgehensweisen erklärt.

Sie werden in diesem Kapitel hauptsächlich lernen bzw. wiederholen:
- wie man Bilder auf dem Bildschirm ausgibt, wobei eine Farbe als Hintergrund gilt und nicht mitkopiert wird
- wie man Bewegungsabläufe ohne Flackern realisiert.

Bevor Sie dieses Kapitel durchgehen, sollten Sie sich Kapitel 4, Unterpunkte 1 und 2 durchgelesen haben.

Erstellen Sie nun bitte eine dialogfeldbasierte MFC-Anwendung namens "Game" und deaktivieren Sie die Einbindung von ActiveX-Steuerelementen sowie die Infobox. Entfernen Sie dann aus Ihrem Dialog die Buttons und den Text, so dass Sie ein leeres Dialogfeld haben.



In unserem Programm geht es darum, eine Figur auf einem Hintergrund darzustellen und zu bewegen.
Bitte laden Sie sich dafür folgende Grafiken herunter und speichern Sie diese als Bitmap-Dateien im Format xxx.bmp (z.Z. liegen sie im GIF-Format vor.)

http://henkessoft.de/C++/MFC/MFC_Kap09/mann1.gif
http://henkessoft.de/C++/MFC/MFC_Kap09/mann2.gif
http://henkessoft.de/C++/MFC/MFC_Kap09/getroffen.gif

http://henkessoft.de/C++/MFC/MFC_Kap09/hintergrund.gif


Nachdem Sie die Dateien heruntergeladen und mittels eines Grafikprogramms als Bitmap gespeichert haben, fügen Sie die Bitmap-Dateien dem Projekt bitte als Ressource hinzu. Dazu gehen Sie in der linken Registerkarte auf die Ressourcenansicht, rechtsklicken auf "Game Ressourcen", wählen "Importieren" und suchen die vier Dateien. Markieren Sie sie und klicken Sie auf "Importieren". Wenn Sie sie nicht finden können, prüfen Sie bitte, ob unter "Dateityp" "Alle Dateien (*.*)" angegeben ist. Nun gehen Sie wieder in die Ressourcenansicht und machen einen Rechtsklick auf jedes einzelne Bild, um dann auf "Eigenschaften" zu klicken. Die Bilder müssen nun von IDB_BITMAP1 etc. umbenannt werden in

IDB_MANN1
IDB_MANN2
IDB_GETROFFEN
IDB_HINTERGRUND

Wir erstellen in der Klassendefinition von CGameDlg nun einige Membervariablen:

class CGameDlg
{
...
protected:

    CBitmap m_BackBuffer;
    CBitmap m_Hintergrund;
    CBitmap m_Mann [3];

    UINT m_Index;
    int m_X;
    int m_Y;
...
}

Wir haben ein Feld von CBitmap-Variablen mit dem Namen m_Mann. Dieses speichert die Bewegungsphasen der Figur.
m_Hintergrund speichert das Hintergrundbild.
m_Index ist der Feldindex, der das aktuell anzuzeigende Bild der Figur festlegt.
m_X und m_Y sind die Position der Figur auf dem Bildschirm.

m_BackBuffer ist ein Bild, in das das fertige Aussehen des Spielfeldes zwischengespeichert wird, bevor wir es ausgeben. Damit werden Flackereffekte verhindert. Man stelle sich vor, folgendes würde bei einer Animation direkt auf das Fenster kopiert werden: Hintergrund, Figur, Hintergrund, Figur, Hintergrund, Figur. Dann gibt es immer einen winzig kleinen Moment, in dem der Hintergrund zu sehen ist, die Figur aber noch nicht. Dadurch entstehen Flackereffekte bei der Figur. Deshalb wird es so gemacht, dass erst der Hintergrund und dann die Figur in den Back-Buffer kopiert werden. Und erst dieser wird direkt auf den Bildschirm kopiert. Somit wird pro Animationsphase nur ein Bild auf den Bildschirm kopiert (der Back-Buffer) und dadurch gibt es kein Flackern. Außerdem hat der Back-Buffer einen weiteren Vorteil: Man will ein Spiel programmieren, bei dem man die Größe, sprich, den Zoom-Faktor des Spielfelds, ändern können soll. Würde man den Back-Buffer nicht benutzen, müßte man die Bewegung und die Position von jedem einzelnen Bildobjekt von der aktuellen Größe abhängig machen. Ein Beispiel: Bei normaler Größe steht die Spielfigur an Position 10,34. Bei halber Spielfeldgröße müßte sie aber an Position 5,17 stehen. Und wenn die Schrittweite normalerweise vier Pixel ist, müßte sie bei halber Größe zwei und bei doppelter Größe acht Pixel sein. Mit dem Back-Buffer muss man sich um sowas nicht mehr kümmern: Sämtliche Bilder sind intern in ihrer originalen Größe gespeichert. Auch der Back-Buffer selbst hat die Größe des originalen Spielfeldes. Auf ihn werden nun alle Spielelemente so kopiert, wie es bei einer Größe von 100% gedacht ist. Und erst ganz am Ende wird der Back-Buffer, der ja dann ein komplettes Bild der aktuellen Situation gespeichert hat, entweder gestreckt oder gestaucht auf dem Fenster ausgegeben. Dieses Phänomen können Sie in unserem Beispiel nachher selbst begutachten, wenn Sie das Dialogfeld in der Größe veränderbar machen: Obwohl wir uns in unserem Spiel bezüglich Animationen etc. nicht um die aktuell angezeigte Größe kümmern, wird das Spiel immer genauso groß wie das Dialogfeld und die Objekte werden an der richtigen Position sein. Das ist einer der Vorteile von Back-Buffering. Doch nun genug geredet, machen wir weiter mit dem Programm.

Wir müssen die Variablen initialisieren. Das erledigen wir in der Funktion OnInitDialog:

BOOL CGameDlg::OnInitDialog ()
{
...
    CClientDC windowDC (this);
    /*Dies ist ein Device Context des Fensters.
      Im Moment wird er lediglich als Hilfsvariable gebraucht,
      um die Bilder zum Fenster kompatibel zu machen.*/

    BITMAP bm;
    /*Diese Bitmapstruktur speichert Daten eines Bildes.
      Wir brauchen sie, um die Größe des Hintergrundes zu ermitteln.*/

    m_Hintergrund.LoadBitmap (IDB_HINTERGRUND);
    //Wir laden das Hintergrundbild in die Variable...

    m_Hintergrund.GetBitmap (&bm);
    //...und ermitteln unter anderem die Größe.

    m_BackBuffer.CreateCompatibleBitmap (&windowDC, bm.bmWidth, bm.bmHeight);
    /*Wir erstellen ein leeres Bild für den Back-Buffer.
      Da der Hintergrund das Spielfeld ausmacht, müssen Hintergrund und
      Back-Buffer gleich groß sein.*/

    m_Mann [0].LoadBitmap (IDB_MANN1);
    m_Mann [1].LoadBitmap (IDB_MANN2);
    m_Mann [2].LoadBitmap (IDB_GETROFFEN);
    //Laden der Bilder für die Figur.

    m_Index=0;
    /*Das erste Bild soll m_Mann [0] sein,
      darum setzen wir den Index auf 0.*/

    m_X=110;
    m_Y=185;
    //Die Anfangsposition für die Figur ist 110,185.
...
}

Für die Ausgabe erstellen wir eine eigene Memberfunktion in der Klasse. Wählen Sie also in der linken Registerkarte die Klassenansicht, klicken Sie mit rechts auf CGameDlg und wählen Sie "Member-Funktion hinzufügen". Geben Sie als Funktionstyp void und als Name der Funktion Ausgabe () an. Alle weiteren Optionen sind für unser Projekt zunächst unerheblich. Klicken Sie auf OK. Gehen Sie nun in die Funktion OnPaint und bereiten Sie diese so vor, dass sie wie folgt aussieht:

void CGameDlg::OnPaint ()
{
    Ausgabe ();

    CDialog::OnPaint ();
}

Damit gehen wir sicher, dass die Ausgabe jedesmal wiederholt wird, sobald ein Teil des Fensters neu gezeichnet werden muss.

Nun kümmern wir uns um die Ausgabe. In der Ausgabe-Funktion muss folgender Code stehen, den ich jeweils erkläre:

CClientDC windowDC (this);
CDC backBufferDC,
    hintergrundDC,
    mannDC,
    maskDC,
    orDC,
    tempDC;

/*Ein Device Context (DC, die entsprechende Klasse ist also CDC) ist eine Art Container für eine Bitmap-Variable. Die Klasse CBitmap liefert keine eigenen Funktionen, um das in ihr gespeicherte Bild zu bearbeiten und zu verändern. Deshalb muss man einen Device Context anlegen, in den man die Bitmap-Variable hineinwählt. Der DC stellt nun Funktionen zur Verfügung, um das Bild in verschiedener Weise zu manipulieren.
Im Gegensatz zu CBitmap-Variablen sollte man DCs niemals dauerhaft speichern, sondern immer nur lokal innerhalb von Funktionen anlegen. Würde man es anders machen und man hätte eine mittelgroße Anzahl an Sprites, könnte das Programm geschwindigkeitstechnisch ganz schön in die Knie gehen. Deshalb würde ich auch empfehlen, die Schritte, die nötig sind, um ein Bild in den Back-Buffer zu kopieren, welche wir hier in einer Funktion abarbeiten, in einem richtigen Programm in mehrere Funktionsaufrufe zu unterteilen, denn sonst hat man bei 10 Bildern, die alle in der Funktion Ausgabe einen DC brauchen, nachher das gleiche Problem. Für ein richtiges Programm sollte man also eine Funktion im Stil von void KopiereBildNachBackBuffer (CBitmap *bild, CBitmap *backBuffer) haben.
CClientDC ist ein besonderer DC. Er ist nämlich automatisch mit dem im Parameter angegebenen Fenster (this, stellt einen Zeiger auf das Objekt der Klasse CGameDlg dar) verbunden. Sämtliche Zeichenoperationen wirken sich dabei also direkt auf das Fenster aus. Durch ihn wird am Ende die eigentliche Ausgabe realisiert. Außerdem benötigen wir ihn, um die anderen DCs zum Fenster kompatibel zu machen.
Der Mask-, Or- und Temp-DC wird gebraucht, um die Figur so auf den Back-Buffer zu kopieren, dass die Hintergrundfarbe transparent wird. (Das wurde bereits in Kapitel 4 besprochen.)*/

CBitmap maskBitmap,
        orBitmap,
        tempBitmap;
//Hier haben wir analog zu den drei zusätzlichen DCs die Bitmap-Variablen.

CRect rect;
BITMAP bm;
/*rect und bm werden gebraucht, um die Größe von Bitmaps (bm)
  bzw. des Fensters (rect) zu ermitteln.*/

/*Im folgenden werden die Back-Buffer kompatibel gemacht
  und mit ihren jeweiligen Bitmaps verknüpft.*/

backBufferDC.CreateCompatibleDC (&windowDC);
backBufferDC.SelectObject (&m_BackBuffer);

hintergrundDC.CreateCompatibleDC (&windowDC);
hintergrundDC.SelectObject (&m_Hintergrund);

mannDC.CreateCompatibleDC (&windowDC);
mannDC.SelectObject (m_Mann [m_Index]);

m_Mann [m_Index].GetBitmap (&bm);

/*Mask-, Or- und Temp-Bitmap werden erst noch erstellt
  und benötigen die gleiche Größe wie die der Figur,
  weshalb dessen Werte in die bm-Variable kopiert wurden.*/

maskBitmap.CreateBitmap (bm.bmWidth, bm.bmHeight, 1, 1, NULL);
//maskBitmap ist monochrom, deshalb hier kein kompatibles Bitmap.

maskDC.CreateCompatibleDC (&windowDC);
maskDC.SelectObject (&maskBitmap);

orBitmap.CreateCompatibleBitmap (&windowDC, bm.bmWidth, bm.bmHeight);
orDC.CreateCompatibleDC (&windowDC);
orDC.SelectObject (&orBitmap);

tempBitmap.CreateCompatibleBitmap (&windowDC, bm.bmWidth, bm.bmHeight);
tempDC.CreateCompatibleDC (&windowDC);
tempDC.SelectObject (&tempBitmap);

m_Hintergrund.GetBitmap (&bm);
backBufferDC.BitBlt (0, 0, bm.bmWidth, bm.bmHeight, &hintergrundDC, 0, 0, SRCCOPY);

/*Die obige Stelle ist die erste wirklich Wesentliche in dieser Funktion. Es wird zuerst die Größe des Hintergrunds ermittelt und dieser dann auf den Back-Buffer kopiert. Das erfolgt mit der Funktion BitBlt. Die ersten beiden Parameter sind hierbei die Position. Der dritte und vierte Parameter stellt die Größe dar, in der der Hintergrund auf dem Back-Buffer erscheinen soll. Wir belassen es bei der Originalgröße. Der fünft Parameter ist der DC des zu kopierenden Bildes. Danach kann man auswählen, ob man vom Hintergrund nur einen bestimmten Teil haben will. Man gibt dazu hier die Anfangsposition desselben an. Wir wollen das ganze Bild kopieren, also setzen wir beide Werte auf 0. Der letzte Parameter gibt die Art an, wie kopiert wird. SRCCOPY ist der Standard. Für weitere Informationen sehen Sie bitte in Kapitel 4 nach.*/

/*Der folgende Teil mit dem Mask-, Or- und Temp-DC ist ziemlich technisch und man muss ihn auch nicht im einzelnen verstehen. (Ich verstehe auch nicht jeden Aspekt.) Man muss nur wissen, dass man mit einem Bild immer so vorgehen muss, um es mit transparenter Hintergrundfarbe auf ein anderes Bild (in unserem Fall den Back-Buffer) zu kopieren.*/

m_Mann [m_Index].GetBitmap (&bm);

mannDC.SetBkColor (RGB (0x0, 0x0, 0x0));
/*Die Hintergrundfarbe wird auf schwarz gesetzt.
  Diese wird nachher nicht mit kopiert, da sie dann als transparent gilt.*/

maskDC.BitBlt (0, 0, bm.bmWidth, bm.bmHeight, &mannDC, 0, 0, SRCCOPY);

orDC.BitBlt (0, 0, bm.bmWidth, bm.bmHeight, &mannDC, 0, 0, SRCCOPY);
orDC.BitBlt (0, 0, bm.bmWidth, bm.bmHeight, &maskDC, 0, 0, 0x220326);

tempDC.BitBlt (0, 0, bm.bmWidth, bm.bmHeight, &backBufferDC, m_X, m_Y, SRCCOPY);
tempDC.BitBlt (0, 0, bm.bmWidth, bm.bmHeight, &maskDC, 0, 0, SRCAND);
tempDC.BitBlt (0, 0, bm.bmWidth, bm.bmHeight, &orDC, 0, 0, SRCPAINT);

backBufferDC.BitBlt (m_X, m_Y, bm.bmWidth, bm.bmHeight, &tempDC, 0, 0, SRCCOPY);
/*Hier wird nun der letzte DC, der nach einem komplizierten Prozeß bearbeitet wurde,
  auf den Back-Buffer kopiert. Es handelt sich um das Bild der Figur ohne Hintergrundfarbe.*/

/*Und nun kommt der finale Teil: Das Bild des Back-Buffers wird auf das Fenster gezeichnet. Und zwar relativ zur Fenstergröße.*/

m_BackBuffer.GetBitmap (&bm);
//Wir ermitteln die Größe des Back-Buffers.

GetClientRect (&rect);
//Wir ermitteln die Größe des Fensters.

windowDC.SetStretchBltMode (COLORONCOLOR);

/*Wir geben den Modus für Streckungen und Stauchungen an. Die beiden Wesentlichen sind hier COLORONCOLOR und HALFTONE. (BLACKONWHITE und WHITEONBLACK werden für Monochrombitmaps verwendet.) COLORONCOLOR ist die übliche Methode: Bei einer Vergrößerung werden Pixelreihen dubliziert, bei einer Verkleinerung werden einige herausgenommen. HALFTONE verursacht dagegen einen Weichzeichnungseffekt, der meiner Meinung nach bei Spielen nicht sonderlich gut aussieht. Ich würde daher immer zu COLORONCOLOR tendieren. Unter Windows 98 gibt es übrigens keinen Unterschied zwischen beiden Versionen. Wenn Sie also unter Windows 98 programmieren, sollten Sie auf HALFTONE ganz verzichten, da das Programm sonst auf Windows 2000 oder XP völlig anders aussehen könnte.*/

windowDC.StretchBlt (0, 0, rect.Width (), rect.Height (), &backBufferDC, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY);

/*Und hier ist sie nun: Die Funktion, die unser Bild ausgibt. Sie ist fast so, wie BitBlt, mit dem Unterschied, dass man das Bild strecken oder stauchen kann. Die letzten vier Parameter vor SRCCOPY stellen den Teil des Originalbildes dar, der kopiert werden soll (in unserem Fall das ganze Bild) und der dritte und vierte Parameter gibt an, wie groß dieses ausgewählte Bild wird (in unserem Fall so groß, wie das Fenster.)*/


Sollte die Figur nun noch immer einen schwarzen Hintergrund haben, dann liegt das daran, dass das Bitmap-Bild, dadurch, dass es aus einer Gif-Datei erstellt wurde, als Hintergrundfarbe nicht das echte Schwarz, sondern eine Farbe hat, die ein ganz kleines Bisschen abweicht. Bearbeiten Sie in diesem Fall die Bilddateien, indem Sie den Hintergrund mit dem Farbfüller erneut mit schwarz (RGB-Wert: 0,0,0) ausfüllen.

Nun werden wir etwas Bewegung ins Spiel bringen. Dazu brauchen wir einen Timer und eine Funktion, die eine Aktion ausführt, wenn die linke Maustaste gedrückt wurde. Gehen Sie also im Menü auf "Ansicht", "Klassen-Assistent". Wählen Sie dort unter "Klassenname" die Klasse CGameDlg. Suchen Sie unter "Nachrichten" die Zeile "WM_TIMER". Markieren Sie sie und klicken Sie auf "Funktion hinzufügen". Erledigen Sie das gleiche mit WM_LBUTTONDOWN. Klicken Sie auf OK. Sie haben jetzt in Ihrer Dialogfeldklasse zwei Funktionen mit Namen OnTimer und OnLButtonDown.

Fügen Sie der Klasse noch eine Member-Variable UINT m_Timer hinzu und initialisieren Sie diese in OnInitDialog mit 0.
Die Timer-Funktion soll jetzt so aussehen:

void CGameDlg::OnTimer (UINT nIDEvent)
{
    m_X+=10;
    /*Jedesmal, wenn der Timer aufgerufen wird, wird die Figur um 10 Pixel nach rechts bewegt.*/

    switch (m_Index)
    {
    case 0:
    case 2:
        m_Index=1;
        break;
    case 1:
        m_Index=0;
        break;
    }
    //Die Animationsphase ändert sich je nach der aktuellen Animationsphase.

    Ausgabe ();
    //Nach jedem Timeraufruf erfolgt die Ausgabe.

    if (m_X>=600)
    {
        KillTimer (m_Timer);
        m_Timer=0;
    }
    /*Wenn die Figur an einer bestimmten Stelle angelangt ist, wird der Timer beendet und die Variable, die den Timerstatus anzeigt, auf 0 zurückgesetzt.*/

    CDialog::OnTimer (nIDEvent);
}

Nun muss der Timer nur noch aufgerufen werden. Eine Bewegung soll beginnen und frühzeitig gestoppt werden, wenn die linke Maustaste geklickt wird. Schreiben Sie in die Maustastenfunktion also folgendes:

void CGameDlg::OnLButtonDown (UINT nFlags, CPoint point)
{
    //Wenn der Timer 0 ist...
    if (m_Timer==0)
    {
        //...und die Figur an der festgelegten Anfangsposition steht,...
        if (m_X==110)
        {
            m_Timer=SetTimer (1, 100, NULL);
            /*...wird der oben beschriebene Timer mit einem Intervall von 100 Millisekunden gesetzt.
                              Die OnTimer-Funktion wird
von nun an also alle 100ms aufgerufen.
                              Da wir nur einen Timer haben, spielt die Timer-ID (erster Parameter) keine große Rolle.*/
        }
        else
        {
            m_X=110;
            m_Index=0;

            Ausgabe ();

            /*Steht die Figur nicht am Anfang und bewegt sich auch nicht, wird sie wieder an den Anfang gesetzt und das Bild ausgegeben.*/
        }
    }
    else
    {
        KillTimer (m_Timer);
        m_Timer=0;

                   /*Bei laufendem Timer bewirkt ein Klick,dass der Timer gestoppt wird und die Figur stehenbleibt.*/
    }

    CDialog::OnLButtonDown (nFlags, point);
}

Klicke einmal und die Figur läuft. Erreicht sie ihr Haus, bleibt sie stehen. Klicke, während sie läuft, und sie bleibt stehen. Klicke, während die Figur irgendwo anders als am Anfang steht, und sie wird an den Anfang zurück teleportiert. So sollte das Programm funktionieren.

Nun zum Finale: Wir werden Funktionen einbauen, mit denen wir auf die Figur schießen. Wir werden dazu den Cursor verändern und bei jedem Schuss einen kurzen Sound ausgeben.

Beginnen wir mit dem Cursor. Bitte laden Sie sich folgende Datei herunter und binden Sie sie in Ihr Projekt als Ressource ein:

Cursor (als zip-Datei)

Nennen Sie sie IDC_ZIELCURSOR.

In die OnInitDialog-Funktion müssen nun folgende Zeilen:

HCURSOR cursor=AfxGetApp()->LoadCursor (IDC_ZIELCURSOR);

SetCursor (cursor);
SetClassLong (GetSafeHwnd (), GCL_HCURSOR, reinterpret_cast<LONG> (cursor));

Damit laden wir den Cursor dauerhaft. (Ich weiß nicht, ob SetClassLong die beste Möglichkeit ist, aber was anderes ist mir nicht eingefallen. Verbesserungsvorschläge sind daher willkommen.) Nun sehen Sie, wenn Sie das Programm starten, ein Fadenkreuz als Mauscursor.
Um zu schießen, benutzen wir die rechte Maustaste. Fügen Sie die Funktion OnRButtonDown genauso hinzu, wie Sie es vorhin mit OnLButtonDown getan haben. Zur Funktion:

void CGameDlg::OnRButtonDown (UINT nFlags, CPoint point)
{
    /*Da wir direkt auf den Bildschirm schießen, unser Hintergrund aber intern nicht die gleiche Größe hat und sich die tatsächliche Position des Schusses
         demnach von der
Position auf dem Hintergrund unterscheidet, müssen wir die Position, auf der der Schuß im Hintergrundbild landen soll, ausnahmsweise
         berechnen.*/


    CRect rect;
    BITMAP bm;

    //Wir ermitteln hierzu erst die Größe von Hintergrund und Fenster:
    GetClientRect (&rect);
    m_Hintergrund.GetBitmap (&bm);

    /*Die Position auf dem Hintergrund ergibt sich nun aus einer einfachen Verhältnisrechnung anhand von Position auf dem Fenster, Größe des
          Fensters und Größe des Hintergrundbildes. Der Parameter point stellt übrigens die Mausposition auf dem Fenster dar und wird für uns gleich als    
          Positionsvariable für das Bild genommen:*/

   
    point.x=point.x*bm.bmWidth/rect.Width ();

    point.y=point.y*bm.bmHeight/rect.Height ();

    m_Mann [m_Index].GetBitmap (&bm);
    //Wir ermitteln die Größe der Figur.

          /*Wenn die Schussposition im Bereich der Figur liegt, wir die Figur also getroffen haben, ...*/
    if (point.x>=m_X && point.x<=m_X+bm.bmWidth-1 && point.y>=m_Y && point.y<=m_Y+bm.bmHeight-1 )
    {
        KillTimer (m_Timer);
        m_Timer=0;

        m_Index=2;

        //... hört sie auf zu laufen und ist blutüberströmt (Bildindex 2).
    }
    else
    {
        //Ansonsten wird ein Loch dauerhaft in den Hintergrund gezeichnet.

        CClientDC windowDC (this);
        CDC dcHintergrund;

        dcHintergrund.CreateCompatibleDC (&windowDC);
        dcHintergrund.SelectObject (&m_Hintergrund);

        //Dazu erstellen wir einen Hintergrund-DC und zeichnen Linien hinein.

        dcHintergrund.MoveTo (point);
        dcHintergrund.LineTo (point.x+1, point.y-1);
        dcHintergrund.LineTo (point.x+1, point.y+1);
        dcHintergrund.LineTo (point.x-1, point.y+1);
        dcHintergrund.LineTo (point.x-1, point.y-1);
        dcHintergrund.LineTo (point.x+1, point.y-1);

        dcHintergrund.MoveTo (point);
        dcHintergrund.LineTo (point.x+4, point.y-4);
        dcHintergrund.MoveTo (point);
        dcHintergrund.LineTo (point.x+5, point.y+3);
        dcHintergrund.MoveTo (point);
        dcHintergrund.LineTo (point.x-2, point.y+5);
        dcHintergrund.MoveTo (point);
        dcHintergrund.LineTo (point.x-4, point.y+3);
        dcHintergrund.MoveTo (point);
        dcHintergrund.LineTo (point.x-5, point.y-4);
        dcHintergrund.MoveTo (point);
        dcHintergrund.LineTo (point.x-3, point.y-5);
        dcHintergrund.MoveTo (point);
        dcHintergrund.LineTo (point.x+2, point.y-3);
    }

    Ausgabe ();
    //Da sich das Bild geändert hat, erfolgt hier noch die Ausgabe.

    CDialog::OnRButtonDown (nFlags, point);
}

Ganz zum Schluß unterlegen wir das Spiel noch mit Sounds. Laden Sie sich bitte folgende Soundeffekte herunter und binden Sie sie als Ressourcen ins Projekt ein:

Sounddateien (als zip-Datei)


Benennen Sie die Ressourcen in Ihrem Projekt bitte in IDR_SCHREI und IDR_TREFFER um.


Um die Soundfunktion benutzen zu können, inkludieren wir in der Datei "GameDlg.cpp" unter unseren anderen Header-Dateien die Datei mmsystem.h. Außerdem benötigen wir dafür noch die Lib-Datei winmm.lib. Der Code sieht also folgendermaßen aus:

#include <mmsystem.h>
#pragma comment (lib, "winmm.lib")

Nun schreiben Sie als letzte Anweisungen in der if-Bedingung:

sndPlaySound (MAKEINTRESOURCE (IDR_SCHREI), SND_RESOURCE|SND_ASYNC);

bzw.

sndPlaySound (MAKEINTRESOURCE (IDR_TREFFER), SND_RESOURCE|SND_ASYNC);


Da der erste Parameter einen String erwartet, müssen wir das Makro MAKEINTRESOURCE verwenden. Die Angaben im zweiten Parameter zeigen an, dass es sich um eine Ressource handelt und das der Sound parallel zum Programmablauf (asynchron) abgespielt wird. Das Programm wird für diesen Moment also nicht komplett gestoppt.

Das war's. Sie haben nun ein kleines Demo-Spiel und mit dem, was Sie hier gelernt haben, dürften Sie eigentlich in der Lage sein, weitere Spiele zu entwickeln.