Dancing Particles: Adding Points, Lines, Fonts, and Input to the Managed Graphics Library

Dancing Particles: Adding Points, Lines, Fonts, and Input to the Managed Graphics Library

.NET Compact Framework 1.0
 

Geoff Schwab
Excell Data Corporation

December 2003

Applies to:
   
Microsoft® .NET Compact Framework 1.0
   Microsoft Visual Studio® .NET 2003
   Microsoft eMbedded Visual C++® 3.0

Summary: This article expands upon the "Dancing Zombies" sample by implementing drawing of points, lines, and custom 1 bit fonts converted from 8 bit bitmaps. It also implements an input system that overrides the functionality of the hardware buttons and tracks button states. (51 printed pages)

Download Sample or GAPINet DLLs
Download Dancing Zombies Sample
Download Dancing Rectangles Sample
Download GAPI
Download eMbedded Visual Tools 2002 Edition


Contents

Introduction
Installing GAPINet
The GAPI Wrapper
The Windows Wrapper
A Basic Graphics Framework
The GXFont Class
The GXGraphics Class
The Font Converter Project
The GXInput Class
Test Application
    ParticleTest
    The Form
Conclusion

Developers only concerned with using the graphics library in an application can download and install the sample, as described in the "Installing GAPINet" section and then skip to the "Test Application" section.

Introduction

The graphics library in this sample, defined in the GXGraphicsLibrary namespace, expands upon the previous "Dancing Zombies Sample" with the following additions to its functionality:

  • Converting, loading, and displaying custom 1 bit fonts
  • Drawing points
  • Drawing lines
  • Drawing unfilled rectangles
  • Input

While the graphics library portion of the sample is only available in C#—due to the need for unsafe code blocks in order to optimize pixel copies with pointer operations—the test application, font converter tool, and GXInput library are available in Visual Basic as well as C#. This demonstrates the flexibility of code interoperability under the .NET Compact Framework. Screenshots from the particle test application are shown below.

Figure 1. Frame 20 Screenshot of a ParticleTest Instance.

Figure 2. Frame 80 Screenshot of a ParticleTest Instance with Wind.

Note:   The test application that produced these screenshots was tested and verified on Smartphone, as well as Pocket PC devices.

This sample is based on a previous sample titled "Dancing Rectangles: Using GAPI to Create a Managed Graphics Library" which demonstrates how to wrap GAPI to create a .NET Compact Framework compatible DLL. These DLL's are required to run this sample and can be downloaded from the sample link at the top of this document. For more information on how these DLL's were created, refer to the "Dancing Rectangles" article.

Installing GAPINet

This sample relies on GAPI and thus requires that GAPI be downloaded and unzipped to a location of your choice. Because some functions return types that are not supported by the .NET Compact Framework, GAPINet is required as well. This DLL provides a wrapper around the incompatible functions. For detailed information on how GAPINet was created, refer to the "Creating GAPINet" section of the "Dancing Rectangles" article.

To properly install these components, follow these steps:

  1. Download GAPI
  2. Download GAPINet and this sample
  3. On the device, copy GAPINet.DLL and GX.DLL (from 1 above) from the appropriate processor directory to the device's Windows directory
Note:   There is no officially supported build of GAPI for the emulators.

The GAPI Wrapper

The GXGraphicsLibrary namespace contains a class named GAPI which encapsulates all functionality necessary to access the graphics portions of the GX and GAPINet DLL's. This class is found in GAPI.cs in the sample and contains the P/Invoke implementations of the original API. This class has not been modified from the previous samples, therefore, for detailed information regarding this class, refer to the "The GAPI Wrapper" section of the "Dancing Rectangle" article.

The Windows Wrapper

The graphics library uses several functions from coredll as well as GAPI and GAPINet. The GetCapture function is used to access a HWND handle of the parent form and LocalAlloc and LocalFree are used to allocate and free heap memory. The class responsible for encapsulating this functionality is defined within the GXGraphicsLibrary namespace as the Windows class and can be found in Windows.cs in the sample. This class has not been modified from the previous sample, therefore, for detailed information regarding this class, refer to the "The Windows Wrapper" section of the "Dancing Rectangle" article.

A Basic Graphics Framework

For information regarding the architecture of a basic game loop, refer to the "A Basic Graphics Framework" section of the "Dancing Rectangles" article.

The graphics engine in this sample is primarily made up of five files. These five files and their respective contents are described below:

  • Pixel.cs: This file contains the Pixel class and is internal to the GXGraphicsLibrary namespace. This class is responsible for maintaining pixel conversion utilities.
  • GXBitmap.cs: This file contains the GXBitmap class and is a public class available to users of the GXGraphicsLibrary. This class implements the functionality for loading, instancing (creating memory sharing copies), saving, and displaying both indexed and direct color bitmaps. When drawing bitmaps, the following draw modes are supported:
    • Source key transparency
    • Destination key transparency
    • Flip X axis
    • Flip Y axis
    • Alpha blending
  • GXAnimation.cs: This file contains the GXAnimation class and is a public class available to users of the GXGraphicsLibrary. This class provides functionality for creating, instancing, updating, and displaying an animated bitmap.
  • GXGraphics.cs: This file contains the GXGraphics class and is a public class available to users of GXGraphicsLibrary. This class acts as the interface for the user to the graphics library, providing functionality for accessing the display buffer and maintaining draw states, as well as drawing bitmaps, text, and animations. This class also provides methods for drawing primitives such as rectangles, filled rectangles, points, and lines.
  • GXFont.cs: This file contains the GXFont class and is a public class available to users of GXGraphicsLibrary. This class is responsible for the functionality involved with loading and displaying the library's custom font data.

Descriptions of features new to this sample are listed below:

  • Fonts: This sample implements a custom font format and provides a Windows application that converts 8 bit bitmaps to data files of the proper format. These files can be loaded by the library and used to display text.
  • Points: Drawing of a single pixel.
  • Lines: Drawing of lines given two end points anywhere on the screen.
  • Rectangles: Using line drawing to draw an unfilled rectangle.
  • Input: This sample provides support for input which overrides hardware button functionality and tracks the state of all buttons.

For more information on features from the previous samples, see:

The GXFont Class

The GXFont class implements the functionality of a custom font format for drawing text to the screen. This font data is loaded from a stream and consists of some header information regarding spacing, pixel offsets corresponding to the starting x location of each character in the bitmap data, and the 1 bit bitmap data that describes the font. The font bitmap data is created by the Font Converter tool described in the section titled "The Font Converter Project" of this document. For a detailed description of the file format refer to that section.

In this custom font format, not all characters need to be supported. If a character is not supported then it is ignored by the font engine. The definition of the unsupported value is the first member of the GXFont class. This value is stored as the x offset for all unsupported characters. The remaining members listed below are listed in the order in which they are read from the file and are defined as:

  • m_spaceWidth: The number of horizontal pixels that make a ' ' space character.
  • m_vertOffset: The number of vertical pixels to offset multiple lines of text. More specifically, to process a carriage return, this value is added to the height of the font.
  • m_horOffset: The number of horizontal pixels to offset each character in a string, i.e., the distance between characters.
  • m_height: The height of the font. This height is uniform for all characters.
  • m_bytesPerRow: The number of bytes in a row of pixels. The data is stored as raw 1 bit data with no bitmap information so this is required to determine how to offset into the image data to find the start of a new row of 1 bit pixels.
protected const ushort kUnsupportedCharacter = 0xffff;
protected int m_spaceWidth;
protected int m_vertOffset;
protected int m_horOffset;
public int Height { get { return m_height; } }
protected int m_height;
protected int m_bytesPerRow;

The m_xOffset array contains the x offset of each character into the image data. In other words, this is the starting x pixel in the image data of each character. Unsupported characters are set to kUnsupportedCharacter.

