ATTENTION READERS! Lucky's VB Gaming Site is no longer active. For updated game programming information and tutorials, please visit The Game Programming Wiki!
Using DirectGraphics for 2D Graphics
If you're like me, you had a multi-stage reaction upon looking at the new DX8
SDK. First was confusion, then realization, and then anger. It seems those goons
at Microsoft have forgotten about us 2D game programmers, leaving out
DirectDraw, and with it, any easy way to do 2D graphics. But have no fear! With
a little extra work, you can simulate a 2D graphics engine using polygon
rectangles that's nearly as easy as DDraw was, and with cool alpha-blending
features to boot! Let's get started...
My graphics engine is basically based off of the Donuts sample from the SDK,
but I've added some of my own structure to make things easier.
I
won't detail the initialization of DX, because a.) It is very self-explanatory
from the sample project, and b.) This has been explained already in other
tutorials. One important thing to know is that we are using a Flexible Vertex
Format for the VertexShader, which is declared thusly:
'Flexible vertex
format the describes transformed and lit vertices.
Const FVF = D3DFVF_XYZRHW Or D3DFVF_TEX1 Or D3DFVF_DIFFUSE Or D3DFVF_SPECULAR
Because all of the polygons are transformed and lit, Direct3D will perform no
lighting calculations, and no coordinate transformation. You don't really have
to understand this, just know that this simplifies things. :)
Since every 2D game is going to be based on animated sprites, I base the
structure around this. First, a few UDTs to start things off:
Public Type Frame
SrcX As Integer
SrcY As Integer
Width As Integer
Height As Integer
End Type
Public Type Animation
Frames() As Frame
CurFrame As Integer
Mode As Integer
Speed As Single
NextTime As Single
End Type
'This structure
describes a transformed and lit vertex.
Public Type TLVERTEX
X As Single
Y As Single
Z As Single
RHW As Single
Color As Long
Specular As Long
TU As Single
TV As Single
End Type
Public Type Texture
InUse As Boolean
Dimension As Long
D3DTexture As Direct3DTexture8
End Type
Public Type Sprite
Alive As Boolean
TextureIndex As Long
SpriteNum As Long
Verts(3) As TLVERTEX
Vel As D3DVECTOR2
Pos As D3DVECTOR2
Anims() As Animation
CurAnim As Long
End Type
Public Sprites() As Sprite
Public Textures() As
Texture
So basically, we have a dynamic array of sprites, with properties such as
Vertices, animations, and a TextureIndex. To help you understand how this all
works, I'll go through the life of a sprite:
1.) Initializing the sprite
In my engine, all that is required to add a new sprite is to call the
InitSprite subroutine. It accepts two arguments - the path of the bitmap with
all the animation frames and an array of animation information. The engine
allows for multiple separate animations for a single sprite. So every sprite has
an array called Anims() with an array called Frames() with the info for each
individual frame in an animation. So here's an example of initializing a simple
1 animation, 1 frame sprite:
Dim TempAnim(0) As
Animation
With TempAnim(0)
.CurFrame = 1 'Starting
frame
.Mode = 0 'You
can use this to have different types of animating (i.e. looping, once-through,
etc.)
.Speed = 0.3 'Speed
of the anim. - how often frames update
ReDim .Frames(1) 'There
will be one frame in the animation
.Frames(1).Height = 255 'Height
of this frame
.Frames(1).Width = 255 'Width
of this frame
.Frames(1).SrcX = 0 'X
coordinate of this frame in the texture
.Frames(1).SrcY = 0 'Y
coordinate of this frame in the texture
End With
Spr1Index =
InitSprite(App.Path & "\temp.bmp", TempAnim) 'Returns
the index in Sprites() of the new sprite
You can set up your own routine for setting up the animation array to supply
InitSprite. (Calculate SrcX and SrcY from the frame height and frame width, or
reading in the anim info. from a file...)
2.) Load the texture
In DDraw, you used surfaces to hold graphics. In DirectGraphics, you'll use
textures, which are similar to surfaces, but take a little more work. To make it
easier, I store all textures in an array called Textures(). Every sprite has a
property called TextureIndex, which points to an entry in Textures() where it's
texture is stored.
So InitSprite starts out:
Function InitSprite(Bitmap
As String, RefAnims() As Animation) As Long
Dim i As Integer, j As
Integer, OpenSpot As Boolean, SpriteNum As Integer
For i = 0 To UBound(Sprites)
If Sprites(i).Alive = False Then
With Sprites(i)
.Alive = True
.TextureIndex
= AllocateTexture(Bitmap)
End With
SpriteNum = i
OpenSpot = True
End If
Next i
If OpenSpot = False Then
ReDim Preserve Sprites(UBound(Sprites) + 1)
With Sprites(i)
.Alive = True
.TextureIndex =
AllocateTexture(Bitmap)
End With
SpriteNum = UBound(Sprites)
End If
Here, the function looks for an open entry in Sprites() to put the new
sprite. If it finds one, we use that entry, if not, it adds an entry onto the
Sprites() array. To load it's texture, we set it's TextureIndex =
AllocateTexture(Bitmap), supplying the bitmap file path. The AllocateTexture
function puts a new texture in the Textures() array, and returns the index of
the new texture:
Public Function
AllocateTexture(Bitmap As String) As Long
'Finds a spot for a
texture in Textures() and puts it there,
'then returns the index in Textures() where the tex. was put
Dim i As Integer, OpenSpot
As Boolean, TextureNum As Integer
Dim SurfDesc As D3DSURFACE_DESC
For i = 0 To
UBound(Textures)
If Textures(i).InUse = False Then
TextureNum = i
Textures(i).InUse = True
OpenSpot = True
End If
Next i
If OpenSpot = False Then
ReDim Preserve Textures(UBound(Textures) + 1)
Textures(UBound(Textures)).InUse = True
TextureNum = UBound(Textures)
End If
'Get the width and height
of the texture
Textures(TextureNum).D3DTexture.GetLevelDesc
0, SurfDesc
Textures(TextureNum).Dimension = SurfDesc.Width - 1
AllocateTexture =
TextureNum
End Function
The &HFF00000 supplied to CreateTextureFromFileEx is the color key that
will be transparent, and you can change this to whatever you want. The
Textures() array is composed of Texture types:
Public Type Texture
InUse As Boolean
Dimension As Long
D3DTexture As Direct3DTexture8
End Type
AllocateTexture sets InUse to true, showing that this array entry in occupied
by a texture in use. D3DTexture is the actual Direct3DTexture8 that holds your
bitmap. Dimension is set to the length of a side of your texture. This brings up
a point - your bitmap must be square, and the length/width must be a power of 2.
Also, you don't want to use a texture bigger than 256x256, as not all hardware
supports it.
So now our brand new sprite has a Textures() array entry with the picture
from the bitmap we supplied.
3.) Load in animation info
Next, we load in the animation information for the new sprite:
'Load in animation info
ReDim
Sprites(SpriteNum).Anims(UBound(RefAnims))
For i = 0 To
UBound(RefAnims)
With Sprites(SpriteNum).Anims(i)
.CurFrame = 1
.Mode = RefAnims(i).Mode
.Speed = RefAnims(i).Speed
ReDim .Frames(UBound(RefAnims(i).Frames))
For j = 1 To UBound(.Frames)
.Frames(j).SrcX
= RefAnims(i).Frames(j).SrcX
.Frames(j).SrcY
= RefAnims(i).Frames(j).SrcY
.Frames(j).Width
= RefAnims(i).Frames(j).Width
.Frames(j).Height
= RefAnims(i).Frames(j).Height
Next j
End With
Next i
4.) Assign this sprite's vertices
Now we have to initialize this sprite's vertices:
UpdateSpriteGeom SpriteNum
In DirectGraphics we use polygons to make up a rectangle, apply the texture
to that rectangular polygon, and then display this to the screen. (In DDraw, we
just used surfaces) All the polygons that we use are made of two triangles that
create a rectangle. The textures are painted on these two polygons to create the
look of a 2d sprite. The Vertices of the sprite define the points on the
polygons, which define it's width and height. Here is how the vertices are
arranged on all rectangular polygon combinations we'll use:
You don't really have to understand this, because my UpdateSpriteVerts
function assigns the vertices based on a sprite's x and y position, and the
width & height of it's current animation frame, taking care of the messy
stuff for you:
Sub
UpdateSpriteGeom(SpriteNum As Integer)
'Update the vertices for
the Sprite
With Sprites(SpriteNum).Verts(0)
.X = Sprites(SpriteNum).Pos.X: .Y = Sprites(SpriteNum).Pos.Y
+ FrmH(SpriteNum)
.TU = TexCoor(FrmX(SpriteNum), SpriteNum): .TV =
TexCoor(FrmY(SpriteNum) + FrmH(SpriteNum), SpriteNum)
.RHW = 1
.Color = &HFFFFFF
End With
With Sprites(SpriteNum).Verts(1)
.X = Sprites(SpriteNum).Pos.X: .Y = Sprites(SpriteNum).Pos.Y
.TU = TexCoor(FrmX(SpriteNum), SpriteNum): .TV =
TexCoor(FrmY(SpriteNum), SpriteNum)
.RHW = 1
.Color = &HFFFFFF
End With
With Sprites(SpriteNum).Verts(3)
.X = Sprites(SpriteNum).Pos.X + FrmW(SpriteNum): .Y =
Sprites(SpriteNum).Pos.Y
.TU = TexCoor(FrmX(SpriteNum) + FrmW(SpriteNum), SpriteNum):
.TV = TexCoor(FrmY(SpriteNum), SpriteNum)
.RHW = 1
.Color = &HFFFFFF
End With
End Sub
This function uses a few helper functions to assign the vertex coordinates
(you can check these out in the sample project):
FrmW - Supplies the width of the current frame of a sprite.
FrmH - Supplies the height of the current frame of a sprite.
FrmX - Supplies the X position of the current frame of a sprite.
FrmY - Supplies the Y position of the current frame of a sprite.
Here is a summary of the attributes a vertex:
X - The X pixel coordinate on the screen of this vertex.
Y - The Y pixel coordinate on the screen of this vertex.
TU - The X coordinate of this vertex on the texture.
TV - The Y coordinate of this vertex on the texture.
Note: Texture coordinates aren't measured as pixel
coordinates, but are seen as a coordinate between 1 and 0. My TexCoor function
will give you the correct texture coordinate from a pixel coordinate.
RHW - For scaling; I'm not doing scaling in this sample
.Color - The RGB color of this vertex - you can set this to an RGB color and the
vertex will have that color fading out from it.
Now, InitSprite returns the index in Sprites() of the new sprite, and we are
ready to display the sprite.
5.) Displaying the sprite
Now we are ready to display the sprite in the game loop. Remember, you have
to update the sprite's vertices every time it's x or y position changes, or the
frame is incremented, because the sprite will have different source x and y
coordinates on the texture. So a simple loop might look like this:
Public Sub MainLoop()
Running = True
TempLoad 'Loads in our
sprite
Do While Running
GetFPS
HandleAnims
Call RenderScene
DoEvents
Loop
TermDX 'Cleans
up after DirectX
Unload frmMain
End
End Sub
HandleAnims handles updating the frames for all the sprites' animations:
Sub HandleAnims()
Dim i As Integer
For i = 0 To UBound(Sprites)
If Sprites(i).Alive = True Then
With Sprites(i)
If
GetTickCount >= .Anims(.CurAnim).NextTime Then
.Anims(.CurAnim).NextTime = GetTickCount + .Anims(.CurAnim).Speed
.Anims(.CurAnim).CurFrame = .Anims(.CurAnim).CurFrame + 1
If .Anims(.CurAnim).CurFrame > UBound(.Anims(.CurAnim).Frames) Then
.Anims(.CurAnim).CurFrame = 1
End If
UpdateSpriteGeom i
End If
End With
End If
Next i
End Sub
The vertices are updated for the sprite we created in HandleAnims. Now we can
draw the sprite (finally! :). In DirectGraphics (As in Direct3D), you have to
called Dev.BegineScene before rendering textures, and Dev.EndScene after you're
done; RenderScene takes care of this, and all the displaying of sprites:
Public Sub RenderScene()
On Local Error Resume Next
Dim HR As Long
'Call TestCooperativeLevel
to see what state the device is in.
HR =
Dev.TestCooperativeLevel
If HR = D3DERR_DEVICELOST
Then
'If
the device is lost, exit and wait for it to come back.
Exit
Sub
ElseIf HR = D3DERR_DEVICENOTRESET Then
'The
device became lost for some reason (probably an alt-tab) and now
'Reset() needs to be called to try and get the device back.
HR =
0
HR = ResetDevice()
'If
the device failed to be reset, exit the sub.
If
HR Then Exit Sub
End If
'Make sure the app isn't
minimized.
If frmMain.WindowState
<> vbMinimized Then
'The
app is ready for rendering.
With
Dev
'Clear the back buffer
Call .Clear(0, ByVal 0&, D3DCLEAR_TARGET, 0, 0, 0)
'Begin the 3d scene
Call .BeginScene
RenderSprite Spr1Index
'End the scene
Call .EndScene
'Draw the graphics to the front buffer.
Call .Present(ByVal 0&, ByVal 0&, 0, ByVal 0&)
End With
End If
End Sub
First the Device is cleared (Like clearing the BackBuffer in DDraw). Then the
scene is begun. Now all the displaying of sprites is done. Then the scene is
ended. This routine also handles Alt+Tabbing for us.
Here's how RenderSprite works:
Public Sub
RenderSprite(SpriteNum As Integer)
With Dev
'Set
the Sprite texture on the device
Call
.SetTexture(0, Textures(Sprites(SpriteNum).TextureIndex).D3DTexture)
'Make sure the device supports alpha blending
If
(D3DCaps.TextureCaps And D3DPTEXTURECAPS_ALPHA) Then
'It does, so turn alpha blending on
Call .SetRenderState(D3DRS_ALPHABLENDENABLE, 1)
If AlphaBlend = True Then
Call .SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE)
Call .SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE)
Else
'State for just drawing transparent color-keyed without alpha-blending
Call .SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA)
Call .SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA)
End If
'Draw the 2 polygons that make up the Sprite
Call
.DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, Sprites(SpriteNum).Verts(0),
Len(Sprites(SpriteNum).Verts(0)))
'If alpha blending was turned on
If
.GetRenderState(D3DRS_ALPHABLENDENABLE) Then
'Turn it back off
Call .SetRenderState(D3DRS_ALPHABLENDENABLE, 0)
End If
End With
End Sub
First we set the texture onto the Device. Then we set up for alpha-blending,
if we need to. Then we draw the 2 polygons that make up our rectangle. Then we
turn alpha-blending back off. There are different RenderStates you can use for
different alpha-blending effects, but I'll let you figure them out for yourself
:)
Well, that's about it! Phew, are you still awake? I know I barely am when
writing this :) I hope this helps you to get started with 2D graphics in DirectX
8. This engine is very modular and can probably used pretty easily without
understanding all the nitty gritty details of it. BE SURE to check out the
sample project code - it is much easier to understand all this when you see it
in action in the code. (Keys for the sample are arrow keys to move sprite
around and 1/2 to switch render modes)