Saya, seorang programmer imperatif Java, ingin memahami bagaimana membuat versi sederhana Space Invaders berdasarkan prinsip-prinsip desain Pemrograman Fungsional (khususnya Transparansi Referensial). Namun, setiap kali saya mencoba memikirkan suatu desain, saya tersesat di dalam tumpukan ketidakmampuan yang ekstrim, ketidakmampuan yang sama yang dijauhi oleh puritan pemrograman fungsional.
Sebagai upaya untuk mempelajari Pemrograman Fungsional, saya memutuskan untuk mencoba membuat game interaktif 2D yang sangat sederhana, Space Invader (perhatikan kurangnya jamak), di Scala menggunakan LWJGL . Berikut persyaratan untuk gim dasar:
Kapal pengguna di bagian bawah layar bergerak masing-masing ke kiri dan kanan dengan tombol "A" dan "D"
Peluru kapal pengguna diaktifkan lurus ke atas diaktifkan oleh space bar dengan jeda minimum antara tembakan menjadi 0,5 detik
Peluru kapal asing yang ditembakkan langsung ke bawah diaktifkan dengan waktu acak 0,5 hingga 1,5 detik di antara tembakan
Hal-hal yang sengaja ditinggalkan dari permainan aslinya adalah alien WxH, perisai pertahanan x3 yang dapat didegradasi, kapal piring berkecepatan tinggi di bagian atas layar.
Oke, sekarang ke domain masalah aktual. Bagi saya, semua bagian deterministik sudah jelas. Ini adalah bagian-bagian non-deterministik yang tampaknya menghalangi kemampuan saya untuk mempertimbangkan cara pendekatan. Bagian-bagian deterministik adalah lintasan peluru begitu mereka ada, gerakan terus-menerus dari alien dan ledakan karena hit pada salah satu (atau keduanya) dari kapal pemain atau alien. Bagian non-deterministik (bagi saya) menangani aliran input pengguna, penanganan mengambil nilai acak untuk menentukan tembakan peluru alien dan menangani output (baik grafis dan suara).
Saya dapat melakukan (dan telah melakukan) banyak jenis pengembangan game ini selama bertahun-tahun. Namun, semua itu dari paradigma imperatif. Dan LWJGL bahkan menyediakan versi Java yang sangat sederhana dari Space Invaders (yang mana saya mulai pindah ke Scala menggunakan Scala sebagai Java-tanpa-titik koma).
Berikut adalah beberapa tautan yang berbicara tentang bidang ini yang tampaknya tidak ada yang secara langsung berurusan dengan ide-ide dengan cara yang dipahami oleh orang yang berasal dari Jawa / pemrograman Imperatif:
Tampaknya ada beberapa ide di permainan Clojure / Lisp dan Haskell (dengan sumber). Sayangnya, saya tidak dapat membaca / menafsirkan kode menjadi model mental yang masuk akal untuk otak imperatif Java sederhana saya.
Saya sangat senang dengan kemungkinan yang ditawarkan oleh FP, saya bisa merasakan kemampuan skalabilitas multi-utas. Saya merasa seperti dapat memahami bagaimana sesuatu yang sederhana seperti waktu + peristiwa + model keacakan untuk Space Invader dapat diimplementasikan, memisahkan bagian-bagian deterministik dan non-deterministik dalam sistem yang dirancang dengan baik tanpa mengubahnya menjadi apa yang terasa seperti teori matematika canggih ; yaitu Yampa, aku akan siap. Jika mempelajari tingkat teori yang tampaknya diperlukan Yampa untuk menghasilkan permainan sederhana dengan sukses, maka biaya untuk memperoleh semua pelatihan dan kerangka kerja konseptual yang diperlukan akan jauh melebihi pemahaman saya tentang manfaat FP (setidaknya untuk eksperimen pembelajaran yang terlalu disederhanakan ini) ).
Setiap umpan balik, model yang diusulkan, metode yang disarankan untuk mendekati domain masalah (lebih spesifik daripada generalitas yang dicakup oleh James Hague) akan sangat dihargai.
sumber
Jawaban:
Implementasi Scala / LWJGL idiomatik dari Space Invaders tidak akan terlihat seperti implementasi Haskell / OpenGL. Menulis implementasi Haskell mungkin merupakan latihan yang lebih baik menurut saya. Tetapi jika Anda ingin tetap menggunakan Scala, berikut adalah beberapa ide cara menulisnya dalam gaya fungsional.
Coba gunakan benda yang tidak bisa diubah saja. Anda bisa memiliki
Game
objek yang memegangPlayer
, seorangSet[Invader]
(pastikan untuk menggunakanimmutable.Set
), dll BerikanPlayer
sebuahupdate(state: Game): Player
(bisa juga mengambildepressedKeys: Set[Int]
, dll), dan memberikan kelas-kelas lain metode yang serupa.Untuk keacakan,
scala.util.Random
tidak kekal seperti milik HaskellSystem.Random
, tetapi Anda bisa membuat generator sendiri yang tidak bisa diubah. Yang ini tidak efisien tetapi menunjukkan ide.Untuk input dan rendering keyboard / mouse, tidak ada jalan lain untuk memanggil fungsi yang tidak murni. Mereka tidak murni di Haskell juga, mereka hanya dienkapsulasi dalam
IO
dll. Sehingga objek fungsi aktual Anda secara teknis murni (mereka tidak membaca atau menulis status sendiri, mereka menggambarkan rutinitas yang dilakukan, dan sistem runtime menjalankan rutinitas tersebut) .Hanya saja, jangan menaruh kode I / O di objek abadi Anda seperti
Game
,Player
danInvader
. Anda dapat memberikanPlayer
suaturender
metode, tetapi harus terlihat sepertiSayangnya ini tidak cocok dengan LWJGL karena sangat berbasis negara, tetapi Anda dapat membangun abstraksi sendiri di atasnya. Anda bisa memiliki
ImmutableCanvas
kelas yang memegang AWTCanvas
, danblit
(dan metode lainnya) dapat mengkloning yang mendasarinyaCanvas
, meneruskannyaDisplay.setParent
, lalu melakukan rendering dan mengembalikan yang baruCanvas
(dalam pembungkus abadi Anda).Pembaruan : Berikut adalah beberapa kode Java yang menunjukkan bagaimana saya akan melakukannya. (Saya akan menulis kode yang hampir sama di Scala, kecuali bahwa set yang tidak dapat diubah adalah bawaan dan beberapa untuk setiap loop dapat diganti dengan peta atau lipatan.) Saya membuat pemain yang bergerak dan menembakkan peluru, tapi saya tidak menambahkan musuh karena kodenya sudah lama. Saya membuat hampir semua hal copy-on-write - saya pikir ini adalah konsep yang paling penting.
sumber
args
jika kode mengabaikan argumen. Maaf atas kebingungan yang tidak perlu.GameState
salinan akan menjadi mahal, meskipun beberapa dibuat setiap centang, karena mereka ~ 32 byte masing-masing. Tetapi menyalinImmutableSet
s bisa mahal jika banyak peluru hidup pada saat yang sama. Kita bisa menggantiImmutableSet
dengan struktur pohon yang inginscala.collection.immutable.TreeSet
mengurangi masalah.ImmutableImage
bahkan lebih buruk, karena menyalin raster besar ketika dimodifikasi. Ada beberapa hal yang bisa kita lakukan untuk mengurangi masalah itu juga, tapi saya pikir akan lebih praktis untuk hanya menulis kode rendering dengan gaya imperatif (bahkan programmer Haskell biasanya melakukannya).Nah, Anda melumpuhkan upaya Anda dengan menggunakan LWJGL - tidak ada yang menentangnya, tetapi itu akan memaksakan idiom yang tidak berfungsi.
Namun, penelitian Anda sejalan dengan apa yang saya rekomendasikan. "Acara" didukung dengan baik dalam pemrograman fungsional melalui konsep-konsep seperti pemrograman reaktif fungsional atau pemrograman aliran data. Anda dapat mencoba Reactive , perpustakaan FRP untuk Scala, untuk melihat apakah itu dapat mengandung efek samping Anda.
Juga, keluarkan satu halaman dari Haskell: gunakan monad untuk merangkum / mengisolasi efek samping. Lihat negara dan IO monad.
sumber
Ya, IO adalah efek samping non-deterministik dan "semua tentang". Itu bukan masalah dalam bahasa fungsional yang tidak murni seperti Scala.
Anda dapat memperlakukan output generator nomor pseudorandom sebagai urutan tak terbatas (
Seq
dalam Scala)....
Di mana, khususnya, apakah Anda melihat perlunya sifat berubah-ubah? Jika saya dapat mengantisipasi, Anda mungkin menganggap sprite Anda memiliki posisi di ruang yang bervariasi dari waktu ke waktu. Mungkin bermanfaat untuk memikirkan "ritsleting" dalam konteks seperti itu: http://scienceblogs.com/goodmath/2010/01/zippers_making_functional_upda.php
sumber