protected ushort[] m_xOffset = null;

The m_image array is the actual image data. This data is stored as 1 bit pixel information. Each row is 1 byte aligned so each row of pixels starts at a new byte. If a pixel is set then the specified font color is drawn for the corresponding pixel, otherwise the pixel is transparent.

protected byte[] m_image = null;

The GXFont class provides one constructor. This constructor requires a stream containing the data defined by the font file (.fnt) format for this engine. The constructor allocates the memory required to store the font data and loads it.

public GXFont(Stream strm)
{
    BinaryReader rdr = new BinaryReader(strm);

    m_spaceWidth = rdr.ReadInt32();
    m_vertOffset = rdr.ReadInt32();
    m_horOffset = rdr.ReadInt32();
    m_height = rdr.ReadInt32();
    m_bytesPerRow = rdr.ReadInt32();

    m_xOffset = new ushort[257];

    for (int i = 0; i < 257; i++)
    {
        m_xOffset[i] = rdr.ReadUInt16();
    }

    m_image = new byte[rdr.BaseStream.Length –
      rdr.BaseStream.Position];
    m_image = rdr.ReadBytes((int)(rdr.BaseStream.Length –
      rdr.BaseStream.Position));
}

The DrawString method draws a string to a GXBitmap given the string, starting location, and text color. Text drawing supports all draw modes including alpha blending and color key transparency. The method loops through each character in the string, skipping unsupported characters, looks up the x offset of the character and accesses the appropriate starting byte and bit in the image data. This starting bit and each subsequent bit represent a pixel in the character's image. If a bit is set then the proper pixel is set to the specified color, otherwise the pixel is ignored.

public void DrawString
(
    GXGraphics gx,
    GXBitmap dstBitmap,
    string strText,
    Color color,
    int x,
    int y
)
{
    int prevXOffset = 0;
    int prevYOffset = 0;

    ushort rgb = gx.PixelConverter.ColorToPixel(color);

    bool bAlpha =
      gx.CheckDrawModes(GXGraphics.DrawFlags.kModeAlphaBlending);
    bool bSrcKey =
      gx.CheckDrawModes(GXGraphics.DrawFlags.kModeSrcKeyTransparency);
    bool bDstKey =
      gx.CheckDrawModes(GXGraphics.DrawFlags.kModeDstKeyTransparency);

    ushort rSrc = 0;
    ushort gSrc = 0;
    ushort bSrc = 0;
    ushort rDst = 0;
    ushort gDst = 0;
    ushort bDst = 0;
    ushort dAlpha = (ushort)(255 - gx.Alpha);

    if (bAlpha)
    {
        gx.PixelConverter.PixelToBGR(rgb, ref bSrc, ref gSrc,
          ref rSrc);
        rSrc = (ushort)(gx.Alpha * rSrc);
        gSrc = (ushort)(gx.Alpha * gSrc);
        bSrc = (ushort)(gx.Alpha * bSrc);
    }

    ushort sourceKey = gx.SourceKey;
    ushort destinationKey = gx.DestinationKey;

    for (int i = 0; i < strText.Length; i++)
    {
        if (strText[i] == ' ')
        {
            prevXOffset += m_spaceWidth;
        }
        else if (strText[i] == '\r')
        {
            prevYOffset += m_height + m_vertOffset;
            prevXOffset = 0;
        }

        if (m_xOffset[strText[i]] == kUnsupportedCharacter)
            continue;

        int nextCharIndex = strText[i] + 1;
        ushort nextCharXOffset = m_xOffset[nextCharIndex];
        while (nextCharXOffset == kUnsupportedCharacter)
        {
            nextCharXOffset = m_xOffset[nextCharIndex++];
        }

        int cStart = m_xOffset[strText[i]];
        int cEnd = nextCharXOffset;

        int xDstPitch = gx.DrawSurface.xPixelPitch;
        int yDstPitch = gx.DrawSurface.yPixelPitch;

        unsafe
        {
            ushort* pCurDstLine = (ushort*)dstBitmap.Pixels.ToInt32() +
              (x + prevXOffset) * xDstPitch + (y + prevYOffset) *
              yDstPitch;

            for (int r = 0; r < m_height; r++)
            {
                ushort* pCurDstPixel = pCurDstLine;

                if (y + prevYOffset + r >= gx.ScreenHeight)
                    break;

                for (int c = cStart; c < cEnd; c++)
                {
                    if (x + prevXOffset + cEnd - c >= gx.ScreenWidth)
                        break;

                    byte curByte = m_image[r * m_bytesPerRow + c / 8];
                    int curBit = c % 8;

                    if ((curByte & (0x80 >> curBit)) != 0)
                    {
                        if (!(bSrcKey && rgb == sourceKey) &&
                            !(bDstKey && *pCurDstPixel == 
                              destinationKey))
                        {
                            if (bAlpha)
                            {
                                gx.PixelConverter.PixelToBGR
                                (
                                    *pCurDstPixel, ref bDst, ref gDst,
                                     ref rDst
                                );

                                rDst = (ushort)((rSrc + rDst * dAlpha)
                                  >> 8);
                                gDst = (ushort)((gSrc + gDst * dAlpha)
                                  >> 8);
                                bDst = (ushort)((bSrc + bDst * dAlpha)
                                  >> 8);

                                *pCurDstPixel =
                                  gx.PixelConverter.BGRToPixelNoShift
                                  (
                                    (byte)bDst, (byte)gDst,
                                    (byte)rDst
                                  );
                            }
                            else
                            {
                                *pCurDstPixel = rgb;
                            }
                        }
                    }
                    pCurDstPixel += xDstPitch;
                }

                pCurDstLine += yDstPitch;
            }
        }

        prevXOffset += cEnd - cStart + m_horOffset;
    }
}

The GetTextExtents method fills in the Rectangle rExtents such that it defines the screen area, in pixels, that the text will require for drawing. This method loops through each character in the string, tracking the largest x and y pixel offset that it encounters using the width of each character and any carriage returns.

public void GetTextExtents(ref Rectangle rExtents, string strText, int x, int y)
{
    rExtents.X = x;
    rExtents.Y = y;

    int maxX = 0;

    int prevXOffset = 0;
    int prevYOffset = 0;

    for (int i = 0; i < strText.Length; i++)
    {
        if (strText[i] == ' ')
        {
            prevXOffset += m_spaceWidth;
        }
        else if (strText[i] == '\r')
        {
            if (prevXOffset > maxX)
                maxX = prevXOffset;

            prevYOffset += m_height + m_vertOffset;
            prevXOffset = x;
        }

        if (m_xOffset[strText[i]] == kUnsupportedCharacter)
            continue;

        int nextCharIndex = strText[i] + 1;
        ushort nextCharXOffset = m_xOffset[nextCharIndex];
        while (nextCharXOffset == kUnsupportedCharacter)
        {
            nextCharXOffset = m_xOffset[nextCharIndex++];
        }

        int cStart = m_xOffset[strText[i]];
        int cEnd = nextCharXOffset;

        prevXOffset += cEnd - cStart + m_horOffset;
    }

    if (prevXOffset > maxX)
        maxX = prevXOffset;

    rExtents.Width = maxX;
    rExtents.Height = prevYOffset;
}

The GXGraphics Class

The GXGraphics class is the primary interface to the graphics engine. The majority of the class was not modified from the previous sample with the exception of adding methods for drawing points, lines, unfilled rectangles, and text to the draw surface. For more information on the functionality of the GXGraphics class, refer to the "The GXGraphics Class" section of the "Dancing Zombies" article.

The DrawPoint method sets the specified pixel to the specified color, honoring the GXGraphics draw modes.

