Setelah projek blog diterbitkan, masih akan ada banyak perbaikan yang perlu dilakukan. Namun, setidaknya blog ini sudah layak untuk dikunjungi. Nah untuk menyibukkan diri, saya membuat projek lain yaitu permainan labirin. Jenis game ini saya pilih karena konsepnya sederhana hanya berupa objek player dan dinding-dinding pembatas. Lumayan untuk meningkatkan kemampuan saya di bidang pemrograman Javascript.
Ide membuat game ini bermula saat saya melihat video Fireship yang membahas mengenai website 3 dimensi dengan Three.js. Pada saat itu saya langsung tertarik dan berpikir untuk membuat permainan. Setahu saya, game hanya membutuhkan input (baik dari mouse maupun keyboard) lalu meneruskannya ke sistem game untuk mengendalikan object (misal player unit atau balok tetris) untuk kemudian menghasilkan output berupa video. Browser punya kemampuan untuk menerima input dari user, dan untuk mengakses kemampuan tersebut pun juga tidak sulit. Kemudian Three.js bisa untuk menangani output dan yang pasti terlihat keren karena 3D. Akhirnya Javascript di era modern sekarang sudah menjadi bahasa yang handal untuk mengimplementasikan berbagai logika pemrograman, termasuk sistem game sederhana semacam permainan labirin.
Teknologi
Saya memakai vanilla Javascript untuk projek ini. Alasannya adalah saya hanya mengikuti videonya Fireship agar tidak pusing, sekalipun video itu bukan tentang game. Setelah selesai saya lalu eksperimen sendiri. Seperti di video, saya memakai Vite. Vite sendiri adalah frontend build tooling yang mempermudah proses pengembangan frontend. Yang terakhir tentu saja saya menggunakan Three.js yang bisa diinstal dengan NPM.
Sedikit mengenai Three.js. Jadi library ini akan membantu kita untuk mengakses fitur yang sudah ada di browser modern sekarang ini, yaitu Web Graphics Library (WebGL). WebGL sendiri semacam API yang bisa menghubungkan browser dengan graphic hardware yang ada di komputer kita. Sejauh yang saya paham, perantaranya adalah OpenGL. Jadi WebGL akan membuat browser bisa menyusun grafis 3 dimensi dengan performa tinggi. Sekedar info, baru-baru ini sudah muncul pengganti untuk WebGL yaitu WebGPU. Nantinya, WebGPU bisa langsung mengakses graphic card dari browser. Kebayang donk power dan kemungkinan-kemungkinannya?
Okey kembali ke Three.js. Di Three.js kita bisa dengan mudah membuat objek 3D seperti balok, silinder, torus, dan lain-lain. Library ini juga bisa untuk import objek 3D dari file yang dibuat dengan software 3D misal Blender. Inti penggunaan Three.js mirip dengan 3D software yaitu ada scene tempat objek berada, kamera penentu sudut pandang, dan renderer untuk membuat objek jadi terlihat. Proses ini akan menggunakan elemen <canvas> dari HTML sebagai tempat menampilkan objek 3D.
Kendali
Well, kita masuk ke pengerjaan projek. Fungsi pertama yang saya buat (seingat saya) adalah kendali orientasi. Bayangan saya, game ini akan menjadi third person perspective seperti di Tomb Raider. Akan ada satu player unit (di sini sebuah torus atau bangun ruang yang bentuknya seperti donat) dan sebauah kamera yang akan selalu mengarah ke unit ini (atau lebih tepatnya di atas unit agar pandangan lebih luas). Dalam game, ketika mouse bergerak kanan-kiri, maka player unit juga akan mengikuti. Untuk gerak atas-bawah, bisa juga unit ini mendongak dan menunduk, tapi sepertinya tidak terlalu penting jadi tidak saya implementasikan. Sedangkan untuk kamera, posisinya akan mengikuti kanan-kiri dari unit ditambah perlu juga naik-turun.
Untuk mewujudkannya, saya perlu memanfaatkan MouseEvent untuk membaca input dari mouse. Events dalam browser adalah sinyal yang muncul saat ada perubahan yang terjadi, contohnya aktivitas menekan keyboard, mengeklik mouse, atau mengubah ukuran browser. Dalam permainan saya ini, pergeseran mouse yang akan digunakan. Dalam Javascript, sinyal ini direpresentasikan dalam mousemove event. Saat sebuah event ditangkap, maka ia akan berisi beberapa properti yang bisa digunakan sebagai input.
Awalnya saya menggunakan offsetX dan offsetY. Kedua properti ini menunjukkan posisi mouse pointer relatif terhadap elemen tempat event ditangkap. Saya lalu membuat sebuah elemen yang memenuhi layar sebagai tempat bergeraknya pointer.
Saat mouse bergerak kanan-kiri, maka nilai offsetX akan berubah. Nilai ini lalu saya gunakan dalam perhitungan yang selanjutnya digunakan untuk mengubah rotasi dari player unit. Untuk kamera, saya membuat function sederhana yang akan membuat posisi X dan Z selalu berada di belakang player unit. Dalam dunia Three.js, posisi X dan Z mengacu pada posisi bidang horizontal. Sedangakan posisi Y mengacu pada posisi ketinggian atas-bawah. Sedangkan untuk arah pandang kamera, terdapat method dalam kameranya Three.js bernama lookAt untuk membuat kamera melihat ke titik tertentu. Untuk persoalan 'sudut pandang' ini ternyata bukan masalah besar.
Nah untuk gerakan mouse atas-bawah, perhitungan jadi sedikit lebih rumit. Saya ingin agar gerakan kamera itu melingkar di sekitar player unit. Jadi area yang mungkin dilewati kamera akan seperti permukaan bola. Makanya, tidak cukup jika perhitungannya hanya sekedar posisi Y melalui koordinat kartesius, melainkan harus dengan konsep koordinat polar.
Dengan konsep koordinat polar, saya ingin menetapkan jarak kamera selalu tetap dengan player unit, yang berubah adalah sudut posisi kamera. Sebut saja sudut ini sebagai angleZero. Kemudian saya tetapkan jarak kamera sebagai cameraDInit. Ketika mouse bergerak atas-bawah, maka input akan merubah besarnya angelZero. Posisi Y kamera akan setara dengan cameraDInit dikali sinus angleZero. Lalu posisi XZ juga harus terpengaruh. Misalnya kamera harus di titik paling jauh saat sejajar dengan player unit, lalu semakin mendekat saat kamera semakin naik atau turun. Untuk itu saya menghitung jarak XZ ini sebagai cameraD. Bisa juga cameraDXZ kalau mau, tapi sepertinya terlalu panjang. Anyway, cameraD ini akan sama dengan cameraDInit dikali dengan cosinus angleZero. Kemudian, nilai cameraD akan digunakan dalam function yang sama yang menentukan posisi kamera agar di belakang player unit.
Mungkin terdengar rumit dan memusingkan. Memang, saya sendiri saat merancang programnya juga pusing. Namun, di sinilah terlihat bahwa pelajaran Matematika SMP dan SMA jadi berguna. Pelajaran menarik dan berharga untuk membuat game yang pada akhirnya digunakan untuk membuang-buang waktu saja, hehehe.
Saat menggunakan konsep offsetX, offsetY, dan elemen, terdapat masalah yang mengganjal. Kendali tidak optimal dan kurang nyaman. Player unit seharusnya bisa berputar tanpa batasan. Masalah ini coba saya atasi dengan berbagai cara. Berhasil, tapi tetap saja kendali tidak nyaman karena tidak seperti game third person sungguhan. Akhirnya, saya menemukan solusi yaitu dengan Pointer Lock API. Dengan API ini, pointer bisa dikunci dalam suatu elemen dan menghilang. Javascript lalu dapat menerima input berupa berapa banyaknya pergeseran mouse alih-alih posisi aktual dari mouse pointer. Mouse jadi bisa bergerak tanpa batasan ukuran elemen. Kode untuk menggunakannya tidak terlalu rumit, juga kode saya sebelumnya tetap bisa digunakan lagi, jadi tidak ada masalah yang berarti. Setelah beralih, sistem kendali saya benar-benar mirip game sungguhan.
Sistem kendali translasi untuk player unit untungnya tidak sesulit gerak rotasinya. Saya hanya perlu menggunakan keydown dan keyup event untuk mendapatkan input dari keyboard. Dengan menggunakan kode if sederhana, saya bisa mengasosiasikan huruf-huruf 'asdw' sebagai kendali kiri, mundur, kanan, dan maju. Memang, perhitungan gerak player unit masih harus memperhitungkan arah rotasi. Jadi, tetap harus pakai sinus cosinus untuk menghasilkan koordinat X dan Z.
Membuat Labirin (di Awal)
Sistem kendali sudah, kita lanjut ke membuat labirinnya. Awalnya, saya gambar pakai spreadsheet. Kalau di Windows berarti Ms. Excel, tapi karena saya pakai Linux maka yang ada hanyalah LibreOffice Calc. Kita bisa membuat balok sederhana dengan Three.js. Karena itu saya gambar dan saya hitung masing-masing ukuran dan posisi dinding di labirin. Proses ini memang tidak susah, tapi perlu ketelitian dan banyak perhitungan yang harus dilakukan.
Agak kurang praktis, jadi saya ubah konsepnya dengan program khusus untuk membangun labirin. Namun sebelumnya, terdapat masalah dalam permainan.
Collision Detection
Dinding seharusnya mencegah player unit berjalan ke arah tertentu. Di Three.js, hal ini tidak secara bawaan terjadi. Ya, torus permainan ini tembus-tembus saja ke balok-balok dinding. Untuk itu, saya perlu sistem bernama collision detection. Saya lalu menemukan caranya di Three.js dengan menggunakan bounding box.
Bounding box adalah kotak yang melingkupi suatu objek. Jika objek itu 3 dimensi berarti bounding box juga akan berbentu kubus/balok. Dalam Three.js, bounding box direpresentasikan dalam Box3. Yang menarik adalah dalam Box3 terdapat method intersectsBox yang digunakan untuk menentukan apakah dua buah Box3 saling beririsan atau tidak. Beririsan berarti bukan hanya bersentuhan, tetapi ada satu area yang merupakan bagian dari kedua bounding box yang dimaksud.
Nah, yang harus dilakukan adalah pertama mengeset bounding box baik untuk player unit maupun semua dinding labirin. Kemudian, saya buat agar kendali translasi menggerakkan Box3 player unit, bukan player unit-nya. Lalu, saya buat sebuah function yang beriterasi dan mengecek apakah ada Box3 dinding yang beririsan dengan Box3 player unit. Jika tidak, maka player unit akan bergerak mengikuti Box3 miliknya. Jika iya, maka Box3-nya 'lah yang akan kembali ke posisi player unit seperti semula. Dengan demikian, player unit akan bisa bergerak ke sana ke mari di area yang tidak tertutup dinding dan setiap terbentur dinding, ia akan terhenti.
Mempermudah Pembuatan Labirin
Sebelumnya saya sebut bahwa saya ingin membuat program khusus untuk membuat labirin. Nah, saya akan jelaskan seperti apa program tersebut di sini.
Konsepnya adalah saya membagi labirin menjadi sel-sel persegi. Di setiap sel terdapat 9 komponen: satu area kosong di tengah, dua balok pipih vertikal dan dua balok pipih horizontal di setiap sisi sebagai dinding, serta empat balok berbentuk tiang di setiap sudut untuk menyatukan dinding. Namun, pastinya dinding dan tiang akan digunakan bersama jika dua sel saling bersebelahan. Misalnya, jika labirin berukuran 2x2 sel (berarti total ada 4 sel), maka akan ada 4 area kosong, 6 balok pipih vertikal, 6 balok pipih horizontal, dan 9 balok tiang. Alias total ada 5x5 (25) komponen. Nah, komponen-komponen ini lalu saya representasikan dalam array 2 dimensi berisi angka di Javascript. Untuk versi sederhana sementara ini, nilai 1 berarti di sana ada dinding/tiang dan nilai 0 berarti tidak ada apa-apa.
Untuk menentukan jenis balok, saya menggunakan ganjil/genap dari indeks array. Jika indeksnya genap-genap, maka balok tiang. Jika indeksnya ganjil-genap, maka balok vertikal. Jika indeksnya genap-ganjil, maka balok horizontal. Lalu bagaimana kalau ganjil-ganjil? Ya berarti itu adalah area kosong yang nilainya pasti 0.
Untuk posisi sedikit rumit. Posisi akan berdasarkan pada 4 hal: jenis balok, ukuran area kosong, tebal dinding, dan indeks array. Bahasa sederhananya, posisi X dan Z merupakan perkalian dari 3 faktor terkahir. Lalu beda jenis balok berarti beda rumus perkaliannya. Untuk lebih jelasnya mungkin harus lihat ke kode programnya. Komen di bawah apakah saya harus buat pos membahas secara detail mengenai hitungan matematika dari game ini ya. Setelah itu kita lanjut ke labirin 3 dimensi.
Labirin Bertingkat
Loh, bukannya game ini memang 3 dimensi? Hehehe, iya. Maksud labirin 3 dimensi adalah labirin yang punya beberapa lantai alias bertingkat. Untuk bisa mengimplementasikan mode ini, saya perlu mengubah program pembuat labirin saya.
Pertama, array labirin harus saya ubah menjadi 3 dimensi juga. Dimensi tambahan ini ada di depan. Jadi ganjil/genap indeks yang pertama sekarang menentukan apakah elemen tersebut merepresentasikan labirin atau pembatas lantai. Jika genap maka pembatas lantai dan jika ganjil berarti labirin berisi dinding-dinding.
Selanjutnya saya perlu cara agar player unit bisa naik turun jika ada lubang di pembatas lantai. Di sini saya buat komponen baru sebut saja namanya lift. Mungkin bagian ini adalah yang paling sulit dalam mengimplementasikan labirin bertingkat. Dalam permainan sederhana saya, saat player unit masuk lift maka akan muncul tombol. Tombol ini bisa diklik yang akan membuat player unit naik atau turun tergantung tombol mana yang user klik.
Objek lift ini saya implementasikan dengan balok tapi sementara ini transparan. Mungkin besok coba-coba kalau ada warnanya bagus/tidak. Awalnya, setiap lift akan ada tombol yang diset false pada properti visible. Nah, untuk mendeteksi apakah player unit ada di dalam lift, maka pakai Box3 lagi. Kali ini, method yang digunakan adalah containsBox. Contain di sini berarti berisi, jadi method ini akan mengembalikan true jika box player unit di dalam box lift, dan akan false jika tidak (termasuk beririsan). Kemudian, jika player unit berada di suatu lift, maka visibility tombol di lift tersebut akan diset true.
Untuk cara klik tombolnya memang agak teknis. Dalam dunia 3D game ada yang disebut ray casting. Konsep ini bertujuan untuk mengolah suasana area 3 dimensi untuk menghasilkan gambar 2 dimensi. Ibarat kata, kamera itu seperti memancarkan cahaya dan setiap bidang yang dikenainya akan jadi gambar di kamera. Nah, ray casting ini bisa juga digunakan untuk memilih objek dalam lingkungan 3D. Misalnya kita buat pointer yang memancarkan sinar laser. Jika ada objek yang terkena, maka objek itulah yang akan tertunjuk oleh pointer. Anggap sinar laser itu titik di antara cahaya yang dipancarkan kamera, jadi ia hanya akan menunjuk tepat satu objek saja.
Dalam Three.js, sudah ada class sendiri untuk raycaster, jadi tidak terlalu pusing kodenya. Hanya saja memang di sini tidak pakai pointer karena sudah dihilangkan pointer lock. Sebagai gantinya, saya buat agar titik tengah layar jadi pointer.
Selanjutnya, saya tinggal memberi tanggapan untuk event saat mouse diklik, yang tentunya saat raycaster menunjuk salah satu tombol lift. Jika tombol berada di atas player unit, maka posisi Y player unit akan ditambah, dan begitu sebaliknya.
Maze Generator
Sampai tulisan ini dibuat, saya telah menyusun 3 level untuk permainan labirin saya. Sekalipun dengan program pembuat labirin, mendesain labirin tetap jadi kegiatan yang memusingkan. Jadi, saya mengimplementasikan program untuk mendesain labirin secara acak.
Ternyata, terdapat banyak algoritma untuk membangun peta labirin. Karena saya agak malas untuk memahami dan mengimplementasikan algoritma dari Wikipedia, saya cari video penjelasan di Youtube. Ternyata ada video yang lumayan bagus dan jelas. Video tersebut menjelaskan algoritma depth-first search. Inti dari algoritma tersebut adalah berpindah dari satu sel labirin ke sel lain di sebelah. Setiap pindah program akan menghilangkan dinding. Perpindahan ini harus acak dan sel tempat berpindah harus belum pernah dikunjungi. Jika tidak ada sel lagi, maka program harus mundur ke sel sebelumnya, lalu lanjut pindah lagi sampai semua sel dikunjungi. Ya kira-kira seperti itu ringkasnya, jika bingung karena terlalu ringkas silakan baca Wikipedia atau lihat sendiri videonya.
Perbedaan di game saya dan video Youtube ini adalah elemen array labirin. Di game saya bentuknya hanya angka, sedangkan di video bentuknya object Javascript. Ya, saya harus mengubah ini dan itu. Namun akhirnya saya berhasil dan dalam permainan labirin saya sekarang ada level yang selalu berubah setiap kali dimainkan.
Mini Map
Ada satu bagian yang hampir lupa saya jelaskan, yaitu mini map. Saya ingin membuat fitur ini agar seperti game balapan yang bisa menunjukkan denah sekaligus tempat di mana player unit berada secara aktual. Kayanya keren saja, hehehe. Pembuatannya? Pusing minta ampun. Mungkin saya lupa bagian ini karena otak saya ingin melupakan trauma saya membuat fitur ini. Halah lebay! Hehehe.
Pertama menggambar denah/peta. Prosesnya ada di dalam program yang sama dengan pembuat labirin. Kosepnya juga serupa, hanya saja jika di labirin itu bentuknya balok, maka di denah bentuknya persegi/persegi panjang. Dalam browser, kita bisa menggambar 2D dengan menggunakan elemen <canvas>. Kode Javascript bawaan juga cukup jelas jika lihat di tutorial, maka cukup straightforward tanpa bantuan library.
Nah, untuk proses gerakannya, itu rumit. Jelas, mini map akan bergeser sesuai posisi XZ player unit dan berputar sesuai rotasi player unit. Untuk geser saya pakai style/CSS prorperty transform dengan nilai translate({x}px, {z}px). Nilai x dan z terlebih dahulu diolah dari posisi X dan Z torus. Untuk putar, nah ini saya perlu pemahaman ekstra dari Javascript dan DOM. Dalam CSS ada yang namanya rotate, tapi jelas kita tidak bisa asal putar denah. Sumbu putarnya perlu diubah terlebih dahulu. Untuk mengubah sumbu, kita bisa menggunakan transformOrigin. Saya lalu mencari rumus untuk menghitung sumbu agar sesuai antara kondisi sebenarnya dan kondisi di denah. Awalnya saya pikir transformOrigin itu relatif terhadap gambar/elemen. Saya buat tambah-kurang, kali-bagi, panjang lebar nan ruwet. Saya coba. Ternyata gagal.
Apa yang salah? Ternyata tidak serumit itu! transformOrigin itu ternyata relatif terhadap layar browser. Jadi saya hanya perlu mengesetnya di posisi yang menunjuk titik tengah dari parent mini map. Ya kira-kira seperti itu.
Update! Saat saya menulis tulisan ini, dan saya coba game tanpa transformOrigin, ternyata bisa! Ya, pada akhirnya saya juga tidak terlalu paham penjelasannya. Saya akan belajar lebih banyak mengenai transform di CSS ini.
Rencana Ke Depannya
Permainan labirin saya ini sudah sangat memuaskan bagi saya. Namun, saya melihat bahwa projek ini bisa jadi bahan untuk belajar lebih banyak hal lagi.
Yang pertama adalah saya ingin menggunakan React.js sebagai frontend builder. Konsekuensinya, saya harus tulis ulang kode saya pakai React Three Fiber agar lebih terasa React-nya. Yang kedua, saya ingin membuat sistem backend di Firebase. Sistem ini akan berisi user authentication, leaderboards, dan lain-lain. Mengapa perlu? Ya saya cuma ingin belajar Firebase sebenarnya. Yang ketiga adalah mengintegrasikan game ini dengan sistem pembayaran. Saya ingin tahu bagaimana sistem pembayaran digital bekerja, dan saya merasa ya sekalian saja projek iseng ini saya gunakan sebagai percobaan. Yang terakhir ini jelas penting. Kalau saya bisa menguasai integrasi pembayaran digital, saya nanti bisa buat online shop saya sendiri, hehehe. Lumayan 'kan?
Penutup
Yah itu rencana ke depan sih, tidak tahu kesampaian atau tidak. Untuk sementara bagi yang ingin melihat game saya ini, bisa klik di tautan ini. Source code bisa lihat di sini. Sampai di sini, banyak sekali ilmu yang saya dapat. Mulai dari penerapan ilmu matematika, konsep desain 3 dimensi, hingga CSS untuk mengatur letak elemen.
Okey berbagi kesan ini menandakan akhir dari tulisan ini. Semoga tulisan ini bermanfaat, komen apa pun yang ingin kalian komen di bawah, dan sampai jumpa lagi di tulisan-tulisan lainnya!
