Teil 6: Und Action

Wenn du die Aufgaben der letzten Teile immer fleissig gelöst hast, sollte dein Hauptprogramm inzwischen so oder so ähnlich aussehen…
Datei /Main.cpp

#include <SDL.h>
#include "Types.h"
#include <game/GameManager.h >
#include <debug/Log.h>
// --------------------------------------------------------------------------------
CLog LOG( "ProjektE.log" );
SDL_Surface* g_pBackSurface = 0;
// --------------------------------------------------------------------------------
bool InitGraphics( bool bFullScreen )
{
  LOG << "INFO: SDL Initialisierung beginnt" << ENDL;
  if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
  {
     LOG << "FEHLER: Kann SDL nicht initialisieren!" << ENDL;
     return false;
  }
  DWORD dwFlags = SDL_HWSURFACE | SDL_DOUBLEBUF;
  if( bFullScreen )
  {
     dwFlags |= SDL_FULLSCREEN;
  }
  g_pBackSurface = SDL_SetVideoMode( 800, 600, 16,  dwFlags );
  if( 0 == g_pBackSurface )
  {
     LOG << "FEHLER: Kann den Videomodus nicht setzen!" << ENDL;
     return false;
  }
  LOG << "INFO: SDL Initialisierung abgeschlossen" << ENDL;
  return true;
}
// --------------------------------------------------------------------------------
void ShutdownGraphics()
{
  SDL_Quit();
}
// --------------------------------------------------------------------------------
int main( int argc, char* argv[] )
{
  LOG << "INFO: Programm startet" << ENDL;
  if( !InitGraphics( false ) )
  {
     return 0;
  }
  GAMEMANAGER->Initialize();
  SDL_Event event;
  while( 1 )
  {
     if( SDL_PollEvent( &event ) && ( event.type == SDL_KEYDOWN ) )
     {
        break;
     }
     SDL_FillRect( g_pBackSurface, 0, SDL_MapRGB( g_pBackSurface->format, 0, 0, 0 ) );
     GAMEMANAGER->BeginFrame();
     GAMEMANAGER->Idle();
     GAMEMANAGER->EndFrame();
     SDL_Flip( g_pBackSurface );
  }
  GAMEMANAGER->Uninitialize();
  ShutdownGraphics();
  LOG << "INFO: Programm beendet" << ENDL;
  return 0;
}

Es hat sich nichts wesentliches verändert. SDL-Initialisierung und -Deinitialisierung sind in extra Funktionen ausgelagert, die Log-Klasse wurde eingebunden und das Programm wurde mit entsprechenden Log-Nachrichten ausgestattet. Die Initialisierung erfolgt nun lediglich wahlweise im Fenster- oder im Vollbildmodus, abhängig vom übergebenen Flag bFullScreen. Weiterhin wird in der Hauptschleife der Backbuffer schwarz gefüllt und mit SDL_Flip angezeigt. Außerdem ist, wie in der letzten Aufgabe besprochen, der GameManager eingebunden.
Ein Teil der letzten Aufgabe war auch, die CGSGame Klasse zu implementieren und einen Pixel auf dem Bildschirm zu verschieben. Bei der Implementierung fällt eine erste Design-Schwäche unseres kleinen Spiels auf: Der Backbuffer ist in der Main.cpp deklariert, man kann nur über das Schlüsselwort extern darauf zugreifen. Ich werde in einem späteren Tutorial darauf eingehen, wie man auch die Grafikfunktionen noch einmal kapseln kann (und sollte). Vorerst wollen wir uns aber auf die Spiellogik konzentrieren. Um einen Punkt zu verschieben, ist es notwendig, die Koordinaten zu speichern. Mit dieser Anforderung ergibt sich damit der folgende Header für unseren ersten kleinen GS:
Datei /game/GSGame.h