public void DrawPoint(Point loc, Color color)
{
    bool bAlpha = CheckDrawModes(DrawFlags.kModeAlphaBlending);
    bool bSrcKey = CheckDrawModes(DrawFlags.kModeSrcKeyTransparency);
    bool bDstKey = CheckDrawModes(DrawFlags.kModeDstKeyTransparency);
    bool bDrawPixel = true;

    ushort rgb = this.m_pixel.ColorToPixel(color);
    ushort rSrc = 0;
    ushort gSrc = 0;
    ushort bSrc = 0;
    m_pixel.PixelToBGR(rgb, ref bSrc, ref gSrc, ref rSrc);
    rSrc = (ushort)(m_alpha * rSrc);
    gSrc = (ushort)(m_alpha * gSrc);
    bSrc = (ushort)(m_alpha * bSrc);

    ushort rDst = 0;
    ushort gDst = 0;
    ushort bDst = 0;
    ushort dAlpha = (ushort)(255 - m_alpha);

    unsafe
    {
        int yPitch = this.DrawSurfaceYPixelPitch;
        int xPitch = this.DrawSurfaceXPixelPitch;

        ushort* pLine = (ushort*)this.DrawSurface.Pixels;

        int y0 = loc.Y;
        int x0 = loc.X;

        bDrawPixel = true;
        if (x0 < 0 || x0 >= ScreenWidth ||
            y0 < 0 || y0 >= ScreenHeight)
        {
            bDrawPixel = false;
        }

        if (bDrawPixel && !(bSrcKey && rgb == m_sourceKey) &&
            !(bDstKey && *(pLine + x0 * xPitch + y0 * yPitch) ==
            m_destinationKey))
        {
            if (bAlpha)
            {
                m_pixel.PixelToBGR(*(pLine + x0 * xPitch + y0 *
                  yPitch), ref bDst, ref gDst, ref rDst);

                rDst = (ushort)((rSrc + rDst * dAlpha) >> 8);
                gDst = (ushort)((gSrc + gDst * dAlpha) >> 8);
                bDst = (ushort)((bSrc + bDst * dAlpha) >> 8);

                *(pLine + x0 * xPitch + y0 * yPitch) =
                  m_pixel.BGRToPixelNoShift((byte)bDst,
                  (byte)gDst, (byte)rDst);
            }
            else
            {
                *(pLine + x0 * xPitch + y0 * yPitch) = rgb;
            }
        }
    }
}

The DrawPoints method is similar to the DrawPoint method except that it takes an array of points and colors. The method loops through each point in the list and sets the corresponding pixel. The colors array must contain either one color or one color for each point. If only one color is specified then that color is used for all points.

public void DrawPoints(Point[] points, Color[] colors)
{
    Debug.Assert(colors.Length >= points.Length || colors.Length == 1,
      "GXGraphics.DrawPoints - Not enough colors specified.");

    // Cache the draw modes
    bool bAlpha = CheckDrawModes(DrawFlags.kModeAlphaBlending);
    bool bSrcKey = CheckDrawModes(DrawFlags.kModeSrcKeyTransparency);
    bool bDstKey = CheckDrawModes(DrawFlags.kModeDstKeyTransparency);

    // Used to simplify some if statements.  Determines whether the
    // pixel is to be drawn.
    bool bDrawPixel = true;

    ushort rSrc = 0;
    ushort gSrc = 0;
    ushort bSrc = 0;
    ushort rDst = 0;
    ushort gDst = 0;
    ushort bDst = 0;
    ushort dAlpha = (ushort)(255 - m_alpha);

    unsafe
    {
        int yPitch = this.DrawSurfaceYPixelPitch;
        int xPitch = this.DrawSurfaceXPixelPitch;

        // Initialize the pixel color if only one color
        // is specified
        ushort rgb = 0;
        if (colors.Length == 1)
        {
            rgb = this.m_pixel.ColorToPixel(colors[0]);

            m_pixel.PixelToBGR(rgb, ref bSrc, ref gSrc, ref rSrc);
            rSrc = (ushort)(m_alpha * rSrc);
            gSrc = (ushort)(m_alpha * gSrc);
            bSrc = (ushort)(m_alpha * bSrc);
        }

        // Loop through each point in the array
        for (int i = 0; i < points.Length; i++)
        {
            // If there is 1 color per point then store the
            // color for this point
            if (colors.Length != 1)
                rgb = this.m_pixel.ColorToPixel(colors[i]);

            ushort* pLine = (ushort*)this.DrawSurface.Pixels;

            // Cache the location of the pixel
            int y0 = points[i].Y;
            int x0 = points[i].X;

            bDrawPixel = true;

            // If bounds checking is allowed then check if the pixel is in
            // a valid location
            if (x0 < 0 || x0 >= ScreenWidth ||
                y0 < 0 || y0 >= ScreenHeight)
            {
                bDrawPixel = false;
            }

            // Attempt to draw the pixel
            if (bDrawPixel && !(bSrcKey && rgb == m_sourceKey) &&
                !(bDstKey && *(pLine + x0 * xPitch + y0 * yPitch) ==
                m_destinationKey))
            {
                if (bAlpha)
               {
                    if (colors.Length != 1)
                    {
                        m_pixel.PixelToBGR(rgb, ref bSrc, ref gSrc,
                          ref rSrc);
                        rSrc = (ushort)(m_alpha * rSrc);
                        gSrc = (ushort)(m_alpha * gSrc);
                        bSrc = (ushort)(m_alpha * bSrc);
                    }

                    m_pixel.PixelToBGR(*(pLine + x0 * xPitch + y0 *
                      yPitch), ref bDst, ref gDst, ref rDst);

                    rDst = (ushort)((rSrc + rDst * dAlpha) >> 8);
                    gDst = (ushort)((gSrc + gDst * dAlpha) >> 8);
                    bDst = (ushort)((bSrc + bDst * dAlpha) >> 8);

                    *(pLine + x0 * xPitch + y0 * yPitch) =
                      m_pixel.BGRToPixelNoShift((byte)bDst,
                      (byte)gDst, (byte)rDst);
                }
                else
                {
                    *(pLine + x0 * xPitch + y0 * yPitch) = rgb;
                }
            }
        }
    }
}

The DrawLines method draws a line list. There can be any number of points in the points array but the indices in the indices array must correspond to a valid index into the points array and there must always be two indices per line. The colors array works in the same manner as that of DrawPoints above. Each pair of indices corresponds to an index of a start and end point of a line in the points array. Figure 3 shows an example of creating a box with 8 points and 24 indices (12 lines).

Figure 3. Example of Line List Indexing.

