
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.
__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);
};
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.
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.__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.
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);
}
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 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(); };
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.
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.