Selasa, 23 September 2008

MEMBUAT APLIKASI MULTIPLAYER DENGAN DIRECTPLAY

MEMBUAT APLIKASI MULTIPLAYER DENGAN DIRECTPLAY

Sudah paham bagaimana mengelola sesi permainan? Kini saatnya diskusi bagaimana menangani pesan-pesan yang dikirim DirectPlay ke aplikasi kita. Zamrony P. Juhara

BAGIAN 3 DARI 3 ARTIKEL

DI BAGIAN TERAKHIR dari tiga seri artikel ini, kita akan focus ke bagaimana menangani pesan-pesan jaringan yang dikirim ke aplikasi. Jika Anda belum membaca dua artikel sebelumnya, sebaiknya baca terlebih dulu, karena Anda akan kehilangan kesinambungan tanpanya.

Menangani Pesan Jaringan

Seperti yang tercantum di Tabel 2 di artikel bagian 1, jumlah pesan jaringan yang dikirim oleh DirectPlay cukup banyak. Di sini, penulis hanya akan membahas beberapa yang paling sering dipergunakan saja.

Pemain Bergabung ke Sesi

Tiap kali ada pemain yang baru bergabung dalam sesi, semua anggota sesi akan menerima pesan DPN_MSGID_CREATE_PLAYER bersamaan dengan data bertipe PDPNMsgCreatePlayer terkait kejadian tersebut. Biasanya yang paling penting adalah mengatur data pemain (player context) dan mendapatkan pengenal pemain tersebut terutama bila Anda perlu menampilkan nama pemain yang ada dalam sesi. Tipe data PDPNMsgCreatePlayer adalah pointer ke tipe record TDPNMsgCreatePlayer yang field-field-nya adalah sebagai berikut:

· dwSize, ukuran struktur data dalam Dword.

· dpnidPlayer, pengenal pemain bertipe TDPNID.

· pvPlayerContext, data pemain bertipe pointer.

Contoh bagaimana menangani pesan DPN_MSGID_CREATE_PLAYER tercantum pada Listing 27.a. Di listing tersebut, pvUserContext aman kita typecast ke tipe TDNNetwork dengan asumsi bahwa pada saat memanggil Initialize(), kita melewatkan alamat instance kelas turunan TDPNetwork (Self) seperti pada Listing 27.b. dwMessageType berisi tipe pesan yang dikirim DirectPlay. Bila pesannya bertipe DPN_MSGID_CREATE_ PLAYER, pMessage dapat kita typecast ke tipe PDPNMsgCreatePlayer dengan aman. Selanjutnya kita panggil metode DoPlayerConnect() yang implementasinya ada pada Listing 28.a. Metode ini adalah metode berlingkup terproteksi (protected) milik kelas TDNPeerNetwork yang merupakan turunan kelas TDPNetwork. Anda bisa mempelajari kelas enkapsulasi DirectPlay ini pada file udplay_network.pas yang ada di CD/DVD.

Pada saat memasuki DoPlayerConnect(), kita alokasi memory untuk data pemain dan juga menampung pengenal player. Tipe PPlayerInfo adalah tipe yang kita deklarasikan untuk mencatat data pemain (Listing 28.b). Anda bisa memodifi kasinya sesuai kebutuhan.

Selanjutnya kita kirim message MSG_DPN_CONNECT ke window yang handle-nya ada di FwindowHandle, pengenal pemain kita kirim melalui wParam. FWindowHandle adalah window milik kelas TDPNetwork yang diciptakan menggunakan AllocateHwnd() pada saat pemanggilan konstruktor kelas TDPNetwork dan didealokasi pada saat pemanggilan destruktor. MSG_DPN_CONNECT adalah window message yang kita definisikan sendiri yang akan dikirim ke window procedure bernama WndProc() milik TDPNetwork (Listing 28.c). Ketika MSG_DPN_CONNECT diterima, even OnPlayerConnected akan dibangkitkan.

Lalu mengapa harus repot mengirim pemberitahuan melalui mekanisme window message? Cara ini penulis pilih untuk menghindari deadlock yang mungkin terjadi. DoPlayerConnect harus membangkitkan event OnPlayerConnected milik TDPNetwork untuk memberitahu adanya pemain yang baru bergabung ke sesi. Event OnPlayerConnected bisa saja diisi dengan kode yang tidak thread-safe, misalnya meng-update kontrol visual, sedangkan DoPlayerConnect() dipanggil dalam thread yang berbeda dari thread aplikasi.