Note:  Lines are drawn using the Bresenham algorithm. There is a lot of information available on this topic on the internet but one of the best resources I have discovered is the following link provided by University of Colorado:
http://gamedev.cs.colorado.edu/tutorials/Bresenham.pdf
public void DrawLines(Point[] points, ushort[] indices, Color[] colors)
{
    Debug.Assert(colors.Length == indices.Length / 2 ||
      colors.Length == 1,
      "GXGraphics.DrawLines - Not enough colors specified.");

    bool bAlpha = CheckDrawModes(DrawFlags.kModeAlphaBlending);
    bool bSrcKey = CheckDrawModes(DrawFlags.kModeSrcKeyTransparency);
    bool bDstKey = CheckDrawModes(DrawFlags.kModeDstKeyTransparency);
    bool bDrawPixel = true;

    ushort rSrc = 0;
    ushort gSrc = 0;
    ushort bSrc = 0;
    ushort rDst = 0;
    ushort gDst = 0;
    ushort bDst = 0;
    ushort dAlpha = (ushort)(255 - m_alpha);

    unsafe
    {
        int yPitch = this.DrawSurfaceYPixelPitch;
        int xPitch = this.DrawSurfaceXPixelPitch;

        ushort rgb = 0;
        if (colors.Length == 1)
        {
            rgb = this.m_pixel.ColorToPixel(colors[0]);
            m_pixel.PixelToBGR(rgb, ref bSrc, ref gSrc, ref rSrc);
            rSrc = (ushort)(m_alpha * rSrc);
            gSrc = (ushort)(m_alpha * gSrc);
            bSrc = (ushort)(m_alpha * bSrc);
        }

        // There are 2 indices per line so loop through each set of
        // indices
        for (int i = 0; i < indices.Length; i += 2)
        {
            // Determine the current color information if each line is
            // unique
            if (colors.Length != 1)
            {
                rgb = this.m_pixel.ColorToPixel(colors[i/2]);
                m_pixel.PixelToBGR(rgb, ref bSrc, ref gSrc, ref rSrc);
                rSrc = (ushort)(m_alpha * rSrc);
                gSrc = (ushort)(m_alpha * gSrc);
                bSrc = (ushort)(m_alpha * bSrc);
            }

            ushort* pLine = (ushort*)this.DrawSurface.Pixels;

            // Cache the end points
            int y0 = points[indices[i]].Y;
            int x0 = points[indices[i]].X;
            int y1 = points[indices[i+1]].Y;
            int x1 = points[indices[i+1]].X;

            // Determine the width and height of the drawing region
            int dy = y1 - y0;
            int dx = x1 - x0;
            int stepx, stepy;

            // Determine at which end to start and which direction
            // to move
            if (dy < 0)
            {
                dy = -dy;
                stepy = -1;
            }
            else
            {
                stepy = 1;
            }

            if (dx < 0)
            {
                dx = -dx;
                stepx = -1;
            }
            else
            {
                stepx = 1;
            }

            // Use the Bresenham algorithm to draw the line...
            dy <<= 1;
            dx <<= 1;

            bDrawPixel = true;
            if (x0 < 0 || x0 >= ScreenWidth ||
                y0 < 0 || y0 >= ScreenHeight)
            {
                bDrawPixel = false;
            }

            if (bDrawPixel && !(bSrcKey && rgb == m_sourceKey) &&
               !(bDstKey && *(pLine + x0 * xPitch + y0 * yPitch) ==
               m_destinationKey))
            {
                if (bAlpha)
                {
                    m_pixel.PixelToBGR(*(pLine + x0 * xPitch + y0 *
                      yPitch), ref bDst, ref gDst, ref rDst);

                    rDst = (ushort)((rSrc + rDst * dAlpha) >> 8);
                    gDst = (ushort)((gSrc + gDst * dAlpha) >> 8);
                    bDst = (ushort)((bSrc + bDst * dAlpha) >> 8);

                    *(pLine + x0 * xPitch + y0 * yPitch) =
                      m_pixel.BGRToPixelNoShift((byte)bDst, (byte)gDst,
                      (byte)rDst);
                }
                else
                {
                    *(pLine + x0 * xPitch + y0 * yPitch) = rgb;
                }
            }

            if (dx > dy)
            {
                int fraction = dy - (dx >> 1);
                while (x0 != x1)
                {
                    if (fraction >= 0)
                    {
                        y0 += stepy;
                        fraction -= dx;
                    }

                    x0 += stepx;
                    fraction += dy;

                    bDrawPixel = true;
                    if (x0 < 0 || x0 >= ScreenWidth ||
                        y0 < 0 || y0 >= ScreenHeight)
                    {
                        bDrawPixel = false;
                    }

                    if (bDrawPixel && !(bSrcKey && rgb == m_sourceKey)
                        && !(bDstKey && *(pLine + x0 * xPitch + y0 *
                        yPitch) == m_destinationKey))
                    {
                        if (bAlpha)
                        {
                            m_pixel.PixelToBGR(*(pLine + x0 * xPitch +
                              y0 * yPitch), ref bDst, ref gDst,
                              ref rDst);

                            rDst = (ushort)((rSrc + rDst * dAlpha) >> 8);
                            gDst = (ushort)((gSrc + gDst * dAlpha) >> 8);
                            bDst = (ushort)((bSrc + bDst * dAlpha) >> 8);

                            *(pLine + x0 * xPitch + y0 * yPitch) =
                              m_pixel.BGRToPixelNoShift((byte)bDst,
                              (byte)gDst, (byte)rDst);
                        }
                        else
                        {
                            *(pLine + x0 * xPitch + y0 * yPitch) = rgb;
                        }
                    }
                }
            }
            else
            {
                int fraction = dx - (dy >> 1);
                while (y0 != y1)
                {
                    if (fraction >= 0)
                    {
                        x0 += stepx;
                        fraction -= dy;
                    }

                    y0 += stepy;
                    fraction += dx;

                    bDrawPixel = true;
                    if (x0 < 0 || x0 >= ScreenWidth ||
                        y0 < 0 || y0 >= ScreenHeight)
                    {
                        bDrawPixel = false;
                    }

                    if (bDrawPixel && !(bSrcKey && rgb == m_sourceKey)
                        && !(bDstKey && *(pLine + x0 * xPitch + y0 *
                        yPitch) == m_destinationKey))
                    {
                        if (bAlpha)
                        {
                            m_pixel.PixelToBGR(*(pLine + x0 * xPitch +
                              y0 * yPitch), ref bDst, ref gDst,
                              ref rDst);

                            rDst = (ushort)((rSrc + rDst * dAlpha) >> 8);
                            gDst = (ushort)((gSrc + gDst * dAlpha) >> 8);
                            bDst = (ushort)((bSrc + bDst * dAlpha) >> 8);

                            *(pLine + x0 * xPitch + y0 * yPitch) =
                              m_pixel.BGRToPixelNoShift((byte)bDst,
                              (byte)gDst, (byte)rDst);
                        }
                        else
                        {
                            *(pLine + x0 * xPitch + y0 * yPitch) = rgb;
                        }
                    }
                }
            }
        }
    }
}

The DrawRect method draws an unfilled rectangle by constructing a point and index array based on the specified rectangle and then calling DrawLines.

public void DrawRect(Rectangle r, Color rectColor)
{
    Point[] rPoints = new Point[4];
    ushort[] rIndices = new ushort[8];
    Color[] rColors = new Color[1];

    rColors[0] = rectColor;

    rPoints[0].X = r.X;
    rPoints[0].Y = r.Y;
    rPoints[1].X = r.X + r.Width - 1;
    rPoints[1].Y = r.Y;
    rPoints[2].X = r.X + r.Width - 1;
    rPoints[2].Y = r.Y + r.Height - 1;
    rPoints[3].X = r.X;
    rPoints[3].Y = r.Y + r.Height - 1;
    rIndices[0] = 0;
    rIndices[1] = 1;
    rIndices[2] = 1;
    rIndices[3] = 2;
    rIndices[4] = 2;
    rIndices[5] = 3;
    rIndices[6] = 3;
    rIndices[7] = 0;

    DrawLines(rPoints, rIndices, rColors);
}

The DrawText method is provided to allow text to be drawn directly to the current draw surface - which is not available outside of the scope of the GXGraphics class.

public void DrawText(string txt, Color txtColor, int x, int y,
  GXFont fnt)
{
    fnt.DrawString(this, m_drawSurface, txt, txtColor, x, y);
}

The Font Converter Project

The font conversion project consists of a single Form that allows the user to configure and convert an 8 bit bitmap file to a font file (.fnt). A screenshot of the application shown in Figure 4 shows a bitmap being converted with the default parameters. The numeric selection lists allow the user to modify the specified parameters because they cannot be extracted from the bitmap file.

Figure 4. Font Converter Application.