#ifndef GSGAME_H
#define GSGAME_H
// ---------------------------------------------------------------------------
#include "Types.h"
#include "GameState.h"
// ---------------------------------------------------------------------------
class CGameManager;
// ---------------------------------------------------------------------------
class CGSGame : public IGameState
{
public:
  CGSGame( CGameManager* pGameManager );
  ~CGSGame();
  void Enter();
  void Idle( DWORD dwDeltaTime );
  void Leave();
private:
  float m_fX;
  float m_fY;
};
#endif

Hier ist dann auch direkt die Funktion “Enter” zu gebrauchen. Hier initialisieren wir die Koordinaten mit Zufallswerten. In der Idle Funktion wird dann der Pixel bewegt, und zwar wie in der Aufgabe beschrieben so, daß er sich 100 Pixel in der Sekunde verschiebt. Wir kennen die Framezeit in tausendstel Sekunden, die der Idle-Funktion übergeben wird. Die Berechnung der Verschiebung ist jetzt einfacher Dreisatz: Wenn der Punkt sich 100 Pixel pro 1000 ms Bewegen soll, bewegt er sich x Pixel in dwDeltaTime ms. Daraus ergibt sich eine Bewegung von 100 * dwDeltaTime / 1000 Pixeln, also gekürzt dwDeltaTime / 10. Ich bewege den Punkt im folgenden Code beispielhaft entlang der x-Achse:
Datei /code/GSGame.cpp

#include "GSGame.h"
#include <SDL.h>
#include <stdlib.h>
// ---------------------------------------------------------------------------
extern SDL_Surface* g_pBackSurface;
// ---------------------------------------------------------------------------
CGSGame::CGSGame( CGameManager* pGameManager )
:   IGameState( pGameManager )
{
}
// ---------------------------------------------------------------------------
CGSGame::~CGSGame()
{
}
// ---------------------------------------------------------------------------
void CGSGame::Enter()
{
  srand( SDL_GetTicks() );
  m_fX = 0.0f;
  m_fY = (float)( rand() % 600 );
}
// ---------------------------------------------------------------------------
void CGSGame::Idle( DWORD dwDeltaTime )
{
  if( SDL_LockSurface( g_pBackSurface ) == 0 )
  {
     ( (WORD*)g_pBackSurface->pixels )[ (int)m_fY * 800 + (int)m_fX ] = SDL_MapRGB( g_pBackSurface->format, 255, 255, 255 );
     SDL_UnlockSurface( g_pBackSurface );
  }
  m_fX += ( (float)dwDeltaTime / 10.0f );
  if( m_fX >= 800.0f )
  {
     m_fX = 0.0f;
     m_fY = (float)( rand() % 600 );
  }
}
// ---------------------------------------------------------------------------
void CGSGame::Leave()
{
}

Wer etwas weiter denkt, bekommt bei diesem Code vielleicht einige Bedenken. Wir befinden uns noch im Fenstermodus und das Fenster kann nur die Farbtiefe des Desktops annehmen. Aber SDL bereitet uns da keine Schwierigkeiten, wir haben ein 16-Bit-Surface angefordert und das kriegen wir auch. Erst beim Flippen werden die Farbinformationen auf die Desktopfarbtiefe umgerechnet, falls diese sich von der Farbtiefe des Backbuffers unterscheidet.
Und so fliegt nun ein kleiner Stern über den Bildschirm. Wer mitzählt, wird feststellen, daß er genau 8 Sekunden braucht. Kann man daraus nicht noch mehr machen? Du merkst, worauf ich hinaus will: Wir bauen uns ein Sternenfeld. Dazu erschaffen wir eine kleine Struktur mit Sterneninformation. Wir werden nicht nur mehr nur X und Y speichern, sondern auch noch die Farbe, die wir durch die Tiefe Z repräsentieren. Wir fügen die folgende Struktur inklusive einem Array also in den privaten Bereich unserer Klassendeklaration ein:

struct TStar
  {
     float m_fX;
     float m_fY;
     float m_fZ;
  };
  enum
  {
     NUM_STARS      = 500
  };
  TStar m_aclStars[ NUM_STARS ];

