DirectXInput:
Keyboard Control
Author:
Jack Hoxley
Written: 17th June 2001
Contact: [EMail]
Download: DI8_Keyboard.Zip
(15kb)
Contents
of this lesson
1. Introduction
2. Starting a DirectX project
3. Polling Based Keyboard Control
4. Event Based Keyboard Control
1.
Introduction
Welcome
to the first lesson in learning DirectInput 8.
Direct Input 8 does exactly what it's name suggests
- input; it allows your application to gain control
of almost every possible type of game controller
and input device known on the PC - keyboards,
Joysticks, steering wheels, gamepads, force feedback,
mice... the list goes on.
DirectInput8
has many advantages over using VB's built in input
controls - Form_KeyUp, Form_KeyDown, Form_MouseMove
etc.. and has more control than the standard Win32
functions - GetCursorPos, GetKeyState ... In general
it is faster, more efficient and much more powerful.
It was designed for games (like all of DirectX8)
and therefore has no excess baggage that will
slow your application down. Just what I like...
We're
going to start by using the keyboard as it's by
far the simplest example - and if you've already
done the DirectXGraphics8
series then you'll be in for a pleasant surprise
- this one is actually quite short, to the point
and very easy. After you've got the basics of
this lesson under your belt, learning how to use
the mouse and joystick is relatively easy...
2.
Starting a DirectX Project
For
those of you who've already been using DirectX8,
or have already read other series on this site
then this section isn't new; feel free to skip
it.
For
those of you who are new to the DirectX experience,
before we do any programming we need to tell visual
basic that we're going to be using DirectX. First
off, you'll need visual basic 5 or 6 (or higher
if available) as DirectX is a COM based system,
and VB 4 and below cant address this sort of thing.
I've read numerous message board posts about people
trying hard to get it working in VB4 - dont waste
your time; it wont work; YOU NEED version 5 or
6. To get a project started using DirectX, follow
these simple steps:
1.
Open up Visual Basic - I hope you know how to
do this ;)
2.
Start a new "Standard EXE" project
3.
On the project menu click on "references".
A window should appear with a long list of libraries
and files that you can use (each with a check
box next to it)
4.
Scroll down until you find a "DirectX 8 for
visual basic type library" (or similiar).
Check it's box.
5.
Click okay. You're project is now linked to DirectX;
if you open a code window and type "As "
the variable list will drop down, start typing
"D" and you'll see a list of several
100 Direct3D this DirectInput that... etc...
6.
Save your project somewhere, or if you're feeling
clever create a template version of the project
so you can start it easily next time...
That
didn't work? Cant find the library? well, in the
references window click on "Browse"
and navigate to C:\Windows\System\ and find a
file Dx8VB.DLL and select it - you should see
an entry (checked) with the name "DirectX
8 for visual basic type library"... continue
as normal.
There
are 2 final notes regarding the distribution of
DirectX based applications.
1.
Do not assume the end user has DirectX installed,
or that it's the correct version. Always check
to see which version is installed. if it's not
the correct version, make them run the setup program
2.
You can freely distribute the DirectX runtimes/distributables
- you'll often find them sitting on the root folder
of a commercial game CD... or magazine coverdisks.
3.
Polling Based Keyboard Control
now
we've got the boring stuff out of the way we can
hopefully get onto doing some more interesting
stuff...
Although,
dont get too excited - using the keyboard isn't
really that amazing to watch :-)
There
are two methods of using the keyboard in DirectX8
- polling or event based; both have their advantages
and disadvantages. In general, as with almost
all aspects of game design I much prefer event
based methods - it makes debugging exceedingly
easy (just log every message sent), and when there's
no messages waiting to be processed your application
doesn't do any work - very efficient. Polling
tends to be slightly more responsive and easy
to control - and you can quite easily gain the
same results from polling as you can event based
and vice versa. If you dont know much about polling
and event based programming - or general program
structure I strongly suggest you go find some
good articles online about them, with respect
to game development Gamasutra
and GameDev
are good places to look; if you intend to get
into game development then the code design and
structure is incredibly important - make it ugly
and you'll kill performance.
Polling
is the simplest method, which is why we start
here - it also allows me to explain the initialisation
procedure - something very important and very
common across all of DirectX.
Step
1: The declarations.
We'll be using a standard form with a text
box on it (multiline, locked, vertical scroll
bars), so set one of those up (Standard EXE should
give you a form to start with).
In
the declarations section of the form, place the
following code:
Option Explicit
'//Important,
only set one of the
following 2 to be
true.
Private Const UsePollingMethod
As Boolean = True
Private Const UseEventMethod
As Boolean = False
'//Status
variables and other
stuff :)
Private bRunning As
Boolean 'for
the polling version,
states when we've
finished.
'//The
following object definitions
are the master controlling
' units for DirectInput.
Private DX As DirectX8
Private DI As DirectInput8
'//These
next two objects are
used to access our
device (keyboard)
Private DIDevice As
DirectInputDevice8
Private DIState As
DIKEYBOARDSTATE
Private KeyState(0
To 255) As Boolean
'so
we can detect if the
key has gone up or
down!
Private Const
BufferSize As Long
= 10 'how
many events the buffer
holds.
'This
can be 1 if using
event based, but 10-20
if polling based...
'Sleep() - stops our
polling loop going
too fast ;)
Private Declare
Sub Sleep Lib "kernel32"
(ByVal dwMilliseconds
As Long)
|
|
|
|
The
first line, Option Explicit, is important - DirectX
likes all it's variables to be explicitly created
- none of this crazy no-declaration variable coding
please! good clean code is the order of the day.
The next part are two control constants, not actually
part of DirectInput programming, but it allows
you to select which version of the program to
run - polling or event based. The next variable
bRunning is a variable that allows us to control
the master loop - something D3D programmers will
be familiar with.
The
next two lines, declaring DX and DI are very important,
the DirectX8 object is the master object - it
controls everything else basically; we'll use
it in a minute to create the DirectInput8 object.
the DI object is equally important, we must have
one of these to use DirectInput. the DirectInput
object, if imagined on a tree, comes as a branch
from the DX object.
The
next 4 lines, starting with DIDevice. DIDevice
is important as well, and if imagined on the tree
analogy again is a sub-branch of the DI object.
the DIDevice represents our device (funny that),
be it a keyboard, mouse or joystick - you can
create multiple devices for different hardware,
ie, mouse and keyboard, but not two device for
1 keyboard. DIState represents the current state
of the keyboard, everytime we poll the device
for information it will fill this structure with
the state of all the keys - up or down. The next
array, KeyState() is one that I've set up to simplify
checking KeyUp events (by default we only *see*
keydown events). The final line, BufferSize, is
so that we can create a buffer for retrieving
keyboard events - it makes things much easier
when detecting keydown events... Note the comment
by this line - if you use event based methods,
we only need a 1-event buffer, otherwise we'll
need 10-20 events to be buffered - depending on
how fast/slow your game loop runs and how fast/slow
the end user is at typing.
The
last line is the Sleep() API, it's required to
slow down the main loop. As you'll see in a second
we're using a Do...Loop structure, this can easily
travel at 1000 loops per second; when polling
this can have the adverse effect of making it
impossible to type properly - if you tap the key
as fast as you can you'll probably register 10-20
keypresses (at 1000fps), typing at normal speed
and suddenly the simplest things end up like hhhhhhhhhhhhhhhhheeeeeeeeeeelllllllllllllllllllllllllllllllllllllllllllllooooooooo
- not good.
Step
2: Initialisation
This part is crucial to the existance of
a DirectInput application, as is it's counter
operation - Termination. DirectInput is not as
simple as an API call, we are integrating ourselves
with a potentially very fragile system and must
treat it as it was designed - or who knows what
will happen! A standard application will have
an Initialisation stage, follows by the actual
gameplay time, followed by Termination just before
it finally closes down. Thankfully this sort of
structure fits excellently with a modular program
structure and the general ideas behind game design
- as I said before, go read up on this stuff if
you're not following me.
Initialisation
follows 3 stages:
1. Create the objects and the devices.
2. Setup the properties for the device and configure
it
3. Tell the device that we want to start using
it.
In
code this will look like:
Private Sub Form_Load() On Local Error GoTo BailOut:
Me.Show
'//0. Any variables
Dim I As Long
Dim DevProp As DIPROPLONG
Dim DevInfo As DirectInputDeviceInstance8
Dim pBuffer(0 To BufferSize) As DIDEVICEOBJECTDATA
'//1. Check options. If UsePollingMethod And UseEventMethod Then
MsgBox "You must select only one of the constants before running"
Unload Me
End
End If
If UsePollingMethod Then txtOutput.Text = "Using Polling Method" & vbCrLf
If UseEventMethod Then txtOutput.Text = "Using Event Based Method" & vbCrLf
'//2. Initialise the selected method
Set DX = New DirectX8 'must create the object.
Set DI = DX.DirectInputCreate
Set DIDevice = DI.CreateDevice("GUID_SysKeyboard") 'the string is important, not just a random string...
DIDevice.SetCommonDataFormat DIFORMAT_KEYBOARD
DIDevice.SetCooperativeLevel frmMain.hWnd, DISCL_BACKGROUND Or DISCL_NONEXCLUSIVE
'set up the buffer...
DevProp.lHow = DIPH_DEVICE
DevProp.lData = BufferSize
DIDevice.SetProperty DIPROP_BUFFERSIZE, DevProp
DIDevice.Acquire 'let DirectX know that we want to use the device now.
'retrieve some information about the device; not really that useful - it'll only tell you
'that your using a keyboard (as if we didn't already know that!)...
Set DevInfo = DIDevice.GetDeviceInfo()
txtOutput.Text = txtOutput.Text & "Product Name: " & DevInfo.GetProductName & vbCrLf
txtOutput.Text = txtOutput.Text & "Device Type: " & DevInfo.GetDevType & vbCrLf
txtOutput.Text = txtOutput.Text & "GUID: " & DevInfo.GetGuidInstance & vbCrLf
'if we've gotten to this point, the user has decided to quit
DIDevice.Unacquire
Set DIDevice = Nothing
Set DI = Nothing
Set DX = Nothing
Unload Me
End
Exit Sub
BailOut:
MsgBox "Error occured in Form_Load() - ", Err.Number, Err.Description
End Sub
|
|
|
|
hopefully
the above doesn't look to bad - it's not that
complicated really; and once you've learnt it
there isn't really a great deal else to be done
here. I've put it all in the Form_Load( ) procedure,
which isn't a great idea - but it's good enough
for this sample program. There is quite a large
section removed from the above code which I'll
show you in a second, but above is all of the
initialisation and termination code for a standard
DirectInput8 program.
Section
1 is nothing special, and is straight forward.
Section 2 is where it gets more interesting. First
we create the DirectX object, DX. I know that
you can declare it initially as "Dim DX as
New DirectX8", but it's actually slower that
way, and for the extra speed it's much easier
to late bind it - as I have done. We then use
the newly created DirectX8 object to create the
DirectInput8 object, DI. We then use the newly
created DirectInput8 object to create a Device,
DIDevice. Notice a pattern here? a heirachy! almost
everything about DirectX is designed as a heirachy
- so get to like it ;)
The
"GUID_SysKeyboard" part in the CreateDevice(
) function is important (as the comment says!).
it tells the DirectInput8 object to create a keyboard
device - there are a lot of GUID's - Globally
Unique IDentifier's... but we only need this GUID
- the other standard one is for a mouse device;
the rest have to be enumerated - a topic for later
on... Either way, you'll be safe to use the "GUID_SysKeyboard"
token.
Once
we've created the device we set it's properties
- what data format it uses, this is so that when
we ask the device for information it formats it
in the correct way for our program to read it.
The data format is either joystick, keyboard or
mouse. The cooperativelevel, this is important
- and quite fun. There are two components to this
line - Background or foreground and exclusive
or non-exclusive - you can mix these as you choose.
If you select background then your program has
access to keyboard data at all times; if you select
foreground then it'll only recieve keyboard information
when your window is the currently active/selected
window. If you select exclusive then no other
device can access your device in exclusive mode
- other apps can still create a non-exclusive
device and see what the keyboard state is, but
you get priority. Non-Exclusive mode indicates
that you'll get notified of events/be able to
access the keyboard, but your application wont
intefere with other applications.
The
next part is about setting up the buffer - we
just tell DirectInput how many events to store;
if more events than fit in the buffer are raised
you will get DIERR_BUFFEROVERFLOW generated and
you wont be able to access any of the data. everytime
you read the buffer it will be cleared, so if
you check it on every loop (as we will) it will
need to be large enough to hold all possible key
events between the last loop - so if the application
is running slowly you'll need to increase the
size of the buffer...
Finally,
we tell the device that we're going to use it
- we acquire the device. We must make sure we
unaquire it when we want to finish up, as shown
in the first line of the termination code.
The
important part about termination is the order
we terminate the objects (Set ?? = Nothing parts).
Whilst in VB it's not usually the end of the world
if you do it wrong, it's good practise to destroy
things in the reverse order to that which you
created them. DX>DI>DIDevice becomes DIDevice>DI>DX
in termination... In some cases it's a good idea
to reset the configuration of the device, but
that's not required in this situation.
Step
3: Getting the keyboard input.
We're going to simulate a game loop using
a standard Do..Loop structure, those familiar
with my DirectXGraphics series will know this
structure well. For the rest of you, most games
are based around a tight loop of core code - the
frame rate of a game is representative of this.
Take a standard game - it'll update the graphics,
AI, physics, sound - and receive input from the
user (not in that order though!). This section
is about recieving input from the user. Because
of the structure (looping, check keys every frame)
polling is a good choice; event based methods
dont fit well into this situation (as shown later
on). The following code is how we do it:
If Not Err.Number Then bRunning = True 'no errors, clear for takeoff...
Do While bRunning
'a. retrieve the information
DIDevice.GetDeviceStateKeyboard DIState 'get the keyboard state
On Error Resume Next 'ignore the prev. err handler
DIDevice.GetDeviceData pBuffer, DIGDD_DEFAULT 'retrieve buffer info.
If Err.Number = DI_BUFFEROVERFLOW Then 'check for an error..
Debug.Print vbCr & "BUFFER OVERFLOW (Compensating)..."
GoTo ENDOFLOOP: 'too much data, just loop around to the next loop.. End If
On Error GoTo BailOut: 'reinstate the old error handler.
'b. sort through this data...
'most apps would look at a specific key rather
'than loop through them all; but we're interested in all of them For I = 0 To 255 'loop through all the keys
If DIState.Key(I) = 128 And (Not KeyState(I) = True) Then 'it's been pressed...
'the value will almost always be 128, indicating a key was pressed...
txtOutput.Text = txtOutput.Text & "{ DOWN } " & KeyNames(CInt(I))& vbCrLf
txtOutput.SelStart = Len(txtOutput.Text)
KeyState(I) = True
End If
Next I
'c. check for any key_up events
For I = 0 To BufferSize
If KeyState(pBuffer(I).lOfs) = True And pBuffer(I).lData = 0 Then
KeyState(pBuffer(I).lOfs) = False
txtOutput.Text = txtOutput.Text & "{ UP } " & KeyNames(CInt(pBuffer(I).lOfs)) & vbCrLf
txtOutput.SelStart = Len(txtOutput.Text)
End If
Next I
Sleep (50)
DoEvents
ENDOFLOOP:
Loop
|
|
|
|
it's
fairly simple really, divided into 3 parts - a,b
and c. The first stage is where we get the current
key-states. This sample uses two ways effectively.
It would be easier to just fill the buffer with
data and read what it says, rather than individually
checking each keystate - but either is good. The
reason for the latter method is so that you could
rewrite it just to look at individual keys, using
the "if DIState.Key(DIK_UP) = &H80&
then"...
So,
the first part retrieves all the data that we
need. The second part checks for any keys that
have been pressed, signified by the value &H80&
- we can then write to the display that the key
has been pressed down; in a normal application
we'd just send this to a processing pipeline and
do whatever the key corresponds with (move the
character left/right for example). The final part
checks for any key-up states; you could do this
in the previous loop - any DIState.Key( ) that's
now 0, but it's KeyState(i) = True would suggest
that the key has been lifted, but to demonstrate
both possible ways I've made it check the buffer.
the lOfs is the keynumber, the lData is the key
value (0 or 128). Note that both b and c use a
function KeyNames( ) - this is a simple function
that will output (as a string) the name of the
key pressed. it's quite long so I haven't included
it here - but you can get it in the downloadable
source code.
4.
Event Based Keyboard Control
Right,
lets wrap this thing up now - I'm hungry and want
some lunch :-) and I'm sure you've done enough
reading now...
Luckily,
event based control is simply an addition to the
polling method, initialisation is almost identical
and the basic principles are the same; we just
need to restructure the data collection and processing
loop. Onwards...
Step
1: Declarations and Initialisation
Whilst I said that they dont change much,
there are a couple of lines here-and-there that
need changing. First off, in the declarations...
Dim hEvent As Long 'a handle for an event...
Implements DirectXEvent8
|
|
|
|
add
those lines in anywhere - it doesn't matter. BUT
notice that if you now click (in the form window)
on the list box that has the objects (forms, buttons
etc..) in it there is now a new entry - one that
has mysteriously appeared! wow! and it's all because
of the "Implements DirectXEvent8" line,
delete it and notice that the object disappears,
type it in again and notice that it re-appears.
This may not be anything special if your a seasoned
VB-Pro; but it may seem a little strange otherwise.
You dont really need to understand the mechanics
of how or why, all you need to know is that it
creates a CallBack function in this case. What's
that? well, if you've never seen one before, you'll
be familiar with the theory that all your program
does is call functions - in your program or in
DLL's. A CallBack function is where another program
(DirectX in this case) calls your program - with
no warning. What you need to understand is that
when an event occurs (a key is pressed/unpressed)
DirectX will tell your program by calling the
DirectXEvent8_DXCallback( ) function; it
wont directly tell you what has happened - but
you, being the clever person that you are will
say "hmm! somethings happened, lets check
the buffers" - and you then look at the keystates
and buffers (as in the polling example) - and
what a surprise! something will have changed.
You can then take the relevent course of action...
The
beauty of event based programming is that you
dont have to keep on querying the device, and
dont have to do any wasted processing. Whilst
polling when there's no changes wont have a big
impact on performance, if you look at it on the
simplest level - you're calling functions and
checking things when absolutely nothing has changed.
when you're trying to squeeze the extra few frames
per second out of your game, this wasted function
call could be important - it may only be 0.7ms
of processing time, but it may be worth it ;)
The
only change to the initialisation procedure is
the next few lines - setting up an event.
If UseEventMethod Then
'event based requires some extra initialisation
hEvent = DX.CreateEvent(frmMain)
DIDevice.SetEventNotification hEvent
End If
|
|
|
|
Simple
really. We use the master DirectX object
to create an event, the hEvent variable
is just a number - when the callback function
is called it will have this number as a
parameter, so you can compare it with hEvent
and decide if it's a keyboard event (you
can have audio events, and other input events
for example) - then process it accordingly.
Once the event is registered, you tell the
DIDevice object to use it when it raises
an event...
One
final note on termination - you must destroy
the event. VB tends to do it for you if
you forget, it's a good practise to do it
yourself... use the following code:
If hEvent <> 0 Then DX.DestroyEvent hEvent
Set DIDevice = Nothing
Set DI = Nothing
Set DX = Nothing
|
|
|
|
I've
put this part in the Form_QueryUnload( ) procedure,
but you can put it wherever you require the termination
to occur...
Step
2: Using the event
Now we've set up the event we need to be
able to do something with it! Notice that alot
of the following code is very similiar to that
of the polling example. This should be no surprise,
as the only difference between polling and event
based is that in polling we check on every pass
of the loop, and in event based mode we check
only when we know something has happened...
Private Sub DirectXEvent8_DXCallback(ByVal eventid As Long)
'//0. any variables
Dim I As Long
Dim pBuffer(0 To BufferSize) As DIDEVICEOBJECTDATA
If eventid = hEvent Then
'this message is for the event we set up; whilst of little point
'in this example it's useful if you set up multiple events - mouse and keyboard for example
If DIDevice Is Nothing Then Exit Sub 'simple error checker...
'//1. we know an event has occured, so we collect data from the device
DIDevice.GetDeviceStateKeyboard DIState
DIDevice.GetDeviceData pBuffer, DIGDD_DEFAULT 'retrieve some information...
'//2. we now check through all the keys to see what happened...
'most applications wouldn't do it this way, they would look for a specific set of keys...
For I = 0 To 255
If DIState.Key(I) = 128 Then '128, &H80& indicates a key_down event.... 0 indicates a keyup.
'this key has triggered the event
If pBuffer(0).lData = 128 Then
txtOutput.Text = txtOutput.Text & "{ DOWN } " & KeyNames(CInt(I)) & vbCrLf
End If
End If
'the above code will not catch a key_up event, so we add this next
'part to catch a key_up...
If (pBuffer(0).lData = 0 And pBuffer(0).lOfs = I) Then
txtOutput.Text = txtOutput.Text & "{ UP }" & KeyNames(CInt(I)) & vbCrLf
End If
txtOutput.SelStart = Len(txtOutput.Text)
Next I
Else
'this is almost never going to happen, but I leave it in here anyway...
MsgBox "Incoming event, but not for me... ", vbInformation, "What the..."
End If
End Sub
|
|
|
|
above
is all that you really need to know - it's not
greatly complicated, and many of the things I
mentioned in the polling example work fine here...
There,
everything you should need to know about using
the keyboard in DirectInput8 - not really that
hard was it. I strongly suggest that you download
the source code for this tutorial from the top
of the page, or from the downloads
page.
|