A sample bitmap named font.bmp and its associated font file named font.fnt are included with the test application project as an example of how to create a compatible bitmap for conversion. The bitmap must be formatted such that separators between characters are white horizontal bars (ARGB: 0x00FFFFFF), drawn pixels are black (ARGB: 0x00000000), and transparent pixels are any other color (hot pink ARGB: 0x00FF00FF was used in the sample bitmap). Figure 5 below shows the first section of the font.bmp file used to create the font.fnt file that is used in the sample to display text. The blue border was added to clarify the bounds of the bitmap but is not part of the original bitmap.

Figure 5. Section of font.bmp.

Note:  To keep the sample simple, only 8 bit bitmaps are supported by the Font Conversion tool.

The first 33 characters of the font in Figure 5 are not supported, as is indicated by the 0 width characters between the separators. Therefore, the first valid character starts at index 33.

The bitmap is loaded by the tool and converted to a 1 bit image format. The format of the entire .fnt file is listed below:

  1. m_spaceWidth (Int32): Pixel width of a space character
  2. m_vertOffset (Int32): Pixel x offset between characters
  3. m_horOffset (Int32): Pixel y offset between text lines
  4. m_height (Int32): Pixel height of the font
  5. m_bytesPerRow (Int32): Bytes in one row of m_image pixels
  6. m_xOffset (ushort[257]): X Pixel offset of each character. The last entry (index 256) is the width of the bitmap in pixels.
  7. m_image (byte[]): Image data as 1 bit per pixel. This data is a result of parsing the bitmap for the pixel information of each character.

After writing the initial 5 parameters to the destination font file, the tool parses the bitmap, searching for the separators which are used to determine where each character's x offset is located. The x offset of the image data is not the same as the x offset of the bitmap because the final image data removes separators to condense the file.

The image data is the last data written to the file and consists of 1 bit pixel information for all supported characters. A black pixel represents a set bit and any other color represents a cleared bit. If the byte currently being set is incomplete at the end of a row of pixels, it is written as is and a new byte is started for the subsequent row, therefore, each row is 1 byte aligned.

Rather than list the entire project source code, only the conversion code is listed below. The rest of the code is straightforward GUI initialization and the conversion code is where the real action is in this project.

private void button_convert_Click(object sender, System.EventArgs e)
{
    // Make sure there is a specified file and it exists
    if (this.textBox_fileName.Text.Length == 0)
    {
        MessageBox.Show("No file specified", "Error",
          MessageBoxButtons.OK, MessageBoxIcon.Error);
        return;
    }

    if (!File.Exists(this.openFileDialog1.FileName))
    {
        MessageBox.Show("The specified file does not exist", "Error",
          MessageBoxButtons.OK, MessageBoxIcon.Error);
            return;
    }

    // Initialize the streams
    Stream strm = null;
    BinaryReader rdr = null;
    BinaryWriter wrtr = null;

    try
    {
        // Open the file
        strm = File.Open(this.openFileDialog1.FileName, FileMode.Open);
        if (strm == null)
            return;

        // Intialize a reader for the bitmap file
        rdr = new BinaryReader(strm);
        if (rdr == null)
            return;

        // Read in the file and info headers
        GXBitmap.BITMAPFILEHEADER fileHdr = new
          GXGraphicsLibrary.GXBitmap.BITMAPFILEHEADER();
        GXBitmap.BITMAPINFOHEADER infoHdr = new
          GXGraphicsLibrary.GXBitmap.BITMAPINFOHEADER();

        fileHdr.ReadStream(rdr);
        infoHdr.ReadStream(rdr);

        // We will only support 8 bpp bitmaps to minimize the amount of
        // work.  This bitmap will be condensed to 1 bpp anyway.
        if (infoHdr.biBitCount != 8)
        {
            MessageBox.Show
            (
              "Only 8 bit bitmaps are supported by this tool.",
              "Error", MessageBoxButtons.OK, MessageBoxIcon.Error
            );
            return;
        }

        // Initialize arrays for the indices and palette
        uint[] m_pixels = new uint[256];
        byte[] m_indices = new byte[infoHdr.biWidth *
          infoHdr.biHeight];

        // Read in the palette
        for (int i = 0; i < 256; i++)
        {
            m_pixels[i] = rdr.ReadUInt32();
        }

        // Bitmap file pixel rows are padded to 32 bits.
        int cExtra = 4 - (infoHdr.biWidth % 4);
        if (cExtra == 4)
            cExtra = 0;

        // Loop through each row
        for (int r = 0; r < infoHdr.biHeight; r++)
        {
            // Start at the last row since bitmaps are stored upside
            // down
            int curIndex = (infoHdr.biHeight - (1 + r)) *
              infoHdr.biWidth;

            // Loop through each column
            for (int c = 0; c < infoHdr.biWidth; c++)
            {
                m_indices[curIndex++] = rdr.ReadByte();
            }

            // Read the padding bytes
            for (int c = 0; c < (cExtra); c++)
            {
                rdr.ReadByte();
            }
        }

        // Open a new file with a .fnt extension
        string dstFileName =
          this.openFileDialog1.FileName.Replace(".bmp", ".fnt");
        strm = File.Open(dstFileName, FileMode.OpenOrCreate);
        if (strm == null)
            return;

        // Truncate the file
        strm.Position = 0;
        strm.SetLength(0);

        // Initialize a writer for the file stream
        wrtr = new BinaryWriter(strm);
        if (wrtr == null)
            return;

        // Find the offsets - these are the starting locations for each
        // character in the font
        UInt16[] xOffsets = new UInt16[257];
        int numOffsets = 0;

        // When the file is reconstructed, the pixels will be shifted
        // because characters that are not implemented will be left out
        // of the file. curOffsetPixel keeps track of the pixel that
        // corresponds to the current character (if it is in the file).
        int curOffsetPixel = 0;

        // Loop through the first row of pixels
        for (int curPixel = 0; curPixel < infoHdr.biWidth; curPixel++)
        {
            // Check if the pixel is a separator designating the start
            // of a new character
            if (m_pixels[m_indices[curPixel]] == 0x00ffffff)
            {
                // There have to be exactly 2^8 = 256 characters in the
                // file
                if (numOffsets == 256)
                {
                    MessageBox.Show
                    (
                      "Too many characters specified in bitmap",
                      "Error", MessageBoxButtons.OK,
                      MessageBoxIcon.Error
                    );
                    return;
                }
                else if (numOffsets == 255)
                {
                    // If this is the last character and the last pixel
                    // then it is not implemented
                    if (curPixel == infoHdr.biWidth - 1)
                        xOffsets[numOffsets] = 0xffff;
                    else
                    {
                        xOffsets[numOffsets] =
                          (UInt16)(curOffsetPixel);
                        curOffsetPixel += infoHdr.biWidth –
                          curOffsetPixel - 1;
                    }
                }
                else
                {
                    // If the next character is a separator then this
                    // character is not implemented
                    if (m_pixels[m_indices[curPixel+1]] == 0x00ffffff)
                        xOffsets[numOffsets] = 0xffff;
                    else
                    {
                        // Set the pixel location of this character
                        xOffsets[numOffsets] =
                          (UInt16)(curOffsetPixel);

                        // Increment the current pixel pointer to the
                        // end of this character.
                        int curOffset = curPixel+1;
                        while (m_pixels[m_indices[curOffset]] !=
                          0x00ffffff)
                        {
                            curOffsetPixel++;
                            curOffset++;
                        }
                    }
                }

                numOffsets++;
            }
        }

        // The last offset will always be the width of the bitmap.
        // This is so that the last character can determine its width.
        xOffsets[256] = (ushort)infoHdr.biWidth;

        // There have to be exactly 2^8 = 256 characters in the file
        if (numOffsets != 256)
        {
            MessageBox.Show
            (
              "Not enough characters specified in bitmap. must be 256",
              "Error", MessageBoxButtons.OK, MessageBoxIcon.Error
            );
            return;
        }

        // Set the width of a space character ' ' rather than draw it
        // as an empty bitmap character
        Int32 spaceWidth = (int)this.numericUpDown_space.Value;

        // Set the space between lines when a carriage return is
        // detected
        Int32 vertOffset = (int)this.numericUpDown_return.Value;

        // Set the space between characters
        Int32 horOffset = (int)this.numericUpDown_sep.Value;

        // Determine the number of bytes required to define one row of
        // characters
        Int32 bytesPerRow = curOffsetPixel / 8;
        if (bytesPerRow % 8 != 0)
            bytesPerRow++;

        // Write the calculated data to the file
        wrtr.Write(spaceWidth);
        wrtr.Write(vertOffset);
        wrtr.Write(horOffset);
        wrtr.Write(infoHdr.biHeight);
        wrtr.Write(bytesPerRow);

        // Write the pixel offsets of each character
        for (int i = 0; i < 257; i++)
        {
            wrtr.Write(xOffsets[i]);
        }

        // We will be creating 1 bit pixels so every eight pixels needs
        // to be shifted into a byte
        byte curByte = 0;
        int curBit = 0;

        // Loop through each row of pixels
        for (int r = 0; r < infoHdr.biHeight; r++)
        {
            // Loop through each pixel in each row
            for (int c = 0; c < infoHdr.biWidth; c++)
            {
                // If this is not the last pixel and it is unsupported
                // then skip it
                if (c != infoHdr.biWidth - 1 &&
                  m_pixels[m_indices[c + r * infoHdr.biWidth]] ==
                  0x00ffffff)
                    continue;

                // If this is a black pixel then set the corresponding
                // bit
                if (m_pixels[m_indices[c + r * infoHdr.biWidth]] ==
                  0x00000000)
                    curByte |= (byte)(0x80 >> curBit);

                curBit++;

                // If all of the bits in the byte are used or this is
                // the last pixel in the row then write out the byte
                if (curBit == 8 || c == infoHdr.biWidth - 1)
                {
                    wrtr.Write(curByte);
                    curByte = 0;
                    curBit = 0;
                }
            }
        }
    }
    finally
    {
        // Clean up the streams
        if (strm != null)
            strm.Close();

        if (rdr != null)
            rdr.Close();

        if (wrtr != null)
            wrtr.Close();
    }
}

