Home | Site Map
Sunlight

Sprite Classes

Introduction

This chapter will introduce the DirectX.NET sprite classes, from the Sunlight.DirectX.Graphics namespace: the Sprite, SpriteManager and TextSprite classes.

This chapter introduces 'pinning' and __try_cast.

Classes

Sprite

This class represents a rectangular textured polygon. It has position and size, and a position within its source texture from which it takes the image. It allows for the use of multiple frames in a rectangular block in the texture. It enforces a 1:1 mapping between texels and pixels. It also stores a reference to a SpriteManager object, which we'll discuss in a moment.

__gc public class Sprite : public Component
{
public:
    Sprite(SpriteManager *pManagerObject,
        System::Drawing::Rectangle rDimensions,
        Texture *pTextureObject,
        System::Drawing::Point ptSourcePosition);
    Sprite(SpriteManager *pManagerObject,
        System::Drawing::Rectangle rDimensions,
        Texture *pTextureObject,
        System::Drawing::Point ptSourcePosition,
        int
nFramesAcross);

    SpriteManager *ManagerObject;
    Texture *TextureObject;
    int Left;
    int Top;
    int Width;
    int Height;
    int SourceLeft;
    int SourceTop;
    int SourceWidth;
    int SourceHeight;
    int FramesAcross;
    int CurrentFrame;
    int ShadowDepth;
    __property Drawing::Rectangle get_Bounds();

    void Draw();

    void FillVertexArray(SpriteManager::SpriteVertex __nogc *vertices);
};

Public Members

Sprite offers a collection of display-related fields. Left, Top, Width and Height describe the display rectangle; SourceLeft, SourceTop, SourceWidth and SourceHeight describe the source rectangle (on the texture). FramesAcross specifies the number of frames horizontally in the sprite frame texture. CurrentFrame gives the index of the frame to be drawn. ShadowDepth signals whether the object should display a small offset shadow, and how far this should be offset.

Bounds returns a Rectangle containing a bounding rectangle for the sprite.

Draw calls the SpriteManager to draw the sprite.

Implementation Notes

Sprite doesn't do much at all; its most interesting point is the FillVertexArray function, which we've seen before:

// Fill an array of SpriteVertexes with the vertex co-ordinates and
// texture co-ordinates.
void Sprite::FillVertexArray(SpriteManager::SpriteVertex __nogc *vertices)
{
    int xReal = SourceLeft + (CurrentFrame % FramesAcross) * SourceWidth;
    int yReal = SourceTop + (CurrentFrame / FramesAcross) * SourceHeight;

    vertices[0].x = vertices[2].x = (float)Left - 0.5f;
    vertices[1].x = vertices[3].x = (float)(Left + Width) - 0.5f;
    vertices[0].y = vertices[1].y = (float)Top - 0.5f;
    vertices[2].y = vertices[3].y = (float)(Top + Height) - 0.5f;

    // z and rhw co-ordinates are fixed at 0.5 and 1.0 respectively
    vertices[0].z = vertices[1].z =
        vertices[2].z = vertices[3].z = 0.5f;

    vertices[0].rhw = vertices[1].rhw =
        vertices[2].rhw = vertices[3].rhw = 1.0f;

    // White colour to avoid clashing with the texture
    vertices[0].dwColor = vertices[1].dwColor =
        vertices[2].dwColor = vertices[3].dwColor = 0xFFFFFFFF;

    // Texture co-ordinates
    vertices[0].tu = vertices[2].tu = (float)xReal /
        (float)TextureObject->Width;
    vertices[1].tu = vertices[3].tu = (float)(xReal + SourceWidth) /
        (float)TextureObject->Width;

    vertices[0].tv = vertices[1].tv = (float)yReal /
        (float)TextureObject->Height;
    vertices[2].tv = vertices[3].tv = (float)(yReal + SourceHeight) /
        (float)TextureObject->Height;
}

This fills in an unmanaged array of vertices with the transformed co-ordinates of the sprite.

Sprite is useful as a container for the fields listed. Since it doesn't do a great deal, you can easily use it as a lightweight base class for your own objects.

SpriteManager

This class handles the drawing of sprites. It wraps an IDirect3DVertexBuffer8 object.

