Juhara.com
Language : English Indonesia

Menggunakan DirectInput untuk mengontrol Game

Zamrony P Juhara
09 October 2006 13:13:00
 (5107 views)
Artikel tentang bagaimana menggunakan DirectInput untuk mengontrol karakter pemain dalam game

Apa dan Mengapa DirectInput?

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..

Apa yang dibutuhkan?

Untuk menjalankan demo yang akan kita buat, kita membutuhkan:

Lalu bagaimana menggunakannya?

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:

  1. Inisialisasi COM dengan memanggil CoInitializeEx().
  2. Panggil CoCreateInstance() menggunakan CLSID_DirectInput8
    CoCreateInstance(CLSID_DirectInput8,
                     punkOuter,
                     CLS_CTX_INPROC_SERVER,
                     IID_IDirectInput8,
                     FDirectInputObj);
  3. Memanggil Initialize() milik IDirectInput8.

Ketiga langkah di atas dijadikan satu dalam DirectInput8Create(). Di artikel ini kita akan menggunakan DirectInput8Create.

Enumerasi Input Device yang Ada.

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:

  • DI8DEVCLASS_ALL
    Semua tipe input device.
  • DI8DEVCLASS_DEVICE
    Semua device yang tidak termasuk dalam kelas lain.
  • DI8DEVCLASS_GAMECTRL
    Semua device yang termasuk game controller seperti joystick, gamepad, steering wheel dan lain-lain.
  • DI8DEVCLASS_KEYBOARD
    Semua device yang tergolong keyboard.
  • DI8DEVCLASS_POINTER
    Semua device yang tergolong sebagai penunjuk. Contohnya mouse, track ball, pen stick, light pen, touch screen dan lain-lain.

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:

  • DIEDFL_ALLDEVICES
    Semua device yang terinstall
  • DIEDFL_ATTACHEDONLY
    Hanya device yang terinstall dan saat ini sedang terpasang
  • DIEDFL_FORCEFEEDBACK
    Device yang support force feedback
  • DIEDFL_INCLUDEALIASES
    Menyertakan device yang merupakan alias device lain.
  • DIEDFL_INCLUDEHIDDEN
    Menyertakan hidden device. Hidden device adalah device fiktif yang dibuat device driver agar bisa membangkitkan event keyboard dan mouse
  • DIEDFL_INCLUDEPHANTOMS
    Menyertakan device yang tergolong placeholder

Callback Enumerasi

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.

  • dwSize, ukuran TDeviceInstance dalam byte.
  • guidInstance, GUID device yang terinstall di WIndows. GUID ini berbeda di dari satu komputer ke komputer lain. GUID ini yang kita perlukan dan harus kita simpan untuk dipakai nanti.
  • guidProduct, GUID produk dibuat oleh vendor hardware.
  • dwDevType, tipe device.
  • tszInstanceName, friendly name instance.
  • tszProductName, friendly name hardware.
  • guidFFDriver, GUID device driver force feedback. GUID ini dibuat oleh vendor.
  • wUsagePage dan wUsage hanya relevan untuk device yang tergolong Human Interface Device (HID).

Kita harus mengembalikan nilai boolean DIENUM_CONTINUE atau DIENUM_STOP untuk memberitahu DirectInput apakah kita ingin melajutkan enumerasi atau tidak.

Menciptakan Device

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.

Mengatur Format Data.

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;

Mengatur Level Kooperatif.

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:

  • DISCL_FOREGROUND
  • DISCL_BACKGROUND
  • DISCL_EXCLUSIVE
  • DISCL_NONEXCLUSIVE
  • DISCL_NONWINKEY

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.

Mendapatkan Akses ke Device (Acquiring Device).

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;

Menghentikan Akses (Unacquiring Device).

Untuk menghentikan akses kita memanggil Unacquire() milik IDirectInputDevice8.

    function Unacquire : HResult; stdcall;

Mendapatkan Device State.

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;

Memproses Keyboard Device State.

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;

Memproses Mouse Device State.

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.

Memproses Joystick Device State.

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;

Menangani Kondisi "Lost Device"

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.

Polling Data

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.

Clean up

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.

Mengembangkan Framework Enkapsulasi DirectInput

Desain

Kita akan menyusun susunan kelas yang diagram UML-nya terlihat seperti gambar di bawah:

Diagram UML enkapsulasi DirectInput

Gbr.1 Diagram UML enkapsulasi DirectInput.

TDIObject

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.

TBaseInput

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.

TKeyboardInput

Kelas TKeyboardInput merupakan turunan TBaseInput, tugasnya adalah menciptakan instance device keyboard. Kelas ini akan menciptakan instance TKeybordData.

TMouseInput

Kelas TMouseInput merupakan turunan TBaseInput, tugasnya adalah menciptakan instance device mouse. Kelas ini juga menciptakan instance TMouseData.

TJoystickInput

Kelas TJoystickInput merupakan turunan TBaseInput, tugasnya adalah menciptakan instance device joystick. Kelas ini akan menciptakan instance TJoystickData.

TDIData

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

TKeyboardData mengelola proses pembacaan data dari keyboard dan insialisasi data format keyboard ke c_dfDIKeyboard.

TMouseData

TMouseData mengelola proses pembacaan data dari mouse dan insialisasi data format mouse ke c_dfDIMouse2.

TJoystickData

TJoystickData mengelola proses pembacaan data dari joystick dan insialisasi data format joystick ke c_dfDIJoystick2.

TInputEnumerator

Kelas ini diturunkan dari TCollection untuk membungkus proses enumerasi input device. Kelas ini dilengkapi dengan metode Search yang berguna memulai proses enumerasi.

TInputItem

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.

Implementasi

Implementasi TDIObject

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;

Implementasi TBaseInput

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.

Implementasi TKeyboardInput

  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.

Implementasi TKeyboardData

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.

Implementasi TMouseInput

  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.

Implementasi 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;

TJoystickInput

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;

Implementasi TJoystickData

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.

Implementasi TInputEnumerator

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.

Implementasi TInputItem

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;

Membuat Demo Aplikasi

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.

Screenshot demo enumerasi

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.

Screenshot aplikasi DirectInput

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. Delicious Bookmark this on Delicious