The GXInput Class

GXInput automates the tracking of button states. An instance of this class will update the state of all buttons when the Update method is called, regardless of what control or application has focus at that time. It will also override the functionality of the hardware keys as long as it is active.

The GXInput class makes use of three P/Invokes.

  • RegisterHotKey is used to receive hardware button input to a MessageWindow
  • UnregisterFunc1 is used to override the designated hardware button functionality
  • GetAsyncKeyState is used to access the current state of a button

The class keeps the previous and current state of each key in a packed byte. These states are related to calls to Update, so each time Update is called, the current state is copied into the previous state and then the current state is stored.

In order to reduce some overhead, keys must be registered if they are to be checked. If a key is not registered then its state will not be updated. The class provides methods for updating some or all keys.

The following modifiers are used by the hot key functions.

protected const uint MOD_ALT = 0x0001;
protected const uint MOD_CONTROL = 0x0002;
protected const uint MOD_SHIFT = 0x0004;
protected const uint MOD_WIN = 0x0008;
protected const uint MOD_KEYUP = 0x1000;

The hardware buttons are defined by the HardwareKeys enum and can be used as the key id to the hot key functions.

public enum HardwareKeys
{
    kFirstHardwareKey = 193,
    kHardwareKey1 = kFirstHardwareKey,
    kHardwareKey2 = 194,
    kHardwareKey3 = 195,
    kHardwareKey4 = 196,
    kHardwareKey5 = 197,
    kLastHardwareKey = kHardwareKey5
}

The P/Invokes are declared in the following code listing.

[DllImport("coredll.dll")]
protected static extern uint RegisterHotKey(IntPtr hWnd, int id,
  uint fsModifiers, uint vk);

[DllImport("coredll.dll")]
protected static extern bool UnregisterFunc1(uint fsModifiers, int id); 

[DllImport("coredll.dll")]
protected static extern short GetAsyncKeyState(int vKey); 

A MessageWindow is used to capture the hardware button input. In this example, the keys are ignored.

protected class InputMessageWindow : MessageWindow
{
    protected const int WM_HOTKEY = 0x0312;

    protected override void WndProc(ref Message msg)
    {
        // Do not process hot keys
        if (msg.Msg != WM_HOTKEY)
        {
            base.WndProc(ref msg);
        }
    }
}
protected InputMessageWindow m_msgWindow = null;

The state of each key is packed into a byte. The following constants are used to access the various states in the bitmask.

protected const byte kCurrentMask = 0x01;
protected const byte kPreviousMask = 0x02;
protected const byte kClearMask = 0xfc;
protected const byte kRegisteredMask = 0x80;
protected const byte kNotRegisteredMask = 0x7f;
protected const byte kNotPreviousMask = 0xfd;
protected const int kCurToPrevLeftShift = 1;

The state of all keys is stored in the m_keyStates array.

protected const int kNumKeys = 256;
protected byte[] m_keyStates = new byte[kNumKeys];

The GXInput class provides one constructor with no parameters. This constructor creates an instance of the MessageWindow, overrides the hardware buttons, and clears the states of all keys.

public GXInput()
{
    m_msgWindow = new InputMessageWindow();

    for (int i = (int)HardwareKeys.kFirstHardwareKey;
      i < (int)HardwareKeys.kLastHardwareKey; i++)
    {
        UnregisterFunc1(MOD_WIN, i);
        RegisterHotKey(m_msgWindow.Hwnd, i, MOD_WIN, (uint)i);
    }

    for (int i = 0; i < kNumKeys; i++)
    {
        m_keyStates[i] = 0x00;
    }
}

The Update method iterates through each registered key and updates its state by copying the current state to the previous state and then storing the current state. Because this information is stored in a single byte, shifting and masking is used to pack and check the states.

public void Update()
{
    for (int i = 0; i < kNumKeys; i++)
    {
        if ((m_keyStates[i] & kRegisteredMask) != 0)
        {
            m_keyStates[i] = (byte)((m_keyStates[i] & kClearMask) |
              ((m_keyStates[i] << kCurToPrevLeftShift) &
              kPreviousMask));
            if ((GetAsyncKeyState(i) & 0x8000) != 0)
            {
                m_keyStates[i] |= kCurrentMask;
            }
        }
    }
}

GXInput provides several methods for registering and deregistering keys. If a key is registered with the GXInput class then its state is updated, otherwise it is ignored by the Update method. Registration is maintained by a single bit in the state bitfield.

public void RegisterAllHardwareKeys()
{
    for (int i = (int)HardwareKeys.kFirstHardwareKey;
      i <= (int)HardwareKeys.kLastHardwareKey; i++)
    {
        RegisterKey((byte)i);
    }

    RegisterKey((byte)Keys.Up);
    RegisterKey((byte)Keys.Down);
    RegisterKey((byte)Keys.Left);
    RegisterKey((byte)Keys.Right);
}

public void RegisterAllKeys()
{
    for (int i = 0; i < kNumKeys; i++)
    {
        RegisterKey((byte)i);
    }
}

public void UnregisterAllHardwareKeys()
{
    for (int i = (int)HardwareKeys.kFirstHardwareKey;
      i <= (int)HardwareKeys.kLastHardwareKey; i++)
    {
        UnregisterKey((byte)i);
    }

    UnregisterKey((byte)Keys.Up);
    UnregisterKey((byte)Keys.Down);
    UnregisterKey((byte)Keys.Left);
    UnregisterKey((byte)Keys.Right);
}