Pemain Meninggalkan Sesi

Ketika pemain meninggalkan sesi permainan, DPN_MSGID_DESTROY_PLAYER akan dikirim ke semua pemain anggota sesi. Cara penanganan pesan ini mirip dengan kejadian pemain bergabung ke sesi. Bedanya kita memanggil DoPlayerDisconnect() (lihat Listing 29). Di metode ini, data pemain yang sudah dialokasi dengan new() kita bebaskan dengan dispose(). Kita juga mengirim pesan MSG_DPN_DISCONNECT ke window TDPNetwork.

Data Diterima

Pesan DPN_MSGID_RECEIVE dikirim ketika kita menerima data dari pemain lain yang dikirim menggunakan SendTo().Contoh bagaimana menangani pesan ini ada pada Listing 30. pMessage kita typecast ke PDPNMsgReceive yang merupakan pointer ke tipe record TDPNMsgReceive yang field-field-nya adalah sebagai berikut:

· dwSize, ukuran struktur data TDPNMsgReceive bertipe Dword.

· dpnidSender, pengenal pengirim bertipe TDPNID.

· pvPlayerContext, data terkait pengirim, bertipe pointer.

· pReceiveData, data yang dikirim, bertipe pointer.

· dwReceiveDataSize, ukuran data yang ada di pReceiveData, bertipe Dword.

· hBufferHandle, handle ke data yang ada di pReceiveData.

· dwReceiveFlags, bertipe Dword, flag terkait cara data diterima. dwReceiveFlags berisi DPNRECEIVE_GUARANTEED bila data dikirim dengan DPNSEND_GUARANTEED.

Selanjutnya metode AddReceive() milik TDPNetwork (Listing 31) dipanggil untuk memproses data yang diterima.

Ketika memasuki metode AddReceive(), kita copy data yang diterima ke buffer kita sendiri. Hal ini harus kita lakukan bila kita mengembalikan nilai DPN_OK pada callback, karena saat keluar dari callback, pointer yang ditunjuk oleh pReceiveData mungkin sudah tidak lagi valid. Selanjutnya data kita tambahkan ke list yang mencatat data yang diterima. Tidak lupa kita cegah akses thread lain ke FReceive yang bertipe TList ketika menambah data.

Bila Anda ingin mencegah DirectPlay membebaskan pReceiveData, atur nilai kembali callback dengan DPNSUCCESS_PENDING. Dengan nilai ini, kepemilikan pReceiveData akan diserahkan ke aplikasi dan selanjutnya aplikasi yang bertanggung jawab membebaskan pReceiveData dengan memanggil ReturnBuffer() milik IDirectPlay8Peer dan melewatkan hBuffer-Handle sebagai parameter.

Kita tidak langsung memproses data yang kita terima, namun hanya kita tambahkan ke antrian untuk mencegah thread DirectPlay terbebani. Pemrosesan data-data dikerjakan ketika kita memanggil metode ProcessNetworkMsg() (Listing 32). Kita tidak akan memproses semua data yang terima dalam sekali pemanggilan ProcessNetworkMsg(). Alasannya, karena data yang dikirim oleh pemain lain bisa sangat banyak, memproses semuanya sekaligus mungkin akan menyebabkan game menjadi tidak responsif. Solusinya adalah dengan memproses pesan-pesan dari jaringan hanya dalam interval waktu tertentu. Jika waktu pemrosesan sudah lewat (timeout) kita segera keluar dari ProcessNetworkMsg(). Oleh karena itu, kelas TDPNetwork memiliki properti NetworkProcTimeOut yang menentukan berapa lama waktu dalam milidetik yang dialokasikan untuk memproses pesan jaringan.

