









DirectInput is one of component of DirectX. Its main purpose is to wrap accessing input hardware (mouse, keyboard, joystick etc) into a uniform interface for diverse input hardwares. There are so many input hardware vendor and how to access their input hardware may different for each device.
If you're back to DOS age, developing game was much more exhaustive. A game should support as much hardware as possible. It means game developer should develop their own code for accesing hardware device for every hardware avalable on the market.
DirectX, especially DirectInput, solves this problem by using thin layer called Hardware Abstraction Layer (HAL). For a input device can be used in Windows operating system, its vendor must implement HAL. Game applications do not access hardware device directly anymore, but through DirectInput. DirectInput communicates with device driver through HAL and finally device driver talks with hardware.
Because DirectInput communicates directly to hardware driver (via HAL), DirectInput bypasses Windows messaging system. Therefore, if we use DirectInput for accessing keyboard, messages such as WM_KEYDOWN or WM_KEYUP will not be sent to our application.
Games (for example fighting genre games) usually use combination of some keys instruct player character in game to do something for example fiercing. It forces players to push arrow down and Enter key simultaneously. Such combination cannot be detected if we use WM_KEYDOWN message. Combinations that can be detected with WM_KEYDOWN are only combination of Shift. Alt, and Ctrl with other keys.
Other problem is delay between time when a key is pressed and time when it becomes key repetition. If you press a key for example A, A will be print and then there will be delay for about half second (depends on your system configuration) before key pressed produce AAAAAAA......on the screen. For games, this delay is not wanted. Games typcally wants more responsive output. If player presses right arrow, player expects character in game will move to the right as soon as possible and keep moving until key is released.
DOS game programmer used to intercept interrupt 09h to address this issue. Fortunately, with DirectInput we don't need to deal with interrupt. Ok let's get it on..
To run demo we are going to make, we need:
To be able to utilize DirectInput, we need instance of IDirectInput8 interface (we will talk DirectX 8 only). How do we do? There are two ways to get IDirectInput8 instance. With DirectInput8Create() or CoCreateInstance().
function DirectInput8Create(hinst : THandle;
dwVersion : Cardinal;
const riidltf : TGUID;
out ppvOut;
punkOuter : IUnknown) : HResult; stdcall;
hInst is handle of application or DLL instance, dwVersion is DirectInput SDK version. Fill with DIRECTINPUT_VERSION or use $8000 value for version 8. riidltf is interface identifier of IDirectInput8. You can use IID_IDirectInput8 or IDirectInput8. ppvout holds address IDirectInput8 instance. punkOuter can be set to nil. This parameter is for COM aggregation that we don't use here. For example:
DirectInput8Create(HInstance,DIRECTINPUT_VERSION,
IID_IDirectInput8,
FDirectInputObj,
nil);
Using DirectInput8Create() is the easiest way. If we use CoCreateInstance() the step is as following:
CoCreateInstance(CLSID_DirectInput8,
punkOuter,
CLS_CTX_INPROC_SERVER,
IID_IDirectInput8,
FDirectInputObj);
Three steps above is integrated into DirectInput8Create(). We use DirectInput8Create in this article.
We need to get pointer to instance of IDirectInputDevice8. To be able to do that, we need to know GUID of input device we will use. We enumerate available device. For keyboard and mouse, DirectInput provide special GUID i.e GUID_SYSKeyboard and GUID_SysMouse, so for this two input devices we do not have to enumerate. For other input devices such asi joystick, gamepad etc which is not a computer standard input devices, we must enumerate them to get their GUID.
Enumeration is done by callingEnumDevice() of IDirectInput8.
function EnumDevices(dwDevType : Cardinal;
lpCallback : TDIEnumDevicesCallback;
pvRef : Pointer;
dwFlags : Cardinal) : HResult; stdcall;
dwDevType is device type we want to enumarate. This parameter will restrict device to enumerate. We can use device class constant as follow:
Except with device class, we can also use spesific device constant. For example, to enumerate gamepad only, we can use DI8DEVTYPE_GAMEPAD. To enumerate steering wheel only, we use DI8DEVTYPE_DRIVING. For flight simulator controller, we use DI8DEVTYPE_FLIGHT and for joystick DI8DEVTYPE_JOYSTICK. Use of DI8DEVCLASS_KEYBOARD is equivalent with DI8DEVTYPE_KEYBOARD. DI8DEVCLASS_POINTER is equivalent with DI8DEVTYPE_MOUSE AND DI8DEVTYPE_SCREENPOINTER.
lpCallback is address of callback function that will be called when a device matched in dwDevType is found. We will discuss about callback right after this. pvRef is our user data pass into callback.
dwFlags is scope of enumeration. We use following constants:
Format of callback looks like following code snippet:
TDIEnumDevicesCallback = function (var lpddi : TDIDeviceInstance;
pvRef : Pointer) : Integer; stdcall;
lpddi is variable of type TDIDeviceInstance which will hold input device data. pvRev is user data (see pvRef in EnumDevice). TDIDeviceInstance data structure is:
TDIDeviceInstance_DX5A = packed record
dwSize : Cardinal;
guidInstance : TGUID;
guidProduct : TGUID;
dwDevType : Cardinal;
tszInstanceName : Array [0..MAX_PATH-1] of AnsiChar;
tszProductName : Array [0..MAX_PATH-1] of AnsiChar;
guidFFDriver : TGUID;
wUsagePage : WORD;
wUsage : WORD;
end;
TDIDeviceInstance_DX5 = TDIDeviceInstance_DX5A;
TDIDeviceInstance = TDIDeviceInstance_DX5;
Actually, TDIDeviceInstance has two versions i.e Unicode and Ansi. For Unicode TDeviceInstance = TDIDeviceInstance_DX5W. The difference lies at tszInstanceName and tszProductName type, where Unicode version use WideChar not AnsiChar. We always use Ansi. Ok, let's discuss its fields.
We must return boolean value DIENUM_CONTINUE or DIENUM_STOP to tell DirectInput to continue or to stop enumeration.
When we get GUID of input device that we need, we create IDirectInputDevice8 by calling CreateDevice() of IDirectInput8.
function CreateDevice(const rguid : TGUID;
out lplpDirectInputDevice : IDirectInputDevice8;
pUnkOuter : IUnknown) : HResult; stdcall;
rguid we set equal to GUID of the instance that we get through enumeration. Or if we use default keyboard or mouse we can set it to GUID_SysKeyboard or GUID_SysMouse. lplpDirectInputDevice holds address of IDirectInputDevice8 instance. If failed it will be set to nil. punkOuter we set to nil.
Once device successfully created, next step is to define device data format device with SetDataFormat() ofk IDirectInputDevice8 interface.
function SetDataFormat(var lpdf : TDIDataFormat) : HResult; stdcall;
lpdf can be set with c_dfDIKeyboard for keyboard data format. For mouse we use c_dfDIMouse or c_dfDIMouse2. We use c_dfDIMouse to handle mouse with number of button is up to 4 buttons. We use c_dfDIMouse2 for mouse with number of button is up to 8 buttons. For joystick we can use c_dfDIJoystick or c_dfDIJoystick2. Version 2 is for joytick with button up to 128 buttons. In this article we always use c_dfDIMouse2 and c_fDIJoystick2. we also can use our own format, but because this article discuss DirectInput basic, I will not explain it. Example:
var aformat:TDIDataFormat;
begin
aformat:=c_dfDIKeyboard;
FDeviceObj.SetDataFormat(aFormat);
end;
Because our application will run in multitasking environment, we need to get a permission from shared resource manager i.e operating systems that our application will utilize input device. we set up level cooperative with SetCooperativeLevel() of IDirectInputDevice8. Cooperative level defines behaviour of our application when sharing resources. There are some cooperative leel types: foreground, background, exclusive, non exclusive.
Foreground cooperative level means our application will only receive input when it is foreground. When our application loses focus, for example because user change to another application, our application do not receive data from input device.
Background cooperative level enable application to continue receive input device data even when the status is background application. If user move to another application with same background cooperative level, our application do not receive data anymore. Foreground and background cannot be combined. An application must choose foreground or background cooperative level but not both.
Exclusive cooperative level means no other application allowed to access device currently being accessed by our application. Othe application with non exclusive level still can use input. Use of this level on mouse causes cursor to disappear. This is because Windows accesses mouse with same level. This level cannot be combined with non exclusive. There is one additional cooperative level, NoWinkey which will disable Windows key on the keyboard.
To set cooperative level we use following constants:
SetCooperativeLevel is declared as follows:
function SetCooperativeLevel(hwnd : HWND;
dwFlags : Cardinal) : HResult; stdcall;
hwnd is set with handle of application window. dwFlags is filled with flag combination above.
Before application can receive data from input hardware, we must acquire input device by calling Acquire() of IDirectInputDevice8.
function Acquire : HResult; stdcall;
To unacquire, we call Unacquire() of IDirectInputDevice8.
function Unacquire : HResult; stdcall;
After we acquired device, to get its current state, we use GetDeviceState().
function GetDeviceState(cbData : Cardinal;
lpvData : Pointer) : HResult; stdcall;
cbData holds size of data pointed by lpvData. lpvData points to address of variable that will receive input device current state. Data structure we pass to this function depends on format data we already set. If we use c_dfDIKeyboard, then lpvData must point to address of variable of type array[0..255] of byte. If we use c_dfDIMouse, then lpvData points to address of variable of type TDIMouseState. Complete list is shown on table below:
| Data Format | Device state structure |
|---|---|
| c_dfDIKeyboard | Array [0..255] of byte |
| c_dfDIMouse | TDIMouseState |
| c_dfDIMouse2 | TDIMouseState2 |
| c_dfDIJoystick | TDIJoyState |
| c_dfDIJoystick2 | TDIJoyState2 |
Example
type
TKeyboardBuffer=array [0..255] of byte;
var FBuffer:TKeyboardBuffer;
begin
FDeviceObj.GetDeviceState(sizeof(TKeyboardBuffer),
@FBuffer);
end;
Each element in array records key press status for each key on the keyboard, where high order bit will be set if coresponding key is pressed, otherwise it will be cleared. DirectInput provides constants for each key. For example DIK_A is for A (a or A is the same), DIK_ESCAPE for ESC key etc. To test whether K key is pressed, we do this:
function K_is_down:boolean;
begin
result:=(FBuffer[DIK_K] and $80)=$80;
end;
To test whether arrow down is pressed:
function ArrowDown_is_down:boolean;
begin
result:=(FBuffer[DIK_DOWN] and $80)=$80;
end;
To test K+arrow down combination, we do this:
if K_is_down and
Arrow_is_down then
doSomething;
Declaration of TDIMouseState is as follows:
type
TDIMouseState = packed record
lX: Longint;
lY: Longint;
lZ: Longint;
rgbButtons: Array [0..3] of Byte; // up to 4 buttons
end;
In relative axis mode (which is default mode), lX will be filled with total distance of horizontal movement relative to previous position. So if previous mouse position is at X=100, and current position is at X=105, then lX=5. In absolute axis mode, lX will contain its absolute position i.e X=105.
In relative axis mode (which is default mode), lY will be filled with total distance of vertical movement relative to previous position. So if previous mouse position is at Y=100, and current position is at Y=105, then lY=5. In absolute axis mode, lX will contain its absolute position i.e Y=105.
In relative axis mode,for mouse with scrolling feature. lZ will be filled with total distance of wheel movement relative to previouse position.
To test a button is pressed we check array rgbButtons where high-order bit will be set if button is pressed. Lef mouse button is rgbButtons[0], right mouse button is rgbButtons[1], middle button is at rgbButtons[2]. Example:
function LeftButton_is_down:boolean;
begin
result:=(FBuffer.rgbButtons[0] and $80)=$80;
end;
where FBuffer is type of TDIMouseState.
TDIMouseState2 is similar to TDIMouseState, except thats rgbButtons is type of array[0..7] of byte.
lX, lY, lZ hold movement position of x, y, z axis coordinat. Test is carried out similar to mouse i.e test element of rgbButtons whether its high order bit is set.
type
TDIJoyState2 = packed record
lX : Longint; (* x-axis position *)
lY : Longint; (* y-axis position *)
lZ : Longint; (* z-axis position *)
lRx : Longint; (* x-axis rotation *)
lRy : Longint; (* y-axis rotation *)
lRz : Longint; (* z-axis rotation *)
rglSlider : array [0..1] of Longint; (* extra axes positions *)
rgdwPOV : array [0..3] of Cardinal; (* POV directions *)
rgbButtons : array [0..127] of Byte; (* 128 buttons *)
lVX : Longint; (* x-axis velocity *)
lVY : Longint; (* y-axis velocity *)
lVZ : Longint; (* z-axis velocity *)
lVRx : Longint; (* x-axis angular velocity *)
lVRy : Longint; (* y-axis angular velocity *)
lVRz : Longint; (* z-axis angular velocity *)
rglVSlider : array [0..1] of Longint; (* extra axes velocities *)
lAX : Longint; (* x-axis acceleration *)
lAY : Longint; (* y-axis acceleration *)
lAZ : Longint; (* z-axis acceleration *)
lARx : Longint; (* x-axis angular acceleration *)
lARy : Longint; (* y-axis angular acceleration *)
lARz : Longint; (* z-axis angular acceleration *)
rglASlider : array [0..1] of Longint; (* extra axes accelerations *)
lFX : Longint; (* x-axis force *)
lFY : Longint; (* y-axis force *)
lFZ : Longint; (* z-axis force *)
lFRx : Longint; (* x-axis torque *)
lFRy : Longint; (* y-axis torque *)
lFRz : Longint; (* z-axis torque *)
rglFSlider : array [0..1] of Longint; (* extra axes forces *)
end;
Lost device is condition when application loses focus, typically because user move to another application. GetDeviceState() will return value of DIERR_INPUTLOST when device is lost. To reacquire we need to call Acquire() again.
Some joystick do not report its state automatically, because of that, for joystick, calling Poll() is highly recommended before calling GetDeviceState().
function Poll : HResult; stdcall;
It returns DIERR_INPUTLOST when device is lost.
If we are finished, we must release IDirectInput8 and IDirectInputDevice8 instance. For Delphi, due to reference counting, instance of interface automatically be freed when it is out of scope, so we don't have to do anything, but if you want to release it explicitly, just set variable which holding IDirectInput8 and IDirectInputDevice8 instance with nil value.
We already have basic knowledge about how to utilize DirectInput, let us continue with how to develop useful framework to wrap DirectInput functionalities into a simple application programming interface.
We are going to develop classes structure as shown in following UML diagram:
Fig.1 DirectInput encapsulation UML diagram.
This class enkapsulate IDirectInput8. Every application use this framework must create one TDIObject instance. It manages IDirectInput8 instance lifetime and it is responsible to instantiate IDirectInput8 instance.
TBaseInput class is base class for every type of input device. It does basic tasks associated with device include creating device, setting up cooperative level, acquiring input device and also data polling. Choosing device type is not be done in this class and must be implemented by derived class.
TBaseInput will need TDIObject instance to initialize device and TDIData.
TKeyboardInput is class derived from TBaseInput, its purpose is to encapsulate keyboard device. This class creates and initializes TKeybordData nstance.
TMouseInput is derived from TBaseInput, its purpose is to encapsulate mouse device. This class maintains lifetime of instance of TMouseData.
TJoystickInput is derived from TBaseInput, its purpose is to encapsulate mouse device. This class maintains lifetime of instance of TJoystickData.
This class is base class for managing data receive input device. It declares Input property which refers to TBaseInput instance. It actually doesn't do anything, derived classes must override GetDeviceState and Init implementation.
TKeyboardData manages data from keyboard and initializes keyboard data format to c_dfDIKeyboard.
TMouseData manages data from mouse and initialize mouse data format to c_dfDIMouse2.
TJoystickData manages data from joystick and initialize joystick data format to c_dfDIJoystick2.
This class is derived from TCollection and it is wrapper for input device enumeration process. It has Search method which its purpose is to start enumeration.
Each time enumeration process finds input device, TInputEnumerator instance will create instance of TInputItem and add it into input device list. This class has properties for holding GUID of input device and name such as guidInstance, guidProduct, ProductName and InstanceName.
TDIObject is declared as follow:
TDIObject=class(TObject)
private
FDirectInputObj: IDirectInput8;
public
constructor Create;
destructor Destroy;override;
published
property DirectInputObj:IDirectInput8 read FDirectInputObj;
end;
Following code snippet is complete implementation
{ TDIObject }
constructor TDIObject.Create;
begin
DirectInput8Create(HInstance,DIRECTINPUT_VERSION,
IID_IDirectInput8,
FDirectInputObj,
nil);
if FDirectInputObj=nil then
raise EDIError.Create('Inisialisasi IDirectInput8 gagal');
end;
destructor TDIObject.Destroy;
begin
FDirectInputObj:=nil;
inherited;
end;
TBaseInput is declared in code below.
TBaseInput=class(TObject)
private
FDeviceObj: IDirectInputDevice8;
FDIObject: TDIObject;
FDIData: TDIData;
FCooperativeLevel: TCooperativeLevel;
FHandle: HWND;
procedure SetDIObject(const Value: TDIObject);
procedure SetDIData(const Value: TDIData);
procedure SetCooperativeLevel(const Value: TCooperativeLevel);
procedure SetHandle(const Value: HWND);
public
constructor Create;virtual;
destructor Destroy;override;
procedure Init(const guid:TGUID);virtual;
procedure Acquire;
procedure Unacquire;
procedure Poll;
published
property DIObject:TDIObject read FDIObject write SetDIObject;
property DIData:TDIData read FDIData write SetDIData;
property DeviceObj:IDirectInputDevice8 read FDeviceObj;
property CooperativeLevel:TCooperativeLevel read FCooperativeLevel write SetCooperativeLevel;
property Handle:HWND read FHandle write SetHandle;
end;
CooperativeLevel property has type of TCooperativeLevel which declared as follow. Its default values are cldForeground and cldNonExclusive.
TCooperativeLevelData=(cldBackground,cldExclusive,
cldForeground,cldNonExclusive,
cldNoWinKey);
TCooperativeLevel=set of TCooperativeLevelData;
Full impelementation is shown in code below. We make create constructor as virtual because child classes need to override this constructor. In destroy destructor, we free FDIData instance. FDIData will be created by derived classes. Acquire, Unacquire and Poll method are wrappers for Acquire, Unacquire and Poll method of IDirectInputDevice8 interface.
{ TBaseInput }
procedure TBaseInput.Acquire;
begin
if FDeviceObj<>nil then
FDeviceObj.Acquire;
end;
constructor TBaseInput.Create;
begin
FDeviceObj:=nil;
FCooperativeLevel:=[cldForeground,cldNonExclusive];
end;
destructor TBaseInput.Destroy;
begin
FDIData.Free;
FDeviceObj:=nil;
inherited;
end;
procedure TBaseInput.Init(const guid: TGUID);
var aflag:cardinal;
hr:Hresult;
begin
if (FDIObject<>nil) and
(FDIObject.FDirectInputObj<>nil) then
begin
FDIObject.FDirectInputObj.CreateDevice(guid,
FDeviceObj,
nil);
if FDeviceObj<>nil then
begin
aflag:=0;
if cldBackground in FCooperativeLevel then
aflag:=aflag or DISCL_BACKGROUND;
if cldExclusive in FCooperativeLevel then
aflag:=aflag or DISCL_EXCLUSIVE;
//foreground tdk boleh dicombine dgn background
if (cldForeground in FCooperativeLevel) and
(not (cldBackground in FCooperativeLevel)) then
aflag:=aflag or DISCL_FOREGROUND;
//nonexclusive tdk boleh dicombine dgn exclusive
if (cldNonExclusive in FCooperativeLevel) and
(not (cldExclusive in FCooperativeLevel)) then
aflag:=aflag or DISCL_NONEXCLUSIVE;
if cldNoWinKey in FCooperativeLevel then
aflag:=aflag or DISCL_NOWINKEY;
hr:=FDeviceObj.SetCooperativeLevel(FHandle,aflag);
if hr<>DI_OK then
raise Exception.Create('Set cooperative level gagal');
if FDIData<>nil then
FDIData.Init;
end else
raise EDIError.Create('Inisialisasi IDirectInputDevice8 gagal');
end;
end;
procedure TBaseInput.Poll;
begin
if FDeviceObj<>nil then
FDeviceObj.Poll;
end;
procedure TBaseInput.SetCooperativeLevel(const Value: TCooperativeLevel);
begin
FCooperativeLevel := Value;
end;
procedure TBaseInput.SetDIData(const Value: TDIData);
begin
FDIData.Free;
if value<>nil then
begin
FDIData := Value;
FDIData.Input:=self;
end;
end;
procedure TBaseInput.SetDIObject(const Value: TDIObject);
begin
FDIObject := Value;
end;
procedure TBaseInput.SetHandle(const Value: HWND);
begin
FHandle := Value;
end;
procedure TBaseInput.Unacquire;
begin
if FDeviceObj<>nil then
FDeviceObj.Unacquire;
end;
What need to get our attention is Init implementation. In this method, we create device based on guid passed from parameter. If we succeed, we set cooperative level. We need to convert cooperative level. Next, Init method of FDIData is called ton initialize data format of input device.
TKeyboardInput=class(TBaseInput)
public
constructor Create;override;
procedure Init(const guid:TGUID);override;
end;
We override Create constructor and Init method. In the constructor, we create instance of TKeyboardData. Input property are then set to instance of TKeyboardInput that create it.
{ TKeyboardInput }
constructor TKeyboardInput.Create;
begin
inherited;
FDIData:=TKeyboardData.Create;
FDIData.Input:=self;
end;
procedure TKeyboardInput.Init(const guid: TGUID);
var aguid:TGUID;
begin
if isEqualGUID(guid,GUID_NULL) then
aguid:=GUID_SysKeyboard
else
aguid:=guid;
inherited Init(aguid);
end;
We override Init method by adding GUID checking. If GUId is equal to GUID_NULL, then we use GUID_SysKeyboard and next we call Init of ancestor.
In TKeyboardData, there are two method that we override Create and Init. We also add new method KeyDown() to test whether a button is pressed ot not. We also declared internal variable FBuffer which type of TKeyboardBuffer i.e array[0..255] of byte.
TKeyboardData=class(TDIData)
private
FBuffer:TKeyboardBuffer;
public
procedure Init;override;
function GetState:pointer;override;
function KeyDown(const key:byte):boolean;
end;
Implementation is as code below:
{ TKeyboardData }
function TKeyboardData.GetState: pointer;
var hr:HResult;
begin
if (FInput<>nil) and
(FInput.FDeviceObj<>nil) then
begin
hr:=FInput.FDeviceObj.GetDeviceState(
sizeof(TKeyboardBuffer),
@FBuffer);
if (hr=DIERR_INPUTLOST) then
FInput.Acquire;
result:=@FBuffer;
end else
result:=nil;
end;
procedure TKeyboardData.Init;
var aformat:TDIDataFormat;
hr:HResult;
begin
inherited;
if (FInput<>nil) and
(FInput.FDeviceObj<>nil) then
begin
aformat:=c_dfDIKeyboard;
hr:=FInput.FDeviceObj.SetDataFormat(aFormat);
if hr<>DI_OK then
raise Exception.Create('Set Data Format gagal');
end;
end;
function TKeyboardData.KeyDown(const key: byte): boolean;
begin
result:=(FBuffer[key] and $80)= $80;
end;
In GetState,we call GetDeviceState to fill FBuffer with keypress data. If application loses focus, we try to acquire again. We override Init by calling SetDataFormat with data format of c_dfDIKeyboard.
TMouseInput=class(TBaseInput)
public
constructor Create;override;
procedure Init(const guid:TGUID);override;
end;
TMouseInput is very similar to TKeyboardInput, except that GUID used in Init is GUID_SysMouse. In the constructor, we create instance of TMouseData.
TMouseData=class(TDIData)
private
FBuffer:TDIMouseState2;
public
procedure Init;override;
function GetState:pointer;override;
function ButtonDown(const btn:byte):boolean;
published
property X:integer read FBuffer.lX;
property Y:integer read FBuffer.lY;
property Z:integer read FBuffer.lZ;
end;
We add ButtonDown to test mouse button. We also add position property to record mouse cursor coordinat (X and Y) and wheel (Z).
{ TMouseData }
function TMouseData.ButtonDown(const btn: byte): boolean;
begin
result:=(FBuffer.rgbButtons[btn] and $80)=$80;
end;
function TMouseData.GetState: pointer;
var hr:HResult;
begin
if (FInput<>nil) and
(FInput.FDeviceObj<>nil) then
begin
hr:=FInput.FDeviceObj.GetDeviceState(
sizeof(TDIMouseState2),
@FBuffer);
if hr=DIERR_INPUTLOST then
FInput.Acquire;
result:=@FBuffer;
end else
result:=nil;
end;
procedure TMouseData.Init;
var aformat:TDIDataFormat;
begin
inherited;
if (FInput<>nil) and
(FInput.FDeviceObj<>nil) then
begin
aformat:=c_dfDIMouse2;
FInput.FDeviceObj.SetDataFormat(aformat);
end;
end;
In TJoystickInput, we only override Create, because only constructor needed to be changed. We add code to instantiate instance of TJoystickData.
TJoystickInput=class(TBaseInput)
private
public
constructor Create;override;
end;
Implementation is as code below:
constructor TJoystickInput.Create;
begin
inherited;
FDIData:=TJoystickData.Create;
FDIData.Input:=self;
end;
TJoystickData is not complete. This class has only basic functionalities for joystick. Functions for advanced data management such flight simulator joystick or steering wheel is not yet impelemented. Here, we only add method for testing joystick button press (ButtonDown), also property to record X,Y, Z coordinat of joystick and rotation coordinat RX, RY, RY.
TJoystickData=class(TDIData)
private
FBuffer:TDIJoyState2;
public
procedure Init;override;
function GetState:pointer;override;
function ButtonDown(const btn: byte): boolean;
published
property X:integer read FBuffer.lX;
property Y:integer read FBuffer.lY;
property Z:integer read FBuffer.lZ;
property RX:integer read FBuffer.lRx;
property RY:integer read FBuffer.lRy;
property RZ:integer read FBuffer.lRz;
end;
Implementation of TJoystickData class is shown in code below:
{ TJoystickData }
function TJoystickData.GetState: pointer;
var hr:HResult;
begin
if (FInput<>nil) and
(FInput.FDeviceObj<>nil) then
begin
hr:=FInput.FDeviceObj.GetDeviceState(
sizeof(TDIJoyState2),
@FBuffer);
if hr=DIERR_INPUTLOST then
FInput.Acquire;
result:=@FBuffer;
end else
result:=nil;
end;
procedure TJoystickData.Init;
var aformat:TDIDataFormat;
begin
inherited;
if (FInput<>nil) and
(FInput.FDeviceObj<>nil) then
begin
aformat:=c_dfDIJoystick2;
FInput.FDeviceObj.SetDataFormat(aformat);
end;
end;
function TJoystickData.ButtonDown(const btn: byte): boolean;
begin
result:=(FBuffer.rgbButtons[btn] and $80)=$80;
end;
You can see that it is not too different from other classes such as TKeyboardData or TMouseData.
In this class, we create a protected method called DevEnumCallback for enumeration callback.
TInputEnumerator=class(TCollection)
private
FDIObject: TDIObject;
FEnumFlag: TEnumFlag;
FDevType: cardinal;
procedure SetDIObject(const Value: TDIObject);
procedure SetEnumFlag(const Value: TEnumFlag);
procedure SetDevType(const Value: cardinal);
protected
procedure DevEnumCallback(dev:PDIDeviceInstance);virtual;
public
procedure Search;
published
property DIObject:TDIObject read FDIObject write SetDIObject;
property EnumFlag:TEnumFlag read FEnumFlag write SetEnumFlag;
property DevType:cardinal read FDevType write SetDevType;
end;
I will only explain Search and DevEnumCallback implementation, because these two method are heart of enumeration.
{ TInputEnumerator }
function _DevEnumCallback(dev:PDIDeviceInstance;
pvRev:pointer):boolean;stdcall;
begin
TInputEnumerator(pvRev).DevEnumCallback(dev);
result:=boolean(DIENUM_CONTINUE);
end;
procedure TInputEnumerator.DevEnumCallback(dev: PDIDeviceInstance);
var item:TInputItem;
begin
item:=Add as TInputItem;
item.InstanceName:=dev.tszInstanceName;
item.ProductName:=dev.tszProductName;
item.guidInstance:=dev.guidInstance;
item.guidProduct:=dev.guidProduct;
item.guidForceFeedbackDriver:=dev.guidFFDriver;
end;
procedure TInputEnumerator.Search;
var dwflag:cardinal;
begin
if (FDIObject<>nil) and
(FDIObject.FDirectInputObj<>nil) then
begin
dwFlag:=0;
if efAll in FEnumFlag then
dwFlag:=dwFlag or DIEDFL_ALLDEVICES;
if efAttached in FEnumFlag then
dwFlag:=dwFlag or DIEDFL_ATTACHEDONLY;
if efForceFeedback in FEnumFlag then
dwFlag:=dwFlag or DIEDFL_FORCEFEEDBACK;
if efIncludeAlias in FEnumFlag then
dwFlag:=dwFlag or DIEDFL_INCLUDEALIASES;
if efIncludeHidden in FEnumFlag then
dwFlag:=dwFlag or DIEDFL_INCLUDEHIDDEN;
if efIncludePhantoms in FEnumFlag then
dwFlag:=dwFlag or DIEDFL_INCLUDEPHANTOMS;
FDIObject.FDirectInputObj.EnumDevices(FDevType,
@_DevEnumCallback,
pointer(self),
dwflag);
end;
end;
procedure TInputEnumerator.SetDevType(const Value: cardinal);
begin
FDevType := Value;
end;
procedure TInputEnumerator.SetDIObject(const Value: TDIObject);
begin
FDIObject := Value;
end;
procedure TInputEnumerator.SetEnumFlag(const Value: TEnumFlag);
begin
FEnumFlag := Value;
end;
In Search method, we do conversion of EnumFlag into cardinal type data. Next, we call EnumDevice and pass device type, callback _DevEnumCallback(), address of instance of TInputEnumerator and enumeration flag. _DevEnumCallback are actual callback. It then call DevEnumCallback method. Using this technique, we can use method of class as callback.
DevEnumCallback is responsible to create instance of TInputItem to store guidInstance.guidProduct, instance name, and name of product.
TInputItem, derived from TCollectionitem, is responsible to store data about enumerated input device for future use. This class does not add new behaviour, just a couple of properties to store input device data.
TInputItem=class(TCollectionItem)
private
FInstanceName: string;
FProductName: string;
FguidForceFeedbackDriver: TGUID;
FguidProduct: TGUID;
FguidInstance: TGUID;
procedure SetguidForceFeedbackDriver(const Value: TGUID);
procedure SetguidInstance(const Value: TGUID);
procedure SetguidProduct(const Value: TGUID);
procedure SetInstanceName(const Value: string);
procedure SetProductName(const Value: string);
published
property InstanceName:string read FInstanceName write SetInstanceName;
property ProductName:string read FProductName write SetProductName;
property guidInstance:TGUID read FguidInstance write SetguidInstance;
property guidProduct:TGUID read FguidProduct write SetguidProduct;
property guidForceFeedbackDriver:TGUID read FguidForceFeedbackDriver write SetguidForceFeedbackDriver;
end;
We have discussed all classes in the framework. Let us create demo to utilize this framework. There are two demos we are going to make. First demo is input device enumeration demo (dienum.dpr). Figure below is the screenshot.

Fig.2 Enumeration demo screenshot.
Second demo (di.dpr), we improve demo Finite State Machine. Here we add new action i.e crouch (Arrow Down), low kick (J+Arrow Down), low punch (K+Arrow Down), and low heavy punch (L+Arrow Down). Figure below is screenshot.

Fig.3 Screenshot of spiderman demo with DirectInput.
Source code of demos and all framework we discussed is available for download here. Ok that's all, see you around for next topic.
Do you like this article? Help this website improve by donating. Any amounts is appreciated.
Or you can help by bookmarking this page.
Bookmark this on Delicious