Membangun dunia terbuka di browser, bagian 29: Satu pengontrol, tubuh apa pun
Oleh Oleg Sidorkin, CTO dan Salah Satu Pendiri Cinevva
Baru mengikuti seri ini? Gunakan panduan seri. Panduan tersebut menjelaskan apa itu spike dan menautkan semua bagiannya.
Bagian 28 membahas rumput dan oklusi. Dua puluh delapan bagian telah membangun dunia untuk dipijak: medan yang dapat Anda pahami, air untuk berenang, dedaunan yang tetap meyakinkan hingga cakrawala, serta server yang mengingat apa yang Anda ubah. Bagian ini membahas sesuatu yang bergerak melintasi semuanya, sekaligus menjadi hasil akhir dari keseluruhan mesin, karena tujuannya bukan sekadar pengontrol pemain. Tujuannya adalah pengontrol yang tidak peduli tubuh apa yang digerakkannya, dari mana animasinya berasal, atau apakah manusia maupun AI yang memegang kendali. Spike 58 membangun pergerakan sebagai tumpukan perilaku yang dapat dipasang di atas mesin fisika yang tidak mengetahui apa pun tentang lokomosi, lalu membuktikan arsitekturnya dengan menjalankan tiga tubuh berbeda melalui satu salinan mesin tersebut. Spike 59 memasang avatar nyata yang telah di-retarget ke pengontrol itu tanpa mengubah satu baris pun. Spike 60 memasang paket animasi berbeda yang sama sekali tidak memerlukan retargeting, lalu memisahkan logika pemilihan klip menjadi sesuatu yang benar-benar dapat diuji.
Mesin fisika yang tidak mengetahui apa pun tentang berjalan
Buka Spike 58 di tab baru ↗ · Lihat sumber
Aturan desainnya ketat dan itulah inti utamanya: mesin kapsul tidak memiliki lokomosi apa pun. Tidak ada berjalan, berlari, melompat, gesekan, batas kecepatan maksimum, bahkan gravitasi. Mesin ini hanya mengintegrasikan kapsul kinematik terhadap medan, tidak lebih. Setiap perilaku lokomosi—berjalan, meluncur di permukaan, melayang, memanjat, berenang, merunduk, stamina—berada dalam pengontrol mandiri yang didaftarkan ke mesin. Pada setiap frame, mesin menjalankan tick untuk setiap pengontrol, menanyakan apakah masing-masing ingin mengambil kendali, lalu mengizinkan pengklaim dengan prioritas tertinggi menulis kecepatan. Berjalan tidak memiliki status khusus. Ia hanyalah pengontrol berprioritas paling rendah yang selalu menjawab ya, sehingga menjadi perilaku default ketika tidak ada yang lain aktif. Sebuah pengontrol terdiri dari enam fungsi kecil: tick yang memperbarui state internalnya sendiri pada setiap frame meskipun sedang tidak aktif, predikat murni wantsControl yang mengklaim frame, applyForces yang hanya dijalankan oleh pemenang untuk menulis kecepatan dan menerapkan gravitasinya sendiri jika diperlukan, ditambah onEnter, onExit, dan stateName yang bersifat opsional. Pengontrol berenang mengembalikan ownsCollision: true dari applyForces untuk mengambil alih penanganan medan, karena jika tidak, pegas daya apungnya akan melawan foot-snap mesin.
Bahkan hal-hal yang bukan lokomosi tetap merembes melalui kontrak tersebut, dan itulah yang memaksa munculnya gagasan berikutnya. Bagian yang membuat arsitektur ini benar-benar bernilai adalah kanal. Desain kanal tunggal sebelumnya menjalankan semuanya melalui satu arbitrase, yang berarti pelacak stamina atau posisi merunduk harus berpura-pura menjadi lokomosi, lalu menolak kendali dengan trik wantsControl yang mengembalikan false hanya agar dapat menjalankan pembukuannya. Solusinya adalah membagi pengontrol ke dalam kanal bernama yang melakukan arbitrase secara independen dan diterapkan dalam urutan tetap: resource, lalu stance, lalu locomotion. Resource berjalan lebih dahulu karena hasil tulisannya, seperti pengurangan stamina, dibaca oleh yang lain. Stance berjalan kedua karena pengurangan tinggi kapsul saat merunduk harus sudah diterapkan sebelum walk membacanya untuk membatasi kecepatan maksimum. Locomotion berjalan terakhir dan memiliki hak atas penulisan kecepatan per frame. Dengan demikian, pengamat stamina, pengubah posisi merunduk, dan pengontrol berenang yang aktif dapat hidup berdampingan dengan bersih, masing-masing di kanalnya sendiri, tanpa ada pengontrol yang harus berbohong tentang identitasnya. Demo memvisualisasikan semua ini dengan kapsul polos yang berubah warna berdasarkan pengontrol aktif, sehingga Anda dapat menyaksikan arbitrase berlangsung: walk berwarna hijau berubah menjadi slide berwarna jingga di lereng curam, menjadi swim berwarna sian di danau, lalu menjadi glide berwarna krem di udara saat Anda mengetuk tombol melayang.
Satu mesin, tiga tubuh
Ujian sesungguhnya bagi prinsip "tidak memiliki lokomosi" bukanlah pemain. Ujiannya adalah apakah mesin yang sama, tanpa disentuh, dapat menggerakkan sesuatu yang sama sekali bukan pemain. createCapsuleEngine adalah factory murni tanpa state tingkat modul, tanpa singleton, dan tanpa efek samping per instance, sehingga spike ini membuat tiga instance darinya. Pemain adalah satu instance dengan set pengontrol lengkap. Kuda tunggangan adalah instance kedua yang hanya mendaftarkan pengontrol berjalan, membuat keseluruhan gagasan tersebut benar-benar harfiah: kosakata pergerakan suatu tubuh hanyalah kumpulan pengontrol yang Anda daftarkan. Karena itu, kuda lebih cepat di permukaan datar dan secara fisik tidak dapat memanjat, berenang, atau melayang karena pengontrol tersebut tidak pernah ditambahkan. NPC otonom adalah instance ketiga, yang dijalankan tick-nya pada setiap frame bersama pemain dan digerakkan oleh pengontrol berkeliaran yang menyintesis inputnya sendiri, sehingga tubuh tersebut dapat mengarahkan dirinya tanpa ada tangan di keyboard. Mesin tidak pernah mengetahui bahwa salah satu tubuhnya adalah kuda atau tubuh lainnya dikendalikan AI. Semuanya merupakan integrator kapsul yang sama dengan daftar pengontrol berbeda.
Menunggang adalah satu-satunya bagian yang sengaja berada di luar mesin. Menukar kendali antara pemain dan kuda berarti mengoordinasikan dua mesin, sementara sebuah pengontrol berjalan di dalam satu mesin dan tidak dapat melihat melampaui batas tersebut. Karena itu, logika menunggang berada di tingkat host: sebuah state machine kecil menentukan mesin mana yang dijalankan pada frame ini, membekukan mesin lainnya, menggeser pengendara dengan mulus ke pelana menggunakan smoothstep selama tiga perempat detik, dan memberi tahu kamera tubuh mana yang harus diikuti. Factory mesin tidak pernah mengetahui apa pun tentang semua itu. Itulah batas yang ditetapkan dan dipertahankan arsitektur ini. Perilaku yang dimiliki satu tubuh adalah pengontrol; koordinasi antartubuh adalah tugas host. Pemisahan ini membuat penambahan tubuh keempat maupun keempat puluh tidak memerlukan biaya arsitektur baru.
Diuji tanpa browser
Karena mesin tidak mengakses window, document, maupun Three.js, semuanya dapat berjalan secara headless. Spike ini menyertakan harness Node yang membuat mock antarmuka medan, menjalankan mesin frame demi frame dengan input terprogram, lalu membuat assertion terhadap state yang dihasilkan. Dengan demikian, regresi yang sangat sulit ditemukan melalui play-testing dapat ditangkap oleh skrip: transisi melompat-dan-mendarat, ambang masuk dan keluar dari berenang, status memanjat yang otomatis dibersihkan ketika permukaan menjadi datar, serta laju pengurasan stamina. Memberikan urutan input dan timestep yang sama sebanyak dua kali, lalu memeriksa state keluaran yang identik hingga tingkat byte, memastikan mesin bersifat deterministik—properti yang pada akhirnya akan diandalkan oleh build berjaringan. Salah satu regresi yang ditemukan harness ini layak dipertahankan sebagai pengujian: berpindah dari tanah yang dapat dilalui ke lereng yang terlalu curam untuk dilalui dahulu mengaktifkan pengontrol slide dan memantulkan pemain kembali ke atas bukit. Solusinya adalah gerbang penurunan. Slide hanya dimulai jika kapsul benar-benar sedang jatuh ke lereng. Dengan demikian, berjalan secara horizontal ke permukaan curam kini terhalang dengan bersih alih-alih membuat pemain meluncur, dan pengujian yang menegaskan "berjalan menuju lereng yang tidak dapat dilalui menghalangi pemain" memastikan masalah itu tetap teratasi.
Memasang avatar nyata tanpa menyentuh pengontrol
Buka Spike 59 di tab baru ↗ · Lihat sumber
Spike 59 menguji apakah lapisan pengontrol benar-benar terpisah dari tubuh. Spike ini mengganti kapsul berwarna dengan karakter skeletal nyata, yaitu rig FBX 3MIKE yang dipasangi klip Quaternius Universal Animation Library yang telah di-retarget, dan pengontrolnya sama sekali tidak berubah. Titik penghubungnya hanyalah satu string. Setiap pengontrol telah melaporkan nama state melalui stateName—idle, walk, run, jump, fall, land, slide, glide, swim, swimIdle—lalu lapisan avatar memetakan nama tersebut ke klip hasil retargeting melalui tabel alias dan melakukan crossfade ketika berganti. Walk memilih sub-state-nya sendiri secara dinamis berdasarkan status grounded, kecepatan vertikal, dan kecepatan horizontal. Dengan demikian, satu pengontrol walk menggerakkan idle, walk, run, jump, fall, dan land, sementara avatar hanya mengikuti nama yang dilaporkan. Karena spike ini hanya untuk pemain tunggal, avatar membuang lapisan tidak langsung berupa integer untuk transmisi dan abstraksi multikarakter dari spike berjaringan sebelumnya, lalu memetakan nama state langsung ke klip. Buktinya adalah seluruh peningkatan visual dari kapsul menjadi manusia ber-rig tidak menyentuh satu baris pun kode lokomosi, dan itulah tepatnya manfaat yang seharusnya diberikan oleh pengontrol yang dapat dipasang.
Paket yang tidak memerlukan retargeting, dan pemilih yang dapat diuji
Buka Spike 60 di tab baru ↗ · Lihat sumber
Spike 60 memasang tubuh ketiga, paket POLYGON Base Locomotion dari Synty, dan loader-nya nyaris tidak melakukan apa pun. Setiap klip disediakan sebagai FBX mandiri yang membawa salinan tertanam dari skeleton Synty yang sama beserta satu animasi baked. Karena rig karakter dan setiap klip menggunakan nama bone yang identik, Anda cukup mengambil klip dari fbx.animations[0] lalu memutarnya langsung pada mixer karakter tanpa melibatkan library retargeting. Three.js mencari target track animasi berdasarkan nama bone, bukan identitas objek, sehingga paket bergaya Synty atau Mixamo yang dibuat untuk rig yang cocok langsung berfungsi. Ini sengaja dikontraskan dengan jalur UAL dari spike sebelumnya, yang membutuhkan retargeting berat karena klip sumber dan rig target dibuat menggunakan skeleton berbeda. Pengontrol yang sama, titik penghubung nama state yang sama, tetapi dua pipeline animasi yang sepenuhnya berbeda di belakangnya.
Bagian lain dari spike 60 adalah membuat logika pemilihan klip dapat diuji. Pemilihan klip yang akan diputar dipenuhi ambang berbasis pertimbangan, dan logika tersebut sebelumnya terkubur di dalam lapisan avatar di samping FBXLoader dan DOM sehingga tidak dapat diuji. Spike ini mengekstrak pemilih tersebut menjadi fungsi-fungsi murni yang tidak menyentuh Three.js maupun window: fungsi itu menerima record pemain biasa (kecepatan, kecepatan horizontal, arah hadap, grounded, normal tanah, kecepatan benturan) serta pengganti clip-action, lalu mengembalikan string alias klip. Dengan demikian, ambangnya dapat hidup sebagai konstanta bernama yang diverifikasi melalui assertion. Lompatan dibagi menjadi varian berjalan, berlari, dan sprint berdasarkan kelompok kecepatan yang diselaraskan dengan ambang sprint aktual milik pengontrol walk; pendaratan dibagi menjadi ringan, sedang, dan keras berdasarkan kecepatan benturan; varian klip menanjak dan menurun dipicu oleh dot product proyeksi lereng dengan deadzone klip datar di bawah sekitar tujuh derajat; transisi idle-ke-lokomosi selalu memilih arah maju agar awal gerakan dari posisi berdiri tidak pernah memutar klip secara terbalik; dan transisi berhenti dibatasi oleh waktu minimum berada dalam loop karena klip berhenti berbasis fase kaki milik Synty berisi sekitar satu detik perlambatan bawaan yang tampak konyol jika ditempelkan pada langkah yang nyaris tidak dilakukan pemain. Debouncer state kecil menunda state fall selama beberapa frame agar hilangnya kontak tanah selama satu frame saat melewati sambungan tidak membuat animasi berkedip. Mengeluarkan semua itu dari shell rendering membuat aturannya dapat diuji dengan unit test tanpa GPU, menggunakan disiplin headless yang sama seperti yang diterapkan pada mesin di spike 58. Inilah perbedaan antara nuansa lokomosi yang disetel dengan menebak-nebak dan nuansa lokomosi yang dapat ditetapkan secara pasti.
Teknologi yang dirujuk dalam bab ini
Mesin fisika tanpa lokomosi. Mesin kapsul mengintegrasikan tubuh kinematik terhadap medan dan tidak memiliki perilaku berjalan, berlari, melompat, gesekan, batas kecepatan, maupun gravitasi. Setiap perilaku merupakan pengontrol terdaftar yang mengekspos tick, wantsControl, applyForces, serta onEnter/onExit/stateName yang bersifat opsional. Walk hanyalah default berprioritas paling rendah yang selalu menjawab ya, dan sebuah pengontrol dapat mengembalikan ownsCollision: true untuk mengambil alih penanganan medan (swim melakukannya agar pegas daya apungnya tidak melawan foot-snap).
Kanal arbitrase independen. Pengontrol didaftarkan ke kanal bernama (resource, stance, locomotion) yang melakukan arbitrase secara terpisah dan diterapkan dalam urutan tetap. Dengan demikian, pengamat seperti stamina dan pengubah seperti crouch dapat hidup berdampingan dengan lokomosi aktif alih-alih berpura-pura mengambil kendali lalu menolaknya. Hasil penulisan resource (stamina) dibaca oleh stance dan locomotion; hasil penulisan stance (tinggi kapsul saat crouch) dibaca oleh batas kecepatan locomotion; locomotion berjalan terakhir dan memiliki hak atas penulisan kecepatan. Satu factory, banyak tubuh. createCapsuleEngine adalah factory murni tanpa singleton, sehingga engine yang sama dapat menggerakkan pemain, kuda tunggangan yang hanya bisa berjalan, dan NPC yang mengarahkan dirinya sendiri menggunakan input sintetis. Kemampuan gerak sebuah tubuh ditentukan sepenuhnya oleh kumpulan controller yang didaftarkan, sehingga kuda tidak dapat memanjat atau berenang karena controller tersebut tidak pernah ditambahkan. Logika menaiki tunggangan berada di level host, bukan di dalam controller, karena logika ini mengoordinasikan dua engine yang tidak dapat saling mengakses. Lihat LOD berbasis GPU.
Pengujian headless yang deterministik. Engine tidak mengakses window, document, ataupun Three.js, sehingga harness Node dapat menjalankannya frame demi frame dan menguji aksi melompat-dan-mendarat, ambang batas berenang, penghentian otomatis saat memanjat, serta pengurasan stamina. Memutar ulang input yang sama dua kali dan memeriksa apakah output-nya identik membuktikan determinisme, sementara sebuah uji regresi mengunci descent gate yang mencegah pemain terdorong mundur ketika berjalan horizontal menuju permukaan curam.
Binding avatar yang independen dari tubuh dengan picker yang dapat diuji. Controller melaporkan string nama state dan lapisan visual memetakannya ke clip, sehingga mengganti kapsul dengan avatar 3MIKE + UAL yang telah di-retarget tidak mengubah kode locomotion sama sekali. Clip Synty POLYGON menggunakan nama tulang yang sama dengan rig dan dapat diputar tanpa retargeting (Three.js mengikat track berdasarkan nama tulang), berbeda dengan jalur UAL yang memerlukan proses retargeting berat. Clip picker dipisahkan menjadi fungsi-fungsi murni dengan konstanta bernama untuk kelompok kecepatan lompatan, tingkat keparahan pendaratan, varian proyeksi kemiringan, idle bridge yang selalu mengarah ke depan, pengaman waktu minimum stop bridge, dan debouncer untuk state jatuh, sehingga nuansa locomotion dapat diuji secara unit, bukan sekadar ditebak.
Bagian 29 dari 30. Sebelumnya: Bagian 28 - Rumput hingga cakrawala, dan tanah yang menyembunyikan dirinya sendiri Berikutnya: Bagian 30 - Kamera yang menghormati dinding Panduan seri: /blog/2026-02-25-open-world-browser-series-guide