public void UnegisterAllKeys()
{
    for (int i = 0; i < kNumKeys; i++)
    {
        UnregisterKey((byte)i);
    }
}

public void RegisterKey(byte vKey)
{
    m_keyStates[vKey] |= kRegisteredMask;
}

public void UnregisterKey(byte vKey)
{
    m_keyStates[vKey] &= kNotRegisteredMask;
}

The KeyJustPressed method determines if a key was pressed in the most recent Update call that was not pressed when the previous call to the Update method was made.

public bool KeyJustPressed(byte vKey)
{
    if ((m_keyStates[vKey] & kCurrentMask) != 0 &&
      (m_keyStates[vKey] & kPreviousMask) == 0)
        return true;

    return false;
}

The KeyJustReleased method determines if a key was released in the most recent Update call that was pressed when the previous call to the Update method was made.

public bool KeyJustReleased(byte vKey)
{
    if ((m_keyStates[vKey] & kCurrentMask) == 0 &&
      (m_keyStates[vKey] & kPreviousMask) != 0)
        return true;

    return false;
}

The KeyPressed method determines if a specific key is pressed, regardless of its previous state.

public bool KeyPressed(byte vKey)
{
    if ((m_keyStates[vKey] & kCurrentMask) != 0)
        return true;

    return false;
}

The KeyReleased method determines if a specific key is released, regardless of its previous state.

public bool KeyReleased(byte vKey)
{
    if ((m_keyStates[vKey] & kCurrentMask) == 0)
        return true;

    return false;
}

Test Application

The test application tests the functionality of the graphics and input libraries. The test consists of creating a particle emitter that emits particles at a specified emission rate. Each particle is a point on the screen with a random velocity that is affected by gravity and wind. The emitter itself is represented by a line list. The gravity and wind can be manipulated by the user through the directional pad and the current values are displayed on the screen. In summary, the following functionality is tested:

  • Points: The particles emitted from the particle emitter are single points that test the DrawPoint method of GXGraphics
  • Lines: The emitter is a pyramid drawn with the DrawLines method of GXGraphics
  • Text: The current gravity and wind values are displayed on the screen using the GXFont class and the DrawText method of GXGraphics
  • Input: The GXInput class is tested by allowing the user to modify the gravity and wind values via the directional pad

ParticleTest

The ParticleTest class defined in ParticleTest.cs contains all of the code concerned with creating and updating the particle emitter test. The majority of the functionality of the particle test itself is encapsulated in the Emitter class which contains, updates, and draws the particles.

The Emitter class also contains a definition for a Particle class which consists of a position and velocity, as well as the functionality required to update and draw a single particle.

protected class Emitter
{
    protected class Particle
    {
        public float m_velX;
        public float m_velY;
        public float m_curX;
        public float m_curY;

        public bool m_expired = false;

The Particle class's Update method modifies the velocity based on the gravity and wind effects and the amount of time elapsed. These modifiers are specified in pixels per second and define the "pull" of the force. For example, if the wind value is -5 pixels/second and the x velocity is 8 pixels/second then for every second that passes, the x velocity is affected by -5 pixels resulting in a velocity of 3 pixels/second after 1 second, -2 pixels/second after 2 seconds, and -7 pixels per second after 3 seconds.

        public void Update(float gravity, float wind, float delta_ms,
          GXGraphics gx)
        {
            if (m_expired)
                return;

            m_velY += gravity * delta_ms / 1000.0f;
            m_velX += wind * delta_ms / 1000.0f;
            m_curX += (delta_ms * m_velX / 1000.0f);
            m_curY += (delta_ms * m_velY / 1000.0f);

            int curX = (int)m_curX;
            if ((curX >= gx.ScreenWidth || curX < 0 ||
              (int)m_curY < 0))
                m_expired = true;
        }

The Draw method draws the particle as a point. The loc parameter is provided so that the location can be assigned to a Point without having to allocate a new point for every Particle.

        public void Draw(GXGraphics gx, Color color, ref Point loc)
        {
            if (m_expired)
                return;

            loc.X = (int)m_curX;
            loc.Y = (int)m_curY;
            gx.DrawPoint(loc , color);
        }
    }

The Emitter class must maintain an array of particles and their color. The size of the array is not known until the rate is specified in the constructor.

    protected Particle[] m_particles = null;
    protected Color m_color;

The member m_location is a cached Point used for drawing Particles so that it does not have to be reallocated for every particle during the draw loop.

    protected Point m_location = new Point(0,0);

The Emitter maintains an instance of Random in order to create new particles every frame.

    protected Random m_rnd = null;

The Emitter class provides properties for getting and setting the gravity and wind affects.

    public float Gravity
    {
        get { return m_gravity; }
        set { if (value >= 0.0f) m_gravity = value; }
    }
    protected float m_gravity;

    public float Wind
    {
        get { return m_wind; }
        set { m_wind = value; }
    }
    protected float m_wind;

The rate that the emitter emits particles is set in the constructor but can be accessed through the ParticlesPerSecond property.

    public int ParticlesPerSecond
      { get { return (int)m_particlesPerSecond; } }
    protected float m_particlesPerSecond;

The PeakUse property returns the highest number of particles used during execution of the test.

    public int PeakUse { get { return m_peakUse; } }
    protected int m_peakUse = 0;

The maximum number of particles is determined in the constructor and based on the emission rate.

    protected int m_maxParticles;

The constructor for the Emitter class is responsible for initializing the wind and gravity effects, initializing the randomizer, setting the location, allocating memory for the particles, storing the particle color, and initializing the particles. For efficiency, all particle memory is allocated in the constructor and never again. The only way a new particle can be allocated is if an existing particle expires. Particles do not have a lifetime parameter in this sample, but rather expire by passing off of the right, left, or bottom of the screen. Once this has occurred, the particle's m_expired member is set to notify the emitter that it is ready for reallocation.

    public Emitter(Point loc, float particlesPerSecond,
      Color particleColor)
    {
        m_wind = 0.0f;
        m_gravity = 25.0f;

        m_rnd = new Random();

        m_location = loc;

        m_maxParticles = (int)(particlesPerSecond * 10);
        m_particlesPerSecond = particlesPerSecond;
        m_particles = new Particle[m_maxParticles];

        m_color = particleColor;

        for (int i = 0; i < m_maxParticles; i++)
        {
            m_particles[i] = new Particle();
            m_particles[i].m_expired = true;
        }
    }

The Emitter's Update method determines the number of new particles to be created for this frame based on the emission rate (particles/second) and the amount of time that has passed since the previous update. The method then loops through the array of particles and reallocates as many expired particles as it can, up to the number of particles calculated for this update. Once all of the new particles are allocated, the method loops through all particles and updates them.

    public void Update(float delta_ms, GXGraphics gx)
    {
        int numParticles = (int)(m_particlesPerSecond *
          delta_ms / 1000.0f);

        int curParticle = 0;

        for (int i = 0; i < m_maxParticles; i++)
        {
            if (m_particles[i].m_expired)
            {
                m_particles[i].m_expired = false;

                m_particles[i].m_curX = (float)m_location.X;
                m_particles[i].m_curY = (float)m_location.Y;

                m_particles[i].m_velX = m_rnd.Next(1,30);
                if (curParticle % 2 == 0)
                    m_particles[i].m_velX = - m_particles[i].m_velX;

                m_particles[i].m_velY = -m_rnd.Next(80,100);

                curParticle++;
                if (curParticle >= numParticles)
                    break;
            }
        }

        int curUse = 0;
        for (int i = 0; i < m_maxParticles; i++)
        {
            if (!m_particles[i].m_expired)
                curUse++;

            m_particles[i].Update(m_gravity, m_wind, delta_ms, gx);
        }

        if (curUse > m_peakUse)
            m_peakUse = curUse;
    }

The Draw method of the Emitter class iterates through every particle and draws it to the screen.