Di kelas TDPNetwork, interpretasi terhadap data dikerjakan oleh aplikasi melalui event OnReceiveData milik TDPNetwork. Kelas TDPNetwork hanya bertanggung jawab mengambil data yang diterima dari daftar antrian dengan urutan First-In-First-Out (FIFO). Setelah data diproses aplikasi, data dihapus dari antrian. Proses ini diulangi sampai waktu pemrosesan sudah terlampaui atau sudah tidak ada data dalam antrian. Tidak lupa kita cegah akses FReceive oleh thread lain selama kita masih menggunakannya.

Pada koneksi peer-to-peer, ketika host meninggalkan sesi dan sesi tersebut dapat berpindah host, DirectPlay akan mengirim DPN_MSGID_HOST_MIGRATE ke semua pemain dalam sesi untuk memberitahukan pemain lain yang ditunjuk sebagai host baru. Data host baru dapat diperoleh dari pMessage yang sebelumnya di-typecast ke tipe PDPNMsgHostMigrate. Tipe ini merupakan pointer ke tipe TDPNMsgHostMigrate yang field- field-nya terdiri atas:

· dwSize, ukuran struktur data TDPNMSgHostMigrate dalam Dword.

· dpnidNewHost, pengenal host yang baru bertipe TDPNID.

· pvPlayerContext, data terkait host yang baru bertipe pointer.

Listing 33 dan 34 berisi contoh bagaimana memproses pesan DPN_MSGID _HOST_MIGRATE. Yang kita lakukan hanya menyimpan pengenal host yang baru. Data pada field pvPlayerContext tidak kita simpan karena, kita dapat mendapatkan data pemain dari pengenalnya.

Enumerasi Host

Tiap host yang kompatibel dengan aplikasi Anda dan yang menjawab permintaan enumerasi aplikasi Anda akan diinformasikan oleh DirectPlay melalui pesan DPN_MSGID_ENUM_HOSTS_RESPONSE. Tipe data yang terkait pesan ini adalah PDPNMsgE-numHostsResponse. Field-fi eld-nya terdiri atas:

· dwSize, ukuran struktur data TDPNMsgEnumHostsResponse, bertipe Dword.

· pAddressSender, instance alamat host, bertipe IDirectlay8Address.

· pAddressDevice, instance alamat device, bertipe IDirectlay8Address.

· pApplicationDescription, deskrpsi aplikasi, bertipe PDNApplicationDesc.

· pvResponseData, pointer ke data respon.

· dwResponseDataSize, ukuran data respon.

· pvUserContext, data yang sama dengan pvUserContext pada EnumHosts().

· dwRoundTripLatencyMS, waktu latency dalam milidetik.

Dari semua field di atas, Anda hanya perlu menyimpan data pAddressSender dan pApplicationDescription. Host sebuah sesi bisa saja menjawab permintaan lebih dari satu kali. Oleh sebab itu, Anda perlu membandingkan field guidInstance pada pAp-plicationDesc. Jika sebelumnya guidInstance ini sudah pernah merespons, abaikan saja.

Proses pencatatan host yang merespon dijalankan dalam metode AddSession() (Listing 35 dan Listing 36). Ketika memasuki AddSession(), kita cegah akses FSessions oleh thread lain. Lalu kita cek apakah guidInstance sudah ada agar tidak ada pencatatan ganda. Jika belum ada, kita copy pAddressSender dengan memanggil metode Duplicate() milik pAddressSender juga deskripsi aplikasi yang kemudian kita tambahkan ke daftar sesi.

Penutupan Sesi

Ketika sesi ditutup dengan TerminateSession() atau host meninggalkan sesi yang tidak mampu berpindah host, pesan DPN_MSGID_TERMINATE_SESSION akan dikirim ke callback. Data terkait pesan ini adalah PDPNMsgTerminateSession dengan field-field-nya:

· dwSize, ukuran struktur data bertipe Dword.

· hResultCode, kode penutupan sesi. Bila host ditutup dengan TerminateSession(). Isinya adalah DPNERR_HOSTTERMI-NATEDSESSION. Bila ditutup dengan Close() atau host terputus koneksi jaringannya, isi field ini adalah DPNERR_CON-NECTIONLOST.

· pvTerminateData, bertipe pointer, data yang dikirim melalui parameter pvTerminateData pada TerminateSession() (Listing 26).

· dwTerminateData, ukuran data di pvTerminateData bertipe Dword.