In der Enter-Methode von CGSGame initialisieren wir nun das Sternenfeld mit zufälligen Werten:

void CGSGame::Enter()
{
  srand( SDL_GetTicks() );
  for( int i = 0; i < NUM_STARS; ++i )
  {
     m_aclStars[ i ].m_fX = (float)( rand() % 800 );
     m_aclStars[ i ].m_fY = (float)( rand() % 600 );
     m_aclStars[ i ].m_fZ = (float)( rand() % 256 );
  }
}

Die Tiefe, CGSGame::TStars::m_fZ, wird also ein Wert zwischen 0 und 255. Wir werden diesen Wert direkt als Grauwert übernehmen. Dunkle Sterne erscheinen also weiter entfernt zu liegen als helle Sterne. Um diesen Eindruck zu perfektionieren, bewegen wir auch die hellen Sterne schneller als die dunklen. Dazu sollen sich die hellen Sterne 10 Mal so schnell bewegen wie die dunklen. Dazu teile ich die Tiefe durch 25.5 und erhalte damit einen Beschleunigungsfaktor zwischen 0 (dunkle Sterne) und 10 (helle Sterne). Hat ein Stern den Bildschirm verlassen, wird seine x-Komponente wieder auf 0 gesetzt, und die anderen beiden mit zufälligen Werten belegt.

void CGSGame::Idle( DWORD dwDeltaTime )
{
  for( int i = 0; i < NUM_STARS; ++i )
  {
     m_aclStars[ i ].m_fX -= (float)dwDeltaTime / 10.0f * m_aclStars[ i ].m_fZ / 25.5f;
     if( m_aclStars[ i ].m_fX < 0.0f )
     {
        m_aclStars[ i ].m_fX = 800.0f;
        m_aclStars[ i ].m_fY = (float)( rand() % 600 );
        m_aclStars[ i ].m_fZ = (float)( rand() % 256 );
     }
  }
  if( SDL_LockSurface( g_pBackSurface ) == 0 )
  {
     for( int i = 0; i < NUM_STARS; ++i )
     {
        DWORD dwColor = SDL_MapRGB( g_pBackSurface->format, m_aclStars[ i ].m_fZ, m_aclStars[ i ].m_fZ, m_aclStars[ i ].m_fZ );
        ( (WORD*)g_pBackSurface->pixels )[ (int)m_aclStars[ i ].m_fY * 800 + (int)m_aclStars[ i ].m_fX ] = dwColor;
     }
     SDL_UnlockSurface( g_pBackSurface );
  }
}

Wie du hier siehst, lassen wir die Sterne jetzt auch von rechts nach links fliegen, damit das später zu unserer Raumschiff-Flugrichtung passt. Der normale Europäer denkt ja “von links nach rechts”, insofern wäre es Blödsinn, wenn das Raumschiff anders herum fliegen würde.
Hurra, damit haben wir das Weltall geschaffen! Oder zumindest eine kleine Illusion dessen. Im übernächsten Teil widmen wir uns dann unserem Raumschiff. Vorher gibt es aber noch etwas zu tun, nämlich in Form von ein paar…

Aufgaben

  1. Experimentiere mit Anzahl Sternen und Geschwindigkeit sowie der Helligkeit
  2. Kapsele den Sternenhimmel in einer Klasse CStarfield in der Datei Starfield.h/.cpp im Verzeichnis /objects.
  3. Stelle eine Initialize und Idle Methode mit Übergabe der Framezeit zur Verfügung, füge das Sternenfeld den Membern von CGSGame hinzu und rufe Initialize und Idle aus den entsprechenden Stellen in CGSGame auf.
  4. Füge Methoden und Attribute zu CStarfield hinzu, um die Geschwindigkeit des Sternenfeldes mit einem zusätzlichen Geschwindigkeitsfaktor beeinflussen zu können.

Download

Leave a Reply

Your email address will not be published. Required fields are marked *