sunlight

Xonix: Step 2

Introduction

In this brief interlude, we will cover:

  • Sound effects
  • Game state
  • Splash screens
  • Frame rate counting
  • Drawing text
  • Per-level music
  • Application icons

Up Previous View Code Download Code

Sound effects

No game is complete without its quota of sound effects, and Xonix is no exception. We will add a few sound effects to spice up the game. Three effects are necessary:

  • when the player fills an area;
  • when the player completes a level;
  • when the player is hit.

Just as we did with the background music, the sound effects are loaded as DirectMusic segments:

    g_pLevelUp = LoadSound(L"chimes.wav");
    g_pDie = LoadSound(L"explode.wav");
    g_pFill = LoadSound(L"camera.wav");

Now we set each of these to single-shot (no repeats):

    // One-shot sound effects
    g_pLevelUp->SetRepeats(0);
    g_pDie->SetRepeats(0);
    g_pFill->SetRepeats(0);

We must play these as secondary buffers, to be played concurrently with the primary buffer (the background music). 

    if (g_bHasDied)
    {
        PlaySound(g_pDie, DMUS_SEGF_SECONDARY);
        g_nLives--;

Game State

Most games have a concept of internal state. Our game has six states: initialising, intro screen, level screen, in-game, paused and shutting down. We can represent this internal state using a variable and an enum:

enum EGameState
{
    Initialising,
    Intro,
    LevelScreen,
    InGame,
    InGamePaused,
    Quitting
};

EGameState  g_nGameState;

We will also need a function to change states. At the moment, this will simply change the state variable; later, we will enhance it to do state changes correctly.

void ChangeToGameState(EGameState nNewState)
{
    g_nGameState = nNewState;
}

Splash Screen

Adding a splash screen is now a relatively simple affair. First, we load the surface:

    lpSplash = LoadSurface(_T("Splash.bmp"));

When we are in the 'Intro' state, we just draw the splash screen:

    switch (g_nGameState)
    {
    case Intro:
        // Draw splash screen
        r.left = r.top = 0;
        r.right = 640;
        r.bottom = 480;
        BackBlt(0, 0, lpSplash, &r, DDBLTFAST_NOCOLORKEY);
        break;

We must also use different input handling. Pressing the button assigned to ACTION_START causes us to start the game:

    switch (g_nGameState)
    {
    case Intro:
        if (g_bStart)
        {
            g_bStart = false;
            ResetGame();
        }
        break;

Level Loading Screen

We will also add a screen between each level to tell the user what level they have reached. This is merely a case of handling the LevelScreen state:

    case LevelScreen:
        // Clear screen to black
        ClearScreen();

        // Draw level counter in center of screen
        _stprintf(szBuffer, _T("Level %d"), g_nLevel);
        g_lpFont->DrawText(szBuffer, 320, 240, DT_CENTER | DT_VCENTER);
        break;

This state will remain for a short time and then change to the InGame state, so we need to store the start time when we enter the state:

void ChangeToGameState(EGameState nNewState)
{
    switch (nNewState)
    {
    case LevelScreen:
        g_dwLevelScreenStartTime = GetTickCount();
        break;
    }
    g_nGameState = nNewState;
}

During OnIdle, we will check the time:

    case LevelScreen:
        if ((GetTickCount() - g_dwLevelScreenStartTime) > LEVELSCREEN_TIME)
            ChangeToGameState(InGame);
        break;

Frame Rate Counting

Frame rate counting is done by averaging the time per frame over a number of frames (say 100). We will therefore need a start time and a counter of frames:

int             g_nFrames, g_nFrameRate;
LARGE_INTEGER   g_liStartTime;

When we start up, we will have to initialise this time:

    QueryPerformanceCounter(&g_liStartTime);

Now, we increment the counter each time we render a frame. If we pass the threshold, we can recalculate the frame rate:

    g_nFrames++;
    if (g_nFrames > 100)
    {
        g_nFrameRate = MulDiv((int)liFreq.QuadPart, 
            g_nFrames, (int)(liEnd.QuadPart - g_liStartTime.QuadPart));
        g_liStartTime = liEnd;
        g_nFrames = 0;
    }

To display this on the screen, we will need some text handling.

Drawing Text

Text handling in DirectDraw can be done in one of two ways: either use GDI to draw text (powerful but slow), or BltFast each letter from a character set to the screen (fast, but restrictive). We will use the latter method.

For this purpose, we will introduce a new class: CDrawText.

class CTextFont
{
// Construction/destruction
public:
    CTextFont();
    CTextFont(IDirectDrawSurface7 *lpSurface, 
        int xImage, int yImage, int cx, int cy, 
        LPCTSTR lpCharSet, 
	TCHAR cDefaultChar = TEXTSPRITE_DEFAULTCHAR, 
        int nFramesAcross = -1);
    CTextFont(CTextFont& s);
    virtual ~CTextFont();

// Operations
public:
    int     GetTextWidth(LPCTSTR lpText);
    BOOL    DrawText(LPCTSTR lpText, 
		int x, int y, UINT nFormat = DT_LEFT | DT_TOP);

// Attributes
public:
    TCHAR               m_cDefaultChar;
    LPCTSTR             m_szCharSet;

    IDirectDrawSurface7 *m_pSurface;
    int                 m_xImage, m_yImage, m_cx, m_cy;
    int                 m_nFramesAcross;
};

This class takes a surface, co-ordinates of a character set on that surface, the width and height of each character, a string corresponding to that character set, a default character (used to substitute for characters not found in the character set), and the width of the character set on the surface.

Drawing text is then simply a matter of calling DrawText. First, this method finds the top left corner of the string:

BOOL CTextFont::DrawText(LPCTSTR lpText, int x, int y, UINT nFormat)
{
    UINT    i, nFrame;
    RECT    r;
    LPTSTR  pChar;

    if (nFormat & DT_RIGHT)
        x -= GetTextWidth(lpText);
    else if (nFormat & DT_CENTER)
        x -= (GetTextWidth(lpText) / 2);

    if (nFormat & DT_BOTTOM)
        y -= m_cy;
    else if (nFormat & DT_VCENTER)
        y -= (m_cy / 2);

Next, it looks for each character in the string in the character set.

    for (i = 0; i < _tcslen(lpText); i++)
    {
        pChar = _tcschr(m_szCharSet, lpText[i]);

If it's not found, it uses the default character.

        if (pChar == NULL)
        {
            // If the default char is \0, just skip any unknown characters.
            if (m_cDefaultChar == _T('\0'))
                continue;

            pChar = _tcschr(m_szCharSet, lpText[i]);
        }

Finally, it calculates the source rectangle from the position in the character set, and draws the character at the appropriate location. 

        nFrame = pChar - m_szCharSet;

        r.left = (nFrame % m_nFramesAcross) * m_cx + m_xImage;
        r.top =  (nFrame / m_nFramesAcross) * m_cy + m_yImage;
        r.right = r.left + m_cx;
        r.bottom = r.top + m_cy;
        BackBlt(x, y, m_pSurface, &r, DDBLTFAST_SRCCOLORKEY);
        x += m_cx;
    }
    return TRUE;
}

Now we have a text-drawing object, we can create one with the Courier New character set I have supplied:

    g_lpFont = new CTextFont(lpImages, TEXTX, TEXTY, TEXTCX, TEXTCY, 
        _T("abcdefghijklmnopqrstuvwxyz")
        _T("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
        _T("01234567890-=_+[]{};'#:@~,")
        _T("./<>?\\|!\"£$%^&*() "), _T('\0'), 26);

We now simply draw the text on the screen at the appropriate positions:

        _stprintf(szBuffer, _T("%d%%"), g_nPercentFilled);
        g_lpFont->DrawText(szBuffer, 
		0, YBLOCKS * BLOCKSIZE, DT_LEFT | DT_TOP);

        if (g_nLives > 1)
            _stprintf(szBuffer, _T("%d lives"), g_nLives);
        else
            _tcscpy(szBuffer, _T("1 life"));
        g_lpFont->DrawText(szBuffer, 
		XSCREEN / 2, YBLOCKS * BLOCKSIZE, DT_CENTER | DT_TOP);

        _stprintf(szBuffer, _T("%d FPS (%dms)"), 
            g_nFrameRate,
            g_nFrameTime);
        g_lpFont->DrawText(szBuffer, 
		XSCREEN, YBLOCKS * BLOCKSIZE, DT_RIGHT | DT_TOP);

What could be easier?

Per-Level Music

We would ideally like to have different music for different levels, so let's investigate this.

First, we must load each of the music files:

BOOL LoadMusic()
{
    g_pIntroMusic = LoadSound(L"passport.mid");
    if (g_pIntroMusic == NULL)
        return FALSE;
    g_pLevelMusic[0] = LoadSound(L"Dance of the Sugar-Plum Fairy.mid");
    if (g_pLevelMusic[0] == NULL)
        return FALSE;
    g_pLevelMusic[1] = LoadSound(L"canyon.mid");
    if (g_pLevelMusic[1] == NULL)
        return FALSE;
    g_pLevelMusic[2] = LoadSound(L"Beethoven's 5th Symphony.mid");
    if (g_pLevelMusic[2] == NULL)
        return FALSE;
    g_pLevelMusic[3] = LoadSound(L"In the Hall of the Mountain King.mid");
    if (g_pLevelMusic[3] == NULL)
        return FALSE;

    // Background music loops forever
    g_pIntroMusic->SetRepeats(DMUS_SEG_REPEAT_INFINITE);
    // Standard MIDI file
    g_pIntroMusic->SetParam(GUID_StandardMIDIFile, 
        0xFFFFFFFF, DMUS_SEG_ALLTRACKS, 0, NULL);

    for (int i = 0; i < NUMMUSICLEVELS; i++)
    {
        g_pLevelMusic[i]->SetRepeats(DMUS_SEG_REPEAT_INFINITE);
        g_pLevelMusic[i]->SetParam(GUID_StandardMIDIFile, 
            0xFFFFFFFF, DMUS_SEG_ALLTRACKS, 0, NULL);
    }
    return TRUE;
}

Now, when we call PlayBackgroundMusic, we should check the game state and play the appropriate music file. Note that this time, we're downloading instruments just before we play.

void PlayBackgroundMusic()
{
    int n;

    if (!g_bMusicEnabled)
        return;

    switch (g_nGameState)
    {
    case Intro:
        if (g_bIntroMusicPlaying)
            return;

        StopBackgroundMusic();
        DownloadSound(g_pIntroMusic);
        PlaySound(g_pIntroMusic);
        g_bIntroMusicPlaying = true;
        break;

    case LevelScreen:
    case InGame:
    case InGamePaused:
        n = (g_nLevel - 1) % NUMMUSICLEVELS;
        
        if (g_bLevelMusicPlaying[n])
            return;

        StopBackgroundMusic();
        DownloadSound(g_pLevelMusic[n]);
        PlaySound(g_pLevelMusic[n]);
        g_bLevelMusicPlaying[n] = true;
        break;
    }
}

Finally, when we change game state, we can take the opportunity to change the music, too.

void ChangeToGameState(EGameState nNewState)
{
    switch (nNewState)
    {
    case LevelScreen:
        g_dwLevelScreenStartTime = GetTickCount();
        break;
    }
    g_nGameState = nNewState;
    // Play the appropriate background music
    PlayBackgroundMusic();
}

Application Icons

To add an icon for the application, we will simply create a resource file and add an icon to it:

IDI_ICON1               ICON    DISCARDABLE     "icon1.ico"

We're done. I believe this game displays many useful techniques that you can leverage to develop your own 2D games, with MIDI music (MP3 music is easy to add), sound effects, multiple controllers for input and great performance. It's also quite good fun... Let me know your thoughts.

You can now continue with the main tutorial.

 

Copyright © David McCabe, 1998 - 2001. All rights reserved.

You will need to download and install the m-math control to display any equations on this Web site. Without this control, you will not see most of the equations. Please do not e-mail me asking why the equations do not display!