Version 2.0!
Features
Tutorials
Files
Glossary
Projects
Contact
Links
Message Board
Extras
LuckyCam
Old News
Sign Guestbook
View Guestbook
VB Horoscope
VB Photo Album
.
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

Set Textures(TextureNum).D3DTexture = D3DX.CreateTextureFromFileEx(Dev, Bitmap, D3DX_DEFAULT, _
D3DX_DEFAULT, D3DX_DEFAULT, 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, _
D3DX_FILTER_POINT, D3DX_FILTER_POINT, &HFF000000, ByVal 0, ByVal 0)

'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(2)
    .X = Sprites(SpriteNum).Pos.X + FrmW(SpriteNum): .Y = Sprites(SpriteNum).Pos.Y + FrmH(SpriteNum)
    .TU = TexCoor(FrmX(SpriteNum) + FrmW(SpriteNum), SpriteNum): .TV = TexCoor(FrmY(SpriteNum) + FrmH(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)

That's it... now go program! :)