Anda perlu memproses pesan ini bila, Anda ingin tahu kapan dan bagaimana sebuah sesi ditutup.

Aplikasi Demo

Di CD/DVD terdapat source code implementasi lengkap kelas TDPNetwork dan TDPPeerNetwork yang mengenkapsulasi proses inisialisasi DirectPlay peer-to-peer, membuat dan bergabung dengan sebuah sesi, melakukan enumerasi host dan mengirimkan data.

Selain itu, terdapat pula contoh aplikasi bagaimana memanfaatkan kelas TDPPeerNetwork untuk membuat aplikasi chat memanfaatkan DirectPlay. Pada aplikasi ini, proses manajemen chat dibungkus dalam kelas TDPChat. Tampilan aplikasi demo ketika sedang dijalankan adalah seperti yang terlihat pada Gambar 6.

Aplikasi demo ditulis menggunakan Turbo Delphi, namun Anda dapat menggunakan kompiler Delphi lain tanpa banyak perubahan. Unit konversi header DirectX yang dipergunakan ditulis Clootie (http://clootie.ru).

Untuk mencoba aplikasi demo, paling tidak Anda membutuhkan dua buah komputer yang terkoneksi jaringan. Aplikasi demo ini menggunakan TCP/IP sehingga Anda perlu memastikan protokol TCP/IP sudah terinstal di sistem Anda. Untuk mencoba aplikasi pada protokol lain seperti serial, Anda bisa mengubah service provider yang digunakan.

Ringkasan

DirectPlay memungkinkan Anda membangun aplikasi jaringan terutama game online multiplayer dengan lebih mudah. Membebaskan Anda dari detil perbedaan protokol dan perangkat keras jaringan yang menghubungkan masing-masing komputer sehingga bisa fokus ke logika aplikasi.

Anda sudah memperoleh informasi bagaimana memanfaatkan DirectPlay termasuk bagaimana menciptakan dan mengelola sesi, mengelola pemain dalam sesi dan mengirim data ke pemain lain. Khusus di artikel ini, Anda telah belajar bagaimana menangani pesan yang dikirim DirectPlay ke aplikasi kita. Sampai di sini Anda tentunya sudah memiliki gambaran bagaimana DirectPlay bekerja dan tentu saja memanfaatkannya.


Listing 27.a

function _NetworkMsgHandler(pvUserContext:pointer;

dwMessageType:Dword;

pMessage:pointer):HResult;stdcall;

var anetwork:TDPNetwork;

begin

result:=S_OK;

anetwork:=TDPNetwork(pvUserContext);

case dwMessageType of

...

DPN_MSGID_CREATE_PLAYER:begin

aNetwork.DoPlayerConnect(

PDPNMsgCreatePlayer(pMessage));

result:=DPN_OK;

end;

...

end;

end;

Listing 27.b

if FPeerNet<>nil then

FPeerNet.Initialize(self,

_NetworkMsgHandler,

Flags);

Listing 28.a

procedure TDPPeerNetwork.DoPlayerConnect(

pResponse: PDPNMsgCreatePlayer);

var aPlayer:PPlayerInfo;

begin

new(aplayer);

aplayer.refCount:=1;

aplayer.dpnidPlayer:=pResponse.dpnidPlayer;

pResponse.pvPlayerContext:=aPlayer;

//metode ini dipanggil dalam callback langsung.

//untuk mencegah deadlock cukup beri notifi kasi

//menggunakan mekanisme message

PostMessage(FWindowHandle,

MSG_DPN_CONNECT,

pResponse.dpnidPlayer,

0);

end;

Listing 28.b

TPlayerInfo=record

refCount:integer;

dpnidPlayer:TDPNID;

end;

PPlayerInfo=^TPlayerInfo;

Listing 28.c

procedure TDPNetwork.WndProc(var msg: TMessage);

begin

case msg.Msg of

MSG_DPN_CONNECT:begin

if Assigned(FOnPlayerConnected) then

FOnPlayerConnected(Self,msg.WParam);

end;

MSG_DPN_DISCONNECT:begin

if Assigned(FOnPlayerDisconnected then

FOnPlayerDisconnected(Self,msg.WParam);

end;

end;

msg.Result:=0;

end;

Listing 29

...

DPN_MSGID_DESTROY_PLAYER:begin

aNetwork.DoPlayerDisconnect(

PDPNMsgDestroyPlayer(pMessage));

result:=DPN_OK;

end;

...

Listing 30

...

DPN_MSGID_RECEIVE:begin

aNetwork.AddReceive(

PDPNMsgReceive(pMessage));

result:=DPN_OK;

end;

...

Listing 31

procedure TDPNetwork.AddReceive(

pResponse: PDPNMsgReceive);

var adata:PReceiveData;

begin

new(adata);

adata.Sender:=pResponse.dpnidSender;

adata.DataSize:=pResponse.dwReceiveDataSize;

adata.BufferHandle:=pResponse.hBufferHandle;

adata.Flag:=pResponse.dwReceiveFlags;

GetMem(adata.Data,pResponse.dwReceiveDataSize);

CopyMemory(adata.Data,

pResponse.pReceiveData,

pResponse.dwReceiveDataSize);

EnterCriticalSection(FReceiveCS);

try

FReceive.Add(adata);

fi nally

LeaveCriticalSection(FReceiveCS);

end;

end;

Listing 32

procedure TDPNetwork.ProcessNetworkMsg;

var adata:PReceiveData;

endTime:cardinal;

begin

endTime:=GetTickCount+FNetworkProcTimeout;

EnterCriticalSection(FReceiveCS);

try

while (endTime>=GetTickCount) and (FReceive.Count>0) do

begin

adata:=FReceive.First;

if Assigned(FOnReceiveData) then

begin

FOnReceiveData(self,adata);

end;

FreeMem(adata.Data,adata.DataSize);

FReceive.Remove(adata);

dispose(adata);

end;

fi nally

LeaveCriticalSection(FReceiveCS);

end;

end;

Listing 33

...

DPN_MSGID_HOST_MIGRATE:begin

aNetwork.DoHostMigrate(

PDPNMsgHostMigrate(pMessage));

result:=DPN_OK;

end;

...

Listing 34

procedure TDPPeerNetwork.DoHostMigrate(

pResponse: PDPNMsgHostMigrate);

begin

FHost:=pResponse.dpnidNewHost;

//metode ini dipanggil dalam callback langsung.

//untuk mencegah deadlock cukup beri notifikasi

//menggunakan mekanisme message

PostMessage(FWindowHandle,

MSG_DPN_HOSTMIGRATE,

pResponse.dpnidNewHost,0);

end;

Listing 35

...

DPN_MSGID_ENUM_HOSTS_RESPONSE:begin

anetwork.AddSession(

PDPNMsgEnumHostsResponse(pMessage));

result:=DPN_OK;

end;

...

Listing 36

procedure TDPNetwork.AddSession(

pResponse: PDPNMsgEnumHostsResponse);

var asession:PSessionRec;

notExist:boolean;

begin

EnterCriticalSection(FSessionCS);

try

notExist:=not isGUIDExists(

pResponse.pApplicationDescription.guidInstance,

FSessions);

fi nally

LeaveCriticalSection(FSessionCS);

end;

if notExist then

begin

new(asession);

pResponse.pAddressSender.Duplicate(asession.Addr);

asession.AppDesc:=pResponse.pApplicationDescription^;

EnterCriticalSection(FSessionCS);

try

FSessions.Add(asession);

fi nally

LeaveCriticalSection(FSessionCS);

end;

end;

end;

Listing 37

...

DPN_MSGID_TERMINATE_SESSION:begin

aNetwork.DoTerminateSession(

PDPNMsgTerminateSession(pMessage));

result:=DPN_OK;

end;

...

Listing 38

procedure TDPPeerNetwork.DoTerminateSession(

pResponse: PDPNMsgTerminateSession);

begin

//metode ini dipanggil dalam callback langsung.

//untuk mencegah deadlock cukup beri notifi kasi

//menggunakan mekanisme message

PostMessage(FWindowHandle,

MSG_DPN_SESSION_TERMINATED,

pResponse.hResultCode,0);

end;

Tidak ada komentar:

Sabilulungan dalam bahasa sunda artinya gotong royong. Makna sabilulungan yaitu seiya sekata, seayun, selangkah, sepengertian, sepamahama...