sunlight

Tidying Up

Introduction

In this part of the tutorial, we will cover:

  • Tidying our existing application into separate files
  • Comparing approaches to frameworks: C++ classes versus linked functions. 

Previous View Code Download Code Next

Tidying Up

As a first step towards making our application easier to understand, we're going to move some general things into separate files. We've already taken a step towards this by using a separate file for DirectInput code; now we will extend this to the other subsystems.

Precompiled Headers

As we are about to generate many source files, compilation speed becomes an issue. Recompiling the Windows and DirectX header files every time is slow enough, but with many source files it will become tedious. Most modern compilers support the concept of precompiled headers, which is a method of compiling a list of definitions and header files once, then re-using this in other compilations.

We can take advantage of this by using a single common header file that simply includes each of the standard header files. We will call this file stdhdr.h:

////////////////////////////////////////////////////////////////////////////////
// stdhdr.h
//
//  Standard header file; includes all standard header files to allow
//   precompiled headers to function.

#ifndef __STDHDR_H__
#define __STDHDR_H__

#include <windows.h>
#include <tchar.h>
#include <ddraw.h>
#include <dinput.h>
#include <dmusicc.h>
#include <dmusici.h>
#include <stdarg.h>
#include <stdio.h>

#endif

We can then set the appropriate project options (or use the appropriate command-line switches) to use the precompiled header file associated with this file. We will also need a single file that is used to create the precompiled header information, and this file need only contain a #include for the header file:

// Standard header file for precompiled headers.
#include "stdhdr.h"

Once this is done, set this file to 'Create a precompiled header file', and all the other source files to 'Use a precompiled header'. You should then replace the list of #includes in each of the source files to just refer to stdhdr.h.

Error Handling

Since error handling is an easily separated part, we will start by moving all the error handling and debugging code into a separate file. We will also add a few useful macros and functions to handle other cases.

Chief among these is the new ability to accurately time an operation. We introduce the macros:

#define DEBUG_TIMING_START()    \
{\
    LARGE_INTEGER   liStart, liEnd, liFreq;\
    QueryPerformanceCounter(&liStart)

#define DEBUG_TIMING_END(s) \
    QueryPerformanceCounter(&liEnd);\
    QueryPerformanceFrequency(&liFreq);\
    TRACE(_T(s) _T(" took %d ms\n"),
        MulDiv((int)(liEnd.QuadPart - liStart.QuadPart), 
		1000000, 
		(int)liFreq.QuadPart));\
}

These macros call QueryPerformanceCounter at the beginning and end of an operation, then at the end calculate the time difference between the two counter values, and output it to the debugger. These functions are very accurate (sub-microsecond accuracy) and so give us an excellent ability to time very fast tasks (like parts of drawing code).

We also use the macro TRACE, which resolves to a call to the function Trace:

void Trace(LPCTSTR lpFormat, ...)
{
    va_list ap;

    va_start(ap, lpFormat);
    wvsprintf(szErrorBuf, lpFormat, ap);
    va_end(ap);

    OutputDebugString(szErrorBuf);
}

which simply generates a string and outputs it to the debugger.

Sound Support

Next, we will split the sound support functions off into another file. This merely consists of moving the InitDirectMusic and ExitDirectMusic functions into a new file, and creating more general functions to play and stop segments.

IDirectMusicSegment8 *LoadSound(LPCWSTR wszFilename)
{
    IDirectMusicSegment8 *pSegment;

    // Load the background music MIDI file
    h = g_pMusicLoader->LoadObjectFromFile(CLSID_DirectMusicSegment,
        IID_IDirectMusicSegment8, (LPWSTR)wszFilename, (void **) &pSegment);
    if (FAILED(h))
    {
        DISPLAYERRORCODE(h, "Sound file could not be loaded.");
        return NULL;
    }
    return pSegment;
}

void DownloadSound(IDirectMusicSegment8 *pSegment)
{
    // Download this to the sound card (if necessary)
    pSegment->Download(g_pMusicPerformance);
}

void PlaySound(IDirectMusicSegment8 *pSegment)
{
    // Play the segment immediately
    g_pMusicPerformance->PlaySegment(pSegment, 
	DMUS_SEGF_AFTERPREPARETIME, 0, NULL);
}

void StopSound(IDirectMusicSegment8 *pSegment)
{
    g_pMusicPerformance->StopEx(pSegment, 0, 0);
}

void UnloadSound(IDirectMusicSegment8 *pSegment)
{
    pSegment->Unload(g_pMusicPerformance);
}

We can now put in this file pragmas to force the inclusion of the libraries that we need:

#pragma comment(lib, "kernel32.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "dxguid.lib")

This ensures you won't need to alter your project when you next make a new one...

Input, Windowing and Graphics Support

These are simply moved into their own files. Note that a new pair of functions (Play and StopBackgroundMusic) have been defined to play and stop the music when the application is activated and deactivated.

////////////////////////////////////////////////////////////////////////////////
// Music handling

void PlayBackgroundMusic()
{
    if ((g_pBackgroundMusic != NULL) && 
	!g_bMusicPlaying && 
	g_bFinishedInit && 
	g_bMusicEnabled)
    {
        // Play the background music
        TRACE0("Playing background music...\n");
        g_bMusicPlaying = true;
        PlaySound(g_pBackgroundMusic);
    }
}

void StopBackgroundMusic()
{
    StopAllSound();
    g_bMusicPlaying = false;
}

We will also add a new function to allow you to clear the screen to a colour, should you need to do so:

BOOL ClearScreen(DWORD dwColour)
{
    DDBLTFX ddfx;
    HRESULT h;

    ddfx.dwSize = sizeof(ddfx);
    ddfx.dwFillColor = dwColour;
   
    h = g_lpBackBuffer->Blt(NULL, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &ddfx);
   
    if (h == DDERR_SURFACELOST)
    {  
        g_pDD->RestoreAllSurfaces();
        ReloadAllSurfaces();
        h = g_lpBackBuffer->Blt(NULL, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &ddfx);
    }
   
    if (h != DD_OK)
        return FALSE;
    return TRUE;
}

Now we've made the application more modular, we can think about the two different strategies for this. 

Class Libraries versus Function Libraries

As it stands, we still have a large amount of standard, 'boilerplate' code which needs to stay to maintain flexibility (e.g. initialisation, surface reloading, shutting down). While this is not good in general, it does make for simplicity of understanding.

The alternative is to move our code to a class library. Providing C++ classes for the application, surfaces, segments and so on would make our programming a lot less error-prone, with automatic surface restoration and improved error-handling, while not sacrificing flexibility.  Changing the code to become a class library would be easier than it sounds, but requires a fairly good knowledge of C++ concepts. We've survived so far without bringing in object-oriented programming, and it's questionable whether we want to do so now. What do you think?

For now, we'll carry on with our function-based framework and add one extra feature: input device customisation, which we'll cover in the next section.

 

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!