Bayangkan gambar ini: akhir dari siklus pengembangan sudah dekat, permainan Anda hampir tidak merayap, tetapi di profiler Anda tidak dapat menemukan area masalah yang jelas. Siapa yang harus disalahkan? Pola memori akses acak dan kesalahan cache persisten. Mencoba untuk meningkatkan kinerja, Anda mencoba untuk memaralelkan bagian-bagian kode, tetapi itu sepadan dengan upaya heroik, dan pada akhirnya, karena semua sinkronisasi yang harus ditambahkan, akselerasi hampir tidak terlihat. Selain itu, kode ini sangat rumit sehingga memperbaiki bug menyebabkan lebih banyak masalah, dan pemikiran untuk menambahkan fitur baru segera dibuang. Terdengar akrab?
Perkembangan acara semacam itu cukup akurat menggambarkan hampir setiap permainan dalam pengembangan yang saya ikuti selama sepuluh tahun terakhir. Alasannya bukan dalam bahasa pemrograman atau alat pengembangan, atau bahkan kurangnya disiplin. Dalam pengalaman saya, sebagian besar, pemrograman berorientasi objek (OOP) dan budaya sekitarnya harus disalahkan. OOP mungkin tidak membantu, tetapi mengganggu proyek Anda!
Ini semua tentang data
OOP telah merambah budaya pengembangan gim video yang ada sehingga ketika Anda memikirkan gim, sulit membayangkan hal lain selain objek. Selama bertahun-tahun sekarang, kami telah menciptakan kelas untuk mobil, pemain, dan mesin negara. Apa alternatifnya? Pemrograman prosedural? Bahasa fungsional? Bahasa pemrograman eksotis?
Desain berorientasi data adalah cara lain merancang perangkat lunak yang dirancang untuk menyelesaikan semua masalah ini. Elemen utama pemrograman prosedural adalah pemanggilan prosedur, dan OOP terutama berkaitan dengan objek. Perhatikan bahwa dalam kedua kasus kode diletakkan di tengah: dalam satu kasus, ini adalah prosedur biasa (atau fungsi), di lain, kode dikelompokkan terkait dengan keadaan internal tertentu. Desain berorientasi data menggeser fokus perhatian dari objek ke data itu sendiri: jenis data, lokasinya di memori, metode untuk membaca dan memprosesnya dalam permainan.
Pemrograman dengan definisi adalah cara mengubah data: tindakan menciptakan urutan instruksi mesin yang menggambarkan proses pemrosesan data input dan pembuatan data output. Gim tidak lebih dari program interaktif, jadi bukankah lebih logis untuk berkonsentrasi terutama pada data, dan bukan pada kode yang memprosesnya?
Agar tidak membingungkan Anda, saya akan segera menjelaskan: desain berorientasi data tidak berarti bahwa program ini didorong oleh data. Gim berbasis data biasanya gim yang fungsinya sebagian besar di luar kode; memungkinkan data untuk menentukan perilaku gim. Konsep ini independen dari desain berorientasi data dan dapat digunakan dalam metode pemrograman apa pun.
Data sempurna
Gambar 1a. Memanggil urutan dengan pendekatan berorientasi objekJika kita melihat program dalam hal data, lalu seperti apa data ideal itu? Itu tergantung pada data itu sendiri dan bagaimana menggunakannya. Secara umum, data ideal adalah dalam format yang dapat digunakan dengan upaya minimal. Dalam kasus terbaik, format akan sepenuhnya bertepatan dengan hasil keluaran yang diharapkan, yaitu pemrosesan hanya terdiri dari menyalin data. Sangat sering, skema data yang ideal terlihat seperti blok besar data homogen yang berdekatan yang dapat diproses secara berurutan. Bagaimanapun, tujuannya adalah untuk meminimalkan jumlah transformasi; jika mungkin, "bakar" data dalam format ideal ini terlebih dahulu, pada tahap menciptakan sumber daya game.
Karena desain berorientasi data mengutamakan data, kita dapat membuat arsitektur seluruh program di sekitar format data yang ideal. Kami tidak akan selalu berhasil membuatnya sepenuhnya sempurna (seperti kode jarang menyerupai OOP dari buku teks), tetapi ini adalah tujuan utama kami, yang selalu kami ingat. Ketika kita mencapai ini, sebagian besar masalah yang disebutkan di awal artikel hanya bubar (lebih lanjut di bagian selanjutnya).
Ketika kita berpikir tentang objek, kita segera mengingat pohon - pohon warisan, pohon bersarang, atau pohon pesan, dan data kita secara alami dipesan dengan cara ini. Oleh karena itu, ketika kita melakukan operasi pada suatu objek, ini biasanya mengarah pada fakta bahwa objek tersebut, pada gilirannya, mengakses objek lain di bawah pohon. Ketika iterasi beberapa objek, melakukan operasi yang sama akan menghasilkan hilir, operasi yang sama sekali berbeda untuk setiap objek (lihat Gambar 1a).
Gambar 1b. Urutan panggilan dalam teknik berorientasi dataUntuk mendapatkan skema penyimpanan data terbaik, akan berguna untuk membagi setiap objek menjadi komponen dan komponen grup yang berbeda dari jenis yang sama dalam memori, terlepas dari objek dari mana kami mengambilnya. Pemesanan semacam itu mengarah pada pembuatan blok besar data homogen, memungkinkan kami untuk memproses data secara berurutan (lihat Gambar 1b). Alasan utama kekuatan konsep desain berorientasi data adalah bahwa ia bekerja sangat baik dengan kelompok objek yang besar. OOP, menurut definisi, bekerja dengan satu objek. Ingat permainan terakhir yang Anda kerjakan: seberapa sering dalam kode ada tempat di mana Anda harus bekerja hanya dengan satu elemen? Satu musuh? Satu kendaraan? Salah satu cara menemukan simpul? Satu peluru? Satu potong? Tidak pernah! Di mana ada satu, ada beberapa lagi. OOP mengabaikan ini dan bekerja dengan masing-masing objek secara individual. Oleh karena itu, kita dapat menyederhanakan pekerjaan untuk diri kita sendiri dan peralatan dengan mengatur data sehingga perlu untuk memproses banyak elemen dari tipe yang sama.
Apakah pendekatan ini terasa aneh bagi Anda? Tapi tahukah Anda? Kemungkinan besar, Anda sudah menggunakannya di beberapa bagian kode: yaitu, dalam sistem partikel! Desain berorientasi data mengubah seluruh basis kode menjadi sistem partikel besar. Ada kemungkinan bahwa metode ini tampak lebih akrab bagi para pengembang game, itu harus disebut pemrograman berbasis partikel.
Manfaat Desain Berorientasi Data
Jika kita pertama-tama memikirkan data dan membuat arsitektur program berdasarkan ini, maka ini akan memberi kita banyak keuntungan.
Paralelisme
Saat ini tidak mungkin untuk menyingkirkan fakta bahwa kita perlu bekerja dengan banyak core. Mereka yang mencoba memparalelkan kode OOP dapat mengkonfirmasi seberapa kompleks, rawan kesalahan, dan mungkin tidak terlalu efisien tugasnya. Seringkali Anda harus menambahkan banyak primitif sinkronisasi untuk menghindari akses simultan ke data dari beberapa utas, dan biasanya banyak utas menganggur untuk waktu yang lama, menunggu utas lainnya selesai bekerja. Akibatnya, peningkatan produktivitas cukup biasa-biasa saja.
Jika kita menerapkan desain berorientasi data, maka paralelisasi menjadi lebih sederhana: kita memiliki data input, fungsi kecil yang memprosesnya dan menghasilkan data. Sesuatu yang serupa dapat dengan mudah dibagi menjadi beberapa aliran dengan sinkronisasi minimal di antara mereka. Anda bahkan dapat mengambil satu langkah lebih maju dan menjalankan kode ini pada prosesor dengan memori lokal (misalnya, dalam SPU prosesor Cell) tanpa mengubah operasi apa pun.
Penggunaan cache
Selain menggunakan multi-core, salah satu cara utama untuk mencapai kinerja tinggi pada peralatan modern dengan pipa instruksi yang dalam dan sistem memori yang lambat dengan beberapa level cache adalah penerapan akses data yang nyaman untuk caching. Desain berorientasi data memungkinkan penggunaan cache perintah yang sangat efisien, karena kode yang sama terus dieksekusi di dalamnya. Selain itu, jika kita mengatur data dalam blok besar yang berdekatan, kita dapat memproses data secara berurutan, mencapai penggunaan cache data yang hampir sempurna dan kinerja yang sangat baik.
Opsi pengoptimalan
Ketika kita berpikir tentang objek atau fungsi, kita biasanya fokus pada pengoptimalan di tingkat fungsi atau bahkan suatu algoritma: kita mencoba mengubah urutan pemanggilan fungsi, mengubah metode penyortiran, atau bahkan menulis ulang bagian dari kode C dalam bahasa assembly.
Optimalisasi semacam itu tentu berguna, tetapi jika Anda memikirkan data terlebih dahulu, kami dapat melangkah mundur dan menciptakan optimisasi yang lebih ambisius dan penting. Jangan lupa bahwa game hanya menangani konversi data tertentu (sumber daya, input pengguna, status) menjadi beberapa data lain (perintah grafik, status game baru). Dengan aliran data ini dalam pikiran, kita dapat membuat keputusan yang lebih tinggi, tingkat informasi lebih banyak berdasarkan pada bagaimana data dikonversi dan diterapkan. Optimalisasi seperti itu dalam teknik OOP yang lebih tradisional bisa sangat kompleks dan memakan waktu.
Modularitas
Semua keunggulan desain berorientasi data di atas terkait dengan kinerja: penggunaan cache, optimisasi dan paralelisasi. Tidak ada keraguan bahwa bagi kami programmer game, kinerja sangat penting. Seringkali ada konflik antara teknik yang meningkatkan produktivitas dan teknik yang mempromosikan keterbacaan kode dan kemudahan pengembangan. Misalnya, jika kita menulis ulang bagian kode dalam bahasa rakitan, kita akan meningkatkan kinerja, tetapi ini biasanya mengarah pada penurunan keterbacaan dan mempersulit dukungan untuk kode.
Untungnya, desain yang berorientasi data menguntungkan produktivitas dan kemudahan pengembangan. Jika Anda menulis kode khusus untuk konversi data, maka Anda mendapatkan fungsi kecil dengan jumlah dependensi yang sangat kecil dengan bagian kode lainnya. Basis kode tetap sangat "datar", dengan banyak fungsi "daun" yang tidak memiliki ketergantungan besar. Tingkat modularitas ini dan tidak adanya dependensi sangat menyederhanakan pemahaman, penggantian, dan pembaruan kode.
Pengujian
Manfaat utama terakhir dari desain berorientasi data adalah kemudahan pengujiannya. Banyak orang tahu bahwa menulis unit test untuk menguji interaksi objek adalah tugas yang tidak sepele. Anda perlu membuat tata letak dan menguji elemen secara tidak langsung. Jujur saja, ini sangat menyakitkan. Di sisi lain, bekerja secara langsung dengan data, menulis unit test sangat mudah: kami membuat beberapa data yang masuk, memanggil fungsi yang mengubahnya, dan memeriksa apakah output cocok dengan data yang diharapkan. Dan itu saja. Sebenarnya, ini adalah keuntungan besar yang sangat menyederhanakan pengujian kode, apakah itu pengembangan berbasis tes atau tes unit penulisan setelah kode.
Kerugian Desain Berorientasi Data
Desain berorientasi data bukanlah "peluru perak" yang menyelesaikan semua masalah dalam pengembangan game. Ini benar-benar membantu dalam menulis kode kinerja tinggi dan membuat program yang lebih mudah dibaca dan lebih mudah dirawat, tetapi dengan sendirinya memiliki beberapa kelemahan.
Masalah utama dengan desain berorientasi data: itu berbeda dari apa yang telah dipelajari dan digunakan oleh kebanyakan programmer. Itu membutuhkan mengubah model mental kita dari program sembilan puluh derajat dan menggeser sudut pandang di atasnya. Agar pendekatan ini menjadi sifat kedua, diperlukan praktik.
Selain itu, karena perbedaan pendekatan, dapat menyebabkan kesulitan dalam berinteraksi dengan kode yang ada yang ditulis dalam gaya prosedural atau OOP. Sulit untuk menulis satu fungsi secara terpisah, tetapi segera setelah Anda dapat menerapkan desain berorientasi data ke seluruh subsistem, Anda bisa mendapatkan banyak keuntungan.
Menggunakan Desain Berorientasi Data
Teori dan ulasan yang cukup. Bagaimana cara mulai menerapkan metode desain berorientasi data? Untuk memulai, pilih area spesifik kode Anda: navigasi, animasi, tabrakan, atau yang lainnya. Kemudian, ketika bagian utama mesin game akan difokuskan pada data, Anda akan dapat menyesuaikan aliran data di sepanjang jalur, dari awal bingkai hingga akhir.
Selanjutnya, perlu untuk mengidentifikasi dengan jelas data input yang diperlukan oleh sistem dan jenis data yang harus dihasilkannya. Anda mungkin berpikir dalam terminologi OOP untuk saat ini, hanya untuk mengidentifikasi data. Misalnya, untuk sistem animasi, bagian dari data input akan berupa kerangka, pose dasar, data animasi, dan keadaan saat ini. Hasilnya bukan "kode animasi animasi," tetapi data yang dihasilkan oleh animasi yang sedang diputar. Dalam hal ini, output akan menjadi kumpulan pose baru dan status yang diperbarui.
Penting untuk mundur dan mengklasifikasikan data yang masuk berdasarkan cara penggunaannya. Apakah hanya baca, baca-tulis, atau hanya-tulis? Klasifikasi semacam itu akan membantu membuat keputusan tentang di mana menyimpan data dan kapan memprosesnya karena ketergantungan pada bagian lain dari program.
Pada tahap ini, Anda harus berhenti memikirkan data yang diperlukan untuk satu operasi, dan mulai berpikir untuk menerapkannya pada puluhan atau ratusan elemen. Kami tidak lagi memiliki satu kerangka, satu pose dasar dan keadaan saat ini: kami memiliki blok masing-masing jenis ini dengan banyak contoh di setiap blok.
Pertimbangkan dengan cermat bagaimana data akan digunakan dalam proses transformasi dari input ke output. Anda mungkin menyadari bahwa untuk mengirimkan data, Anda perlu memindai bidang tertentu dalam struktur, dan kemudian Anda perlu menggunakan hasilnya untuk melakukan lintasan lain. Dalam hal ini, mungkin lebih logis untuk membagi bidang sumber ini ke dalam blok memori terpisah, yang dapat diproses secara terpisah, yang akan membuat penggunaan cache lebih baik dan menyiapkan kode untuk potensi paralelisasi. Atau, Anda mungkin perlu membuat vektor bagian dari kode jika Anda perlu menerima data dari tempat yang berbeda untuk menempatkannya dalam satu register vektor. Dalam hal ini, data akan disimpan berdampingan sehingga operasi vektor dapat diterapkan secara langsung, tanpa konversi yang tidak perlu.
Anda sekarang harus memiliki pemahaman yang sangat baik tentang data Anda. Menulis kode untuk mengonversinya akan menjadi lebih mudah. Ini akan seperti membuat kode dengan mengisi spasi. Anda akan terkejut bahwa kode tersebut ternyata jauh lebih sederhana dan lebih ringkas daripada yang Anda pikirkan, dibandingkan dengan kode OOP yang sama.
Sebagian besar posting di blog saya mempersiapkan Anda untuk jenis desain ini. Sekarang kita perlu berhati-hati tentang bagaimana data diatur, memanggang data dalam format input sehingga dapat digunakan secara efisien dan menggunakan tautan tanpa petunjuk di antara blok data sehingga mereka dapat dengan mudah dipindahkan.
Apakah ada ruang tersisa untuk menggunakan OOP?
Apakah ini berarti bahwa OOP tidak berguna dan tidak boleh digunakan saat membuat program? Saya tidak bisa mengatakan itu. Berpikir dalam konteks objek tidak berbahaya jika kita hanya berbicara tentang satu instance dari setiap objek (misalnya, perangkat grafis, log manager, dll.), Meskipun dalam kasus ini kode dapat diimplementasikan berdasarkan pada fungsi C-style yang lebih sederhana dan statis. data tingkat file. Dan bahkan dalam situasi ini, masih penting bahwa objek dirancang dengan penekanan pada transformasi data.
Situasi lain di mana saya masih menggunakan OOP adalah sistem GUI. Mungkin ini karena di sini kita bekerja dengan sistem yang sudah dirancang dengan cara berorientasi objek, atau mungkin karena kinerja dan kompleksitas bukan faktor penting untuk kode GUI. Meskipun demikian, saya lebih suka API GUI yang sedikit menggunakan pewarisan dan memaksimalkan bersarang (contoh yang baik di sini adalah Cocoa dan CocoaTouch). Sangat mungkin bahwa untuk game Anda dapat menulis sistem GUI yang tampak bagus dengan orientasi data, tetapi sejauh ini saya belum melihatnya.
Pada akhirnya, tidak ada yang mencegah Anda membuat gambar mental berdasarkan objek, jika Anda lebih suka memikirkan permainan dengan cara ini. Hanya saja esensi musuh tidak akan menempati satu tempat fisik dalam memori, tetapi akan dibagi menjadi subkomponen yang lebih kecil, yang masing-masing membentuk bagian dari tabel data besar komponen serupa.
Desain berorientasi data agak jauh dari metode pemrograman tradisional, tetapi jika Anda selalu berpikir tentang data dan cara yang diperlukan untuk mengubahnya, itu akan memberi Anda keuntungan besar dalam hal produktivitas dan kemudahan pengembangan.