Table of Contents
TexasQuest is a 2D side scroller platform game similiar to Super Mario
brothers. The object of the game is for the character 'Aaron' to reach the end
where he will rescue and save his girlfriend from a terrible boss who works his
employees too hard. Along the way there are enemies that will try and stop
Aaron from reaching his destination. However, Aaron has tools to help him
(i.e. soda, freeze pops, etc.) along in his journey. Can you help Aaron reach
his girlfriend and save her from an otherwise productive day of work
before he is snapped up by the bosses minions?
This is actually and update to the previous version of TexasQuest where
I used Microsoft's DirectX technology. In this version I have switched over to
using the open source project called SlimDX. More information about that
project can be found at http://slimdx.org
[^]
Second, to pulling completely away from utilizing proprietary DirectX
technology I am trying out the NAudio open source project here in this release.
More information can be found
here
[^]
regarding that project.
Let me start by saying that, no this is not original
code. The original C# code can be found
here. I simply needed to port it over to VB.NET so
that I could study it more. The original author Aaron Dail wrote it for his girlfriend. Read more and
download here, and see more screenshots. Aaron has generously made the full source
available for the game, so if you want some 2D scrolling sample code in
C#, this would be a great place to look.
The purpose for me in this application was to learn programming in VB.NET.
I can not explain to you how many times I have searched the web looking for
decent examples on how VB developers can implement and create really good
games. Video games are not one of the main reasons I got into programming,
but I find that a lot of people want to be entertained. So, I say, if the people
want cake, give them cake.
The general principles learned here could be applied to many types of
games, be they 2D strategy games, 2D overhead adventures, 2D puzzles,
or more 2D strategy games. I am not sure about the utility of this for 3D
games, except that it will be completely useless for graphics.
We will cover how to get the rendering part of your game up and running
using the open source project called SlimDX
[^].
Second, we will talk about images and how to draw
them. After that, you should be knowledgeable enough to actually put a
game together. Then, we will cover level architecture and how to draw a
level (and by level architecture, I mean what structures define a level,
not an individual level's contents). Next up would probably be the basics
of display characters and objects in levels.
Then, we should probably cover the current endeavor a character is
undertaking, and actions they are executing, if any. Next, we will do 'hit
detection', and one of the most complicated to get right topics: event
handling. That is, when A jumps on B, X happens, but when B jumps on A, Y
happens. You got to handle all that stuff. That is tough to get right.
That flows right into changing a character's goal or endeavor in response
to events, or what I will call thinking. Actually, user input would be
covered there too. And, that there is a decently complete overview of a
game.
- SlimDX.dll
- NAudio.dll
- NAudio.WindowsMediaFormat.dll
Your "Build" directory should start out looking this way.
SlimDX renders to a target of some sort, which in .NET is either a
Form
or a Control
(technically, since a
Form
is a Control
, it is always to a
Control
). So first, you have to have a window, and
optionally, you can have a Control
on that window to render
to (this lets you surround the rendering with other controls, like
buttons or list boxes). Simple enough, right? Well, lets write a
Main
function that does this. So, start your IDE, and create
a VB.NET Windows Application project.
Public Shared Sub Main(ByVal args As String())
Dim mw As MainWindow = New MainWindow ()
mw.Show()
Do While mw.Created
Application.DoEvents()
Loop
End Sub
This goes in your MainWindow
class. It should be pretty
straightforward what we are doing. Because we aren’t doing much, or
anything related to DirectX at this point. All we are doing is creating a
window, showing it, then while it is created, processing its events
repeatedly (when you click the Close box, the Created
property of the Form
becomes False
).
Now, compile, build, and run. Should work like a champ.
Now, we are ready to get to the good stuff. I like to separate the
rendering from the internals required to set up the rendering, and
separate that from the game engine as well as the code to start and stop
the app, as best as possible. This leads to a few more classes which can
be confusing at first, but once you figure it out, it works quite
nicely.
Basically, the structure I have grown to like is to have a
Controller
class that handles everything related to getting
the rendering ready to go, but doesn't render anything. The rendering is
actually done through a Renderer
class of which the
Controller
object has an instance. The reason is that you can
toss out and plug in Renderer
objects for different tasks on
the fly without much work. In my game, I have a Renderer
for
the main part of the game that displays the map and everything, and then
several for menus. It is not that hard really, and it keeps your code
cleaner.
Now, what I said about the engine being separate from the rendering is
very important too. All the Renderer
does is display the
current state of the game. It does not affect game state at all, ever. The
engine does this. While they usually run in tandem, that is not required.
Meaning, you can pause the engine and still render, or you can play 1000
turns of the game without rendering any of them. This is basic, but
important. So, let's go ahead and create the Controller
class
I was talking about. Add a new class called Controller
to
your project. Add the following Import statements:
Imports System.Windows.Forms
Imports Microsoft.DirectX
Imports Microsoft.DirectX.Direct3D
Now, add the following private variables to the Controller
class:
Private m_Target As Control
Private m_Device As Device
Private m_Renderer As Renderer
Private m_presentParameters As PresentParameters
Private DeviceLost As Boolean
m_Target
variable is the target we talked about earlier,
it is where all the rendering goes. The m_Device
is the
DirectX device interface that does the rendering and stuff to the target.
The m_presentParameters
is some information we setup to
create the device. The reason we keep this is that occasionally, the
device could be lost, and we might need to recreate it without having to
restart the app. Which is related to the IsDeviceLost
, which
is a flag that is set when the device is lost. The m_Renderer
variable is the Renderer
we talked about earlier.
Now, change your constructor to be like this:
Public Sub New(ByVal target As Control, ByVal renderer As Renderer)
Me.m_Target = target
Me.m_Renderer = renderer
IsDeviceLost = False
m_presentParameters = New PresentParameters ()
InitializeGraphics()
End Sub
Pretty simple, just sets up the private members, and calls
IntializeGraphics
, which should look like:
Protected Sub InitializeGraphics()
m_presentParameters.Windowed = True
m_presentParameters.SwapEffect = SwapEffect.Discard
m_presentParameters.AutoDepthStencilFormat = DepthFormat.D16
m_presentParameters.EnableAutoDepthStencil = True
Dim adapterOrdinal As Integer = Manager.Adapters.Default.Adapter
Dim caps As Caps = Manager.GetDeviceCaps(adapterOrdinal,DeviceType.Hardware)
Dim createFlags As CreateFlags
If caps.DeviceCaps.SupportsHardwareTransformAndLight Then
createFlags = CreateFlags.HardwareVertexProcessing
Else
createFlags = CreateFlags.SoftwareVertexProcessing
End If
If caps.DeviceCaps.SupportsPureDevice Then
createFlags = createFlags Or CreateFlags.PureDevice
End If
m_device = New Device(adapterOrdinal, DeviceType.Hardware, target, createFlags, _
presentParameters)
AddHandler m_device.DeviceReset, AddressOf OnDeviceReset
SetupDevice()
End Sub
Pretty complicated. And, heck if I know all of what it's doing. Like I
said, read the other tutorials, or buy a book. Basically, most of what
this does is get ready to create the device and then it creates it.
Telling SlimDX a whole lot of information about how to create it. It also
does two things at the end, add a handler for DeviceReset
and
call SetupDevice
. The device reset handler calls the setup
device too because that is supposed to handle when the device has been
lost and can hopefully be recovered.
SetupDevice
does the camera, lighting, and view setup:
Protected Sub SetupDevice()
m_device.RenderState.AlphaBlendEnable = True
m_device.SetSamplerState(0, SamplerStageStates.MinFilter, _
CInt((TextureFilter.Linear)))
m_device.SetSamplerState(0, SamplerStageStates.MagFilter, _
CInt((TextureFilter.Linear)))
m_device.SetSamplerState(0, SamplerStageStates.MipFilter, _
CInt((TextureFilter.Linear)))
m_device.RenderState.Lighting = False
Dim width As Single = CSng(target.Size.Width)
Dim height As Single = CSng(target.Size.Height)
Dim centerX As Single = width / 2.0f
Dim centerY As Single = height / 2.0f
Dim cameraPosition As Vector3 = New Vector3(centerX, centerY, -5.0f)
Dim cameraTarget As Vector3 = New Vector3(centerX, centerY, 0.0f)
m_device.Transform.View = Matrix.LookAtLH(cameraPosition, cameraTarget, _
New Vector3(0.0f, 1.0f, 0.0f))
m_device.Transform.Projection = Matrix.OrthoLH(width, height, 1.0f, 10.0f)
End Sub
Protected Sub OnDeviceReset(ByVal sender As Object, ByVal e As EventArgs)
SetupDevice()
End Sub
This basically gets the device (camera, view, lighting) ready to draw
the 2D stuff normally. Now, let's add the all important
Render
method:
Public Sub Render()
If DeviceLost Then
AttemptRecovery()
End If
If DeviceLost Then
Return
End If
m_Device.Clear(ClearFlags.Target Or ClearFlags.ZBuffer, _
System.Drawing.Color.Blue, 1.0f, 0)
m_Device.BeginScene()
m_Renderer.Render(Me)
m_Device.EndScene()
Try
m_Device.Present()
Catch e1 As DeviceLostException
m_DeviceLost = True
System.Diagnostics.Debug.WriteLine("Device was lost")
End Try
End Sub
First, we see if the device has been lost, and if so, we want to try to
recover it. If after that, the device is still lost, then we return from
this method without doing anything. As a note, the device can be lost when
the screensaver comes on or things like that. If everything is OK, then we
clear the display, and do BeginScene
. All rendering must
occur between the calls to BeginScene
and
EndScene
. That’s just how SlimDX works. Between the two, we
call our Renderer
’s Render
method which should
do all the drawing. Then, after that, we try to present the work we have
done, and that is when we might notice we’ve lost the device, so we set
that deviceLost
flag to True
in
this case (so, the next time Render
is called, we will
attempt to get the device back).
Now, AttemptRecovery
:
Protected Sub AttemptRecovery()
Try
device.TestCooperativeLevel()
Catch e1 As DeviceLostException
Catch e2 As DeviceNotResetException
Try
device.Reset(presentParameters)
deviceLost = False
System.Diagnostics.Debug.WriteLine("Device successfully reset")
Catch e3 As DeviceLostException
End Try
End Sub
Let's just assume this works. Finally, we have a simple property to get
and set the Renderer
:
Public Property Renderer() As Renderer
Get
Return m_Renderer
End Get
Set(ByVal value As Renderer)
m_Renderer = value
End Set
End Property
And, that is all for the Controller
class for now. You
can’t built it or run it yet, because we haven’t created the
Renderer
yet, so let's do that next.
Create a new class in your project called Renderer. Add the Imports
directives:
Imports SlimDX
Imports SlimDX.Direct3D
Then, add the Render
method:
Public Class Renderer
Public Sub New()
End Sub
Public Overridable Sub Render(ByVal controller As Controller)
End Sub
End Class
And, that’s it. Right now the Render
class is going to be
dirt simple because it doesn’t do anything yet. Actually, the
Render
class itself will never really do anything, but its
sub-classes will. In fact, we will make it an abstract class in the next
version, because no one should ever need to use it as it is. For now
though, if it was abstract, we couldn't test our fledgling program, so
it's better to make it concrete.
The last part we have to do is rewrite the Main
function a
bit:
Public Shared Sub Main(ByVal args As String())
Dim mw As MainWindow = New MainWindow ()
Dim r As Renderer = New Renderer ()
Dim c As Controller = New Controller (mw, r)
mw.Show()
Do While mw.Created
c.Render()
Application.DoEvents()
Loop
End Sub
Build it and run it. You should be the proud parent of a deeply blue
window. Congratulations.