Alex Russell's Game Programming Tutorial using DX

Introduction . Chapter 1 . Chapter 2 . Chapter 3 . Chapter 4 . Chapter 5 . Chapter 6 . Chapter 7

Chapter 3

Sound and Music

DX supports the playback of two types of sounds: digital and midi. Digital sounds are recordings of the actual sounds. The real sound is simply "sampled" and stored in a file for playback. The advantage of digital sound is that it will sound the same on all computers. The drawback of digital sound is that the file are very large compared to midi files for music. Digital sounds must be used for most special effects and non-music sounds.

MIDI files contain the instructions required to play a song, the notes, timing, and instruments to play. This makes them very small, but they can sound very different on different hardware. How a midi song sounds can change a lot from sound card to sound card depending on the built in "instruments" supported by the sound card. Most newer sound cards support downloading custom instruments which helps make the music come out as the artist expected.

DX has excellent support for both digital and midi sound. DX supports "mixing" multiple sounds (playing more than one digital sound file at a time, as well as panning, volume control, and 3D sound effects.

DX's support of midi music is also very good. Only one song is played at once, but DX supports changing tempo and other varibles of a midi song on the fly.

We will be covering the bare basics of sound under DX - loading and playing digital sounds, and playing a simple midi song. The required initialization of the DX sound buffers is not covered. See Initsound() in the CgsdxIO class for details on the initialization required.

Digital Sound

Before playing a digital sound the cooperative level must be set, and a sound buffer created. These two steps are handled by InitSound(). Once this housekeeping is done the steps to play a sound are:

  • Load the wav file into a sound buffer.
  • Play the sound.
  •         DWORD soundID;
            DWORD soundID2;
    
            dx.LoadSound("horn.wav", &soundID);
            dx.LoadSound("1125SC1.WAV", &soundID2);
            dx.PlaySound(soundID);
            dx.PlaySound(soundID2);
    

    Other topics to explore on your own for digital sounds are volume control, panning (changing the volume of the two stero channels to make the sound seem to move), and 3D sound.

    DX handles playing multiple simultaneous sounds for you, mixing the different sounds on the fly.

    Midi Sound

    Once the related DX sound and midi objects are initialized you can play a midi song with a single call to PlayMusic() in the CgsdxIO class.

            dx.PlayMusic("Meteor Blast 4.mid", 1);
    

    DX also supports functions to modify the tempo and other varibles of a midi song on the fly.

    Text

    Text can be drawn a few different ways under DX. Text can be drawn using small bitmaps that are "pictures" of text. Text can be drawn as a series of lines and curves, and text can be drawn by re-directing the output of the normal win32 GDI text drawing functions to the DX surface. This last method is the one used by the Cgsdxio class. The advantage of this method is that a wide variety of fonts can be drawn with little work. The downside is a slight loss of speed. The fastest way to draw text is using small, pre-drawn, bitmaps.

    A hybrid method is to use GDI to draw the text to an off-screen buffer then turn the images on the off-screen buffer into bitmaps that can be used to draw text. This is a bit more work to setup, but has the flexability of using GDI and the speed of using bitmaps. Only the direct use of GDI is used in Cgsdxio.

    The steps to draw text are: create a font, draw text using the font.

        HFONT font=NULL;
        SIZE size;
    
        font=dx.CreateFont(24, FW_MEDIUM, 0, 1);
        dx.DrawText(font, 50, 230, RGB(255, 255,255), 0, "Hi World.");
        dx.GetTextSize(font, "Hi World", &size);
    

    It is often useful to know how big text is that you are going to draw. GetTextSize() sets a SIZE structure with the width and height of the string if draw with the given font.

    CreateFont() creates a normal win32 font. DrawText() get a pointer to the current surface, redirects GDI to this surface and uses the GDI text functions to draw the text. Here is some stripped down code to draw text suing GDI and DX. font is a font made with CreateFont(), x, y is where the text is drawn, fore is the text colour, back is the text background colour, s is the string to draw, and s is a SIZE structure to hold the size of the rendered text.

    int CgsdxIO::DrawText(HFONT font, int x, int y, DWORD fore, DWORD back, LPCSTR s, SIZE *size);
    
        int err=0;
        HFONT oldFont;
        HRESULT hr;
        HDC lhdc;
    
        m_backSurface->GetDC(&lhdc);  // get a HDC that can be used by GDI functions
        if ( back & GSDX_TRANSPARENT_TEXT )
            {
            SetBkMode(lhdc, TRANSPARENT);
            }
        else
            {
            SetBkMode(lhdc, OPAQUE);
            SetBkColor(lhdc, back);
            }
        SetTextColor(lhdc, fore);
        if ( font )
            {
            oldFont=(HFONT )SelectObject(lhdc, font);
            TextOut(lhdc, x, y, s, strlen(s));
            if ( size )
                GetTextExtentPoint32(lhdc, s, strlen(s), size);
             
            SelectObject(lhdc, oldFont);
            }
        else
            {
            TextOut(lhdc, x, y, s, strlen(s));
            if ( size )
                GetTextExtentPoint32(lhdc, s, strlen(s), size);
            }
    
        m_backSurface->ReleaseDC(lhdc); // unlock the HDC
    
    

    Blitting

    "Bit Blit" means quickly copying a rectangular bitmap from one place to another. In practise this usually means drawing a small drawing on screen.

    DX's DirectDraw supports hardware accelerated blitting. DX also supports "colour keying". Colour Keying means that all pixels in the source bitmap of a certain colour are not copied over - in effect they are transparent. The main steps for blitting are: load the images into video or system memory. Set up colour keying if wanted. Copy the pre-loaded image to display memory to "draw" it.

    DX also supports copying part of a loaded bitmap to display memory. This is a good feature as you could have 10 small images in one bmp file, load the whole thing into video memory, then draw the image you want by specifying the source rectangle in the large bitmap. You can also "scale" bitmaps. By making the destination rectangle smaller or larger than the source rectangle DX will scale the bitmap to fit.

    Loading images from drawing programs

    CgsdxIO has a function called LoadBitmap() that loads bmps into video memory. It handles all the file decoding etc... required. Win32 and later versions of DX also have functions that can help simplify this task.

    CgsdxIO::LoadBitmap() is also passed a RECT that indicates where on the whole bmp file this particular bitmap exists. LoadBitmap() can be called multiple times with the same BMP source file, but different RECT parameters to define where the images are on a single large BMP. LoadBitmap() only loads the main BMP to video memory once. LoadBitmap() does the following:

    • load the bmp file from disk, and decode
    • copy the bmp file to a DX surface
    • store information about the surface for future use
    • make whatever pixel is in the top, left corner of the main image the "colour key"
    • return a pointer that can be used to draw the bitmap that RECT points to
    Please see the source code in the CgsdxIO libraries for more detail.

    Display a bitmap

    Loading the bitmap is actually the hard part. Once a bitmap is loaded a single call the the DX function Blit() draws the bitmap to display memory. CgsdxIO supplies two versions of Blit(), one for non-scaled blits, and one for scaled blits. To scale a blit a destination rectangle is specified. DX automatically scales the source image to fit the destination image.

        // pointers to bitmap info
        gsdxBitmap_t *topLeft;
        gsdxBitmap_t *topRight;
        gsdxBitmap_t *bottomLeft;
        gsdxBitmap_t *bottomRight;
        RECT r;
        CPoint dest;
        char temp[100];
    
        // load four bitmaps
        // each bitmap is actually a rectangle on one large bitmap.
        // LoadBitmap() knows it has loaded "square4.bmp" when it does
        // the second, third, and forth loads, so it returns pointers to
        // the different areas of the same main bitmap
        r.left=0;
        r.right=320;
        r.top=0;
        r.bottom=240;
        topLeft=dx.LoadBitmap("square4.bmp", &r);
        r.left=320;
        r.right=640;
        r.top=0;
        r.bottom=240;
        topRight=dx.LoadBitmap("square4.bmp", &r);
        r.left=0;
        r.right=320;
        r.top=240;
        r.bottom=480;
        bottomLeft=dx.LoadBitmap("square4.bmp", &r);
        r.left=320;
        r.right=640;
        r.top=240;
        r.bottom=480;
        bottomRight=dx.LoadBitmap("square4.bmp", &r);
    
        dx.RectFill(NULL, 0);  // clear screen to black
        dest.x=0;
        dest.y=0;
        dx.Blit(&dest, topLeft); // draw the bitmap
        dx.Flip();
        getch();
    
        dx.RectFill(NULL, 0);
        dest.x=0;
        dest.y=0;
        dx.Blit(&dest, topRight);
        dx.Flip();
        getch();
    
        dx.RectFill(NULL, 0);
        dest.x=0;
        dest.y=0;
        dx.Blit(&dest, bottomLeft);
        dx.Flip();
        getch();
    
        dx.RectFill(NULL, 0);
        dest.x=0;
        dest.y=0;
        dx.Blit(&dest, bottomRight);
        dx.Flip();
        getch();
    
        // draw scaled bitmaps
        // each time through this loop the bitmap is drawn larger
        r.left=0;
        r.right=topLeft->bmRect.right - topLeft->bmRect.left ;
        r.top=0;
        r.bottom=topLeft->bmRect.bottom - topLeft->bmRect.top;
        for ( i=0; i < 7; i++ )
            {
            // draw the "topLeft" bitmap to fit into the rectangle r
            dx.Blit(&r, topLeft, 0);
    
            // make r bigger, and move right and down
            r.left+=120;
            r.right+=150;
            r.top+=50;
            r.bottom+=80;
           
            dx.Flip();
            getch();
            }
    
    
    

    Sprites

    A sprite is a collection of related bitmaps used to display simple animation. For example, if you need to show a character walking you would need a number of bitmaps of the same character with his feet and arms in different positions as he walks. A typical sprite would have a number of bitmaps, plus information about which order the bitmaps are displayed, and when to switch bitmaps.

    SpriteLib (CSprite class) in DXSmith inlcudes a sprite editor, and functions to display sprites. Displaying a sprite just means showing the current bitmap from the sprites collection of bitmaps. CSprite has "banks" of sprites, and each sprite has a number of bitmaps. CSprite handles selecting the current bitmap to display for the sprite, but does not handle where on the screen the sprite will be displayed. That is handled outside the CSprite class using the normal methods for any bitmap. CSprite just handles the routine housekeeping of moving from one bitmap to the next within the sprite.

    The following code is from the CSprite class.

    typedef struct
        {
        int dx, dy;     // offset used to draw this frame, helps centre rotating sprite
        RECT collision; // defaults to the whole frame
        int numAttach;
        POINT *attach;
        gsdxBitmap_t *bitmap; // this frames bitmap
        }
    spriteFrame_t;
    
    typedef struct
        {
        DWORD ID;
        int numFrames;
        int delay;      // between frames in milli-seconds
        int direction;  // for pong
        DWORD changeMode;
        int currentFrame;  // index
        DWORD nextFrameTime; // time at which this the next frame is used
        spriteFrame_t *frames;
        }
    sprite_t;
    
    typedef struct
        {
        int numSprites;
        sprite_t *sprites;
        }
    spriteBank_t;
    
    // index is the index of the sprite within this "bank" of sprites
    int CSprite::DrawSprite(int index, int x, int y)
    {
        int err=0, dx, dy;
        POINT pnt;
    
        if ( m_bank )
            {
            if ( m_bank->sprites[index].numFrames < 1 )
                return 1;
    
            dx=m_bank->sprites[index].frames[m_bank->sprites[index].currentFrame].dx;
            dy=m_bank->sprites[index].frames[m_bank->sprites[index].currentFrame].dy;
            pnt.x=x + dx;
            pnt.y=y + dy;
            err=m_dx->Blit(&pnt, m_bank->sprites[index].frames[m_bank->sprites[index].currentFrame].bitmap, 1, m_hdc);
            }
    
        return err;
    }
    
    

    Introduction . Chapter 1 . Chapter 2 . Chapter 3 . Chapter 4 . Chapter 5 . Chapter 6 . Chapter 7

    Copyright 2004 (c), Alex Russell, All rights reserved