__gc public class SpriteManager : public Component
{
public:
    // This needs to be public so Sprite can get to it...
    __nogc struct SpriteVertex
    {
        float       x, y, z, rhw;   // The transformed position for the vertex.
        unsigned    dwColor;        // The vertex colour.
        float       tu, tv;         // Texture co-ordinates.
    };

protected:
    static const int VertexFormat = (int)(D3DFVF_XYZRHW|D3DFVF_DIFFUSE|D3DFVF_TEX1);

    __gc class ObjectListElement
    {
    public:
        ObjectListElement(Sprite *pSprite);
        Texture      *TextureObject;
        SpriteVertex Vertices __nogc [4];
        bool         Shadowed;
    };

    IDirect3DVertexBuffer8  __nogc  *m_vb;
    int                             m_nVBSize;
    Collections::ArrayList          *m_plistObjects;
    Device                          *m_pDevice;

    // Called when the parent device is released or reset.
    void OnDeviceRelease(Object *sender, EventArgs *e);
    // Destroys the Direct3D vertex buffer associated with this object.
    void Destroy();

public:
    SpriteManager();
    ~SpriteManager();

    __property Device *get_DeviceObject();
    __property void set_DeviceObject(Device *);

    void Draw(Sprite *sprite);
    void FinishDraw();
    void AbortDraw();
};

As you can see, SpriteManager is somewhat more interesting.

Implementation Notes

SpriteManager uses a vertex buffer. When the device is destroyed, or changed, we need to be able to release this vertex buffer (since it is a DirectX Graphics object). SpriteManager deals with this by making DeviceObject a property, and hooking the OnLost event when the device is set:

void SpriteManager::set_DeviceObject(Device *pDevice)
{
    if (pDevice == m_pDevice)
        return;
   
    if (m_vb != NULL)
        Destroy();

    if (m_pDevice != NULL)
        // Unhook from the current device
        __unhook(&Device::OnLost, m_pDevice,
            &SpriteManager::OnDeviceRelease, this);

    m_pDevice = pDevice;
   
    // Hook the device's OnLost event so we can release our texture
    // when it is reset
    __hook(&Device::OnLost, m_pDevice,
        &SpriteManager::OnDeviceRelease, this);
}

This technique allows us to use the vertex buffer transparently, without the application needing to release it before mode-switching, resetting the device or shutting down.

SpriteManager handles drawing in an odd way, in order to allow you to set the order of sprites independently of the actual scene drawing code. When a sprite is 'drawn', it is parsed into a texture and co-ordinate set, and stored in a list:

// Draw a Sprite object with this SpriteManager.
void SpriteManager::Draw(Sprite *sprite)
{
    // Draw shadow first...
    if (sprite->ShadowDepth > 0)
        m_plistObjects->Add(new ObjectListElement(sprite, sprite->ShadowDepth));
    m_plistObjects->Add(new ObjectListElement(sprite));
}

ObjectListElement takes a Sprite, and does the parsing:

SpriteManager::ObjectListElement::ObjectListElement(Sprite *pSprite,
    int
nShadowDepth)
{
    TextureObject = pSprite->TextureObject;

    SpriteVertex __pin *pVertices = &Vertices[0];

    pSprite->FillVertexArray(pVertices);

    if (nShadowDepth > 0)
    {
        for (int i = 0; i < 4; i++)
        {
            pVertices[i].x += nShadowDepth;
            pVertices[i].y += nShadowDepth;
            pVertices[i].dwColor = 0x80808080;
        }
        Shadowed = true;
    }
    else
        Shadowed = false;
}

This dissociation allows you to draw one Sprite many times (at different positions) in the same frame. Note that in order to get an unmanaged pointer to the vertex array, we have had to pin the Vertices array. This forces the object to stay in the same place in memory until pVertices goes out of scope.

The shadow effect is displayed here. The sprite is shifted down and right by the depth specified.

When the application wants to draw all the sprites, it calls FinishDraw. FinishDraw creates the vertex buffer, if necessary:

