Das erste SpielUniversität zu Köln
Historisch Kulturwissenschaftliche Informationsverarbeitung
WS 12/13Übung: Visuelle Programmierung I – Simulation und 3D
ProgrammierungProf. Dr. Manfred Thaller
Referentin: Marietta Steinhöfel
Gliederung1. Das Spielprinzip2. Das Spielgerüst3. Der Code
Die Grundklasse CBreakanoid Das Titelbild Das Hauptmenü Sound
1. Das Spielprinzip Name: „Breakanoid“ (Arkanoid +
Breakout) „2.5D-Grafik“:
Grafik: 3D Bewegung: 2D (xz-Ebene)
Kamera synchron zu Schläger
2. Das Spielgerüst Die Spielzustände enum-Aufzählung verwaltet Spielzustände:
EGameState (speichert Wert für Spielzustand)
1. Intro = Titelbild, über das man zum Hauptmenü gelangt GS_INTRO
2. Hauptmenü = Hintergrundbild & Vordergrund (Auswahl Menüeintrag) GS_MAIN_MENU
3. Spiel GS_GAME
4. Kein Spielzustand GS_NONE
2. Das Spielgerüst Die Breakanoid-Klasse Grundklasse, die das ganze Spiel verwaltet:
CBreakanoid1. Instanz der Klasse wird in WinMain-Funktion erzeugt 2. Methode wird aufrufen, um Spiel zu starten3. Am Ende wird Instanz wieder gelöscht
2. Das Spielgerüst Die Spielzustandklassen Zusätzlich Klassen für Spielzustände:
CIntro CMainMenu CGame
Zeiger auf Instanzen d. Klassen in CBreakanoid gespeichert & erstellt
2. Das Spielgerüst Methoden CBreakanoid- und Spielzustandklassen haben folgende Methoden:
1. Load lädt Daten für ganzes Spiel (CBreakanoid::Load) oder
bestimmte Zustände (CIntro::Load)2. Unload
Herunterfahren3. Init
Initialisierung des kompletten Spiels bzw. Spielzuständen Aufruf der Load -Methode
4. Exit Macht Schritte rückgängig Aufruf der Unload-Methode
2. Das Spielgerüst Verwaltung der Spielzustände Spielzustand ändern mit Methode CBreakanoid::SetGameState
bekommt gewünschten Wert übergeben (GameState)1. Aktuellen Zustand verlassen
z.B.: CIntro::Exit Methode aufrufen2. Neuen Zustand initialisieren
z.B.: CMainMenu::Init3. Beginn & Ende = kein Spielzustand
GS_NONE
3. Der Code
3. 1 Die Grundklasse Breakanoid Breakanoid.cpp Breakanoid.h
Variablen I// CBreakanoid-Klasseclass CBreakanoid{public:// VariablentbConfigm_Config; // Konfiguration TriBase-EnginePDIRECT3DSTATEBLOCK9m_pStateBlock; // Statusblock für Direct3D
// Instanzen der Spielzustände – ihre Zeiger werden in Klasse gespeichert
CIntro* m_pIntro; // Intro CMainMenu* m_pMainMenu; // Hauptmenü CGame* m_pGame; // Spiel EGameState m_GameState; // Speichern des Aktuellen
Spielzustands floatm_fTime; // Stoppuhr = zählt wie viele
Sek. Zustand schon aktiv ist
[...]
Variablen II// Globale Variablenextern CBreakanoid*g_pBreakanoid; // Breakanoid-Zeigerextern float*g_pfButtons; // Array mit float-Werten zur
Abfrage der Eingabegeräte speichert Zustand analoger Knöpfeextern BOOL*g_pbButtons; // Array für digitale Werte d.
Knöpfe
Methoden SetGameState I// Setzt einen neuen SpielzustandtbResult CBreakanoid::SetGameState(EGameState NewGameState)
//erwartet EGameState-Wert
{tbResult r = TB_OK;
// 1. Alten Spielzustand entladenSwitch (m_GameState) // GameState: speichert aktuellen/alten
Zustand{ case GS_INTRO: m_pIntro->Exit(); break; // Alten Zustand
herunterfahren case GS_MAIN_MENU :m_pMainMenu->Exit(); break; //durch Aufruf der Exit-
Methodecase GS_GAME :m_pGame->Exit(); break;}
// Zeit zurücksetzenm_fTime = 0.0f; // Stoppuhr auf null zurück, weil Zustand nicht mehr
aktiv
Methoden SetGameState II// 2. Neuen Spielzustand ladenm_GameState = NewGameState; // Neuen Spielzustand initialisierenswitch (m_GameState) // Init-Methoden Aufruf für
entsprechenden Spielzustand{case GS_INTRO: r = m_pIntro->Init(); break;case GS_MAIN_MENU: r = m_pMainMenu->Init(); break;case GS_GAME: r = m_pGame->Init(); break;}
// Eventuelle Fehler abfangenif(r) TB_ERROR("Fehler beim Laden des Spielzustands!", TB_ERROR);
return TB_OK;}
Methoden Load I// Lädt das SpieltbResult CBreakanoid::Load() // lädt relevante Spiel-Daten{char acFilename[256];
// Direct3D initialisieren - Aufruf DirectX-Klassen der TriBase-Engine:
// Einstellungen des Konfig.Dialog liegen schon in m_Config (Header)
// IDI_ICON1 =Ressource, die Icon des Spiels enthält (zB. Spielszene)
if(tbDirect3D::Instance().Init(&m_Config, "Breakanoid", NULL, LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_ICON1)))){// Fehler!TB_ERROR("Fehler beim Initialisieren von Direct3D!", TB_ERROR);}
Methoden Load II[…]// DirectInput initialisierenif(tbDirectInput::Instance().Init())
// Speicher für die analogen Knöpfe reserviereng_pfButtons = new float
[tbDirectInput::Instance().GetNumButtons()]; // So viel Speicher für Array reserviert - wie analoge Knöpfe
g_pbButtons = new BOOL [tbDirectInput::Instance().GetNumButtons()];
// Und nun noch DirectSound...if(tbDirectSound::Instance().Init(&m_Config, NULL, DSSCL_PRIORITY,
FALSE))// FALSE, weil in dem Spiel kein 3D-Sound ist
{// Fehler!TB_ERROR("DirectSound konnte nicht initialisiert werden!",
TB_ERROR);}
Methoden Init I// Initialisiert das Spiel kompletttbResult CBreakanoid::Init(){tbResult r;
// TriBase-Engine initialisieren und den Konfigurationsdialog aufrufen:
if (tbInit()) return TB_ERROR;r = tbDoConfigDialog(&m_Config); //Konfigurationsdialog Aufruf &
abspeichern in m_Conig
//TB_CANCELED = wird von tbDoConfigDialog zurück geliefert, wenn Benutzer im Dialog auf ABRECHEN klickt
if(r == TB_CANCELED) return TB_CANCELED;else if(r) TB_ERROR("Engine konnte nicht initialisiert werden!",
r);
// Laden...if(Load()) TB_ERROR("Fehler beim Laden des Spiels!", TB_ERROR);
//Spieldaten laden durch LOAD Methoden Aufruf
Methoden Init II// Klassen für alle Spielzustände erstellen als Instanzen durch
NEWm_pIntro = new CIntro;m_pMainMenu = new CMainMenu;m_pGame = new CGame;
// Wir beginnen beim Intro!SetGameState(GS_INTRO); // SetGameState setzt Spielzustand aufs
Titelbild (Intro)
return TB_OK;}
Methoden Move I// Bewegt das SpieltbResult CBreakanoid::Move(float fTime) // liefert seit letztem Frame
vergangene Zeit in Sek.{tbResult r = TB_OK;
// Eingabegeräte abfragen. Wertet Eingabe des Benutzers aus + speichert
tbDirectInput::Instance().GetState(g_pfButtons, g_pbButtons);
[…]
// Aktuellen Spielzustand bewegenswitch(m_GameState) //ruft Move-Funktion für jeweilige Klasse auf{case GS_INTRO: r = m_pIntro->Move(fTime); break;case GS_MAIN_MENU: r = m_pMainMenu->Move(fTime); break;case GS_GAME: r = m_pGame->Move(fTime); break;}
Methoden Move II// Eventuelle Fehler abfangenif(r) TB_ERROR("Fehler beim Bewegen des Spielzustands!",
TB_ERROR);
// Frame-Zeit-Wert zu Zustand-Laufzeit-Stopuhr addierenm_fTime += fTime;
return TB_OK;}
Methoden Render// Rendert das SpieltbResult CBreakanoid::Render(float fTime){ […]// Aktuellen Spielzustand rendernswitch(m_GameState){case GS_INTRO: r = m_pIntro->Render(fTime); break;case GS_MAIN_MENU: r = m_pMainMenu->Render(fTime);
break;case GS_GAME: r = m_pGame->Render(fTime); break;}
// Eventuelle Fehler abfangenif(r) TB_ERROR("Fehler beim Rendern des Spielzustands!",
TB_ERROR);
return TB_OK;}
Methoden Run// Move- und Render-Funktion (Kapselung)tbResult Move(float fTime) {return g_pBreakanoid->Move(fTime);}tbResult Render(float fTime) {return g_pBreakanoid-
>Render(fTime);}
// Lässt das Spiel laufen tbResult CBreakanoid::Run(){// Nachrichtenschleife betreten. Ruft in jedem Frame Move und
Render aufif(tbDoMessageLoop(::Move, ::Render)){// Fehler!TB_ERROR("Fehler in der Nachrichtenschleife!", TB_ERROR);}
return TB_OK;}
HauptfunktionWinMain I// Windows-Hauptfunktion int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, char*
pcCommandLine, int iShowCommand){tbResult r;
// Spiel initialisiereng_pBreakanoid = new CBreakanoid;r = g_pBreakanoid->Init(); // Init-Funktionsaufruf
// Wenn Benutzer "ABBRECHEN" drückt = Programm beendenif(r == TB_CANCELED){
TB_SAFE_DELETE(g_pBreakanoid);return 0;
}// Wenn es nicht die Benutzereingabe war, handelt es sich um ein Fehlerelse if(r){
g_pBreakanoid->Exit();TB_SAFE_DELETE(g_pBreakanoid);MessageBox(NULL, "Fehler beim Initialisieren des Spiels!",
"Fehler", MB_OK | MB_ICONEXCLAMATION);return 1;}
HauptfunktionWinMain II// Spiel laufen lassenif(g_pBreakanoid->Run()){g_pBreakanoid->Exit(); // Abfangen von Fehlern TB_SAFE_DELETE(g_pBreakanoid);MessageBox(NULL, "Fehler im Spiel!", "Fehler", MB_OK | MB_ICONEXCLAMATION);return 1;}
// Spiel verlasseng_pBreakanoid->Exit();TB_SAFE_DELETE(g_pBreakanoid);
return 0;}
3.2 Das Titelbild
Das TitelbildCIntro// Intro.h// Klasse für das Introclass CIntro{public:// VariablenPDIRECT3DTEXTURE9 m_pTitle; // Titelbild-Textur
// Konstruktorinline CIntro(): m_pTitle(NULL){}
// MethodentbResult Init(); // Initialisierung = Betreten des SpielzustandstbResult Exit(); // Herunterfahren = Verlassen d. S. tbResult Load(); // Laden aller DatentbResult Unload(); // EntladentbResult Move(float fTime); // BewegentbResult Render(float fTime); // Rendern};
Das TitelbildDie Schrift// Load-Methode in Breakanoid.cpp// Schriftarten laden
m_pFont1 = new tbFont; // Schriftart 1
// Schrift besteht immer aus zwei Dateien -> liegen im Data-Ordner:
if(m_pFont1->Init("Data\\Font1.tga", "Data\\Font1.tbf")) {// Fehler!TB_ERROR("Fehler beim Laden der Schriftart Data\\Font1!",
TB_ERROR);}
[analog zu Schriftart 2]
Das TitelbildInitialisieren, Laden, Entladen //Intro.cpptbResult CIntro::Load() // Init ruft Load auf{// Titelbild laden (als Textur)m_pTitle = tbTextureManager::Instance().GetTexture("Data\\Title.jpg");if(m_pTitle == NULL)
TB_ERROR("Fehler beim Laden von Data\\Title.jpg!", TB_ERROR);
return TB_OK;}// __________________________________________________________________
tbResult CIntro::Unload() // Exit ruft Unload auf{// Die Textur löschentbTextureManager::Instance().ReleaseTexture(m_pTitle);
return TB_OK;}
Das TitelbildRendern I
//Intro.cpp// Vertizes für das Titelbildstruct STitleVertex{tbVector 3vPosition;float fRHW;D3DCOLOR Color;tbVector2 vTex0;static const DWORD dwFVF;};
const DWORD STitleVertex::dwFVF = D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1;
Rendern erfolgt durch ein Rechteck, das mit der Textur des Bildes überzogen wird ‚Transformierte Vertizes‘
Das TitelbildRendern II// Rendert den SpielzustandtbResult CIntro::Render(float fTime){STitleVertex aVertex[4]; // 4 Vertizes, für jede Bildschirmecke ein
Vertex
// Puffer leeren und Szene beginnentbDirect3D& D3D = tbDirect3D::Instance();D3D->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, tbColor(0.0f,
0.0f, 0.0f), 1.0f, 0); D3D->BeginScene();
// ------------------------------------------------------------------
// Vertexformat und Titelbildtextur setzen, Z-Buffer ausD3D.SetFVF(STitleVertex::dwFVF);D3D.SetTexture(0, m_pTitle);D3D.SetRS(D3DRS_ZENABLE, D3DZB_FALSE);
Das TitelbildRendern III// Die vier Vertizes des Titelbilds erstellen (Rechteck)// Links untenaVertex[0].vPosition = tbVector3(0.0f, D3D.GetScreenSize().y,
0.5f); // Position der Pixelkoordinate
aVertex[0].fRHW = 1.0f; // Kehrwert der w-Koordinate
aVertex[0].Color = tbColor(1.0f, 0.8f, 0.8f);aVertex[0].vTex0 = tbVector2(0.0f, 1.0f); //
Texturkoordinate
// Links oben aVertex[1].vPosition = tbVector3(0.0f, 0.0f, 0.0f); aVertex[1].fRHW = 1.0f;aVertex[1].Color = tbColor(0.8f, 1.0f, 0.8f);aVertex[1].vTex0 = tbVector2(0.0f, 0.0f);
// ...andere genauso […]
Das TitelbildRendern IIII// Texturkoordinaten sinusförmig verschieben ("wabbeln")// Texturkoordinaten werden für jedes Vertex, in jedem frame
geändert:for(DWORD dwVertex = 0; dwVertex < 4; dwVertex++){
aVertex[dwVertex].vTex0.x += sinf(g_pBreakanoid->m_fTime + (float)(dwVertex)) * 0.01f;
aVertex[dwVertex].vTex0.y += cosf(g_pBreakanoid->m_fTime + (float)(dwVertex)) * 0.01f;
}
// Als Dreiecksfolge zeichnenD3D->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, aVertex,
sizeof(STitleVertex)); // Bild mit Trainlges-Trip zeichnen
[…]
Das TitelbildMove// Bewegt den SpielzustandtbResult CIntro::Move(float fTime){// Wenn eine der typischen Tasten gedrückt wurde: zum Hauptmenü!// Prüft, ob Eingabe getätigt wurde. Akzeptierte Eingaben:if( g_pbButtons[TB_KEY_NUMPADENTER] || // ENTER g_pbButtons[TB_KEY_RETURN] || g_pbButtons[TB_KEY_SPACE] || // LEERTASTE g_pbButtons[TB_MOUSE_BUTTON(0)] || // MAUSTASTEN
g_pbButtons[TB_MOUSE_BUTTON(1)])
// gehe mit Verzögerung von 100 Millisek. zum Hauptmenü {
tbDelay(100);G_pBreakanoid->SetGameState(GS_MAIN_MENU);
}return TB_OK;}
3.3 Das Hauptmenü
Das HauptmenüVariablen// MainMenu.hclass CMainMenu{public:// VariablenLPDIRECT3DTEXTURE9 m_pBackground; // speichert HintergrundbildInt m_iCursor; // Menücursor = merkt
sichAusgewählten der 3 Einträge. [0]=erster.
BOOL m_bShowingHelp;// Wird Hilfetext angezeigt?
// Konstruktorinline CMainMenu(): m_pBackground(NULL), m_iCursor(0), m_bShowingHelp(FALSE) // bei TRUE (Cursor auf ‚Hilfe anzeigen‘):
// Menueinträge verschwindet + Hilfekasten wird angezeigt{}
Das HauptmenüMethoden I CMainMenu::Init()
Betreten des Hauptmenüs ruft Load-Methode auf lädt nur die Textur des Hintergrundbildes Cursor auf null setzen damit zu Beginn erster Menüeintrag
ausgewählt ist CMainMenu::Exit()
Verlassen des Hauptmenüs ruft Unload auf löscht Textur aus Speicher
Das HauptmenüRender I Rendern des Bildes wie bei Intro (‚wabbeln‘) Texte für 3 Menüeinträge zeichnen. Dafür gibt es ein Array mit drei
Einträgen:
tbResult CMainMenu::Render(float fTime){SBackgroundVertex aVertex[4];char* apcMenuEntry[3] = {"Spiel starten",
"Hilfe anzeigen", "Spiel beenden"};tbVector2 vPosition;tbColor Color;
[…]
Das HauptmenüRender II If-Abfrage prüft, ob Hilfetext oder Menüeinträge gezeichnet werden
sollen
1. Position:if(!m_bShowingHelp)
{//jeden Text mit Schrifart 'g_pBreakanoid->m_pFont1' renderng_pBreakanoid->m_pFont1->Begin();
// Die Menüeinträge zeichnen. Jeder der 3 Einträg durchläuft Schleife
for(int iEntry = 0; iEntry < 3; iEntry++){// Die Position für den Text dieses Eintrags berechnenvPosition.x = 0.5f; // erster Menüeintrag liegt bei (0.5,
0.4) //jeden weiteren um 0.125 Einheiten nach unten
verschieben:vPosition.y = 0.4f + (float)(iEntry) * 0.125f;…
Das HauptmenüRender III2. Bewegung:// Wenn der Cursor auf diesem Eintrag liegt, dann schwingt der Text// Wenn render-Eintrag = ausgewählter Eintrag -> dann schwingenif(m_iCursor == iEntry) vPosition.x += 0.05f * sinf(g_pBreakanoid->m_fTime);
3. Farbe berechnen:// Normalerweise ist Eintrag COLOR dunkelblau.// Wenn der Cursor aber darauf liegt, dann ist er heller.if(m_iCursor != iEntry) Color = tbColor(0.3f, 0.3f, 0.9f, 0.75f); //
Standardfarbe Blau...else Color = tbColor(0.5f, 0.5f, 1.0f, 1.0f); //... heller & transparenter
3. Text zeichnen:g_pBreakanoid->m_pFont1->DrawText(vPosition, apcMenuEntry[iEntry],
// es werden relative und zentrierte Koordinaten/Größen verwendet: TB_FF_ALIGN_HCENTER | TB_FF_ALIGN_HCENTER | TB_FF_RELATIVE |
TB_FF_RELATIVESCALING, // Text wird mit 1,5 skaliert-1, Color, Color + tbColor(-0.3f, 0.4f, 0.0f), tbVector2(1.5f, 1.5f));
Das HauptmenüMove I// Bewegt den SpielzustandtbResult CMainMenu::Move(float fTime){
if(!m_bShowingHelp) // Wenn showing Help = FALSE Pfeiltasten werden bewegt{// Wird Taste nach unten/oben gedrückt, wird Cursor durchs Hauptmenüs bewegt
if(g_pbButtons[TB_KEY_UP]) // Cursor nach unten bewegen{
m_iCursor--;tbDelay(80); // … mit Verzögerung
}if(g_pbButtons[TB_KEY_DOWN]) // Cursor nach oben bewegen
{m_iCursor++;tbDelay(80);
}// Cursor in die Grenzen weisen es gibt ja nur drei Menüpunkt zum Wählen [0,1,2]if(m_iCursor < 0) m_iCursor = 2;if(m_iCursor > 2) m_iCursor = 0; […]
Das HauptmenüMove II// Wenn die Enter-, Leer- oder Return-Taste gedrückt wurde, // dann möchte der Benutzer einen Eintrag auswählen oder den Hilfetext wieder ausblenden.if(g_pbButtons[TB_KEY_RETURN] || g_pbButtons[TB_KEY_NUMPADENTER] || g_pbButtons[TB_KEY_SPACE]){
if(!m_bShowingHelp){// Nun kommt es darauf an, was gerade ausgewählt ist! // -> Wenn Hilfetext = FALSE, Cursor navigieren!
switch(m_iCursor){
case 0: // Spiel starteng_pBreakanoid->SetGameState(GS_GAME); // Spielzustand auf GS_GAME
setzenbreak;
case 1: // Hilfe anzeigenm_bShowingHelp = TRUE;tbDelay(100);break;
case 2: // Spiel beendenPostQuitMessage(0);break;
}}
Das HauptmenüMove IIIelse // Ist Hilfetext = TRUE, dann abschalten
{// Die Hilfe wieder deaktivierenm_bShowingHelp = FALSE;tbDelay(100);
}[…]
Das HauptmenüSound I Sorgt für Töne beim Bewegen u. Betätigen des Cursors tbDirectSound-Klasse in Breakanoid initialisert!
Breakanoid.h: Sound Array [12] es gibt 12 unterschiedliche Sounds
Breakanoid.cpp: Sounds werden in CBreakanoid::Load() geladen:
// Sounds ladenfor(DWORD s = 0; s < 12; s++){
sprintf(acFilename, "Data\\Sound%d.wav", s + 1);m_apSound[s] = new tbSound;if(m_apSound[s]->Init(acFilename, DSBCAPS_STATIC | DSBCAPS_LOCDEFER | DSBCAPS_CTRLFREQUENCY)){
// Fehler!TB_ERROR("Fehler beim Laden eines Sounds!", TB_ERROR);
}}
Das HauptmenüSound II MainMenu.cpp:
CMainMenu::Move spielt den Sound ab If-Abfrage: Verschieden Töne für versch. Eingaben:
// Sound Nr.1 beim Drücken UP/DOWN if(g_pbButtons[TB_KEY_UP]){
g_pBreakanoid->m_apSound[0]->PlayNextBuffer();m_iCursor--;tbDelay(80);
}if(g_pbButtons[TB_KEY_DOWN]) {
g_pBreakanoid->m_apSound[0]->PlayNextBuffer();m_iCursor++;tbDelay(80);
}