DirectInput Programlama – Bölüm 2
Bu makalede, birinci bölümde öğrendiklerimizi, uygulamaya nasıl geçireceğimizi öğreneceğiz. Ayrıca bir aygıttan nasıl veri okuyacağımızı da göreceğiz. Bunun için örnek bir klavye ve bir mouse nesnesi oluşturacağız. Tabi ki, oluşturduğumuz bu nesneleri, uygulamalarımız ile nasıl ilişkilendirebileceğimizi de göreceğiz. Bu makale, önceki sürümünden biraz farklılık gösteriyor. Önceki makalede eksik gördüğüm bazı yerlerde düzenlemeler yaptım.
İçindekiler
Bölüm 2
Aygıtın Odaklanma(Focus) Durumu
Hepsini Birleştirelim
Daha önceden bir pencere oluşturup, Direct3D gibi olayları atladığınızı varsayıyorum.
Bu makalemizde uygulamaımızı temsil eden bir adet cApplication nesnesi olsun. Bu sınıfa DirectInput nesnesini oluşturacak bir fonksiyon ekleyelim. Hatırlatmak gerekirse, başlamadan önce dinput8.lib ve d3dguid.lib kütüphanelerini projemize eklemiş olmamız lazım. Fonksiyonumuz şu şekilde olsun:
//----------------- cApplication.h dosyası ----------
#include "dinput.h"
//DirectInput'un bize lazım olan sürümü
#define DIRECTINPUT_VERSION 0x0800
class cApplication
{
public:
IDirectInput8* DirectInput; //Genel DirectInput nesnemiz
void InitDirectInput(void); //DirectInput nesnesini hazırlayan metod
}
//----------------- cApplication.cpp dosyası ----------
void cApplication::InitDirectInput()
{
if (FAILED(DirectInput8Create(hInst, DIRECTINPUT_VERSION,
IID_IDirectInput8, (void**)&DirectInput, NULL)))
ShowError("Sisteminizde en az DirectX 8 kurulu olması lazım.");
}
DirectInput nesnesini oluşturma kısmını zaten Bölüm-1′den hatırlıyoruz. En sondaki ShowError fonksiyonu, hata mesajları göstermek için tarafımca yazılan bir fonksiyon. Siz burada MessageBox fonksiyonunu kullanabilirsiniz. DirectInput nesnesini oluşturduktan sonra iki adet listener sınıfı hazırlayacağız birisi klavye için diğeri mouse için. İkisi de zor değil ama klavye’de mouse göre daha fazla yapılacak iş var. İsterseniz ilk başta klavyeden başlayalım. Klavye için ilk önce bize özel bir sınıf oluşturalım ve işimizi kolaylaştıracak metodlar tanımlayalım.
//----------------- cKeyboard.h dosyası ----------
#include "cApplication.h"
struct IKeyboardReceiver
{
virtual void OnKeyDown(int Key) = 0;
virtual void OnKeyUp(int Key) = 0;
}
class cApplication;
class cKeyboard
{
private:
IKeyboardReceiver* pReceiver;
IDirectInputDevice8* KeyboardDevice; //Aygıt nesnemiz
char KeyStates[256]; //Standart klavye veri formatı en fazla 256 tuşu destekler
public:
cKeyboard(cApplication* Owner, IKeyboardReceiver* Receiver);
virtual ~cKeyboard(void);
bool KeyPressed(int Key);
void ClearTable(void);
void Update(void);
}
Listener oluşturmayı zaten Bölüm-1′den gördük. Ek olarak buradaki bazı metodların açıklanması gerek. Öncelikle tuşların basıldı basılmadı gibi durumlarını KeyStates isimli bir dizide tutuyoruz. Constructor’ın aldığı ilk parametre olan Owner, cApplication sınıfından bir nesne alıyor. Bunu almamızın sebebi, aygıtı oluştururken gerekecek olan DirectInput nesnesi ve Pencerenin Handle değeridir. Bu nesneler, cApplication sınıfımızda bulunmaktadır. İkinci parametre olan Receiver, bizim tanımlamış olduğumuz klavye arayüzünden türeyecek olan bir nesnedir. İstenirse bu işlem sadece fonksiyonlarla da halledilebilir. Ama biz nesne tabanlı programlama mantığına göre gitmek istiyoruz. Üstelik ileride bunun işmiz ne kadar kolaylaştırdığını da göreceksiniz. Diğer fonksiyonları kodlarını verdikten sonra anlatalım. Å?imdi sınıfımızın constructor ‘ını yazalım.
//----------------- cKeyboard.cpp dosyası ----------
#include "cKeyboard.h"
cKeyboard::cKeyboard(cApplication* Owner, IKeyboardReceiver* Receiver)
{
if (Receiver)
{
pReceiver = Receiver;
if (FAILED(Owner->DirectInput->CreateDevice(GUID_SysKeyboard,
&KeyboardDevice, NULL)))
ShowError(L"Aygıtınızın DirectInput uyumlu değil");
KeyboardDevice->SetDataFormat(&c_dfDIKeyboard);
KeyboardDevice->SetCooperativeLevel(Owner->Handle,
DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
ClearTable();
}
}
Aslında burada bize yabancı olan pek bir şey yok. Çünkü birinci bölümde aygıt nesnesini oluşturmayı öğrenmiştik. Burada ek olarak bunları bir sınıf tanımlaması içinde görüyoruz. Merak edebileceğiniz diğer bir nokta ise, pRecevier nesnemizi, SetReceiver gibi ayrı bir fonksiyon ile de alabilirdik. Ama ikinci bir fonksiyon çağırımı olmasın diye bu işi de constructor’a yerleştirdim. Bize yabancı olan tek şey sondaki ClearTable() metodudur. Bu da bizim tanımladığımız sınıfın bir metodudur. Bunu da şu şekilde ifade edelim:
//----------------- cKeyboard.cpp dosyası Devamı ----------
void cKeyboard::ClearTable(void)
{
memset(KeyStates, 0, 256*sizeof(bool));
}
Burada tek yaptığımız işlem tuşların durmlarını saklayan dizimizi sıfırlamak. Yani kalvye tuşlarını tutan dizimizi ilk haline çeviriyoruz. Böylece bu fonksiyon ile istediğimiz zaman tuşları ilk haline yani basılmamış haline çevirebiliriz. Kalvye sınıfımızdaki geriye kalan metodlarımızı da aşağıda verelim:
//----------------- cKeyboard.cpp dosyası Devamı ----------
cKeyboard::~cKeyboard(void)
{
if (KeyboardDevice)
{
KeyboardDevice->Unacquire();
KeyboardDevice->Release();
KeyboardDevice = NULL;
}
}
bool cKeyboard::KeyPressed(int Key)
{
if (KeyStates[Key] & 0x80)
return true;
return false;
}
bool cKeyboard::Update(void)
{
char NewState[256];
HRESULT Result;
Result = KeyboardDevice->Poll();
Result = KeyboardDevice->GetDeviceState(sizeof(NewState), (LPVOID) &NewState);
//Eğer başarısız olursa keyboard focus
//kaybolmuştur ya da yoktur. Bu yüzden kaybolan
//focusu alabilmek için Acquire yapıyoruz.
if (FAILED(Result))
{
Result = KeyboardDevice->Acquire();
if (FAILED(Result))
return false; //Acquire yaparken hata olursa çıkıyoruz.
//Focusu aldıktan sonra tekrar tuşları çekiyoruz.
Result = KeyboardDevice->Poll();
Result = KeyboardDevice->GetDeviceState(sizeof(NewState),
(LPVOID) &NewState);
if (FAILED(Result))
return false;
}
if (Receiver)
{
for (int i = 0; i < 256; i++)
{
//son basılan tuş önceden basılı değilse.
if (KeyStates[i] != NewState[i])
{
if (!(NewState[i] & 0x80))
Receiver->OnKeyUp(i); //Tuştan parmak kaldırılmış.
else
{
//Hiç bir şey yapmıyoruz. Çünkü, biz tuşun Up
//olmasını algılayamadan
//hemen takrar aynı tuşa basılmış. Yani çok hızlı
//bir şekilde iki kere
//Down olmuş. Bu uzun süre basmakla aynı şey.
}
}
//Son tuş durmunu kaydediyoruz.
KeyStates[i] = NewState[i];
if (KeyPressed(i))
Receiver->OnKeyDown(i);
}
}
else
{
//Receiver tanımlı olana kadar, keystate'leri saklayalım.
memcpy(KeyStates, NewState, sizeof(KeyStates));
}
return true;
}
İlk başta KeyPressed metodumuza bakalım. Bu fonksiyon, KeyStates dizisinin o anda Key ile ifade edilen sıradaki tuşun basılı olup olmadığını kontrol eder. Yani eğer tuş basılı durumdaysa sonuç true olarak dönecektir.
Update metoduna bakacak olursak, uzun bir kod yığını görürüz. Bu metod, ileride de göstereceğimiz gibi sahneyi çizdirmeden hemen önce çağırabiliriz. Genel olarak bu fonksiyonun yaptığı iş, tuşlara basılı olup olmadıklarını kontrolle beraber, herhangi bir klavye olayında Receiver nesnemizin ilgili fonksiyonuna gerekli verileri yollamaktır. Evet böyle uzun bir cümle ile anlatmaya çalışınca işler biraz daha karışabiliyor. Bu metod, hemen hemen esas işi yaptığından dolayı ayrıntılarını ayrı başlıklar halinde açıklamak istiyorum. Å?imdi bu metodun ayrıntılarına geçelim.
Aygıtın Odaklanma Durumu
Windows programlamadan bildiğiniz Focus hadisesi, burada da geçerli. Mesela bir bir mouse nesnesi, bir pencereye focus olduğunda ya da buradaki değimi ile Acquire() olduğunda, pencere üzerindeki icraatlarından dolayı söz hakkına sahip olmuş olur. Veya mesela bir klavye nesnesi, bir textbox’a odaklanmamış ise, ya da buradaki ifadesi ile acquire olmamış ise o textbox, klavyeden verileri okuyamaz. Hangi nesne klavye odağına sahipse o klavye verilerini alacaktır. Eğer bir nesne Acquire olmamış ise herhangi bir veri alamayız. Tabi ki focus’u yani acquire işlemini, nesnemiz silindiğinde iptal etmemiz gerekmektedir. Bunun için, destructor’da (cKeyboard::~cKeyboard) gördüğünüz Unacquire() fonksiyonunu kullanıyoruz. Aksi halde, mesela mouse DISCL_EXCLUSIVE olarak tanımlanmış ise, hatırlayacağınız üzere, Unacquire() yapmadığınız sürece Windows mouse’dan mesaj alamayacaktır.
Aygıt Verilerinin Okunması
Result = KeyboardDevice->Poll();
Result = KeyboardDevice->GetDeviceState(sizeof(NewState), (LPVOID) &NewState);
Burada Poll işleminden sonra gelen verileri alabilmek için GetDeviceState metodunu kullanıyoruz. ilk parametre veri formatımızın boyutunu, ikinci parametre de LPVOID cinsinden veri formatımızın referansını almaktadır. Bu örnekte, klavye durumları yeni bir diziye alınıp o şekilde işlem yapılıyor. Bu iki metoddan birisi başarısız olduğunda FAILED makrosu ile hatalı olarak çıkıyoruz.
Ardından gelen for döngüsündeki açıklama satırları kafi geleceği kanaatindeyim bu yüzden ek açıklama yapmıyorum.
Å?imdi bu bilgiler eşliğinde ilk başta Update olmak üzere diğer fonksiyonları da gözden geçirelim. Ardından biraz ara verelim, gözlerimizi dinlendirelim. Çünkü gerçekten çok işleri hallettik. Ardından bunu uygulamamızda nasıl kullanacağımızı görelim.
Uygulamamızda Kullanalım
Buraya kadar herşey güzel de, bunu uygulamamızda nasıl kullanacağız? Tek yapmamız gereken önceden de dediğimiz gibi Receiver arayüzlerinden sınıfımızı türetmek olacaktır. Aşağıda cApplication nesnemizden türeyen bir sınıf mevcut. Burada Direct3D’ye girmeyeceğiz. Sadece DirectInput’u uygulamamızda nasıl kullanacağız onu görelim.
//------------ cExample.h dosyası -----------
#include "cApplication.h"
#include "cKeyboard.h"
#include "cMouse.h"
class cExample:
public cApplication, public IKeyboardReceiver, public IMouseReceiver
{
.......
cMouse* Mouse;
cKeyboard* Keyboard;
cExample(void);
void Render(void);
virtual void OnKeyDown(int Key);
virtual void OnKeyUp(int Key);
virtual void OnMouseDown(int Key);
virtual void OnMouseUp(int Key);
virtual void OnMouseMove(int Key);
......
}
//------------ cExample.cpp dosyası -----------
#include "cExample.h"
cExample::cExample(void)
{
......
Mouse = new Mouse(this, this); //Birinci parametre Owner, diğeri Receiver
Keyboard = new Keyboard(this, this);
}
void cExample::Render(void)
{
Keyboard->Update();
Mouse->Update();
Clear(...);
BeginScene();
...
EndScene();
Present(...);
}
cExample::onKeyDown(int Key)
{
switch(Key)
{
case DIK_LEFT: Adamı_Sola_Gotur();
case DIK_SPACE: Adamı_Zıplat();
.....
}
}
Gördüğünüz gibi fazla yapılacak bir şey yok. Hangi aygıtı kullanacaksak, ona ait Receiver arayüzlerinden nesnemizi türetiyoruz. Ardından ona ait metodları kullanabiliyoruz. Tabiki Keyboard ve Mouse nesnelerimizi oluşturduktan sonra… Keyboard ve Mouse nesnelerini oluştururken verdiğimiz iki parametre’ye dikkat edin. İkisi de this‘dir. Çünkü cExample nesnesi hem cApplication sınıfındandır hem de IKeyboardReceiver ve IMouseReceiver özelliklerini taşır. Böylece cExample sınıfı ile hem Direct3D fonskiyonlarını çalıştırıp çizim yapabiliyoruz hem de kalvye ve mouse aygıtlarını kullanabiliyoruz. Ama burada eksik kalan tek şey, mouse aygıtının tanımlanmasıdır. Å?imdi dilerseniz, klavye’ye ye göre farkllarını görelim.
Mouse Aygıtı
Önceden de bahsettiğim gibi bu aygıtın programlaması, klavyeye göre daha basittir. Farklı olan yanlarını şimdi göstermeye çalışalım.
Result = MouseDeivce->SetCooperativeLevel(Owner->Handle, DISCL_EXCLUSIVE |
DISCL_NOWINKEY | DISCL_FOREGROUND );
Klavyeden farklı olarak, eğer tam ekran çalışıyorsak, mouse sadece bize hizmet etmelidir. Tabi ki uygulamamız aktif olduğunda. İsteğe göre DISCL_NOWINKEY sabiti çıkartılabilir. Bu fonksiyonu birinci bölümde görmüştük. Ayrıca MouseDevice nesnesinin nasıl oluşturulacağını da görmüştük.
Klavye tuşlarının durumlarını ifade ederken 256 elemanlı bir dizi kullanmıştık. Mouse’da ise verileri tutabilmek için DIMOUSESTATE isimli hazır bir struct’ı kullanıyoruz. İstersek bunu kendimiz de oluşturabilirz. Bununla ilgili olarak 1. Bölümde bir şeyler anlatmıştık.
DIMOUSESTATE LastState;
Bunu Mouse için sınıf tanımlamasında bir yere yerleştirebiliriz. Ardından aygıt nesnemizi oluşturduktan sonra bu struct’ı sıfırlayalım.
LastState.lX = 0;
LastState.lY = 0;
LastState.lZ = 0;
LastState.rgbButtons[0] = 0;
LastState.rgbButtons[1] = 0;
LastState.rgbButtons[2] = 0;
LastState.rgbButtons[3] = 0;
Buradaki ilk üç öğenin mouse’un 3 ekseni, diğer 4 öğenin de mouse’un 4 tuşu olduğunu anlamışsınızdır. Örnek olarak DIMOUSESTATE2 yapısnı da SDK yardım dosyalarından bulup inceleyebilirsiniz. Duruma göre bu struct değişik biçimlerde tanımlanabilir. Ama bu verdiğimiz struct, 3 eksenli 4 tuşlu bir mouse içindir. Günümüzdeki mouse’lar için yeter artar bile.
Å?imdi mouse için Update metoduna bir göz atalım:
bool cMouse::Update()
{
DIMOUSESTATE CurrentState;
HRESULT Result;
Result = MouseDevice->Poll();
Result = MouseDevice->GetDeviceState(sizeof(DIMOUSESTATE),
(void*)&CurrentState);
if(FAILED(Result))
{
Result = MouseDevice->Acquire();
if(FAILED(Result))
{
return false;
}
Result = MouseDevice->Poll();
Result = MouseDevice->GetDeviceState(sizeof(DIMOUSESTATE),
(void*)&CurrentState);
if(FAILED(Result))
{
return false;
}
}
if(pReceiver)
{
int dx = CurrentState.lX;
int dy = CurrentState.lY;
if(dx || dy)
{
pReceiver->OnMouseMove(dx, dy);
}
if(CurrentState.rgbButtons[0] & 0x80)
{
// 1. düğmeye basıldı
pReceiver->OnMouseDown(0);
}
if(CurrentState.rgbButtons[1] & 0x80)
{
// 2. düğmeye basıldı
pReceiver->OnMouseDown(1);
}
if(CurrentState.rgbButtons[2] & 0x80)
{
pReceiver->OnMouseDown(2);
}
if(!(CurrentState.rgbButtons[0] & 0x80) && (LastState.rgbButtons[0]
& 0x80))
{
// birinci düğme kaldırıldı.
pReceiver->OnMouseUp(0);
}
if(!(CurrentState.rgbButtons[1] & 0x80) && (LastState.rgbButtons[1]
& 0x80))
{
pReceiver->OnMouseUp(1);
}
if(!(CurrentState.rgbButtons[2] & 0x80) && (LastState.rgbButtons[2]
& 0x80))
{
pReceiver->OnMouseUp(2);
}
}
LastState = CurrentState;
return true;
}
Aslında klavyeden pek farkı yok. Tek yaptığımız ilk başta aygıt nesnemizin Poll metodunu çağırmak ve ardından GetDeviceState ile istediğimiz formatta verileri almak. Buradaki diğer kısımlar tuşun basılıp basılmaması ile ilgili mantık kısımlarıdır. Fazla anlaşılması zor değiller. Klavyede olduğu gibi KeyPressed gibi ekstra bir metod tanımlayabilir ve durumları 80h sayısal değeri ile and işlemine tabi tutabilirdik. Son durumu ifade eden LastState struct’ını oluşturabilmek için, CurrentState gibi aynı yapıda geçici bir struct tanımlıyoruz. Ve işlemleri bununla yapıyoruz. Bu Update metodunun çağrıldığı yeri cExample örnek sınıfımızda Render metodunda göstermiştik. Böylece mouse verilerini de uygulamamızda rahatlıkla işleyebiliyoruz.
Başka Konular
Normalde şu haliyle uygulamamız kalvye ve mouse verilerini işleyebilmektedir. Ama isteğe göre bu veriler özelleştirilebilir. Mesela klavyede tuşların basılı tutulma hadisesini iptal etmek istiyoruz. Yani klavyeden bir tuşa sürekli basıldığında bir çok işlem yerine sadece bir işlem yaptırtmak istiyoruz. Bunun için kullanacağınız mantık; tuşların adedi kadar bir boolean dizi tanımlayıp, her tuş kontrolünde bu diziyi de kontrol edilmesidir. Yani, "A" tuşuna ne kadar basılı tutulursa tutulsun, sanki tek bir A harfine basmış gibi algılatmak için; boolean dizimizdeki A harfine denk gelen elemanı, A harfine basıldığında true yapmalı, sonra da bunu istenilen yerde kontrol etmek yeterlidir. OnKeyDown metodunda A’yı algıladığımızda, dizimizde A’nın kodu olan 65′inci elemanı true yapıyoruz. OnKeyDown metodunda switch case’e tekrar A harfi gelirse bu sefer kontrol ettiriyoruz, acaba dizimizdeki 65′inci eleman true’ mu eğer true ise hiç bir işlem yapmıyoruz, false ise dizimizdeki 65′inci elemanı true yapıp, A’ya basılınca yapılması gereken işlemleri yapıyoruz.
Bunun dışında karşılaşabileceğiniz olaylardan birisi de joystick kullanımıdır. Önceden de dediğim gibi joystick aygıtı tanımlaması klavye ve mouse ile aynıdır. Buradaki bilgililer eşliğinde SDK help dosyalarının da yardımı ile joystick ve joypadler sizin için kolay olacaktır.
Sonuç Olarak
Artık makalenin sonuna geldik. Buraya kadar anlattıklarımız, üzerinde uygulama yapılması gereken bilgilerdir. DirectX’in hiç bir konusu yüzeysel okunup geçilecek şeyler değildir. Bu yüzden bol bol örnek incelemeli ve sdk yardım dosyalarını karıştırmalısınız.
Bir başka makalede görüşmek dileği ile.
Fatih Tolga Ata © 2007




teşekkürler