    public void Draw(GXGraphics gx)
    {
        Point loc = new Point(0,0);
        for (int i = 0; i < m_maxParticles; i++)
        {
            m_particles[i].Draw(gx, m_color, ref loc);
        }
    }
}

The ParticleTest class maintains several members needed to run the test. Input is monitored through an instance of GXInput, and the emitter is drawn and created through a line list, and Emitter class instance, respectively. The m_done member is used to determine when the owner of the test has requested that it quit.

protected GXInput m_gi = null;
protected Emitter m_emitter = null;
protected Point[] m_linePoints = null;
protected Color[] m_lineColor = null;
protected ushort[] m_lineIndices = null;
protected bool m_done = false;

The ParticleTest constructor creates and initializes the GXInput instance, creates an Emitter positioned at the bottom center of the screen, and initializes the line list used to draw the emitter.

public ParticleTest(GXGraphics gx)
{
    m_gi = new GXInput();
    m_gi.RegisterAllHardwareKeys();

    Random rnd = new Random();

    Point loc = new Point(gx.ScreenWidth / 2, gx.ScreenHeight - 1);
    m_emitter = new Emitter(loc, 150, Color.Yellow);

    m_linePoints = new Point[5];
    m_lineIndices = new ushort[16];
    m_lineColor = new Color[1];

    m_linePoints[0].X = gx.ScreenWidth / 2 - 15;
    m_linePoints[0].Y = gx.ScreenHeight - 20;
    m_linePoints[1].X = gx.ScreenWidth / 2 - 10;
    m_linePoints[1].Y = gx.ScreenHeight - 25;
    m_linePoints[2].X = gx.ScreenWidth / 2 + 15;
    m_linePoints[2].Y = gx.ScreenHeight - 25;
    m_linePoints[3].X = gx.ScreenWidth / 2 + 10;
    m_linePoints[3].Y = gx.ScreenHeight - 20;
    m_linePoints[4].X = gx.ScreenWidth / 2;
    m_linePoints[4].Y = gx.ScreenHeight - 1;
    m_lineIndices[0] = 0;
    m_lineIndices[1] = 1;
    m_lineIndices[2] = 1;
    m_lineIndices[3] = 2;
    m_lineIndices[4] = 2;
    m_lineIndices[5] = 3;
    m_lineIndices[6] = 3;
    m_lineIndices[7] = 0;
    m_lineIndices[8] = 0;
    m_lineIndices[9] = 4;
    m_lineIndices[10] = 1;
    m_lineIndices[11] = 4;
    m_lineIndices[12] = 2;
    m_lineIndices[13] = 4;
    m_lineIndices[14] = 3;
    m_lineIndices[15] = 4;
    m_lineColor[0] = Color.Fuchsia;
}

The Quit method sets m_done to true so that the test will exit its loop and quit.

public void Quit()
{
    m_done = true;
}

The Draw method starts and runs the test. This method loads the font required for displaying text on the screen and then loops until m_done is set to true. The loop clears the screen, updates and draws the particle emitter, and displays text relating the current wind and gravity to the user. When the test is completed, a MessageBox displays some statistics about the frame rate and particle use.

public void Draw(GXGraphics gx)
{
    Assembly asm = Assembly.GetExecutingAssembly();
    Stream strm = asm.GetManifestResourceStream(asm.GetName().Name +
      ".font.fnt");
    GXFont font = new GXFont(strm);

    StopWatch sw = new StopWatch();
    sw.Clear();

    UInt64 numLoops = 0;

    Rectangle scrnRegion = new Rectangle(0, 0, gx.ScreenWidth,
      gx.ScreenHeight);

    sw.Start();
    Int64 prevStart = sw.StartTime;

    while (!m_done)
    {
        sw.Start();

        gx.BeginDraw();

        float delta_ms = (float)((1000 * (sw.StartTime - prevStart)) /
          sw.Freq);

        gx.DrawFilledRect(scrnRegion, Color.Black);

        gx.DrawLines(m_linePoints, m_lineIndices, m_lineColor);

        m_emitter.Update(delta_ms, gx);
        m_emitter.Draw(gx);

        prevStart = sw.StartTime;

        gx.DrawText(string.Format(
          "Gravity: {0} pixels/second\rWind: {1} pixels/second",
          m_emitter.Gravity, m_emitter.Wind), Color.Blue, 5, 5, font);

        gx.EndDraw();

        sw.Stop();

        Application.DoEvents();

        m_gi.Update();

        if (m_gi.KeyPressed((byte)Keys.Down))
        {
            m_emitter.Gravity += 0.5f;
        }
        else if (m_gi.KeyPressed((byte)Keys.Up))
        {
            m_emitter.Gravity -= 0.5f;
        }

        if (m_gi.KeyPressed((byte)Keys.Right))
        {
            m_emitter.Wind += 0.5f;
        }
        else if (m_gi.KeyPressed((byte)Keys.Left))
        {
            m_emitter.Wind -= 0.5f;
        }

        numLoops++;
    }

    if (sw.MeanTime_ms > 0)
        MessageBox.Show(String.Format(
          "{0} Particles / second, {1} Peak, {2} Loops: {3} fps",
          m_emitter.ParticlesPerSecond, m_emitter.PeakUse, numLoops,
          1000 / sw.MeanTime_ms), "Results", MessageBoxButtons.OK,
          MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
    else
        MessageBox.Show(String.Format(
          "{0} Particles / second, {1} Peak, {2} Loops: Inf. fps",
          m_emitter.ParticlesPerSecond, m_emitter.PeakUse, numLoops),
          "Results", MessageBoxButtons.OK, MessageBoxIcon.None,
          MessageBoxDefaultButton.Button1);
}

The Form

The Form for the test application is very simple since the majority of the code is encapsulated in the ParticleTest class.

The OnPaint and OnPaintBackground methods are overridden in the Form so as not to interfere with the drawing being done by the test.

protected override void OnPaint(PaintEventArgs e){}
protected override void OnPaintBackground(PaintEventArgs e){}

The form will quit out of the test when the OnMouseDown event is triggered.

protected override void OnMouseDown(MouseEventArgs e)
{
    if (m_test != null)
        m_test.Quit();

    base.OnMouseDown (e);
}

Of course, an instance of the test is required.

protected ParticleTest m_test = null;

The From's load function contains the code that initializes and launches the test. Once the Form is setup properly, an instance of GXGraphics is created and passed to the test instance's constructor and draw function.

private void Form1_Load(object sender, System.EventArgs e)
{
    this.ControlBox = false;
    this.Menu = null;
    this.WindowState = FormWindowState.Maximized;
    this.FormBorderStyle = FormBorderStyle.None;

    GXGraphics gx = null;

    try
    {
        gx = new GXGraphics(this,
          GXGraphics.DisplayBufferModes.kDoubleBuffer);

        if (gx.Inititialized == false)
        {
            MessageBox.Show("Failed to create GXGraphics object",
              "ERROR", MessageBoxButtons.OK,
              MessageBoxIcon.Exclamation,
              MessageBoxDefaultButton.Button1);

            return;
        }

        m_test = new ParticleTest(gx);
        m_test.Draw(gx);
    }
    finally
    {
        if (gx != null)
            gx.Dispose();

        this.Close();
    }
}

Conclusion

The sample corresponding to this article provides a nearly complete game engine that can provide a viable graphics solution in managed code. Subsequent articles will describe how to implement sound and create a single level of a game as a complete test of the engine.

Show:
© 2016 Microsoft