// Complete the drawing of all the Sprite objects.
void SpriteManager::FinishDraw()
{
    if (m_pDevice == NULL)
        throw new ArgumentNullException(S"DeviceObject");
   
    if (!m_pDevice->IsCreated)
        return;

    IDirect3DDevice8    *dev = m_pDevice->Direct3DDevice;

    if ((m_nVBSize < m_plistObjects->Count) || (m_vb == NULL))
    {
        if (m_vb != NULL)
        {
            m_vb->Release();
            m_vb = NULL;
        }

        // Vertex buffer is too small or not initialized

        IDirect3DVertexBuffer8 __nogc *pVB;

        // 10 sprites margin for next time
        HRESULT h = dev->CreateVertexBuffer((m_plistObjects->Count +
                LIST_GRANULARITY) * 4 * sizeof(SpriteVertex),
            D3DUSAGE_WRITEONLY, VertexFormat,
            D3DPOOL_MANAGED, &pVB);
        if (FAILED(h))
            throw new Sunlight::DirectX::DirectXException(
                S"IDirect3DDevice8::CreateVertexBuffer", h);

        m_vb = pVB;
       
        m_nVBSize = m_plistObjects->Count + LIST_GRANULARITY;
    }

Next, we copy the vertices from the object list to the vertex buffer. Note the use of __try_cast, which throws an exception if the list elements are not of the type specified. This is not strictly necessary, but is a useful demonstration.

    if (m_nVBSize == 0)
        return;

    SpriteVertex *pVertices;

    // Lock the vertex buffer and copy all the vertices into it
    m_vb->Lock(0, 0, (BYTE **)&pVertices, D3DLOCK_DISCARD);
    for (int i = 0; i < m_plistObjects->Count; i++)
    {
        ObjectListElement *s =
            __try_cast<ObjectListElement *>(m_plistObjects->Item[i]);
        SpriteVertex __pin *pSrcVertices = &s->Vertices[0];
        memcpy(pVertices, pSrcVertices, 4 * sizeof(SpriteVertex));
        pVertices += 4;
    }
    m_vb->Unlock();

Finally, we draw the objects.

    // Set the device to get data from this vertex buffer
    dev->SetStreamSource(0, m_vb, sizeof(SpriteVertex));
    dev->SetVertexShader(VertexFormat);

    // For each sprite, set the texture then draw the vertices
    for (int i = 0; i < m_plistObjects->Count; i++)
    {
        ObjectListElement *s =
            static_cast
<ObjectListElement *>(m_plistObjects->Item[i]);
        dev->SetTexture(0, s->TextureObject->Direct3DTexture);
        if (s->Shadowed)
        {
            // Get the alpha information from the texture multiplied by
            // the diffuse colour.
            dev->SetTextureStageState(0, D3DTSS_ALPHAOP,   D3DTOP_MODULATE);
            dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
            dev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);

            // Get the colour information from the inverted alpha.
            dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
            dev->SetTextureStageState(0, D3DTSS_COLORARG1,
                D3DTA_ALPHAREPLICATE | D3DTA_COMPLEMENT | D3DTA_TEXTURE);
        }
        else
        {
            // Get the alpha information solely from the texture.
            dev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
            dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);

            // Get the colour information solely from the texture.
            dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
            dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
        }
        dev->DrawPrimitive(D3DPT_TRIANGLESTRIP, i * 4, 2);
    }

    // Clear the drawing list for next time
    m_plistObjects->Clear();
}

A word is necessary here about the shadowing. The shadow alpha is given by the texture alpha. In order to achieve translucent shadows, we modulate (multiply) this by the colour of the object. The diffuse colour was set to half, earlier, so this results in a 50% transparent shadow. The shadow colour needs to be black when the object is fully opaque (the alpha is full white), and so we take the inverted alpha component from the texture as the colour.

That's it. We now have two more managed objects that handle 2-D sprites - with shadows - with the minimum of fuss.

TextSprite

TextSprite extends Sprite to represent a string of symbols (usually text) from a graphical symbol set with a fixed cell size.

__gc public class TextSprite : public Sprite
{
public:
    TextSprite(SpriteManager *manager, System::Drawing::Rectangle dimensions, 
        Texture *texture, System::Drawing::Point sourcePosition, int framesAcross,
        String *characterSet);

    // Sets/returns the string of characters to draw.
    String  *Text;
    // Sets/returns the set of characters in the texture.
    String  *CharacterSet;
    // Sets/returns the character used when a character is not found in the set.
    wchar_t DefaultCharacter;

    // Gets a rectangle enclosing the object.
    __property Drawing::Rectangle get_Bounds();
    // Draw this sprite using its SpriteManager.
    void Draw();
};

Public Members

The TextSprite class contains just a few additional members to represent a string of symbols. The Text field represents the string of characters. CharacterSet describes the symbols present in the texture. DefaultCharacter defines the character used if a character in Text is not found in CharacterSet; it defaults to the first character in CharacterSet.

Implementation Notes

TextSprite is not terribly notable, since Sprite does most of the work. The meat of TextSprite is in the overloaded Draw method:

// Draw this sprite using its SpriteManager.
void TextSprite::Draw()
{
    int OldLeft = Left;

    for (int i = 0; i < Text->Length; i++)
    {
        wchar_t c = Text->Chars[i];
        int index = CharacterSet->IndexOf(c);

        if (index == -1)
            index = CharacterSet->IndexOf(DefaultCharacter);

        if (index != -1)
        {
            Left = OldLeft + i * Width;
            CurrentFrame = index;
            Sprite::Draw();
        }
    }
    Left = OldLeft;
}

For each character, TextSprite first performs the relatively simple task of locating the index of it in CharacterSet. This index gives the frame index into the texture. TextSprite then cheats to draw the character by moving the sprite along to the appropriate position, setting the frame and then making Sprite.Draw perform the actual drawing. At the end of the method, Left is reset to its original value.

In the next chapter, we'll move on to DirectInput.