









DirectInput adalah salah satu komponen dalam DirectX yang fungsi utamanya adalah untuk membungkus proses akses perangkat input (mouse, keyboard, joystick dan lain-lain) menjadi suatu antar muka yang seragam untuk berbagai macam perangakt input yang berbeda-beda. Produsen perangkat input sangat banyak, cara mengaksesnya pun bisa berbeda-beda.
Jika anda balik ke jaman DOS dulu, mengembangkan aplikasi game jauh lebih melelahkan. Sebuah aplikasi game, jika ingin memiliki pangsa pasar luas, wajib mendukung sebanyak mungkin hardware yang mungkin dimiliki end-user. Ini berarti pengembang aplikasi game harus mengembangkan sendiri kode-kode untuk mengakses hardware yang berbeda-beda atau menggunakan produk pihak ketiga.
DirectX, khususnya DirectInput, menjembatani problem ini dengan menggunakan layer tipis yang disebut Hardware Abstraction Layer (HAL). Agar suatu perangakat input dapat digunakan di sistem operasi Windows, vendor hardware wajib mengimplementasikan HAL. Aplikasi game tidak lagi mengakses perangkat keras secara langsung, namun melalui perantara DirectInput. DirectInput berkomunikasi dengan device driver melalui HAL dan perangkat keras diakses oleh device driver.
Karena DirectInput berkomunikasi langsung ke hardware driver, DirectInput mem-bypass messaging system Windows. Oleh karena itu, jika kita menggunakan DirectInput untuk mengakses keyboard, maka message-message seperti WM_KEYDOWN atau WM_KEYUP tidak akan di kirim ke aplikasi kita.
Aplikasi game (contohnya game bergenre fighting) biasanya menggunakan kombinasi beberapa tombol untuk memerintahkan karakter dalam game melakukan suatu aksi tertentu misalkan menendang kearah bawah, player harus menekan tombol panah bawah dan tombol Enter bersamaan. Kombinasi semacam ini tidak dapat dideteksi jika kita menggunakan message WM_KEYDOWN. Kombinasi yang dapat dideteksi menggunakan WM_KEYDOWN hanya untuk tombol Shift. Alt, dan Ctrl dengan tombol-tombol lain.
Problem lain yang menjadi masalah adalah delay yang terjadi sewaktu sebuah tombol ditekan sebelum akhirnya dianggap sebagai pengulangan penekanan tombol. Untuk aplikasi game, delay ini tidak diinginkan. Aplikasi game biasanya menginginkan ketika tombol panah kanan ditekan, karakter dalam game segera berpindah ke kanan terus-menerus sampai tombol panah dilepaskan.
Programmer game DOS dulu, biasanya membelokkan interrupt 09h untuk mengatasi problem tombol kombinasi dan problem delay di atas. Dengan DirectInput kita tidak perlu berurusan dengan interrupt. Ok let's get it on..
Untuk menjalankan demo yang akan kita buat, kita membutuhkan:
Untuk bisa menggunakan DirectInput, kita perlu mendapatkan alamat instance IDirectInput8 (di sini kita akan membahas DirectX versi 8 keatas saja). Bagaimana caranya? Ada dua cara yakni dengan menggunakan fungsi DirectInput8Create() atau dengan CoCreateInstance().
function DirectInput8Create(hinst : THandle;
dwVersion : Cardinal;
const riidltf : TGUID;
out ppvOut;
punkOuter : IUnknown) : HResult; stdcall;
hInst adalah handle instance aplikasi atau DLL, dwVersion adalah versi SDK DirectInput. Isi dengan konstanta DIRCTINPUT_VERSION atau menggunakan $8000 untuk versi 8. riidltf adalah pengenal interface IDirectInput8. Anda bisa menggunakan IID_IDirectInput8 atau bisa juga langsung nama interfacenya yakni IDirectInput8. ppvout berisi variabel yang akan menampung alamat instance IDirectInput8. punkOuter bisa diisi dengan nil. Parameter ini digunakan untuk agregasi COM yang di sini tidak kita pergunakan. Contoh:
DirectInput8Create(HInstance,DIRECTINPUT_VERSION,
IID_IDirectInput8,
FDirectInputObj,
nil);
Menggunakan DirectInput8Create() adalah cara termudah. Jika inin menggunakan CoCreateInstance() langkahnya adalah sebagai berikut:
CoCreateInstance(CLSID_DirectInput8,
punkOuter,
CLS_CTX_INPROC_SERVER,
IID_IDirectInput8,
FDirectInputObj);
Ketiga langkah di atas dijadikan satu dalam DirectInput8Create(). Di artikel ini kita akan menggunakan DirectInput8Create.
Selanjutnya kita perlu mendapatkan pointer ke IDirectInputDevice8. Untuk bisa melakukan hal tersebut, kita perlu tahu GUID input device yang akan kita gunakan. Oleh karena itu kita perlu melakukan enumerasi input device. Untuk keyboard dan mouse, DirectInput sudah menyediakan GUID khusus yakni GUID_SYSKeyboard dan GUID_SysMouse, jadi untuk dua input device standar ini, kita tidak wajib melakukan enumerasi. Untuk input device lain seperti joystick, gamepad dan lain-lain yang bukan input device standar komputer, kita wajib mengetahui GUID-nya.
Proses enumerasi input device dikerjakan dengan memanggil fungsi EnumDevice() milik IDirectInput8.
function EnumDevices(dwDevType : Cardinal;
lpCallback : TDIEnumDevicesCallback;
pvRef : Pointer;
dwFlags : Cardinal) : HResult; stdcall;
dwDevType adalah tipe device yang ingin kita enumerasi. Parameter ini akan membatasi device yang di enumerasi. Kita bisa menggunakan konstanta kelas device berikut ini:
Selain menggunakan konstanta device class, kita bisa juga menggunakan kontanta untuk device yang spesifik. Misalnya untuk melakukan enumerasi khusus untuk input device gamepad saja kita bisa menggunakan DI8DEVTYPE_GAMEPAD. Untuk enumerasi steering wheel saja, kita menggunakan DI8DEVTYPE_DRIVING. Untuk kontroler flight simulator DI8DEVTYPE_FLIGHT dan untuk joystick DI8DEVTYPE_JOYSTICK. Penggunaan DI8DEVCLASS_KEYBOARD equivalen dengan DI8DEVTYPE_KEYBOARD dan DI8DEVCLASS_POINTER equivalen dengan DI8DEVTYPE_MOUSE dan DI8DEVTYPE_SCREENPOINTER.
lpCallback berisi alamat fungsi callback yang akan dipanggil ketika device yang tergolong dalam dwDevType ditemukan. Kita akan membahas callback ini segera. pvRef adalah user data yang akan di kirim ke callback.
dwFlags berisi scope enumerasi. Kita menggunakan konstanta berikut ini:
Callback ini bertipe fungsi berformat sebagai berikut:
TDIEnumDevicesCallback = function (var lpddi : TDIDeviceInstance;
pvRef : Pointer) : Integer; stdcall;
lpddi berisi variabel bertipe TDIDeviceInstance yang akan menampung data input device. pvRev adalah user data yang dikirim (lihat pvRef pada EnumDevice). Struktur data TDIDeviceInstance adalah sebagai berikut:
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;
TDIDeviceInstance sebenarnya ada dua versi yakni Unicode dan Ansi. Untuk Unicode TDeviceInstance = TDIDeviceInstance_DX5W. Perbedaannya terletak pada tipe tszInstanceName dan tszProductName, dimana versi Unicode menggunakan WideChar bukan AnsiChar. Kita selalu akan menggunakan Ansi. OK mari kita bahas field-fieldnya.
Kita harus mengembalikan nilai boolean DIENUM_CONTINUE atau DIENUM_STOP untuk memberitahu DirectInput apakah kita ingin melajutkan enumerasi atau tidak.
Setelah kita mendapatkan GUID input device yang kita inginkan, kita dapat menciptakan IDirectInputDevice8 menggunakan CreateDevice() milik IDirectInput8.
function CreateDevice(const rguid : TGUID;
out lplpDirectInputDevice : IDirectInputDevice8;
pUnkOuter : IUnknown) : HResult; stdcall;
rguid kita set dengan GUID instance yang sudah kita peroleh lewat enumerasi. Atau jika menggunakan keyboard atau mouse bisa kita isi dengan GUID_SysKeyboard atau GUID_SysMouse. lplpDirectInputDevice menampung alamat pointer ke interface IDirectInputDevice8. Jika gagal akan diisi nil. punkOuter kita set nil.
Jika device sukses diciptakan, langkah selanjutnya adalah menentukan format data device menggunakan SetDataFormat() milik IDirectInputDevice8.
function SetDataFormat(var lpdf : TDIDataFormat) : HResult; stdcall;
lpdf bisa diisi dengan c_dfDIKeyboard untuk format data keyboard. Untuk mouse bisa kita gunakan c_dfDIMouse dan c_dfDIMouse2. c_dfDIMouse kita pergunakan untuk menghandle mouse yang memiliki tombol hingga 4 button sedangkan c_dfDIMouse2 kita pergunakan untuk menghandle mouse yang memiliki tombol hingga 8 button. Untuk joystick kita bisa menggunakan c_dfDIJoystick atau c_dfDIJoystick2. Versi 2 menghandle joytick dengan tombol hingga 128 tombol. Di artikel ini kita akan selalu menggunakan c_dfDIMouse2 dan c_fDIJoystick2. Sebenarnya kita bisa menggunakan format milik kita sendiri, namun mengingat artikel ini membahas dasar-dasar DirectInput, sengaja tidak saya jelaskan. Contoh:
var aformat:TDIDataFormat;
begin
aformat:=c_dfDIKeyboard;
FDeviceObj.SetDataFormat(aFormat);
end;
Mengingat aplikasi kita berjalan pada lingkungan multitasking, kita perlu meminta ijin kepada pengelola shared resource yakni sistem operasi bahwa aplikasi kita hendak memanfaatkan input device. Kita perlu mengatur level kooperatif aplikasi kita menggunakan SetCooperativeLevel() milik IDirectInputDevice8. Level kooperatif menentukan perilaku aplikasi ketika berbagi resource. Ada beberapa jenis level kooperatif. yang utama adalah foreground, background, eksklusif, non eksklusif.
Level kooperatif foreground berarti aplikasi kita hanya akan menerima input bila statusnya foreground. Jika aplikasi kehilangan fokus, misal karena user berpindah ke aplikasi lain, aplikasi kita tidak menerima data dari input device.
Level kooperatif background menyebabkan aplikasi kita tetap dapat menerima data dari input device meskipun statusnya background. Jika user berpindah ke aplikasi lain dengan level kooperatif background, aplikasi kita tidak lagi dapat menerima data. Level foreground dan background tidak dapat di kombinasi, satu aplikasi harus memiliki salah satu level foreground/background namun tidak dapat memiliki level foreground dan background sekaligus.
Dengan level eksklusif, tidak ada objek aplikasi lain yang diijinkan mengakses device yang sedang diakses oleh aplikasi. Namun aplikasi lain yang berada pada level non eksklusif tetap dapat menggunakan input. Penggunaan level ini pada mouse menyebabkan kursor akan hilang hal ini dikarenakan Windows mengakses mouse menggunakan level yang sama. Level ini tidak dapat dikombinasi dengan level non eksklusif dan aplikasi harus memilih salah satu. Selain keempat level diatas, ada satu level kooperatif tambahan yakni NoWinkey yang akan menonaktifkan tombol Windows di keyboard.
Untuk mengatur level kooperatif kita menggunakan konstanta berikut:
Fungsi SetCooperativeLevel deklarasinya adalah sebagai berikut:
function SetCooperativeLevel(hwnd : HWND;
dwFlags : Cardinal) : HResult; stdcall;
hwnd kita isi dengan handle form aplikasi. dwFlags kita isi dengan kombinasi flag di atas.
Sebelum aplikasi kita dapat menerima data dari input hardware, kita harus mendapatkan akses ke input device dengan memanggil fungsi Acquire() milik IDirectInputDevice8.
function Acquire : HResult; stdcall;
Untuk menghentikan akses kita memanggil Unacquire() milik IDirectInputDevice8.
function Unacquire : HResult; stdcall;
Setelah device kita dapatkan aksesnya, untuk mengambil status input device saat ini, kita menggunakan fungsi GetDeviceState().
function GetDeviceState(cbData : Cardinal;
lpvData : Pointer) : HResult; stdcall;
cbData kita isi dengan ukuran data yang alamatnya ditunjuk oleh lpvData. lpvData berisi alamat variabel yang akan menampung status input device saat ini. Struktur data yang kita kirim ke fungsi ini sangat berkaitan erat dengan proses mengatur format data yang kita diskusikan di atas. Jika format data yang kita gunakan adalah c_dfDIKeyboard, maka lpvData harus berisi alamat varibel bertipe array sebanyak 256 byte. Jika kita menggunakan c_dfDIMouse, maka lpvData berisi alamat variabel bertipe TDIMouseState. Untuk lengkapnya lihat tabel di bawah:
| Format Data | Struktur device state |
|---|---|
| c_dfDIKeyboard | Array [0..255] of byte |
| c_dfDIMouse | TDIMouseState |
| c_dfDIMouse2 | TDIMouseState2 |
| c_dfDIJoystick | TDIJoyState |
| c_dfDIJoystick2 | TDIJoyState2 |
Contoh
type
TKeyboardBuffer=array [0..255] of byte;
var FBuffer:TKeyboardBuffer;
begin
FDeviceObj.GetDeviceState(sizeof(TKeyboardBuffer),
@FBuffer);
end;
Tiap elemen dalam array mencatat status penekanan tiap-tiap tombol keyboard, di mana high order bit akan bernilai 1 bila tombol tersebut ditekan. DirectInput menyediakan kontanta untuk masing-masing tombol keyboard. Contohnya DIK_A untuk tombol huruf A (a atau A sama saja), DIK_ESCAPE untuk tombol ESC dan lain-lain. Jadi untuk menguji apakah tombol K ditekan kita lakukan hal berikut:
function K_is_down:boolean;
begin
result:=(FBuffer[DIK_K] and $80)=$80;
end;
Untuk menguji apakah panah bawah ditekan:
function ArrowDown_is_down:boolean;
begin
result:=(FBuffer[DIK_DOWN] and $80)=$80;
end;
Untuk menguji tombol kombinasi K+Panah bawah, kita tinggal mengecek seperti berikut ini:
if K_is_down and
Arrow_is_down then
doSomething;
Deklarasi TDIMouseState adalah sebagai berikut:
type
TDIMouseState = packed record
lX: Longint;
lY: Longint;
lZ: Longint;
rgbButtons: Array [0..3] of Byte; // up to 4 buttons
end;
Pada mode axis relatif (yang merupakan mode default), lX akan diisi dengan total jarak perpindahan horizontal relatif terhadap posisi sebelumnya. Jadi jika sebelumnya, mouse berada di koordinat X=100, dan posisi berikutnya berada di X=105, maka lX=5. Pada mode axis absolut, yang dikembalikan adalah posisi absolutnya yakni X=105.
Pada mode axis relatif, lY akan diisi dengan total jarak perpindahan vertikal relatif terhadap posisi sebelumnya. Jadi jika sebelumnya, mouse berada di koordinat Y=100, dan posisi berikutnya berada di Y=105, maka lY=5. Pada mode axis absolut, yang dikembalikan adalah posisi absolutnya yakni Y=105.
Pada mode axis relatif, untuk mouse dengan fitur scrolling. lZ akan diisi dengan total jarak perpindahan wheel relatif terhadap posisi sebelumnya.
Penekanan tombol diuji dengan mengecek array rgbButtons di mana high-order bit akan diset 1 bila tombol ditekan. Tombol mouse kiri adalah rgbButtons[0], tombol mouse kanan adalah rgbButtons[1], tombol tengah rgbButtons[2]. Contoh:
function LeftButton_is_down:boolean;
begin
result:=(FBuffer.rgbButtons[0] and $80)=$80;
end;
di mana FBuffer bertipe TDIMouseState.
TDIMouseState2 hampir sama dengan TDIMouseState, perbedaannya rgbButtons bertipe array[0..7] of byte.
lX, lY, lZ berisi posisi perpindahan koordinat sumbu x, y, z. Untuk menguji penekanan tombol, caranya sama dengan pada mouse yakni menguji elemen pada rgbButtons apakah high-ordernya diset 1.
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 adalah kondisi yang terjadi saat aplikasi kehilangan fokus, biasanya karena user berpindah ke aplikasi lain. Fungsi GetDeviceState() mengembalikan nilai DIERR_INPUTLOST bila device lost. Untuk mendapatkan kembali akses kita perlu memanggil Acquire() lagi.
Beberapa joystick tidak otomatis melaporkan statusnya, oleh karena itu untuk joystick, memanggil fungsi Poll() sangat disarankan sebelum memanggil GetDeviceState().
function Poll : HResult; stdcall;
Fungsi ini mengembalikan DIERR_INPUTLOST bila device lost.
Jika sudah selesai, aplikasi wajib merelease instance IDirectInput8 dan IDirectInputDevice8. Untuk kompiler Delphi, karena instance interface otomatis di free bila sudah out of scope, kita tidak harus melakukan apa-apa, tapi jika ingin merelease secara explisit, isi saja variabel yang menyimpan alamat IDirectInput8 dan IDirectInputDevice8 dengan nil.
Kita sudah memiliki pengetahuan dasar mengenai bagaimana menggunakan DirectInput, mari lanjut ke bagaimana menyusun framework yang berguna untuk membungkus fungsionalitas DirectInput menjadi antar muka yang sederhana.
Kita akan menyusun susunan kelas yang diagram UML-nya terlihat seperti gambar di bawah:
Gbr.1 Diagram UML enkapsulasi DirectInput.
Kelas ini merupakan enkapsulasi IDirectInput8. Aplikasi yang menggunakan framework ini wajib menciptakan satu instance TDIObject. Kelas ini mengatur lifetime dari instance IDirectInput8 dan bertanggung jawab menginisialisasi instance IDirectInput8.
Kelas TBaseInput adalah kelas dasar bagi semua tipe input device. Kelas ini melakukan pekerjaan-pekerjaan dasar berkaitan device meliputi menciptakan device, mengatur level kooperatif, mendapatkan akses ke input device (acquire) dan juga melakukan polling. Proses menentukan tipe device tidak dilakukan di kelas ini dan harus diimplementasikan oleh kelas turunan.
TBaseInput akan membutuhkan instance TDIObject untuk proses insialisasi device dan juga TDIData.
Kelas TKeyboardInput merupakan turunan TBaseInput, tugasnya adalah menciptakan instance device keyboard. Kelas ini akan menciptakan instance TKeybordData.
Kelas TMouseInput merupakan turunan TBaseInput, tugasnya adalah menciptakan instance device mouse. Kelas ini juga menciptakan instance TMouseData.
Kelas TJoystickInput merupakan turunan TBaseInput, tugasnya adalah menciptakan instance device joystick. Kelas ini akan menciptakan instance TJoystickData.
Kelas ini adalah kelas dasar manajemen pengelolaan data yang diterima dari input device. Kelas ini mendeklarasikan property Input yang akan mengacu pada kelas TBaseInput. Defaultnya kelas ini tidak melakukan apa-apa, kelas turunan wajib mengoverride fungsi GetDeviceState, dan Init.
TKeyboardData mengelola proses pembacaan data dari keyboard dan insialisasi data format keyboard ke c_dfDIKeyboard.
TMouseData mengelola proses pembacaan data dari mouse dan insialisasi data format mouse ke c_dfDIMouse2.
TJoystickData mengelola proses pembacaan data dari joystick dan insialisasi data format joystick ke c_dfDIJoystick2.
Kelas ini diturunkan dari TCollection untuk membungkus proses enumerasi input device. Kelas ini dilengkapi dengan metode Search yang berguna memulai proses enumerasi.
Tiap kali proses enumerasi menemukan input device, instance TInputEnumerator akan menciptakan instance TInputItem dan menambahkan ke daftar input device. Kelas ini dilengkapi property-property yang mencatat GUID input device seperti guidInstance, guidProduct, ProductName dan InstanceName.
Deklarasi kelas TDIObject adalah sebagai berikut:
TDIObject=class(TObject)
private
FDirectInputObj: IDirectInput8;
public
constructor Create;
destructor Destroy;override;
published
property DirectInputObj:IDirectInput8 read FDirectInputObj;
end;
Kode implementasi lengkapnya
{ 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;
Berikut ini adalah deklarasi kelas TBaseInput.
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;
Property CooperativeLevel bertipe TCooperativeLevel yang deklarasinya adalah sebagai berikut. Nilai defaultnya akan berisi cldForeground dan cldNonExclusive
TCooperativeLevelData=(cldBackground,cldExclusive,
cldForeground,cldNonExclusive,
cldNoWinKey);
TCooperativeLevel=set of TCooperativeLevelData;
Implementasi lengkapnya adalah sebagai berikut. Konstruktor create kita jadikan virtual karena kelas turunan perlu meng-override konstruktor. Pada destruktor destroy, kita bebaskan FDIData. FDIData akan di create oleh kelas turunan. Metode Acquire, Unacquire dan Poll sekedar pembungkus bagi fungsi Acquire, Unacquire dan Poll milik IDirectInputDevice8.
{ 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;
Yang perlu mendapat perhatian mungkin implementasi Init. Di metode ini, kita ciptakan device berdasarkan guid yang dilewatkan parameter. Jika sukses, kita set level kooperatif. Kita perlu mengkonversi data kooperatif level. Selanjutnya metode Init milik FDIData dipanggil untuk menyiapkan data format input device.
TKeyboardInput=class(TBaseInput)
public
constructor Create;override;
procedure Init(const guid:TGUID);override;
end;
Konstruktor Create dan Init kita override. Di konstruktor create, kita ciptakan instance TKeyboardData. Property Input kita set ke instance TKeyboardInput yang menciptakannya.
{ 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;
Metode Init kita override dengan menambahkan pengecekan guid. Jika guid sama dengan GUID_NULL, maka kita gunakan GUID_SysKeyboard dan selanjutnya kita panggil Init milik ancestor.
Di kelas TKeyboardData, dua metode kita override dan kita tambahkan fungsi baru KeyDown() yang berguna untuk menguji apakah sebuah tombol ditekan atau tidak. Variabel internal FBuffer bertipe TKeyboardBuffer yang berupa 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;
Implementasinya adalah sebagai berikut:
{ 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;
Pada GetState, kita panggil GetDeviceState untuk mengisi FBuffer dengan data penekanan tombol. Jika aplikasi kehilangan fokus, kita coba lakukan acquire lagi. Metode Init kita override dengan memanggil SetDataFormat menggunakan data format c_dfDIKeyboard.
TMouseInput=class(TBaseInput)
public
constructor Create;override;
procedure Init(const guid:TGUID);override;
end;
Implementasi TMouseInput sangat mirip TKeyboardInput, kecuali bahwa guid pada metode Init yang kita pakai adalah GUID_SysMouse. Di konstruktor kita ciptakan instance 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;
Kita tambahkan fungsi ButtonDown untuk menguji penekanan tombol mouse. Kita juga menambahkan property posisi koordinat kursor mouse (X dan Y) dan koordinat 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;
Di kelas TJoystickInput yang kita override hanya Create, karena di konstruktor kita perlu menambahkan kode untuk menciptakan instance TJoystickData.
TJoystickInput=class(TBaseInput)
private
public
constructor Create;override;
end;
Implementasinya adalah seperti di bawah ini:
constructor TJoystickInput.Create;
begin
inherited;
FDIData:=TJoystickData.Create;
FDIData.Input:=self;
end;
Kelas TJoystickData sesungguhnya belum lengkap. Kelas ini baru memiliki fungsi-fungsi dasar untuk joystick, fungsi-fungsi pengelolaan data yang tergolong advanced seperti untuk joystick flight simulation atau steering wheel belum diimplementasikan. Di sini kita baru menambahkan metode untuk menguji penekanan tombol joystick (ButtonDown), serta property untuk mencatat koordinat X,Y, Z joystick dan koordinat rotasi 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;
Implementasi kelas TJoystickData adalah sebagai berikut:
{ 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;
Tidak terlalu jauh berbeda dengan kelas lain seperti TKeyboardData atau TMouseData.
Pada kelas ini kita buat sebuah metode protected bernama DevEnumCallback yang berfungsi sebagai callback proses enumerasi.
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;
Implementasinya hanya akan saya jelaskan untuk metode Search dan DevEnumCallback, karena kedua metode ini adalah jantung dari proses enumerasi.
{ 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;
Di dalam metode Search, kita lakukan konversi EnumFlag menjadi representasi data cardinal. Kemudian kita panggil EnumDevice dengan melewatkan parameter device type, fungsi callback _DevEnumCallback(), alamat instance TInputEnumerator dan flag enumerasi. Fungsi _DevEnumCallback berfungsi sekedar untuk mengalihkan pemanggilan ke metode DevEnumCallback. Dengan cara ini kita bisa menggunakan metode kelas sebagai callback.
DevEnumCallback kemudian bertanggung jawab menciptakan instance TInputItem yang kemudian digunakan untuk menyimpan data-data guidInstance.guidProduct, nama instance, dan nama produk.
Kelas TInputItem adalah turunan TCollectionitem, bertanggung jawab menyimpan data-data mengenai input device yang sudah dienumerasi untuk digunakan kelak. Kelas ini tidak menambahkan perilaku baru, cuma beberapa property untuk menyimpan data terkait dengan input device.
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;
Semua kelas dalam framework sudah kita diskusikan. Mari membuat demo untuk memanfaatkan framework yang sudah disusun. Ada dua demo yang akan kita buat. Demo pertama adalah demo proses enumerasi input device (dienum.dpr). Berikut ini adalah screenshotnya.

Gbr.2 Screenshot demo enumerasi.
Demo kedua (di.dpr), kita memperbaiki aplikasi demo Finite State Machine. Di sini kita akan menambahkan aksi baru yakni, membungkuk (crouch) (Arrow Down), menendang ke arah bawah (J+Arrow Down), memukul ke arah bawah (K+Arrow Down), dan memukul dengan keras ke arah bawah (L+Arrow Down). Berikut ini adalah screenshot aplikasinya.

Gbr.3 Screenshot demo spiderman menggunakan DirectInput.
Source code aplikasi demo dan seluruh framework yang kita bahas dapat di download di sini. Ok itu saja, sampai jumpa di topik berikutnya.
Anda suka artikel ini? Bantu website ini berkembang dengan menyumbang. Berapapun jumlahnya akan sangat dihargai.
Atau Anda dapat membantu dengan membuat bookmark.
Bookmark this on Delicious