Dengan Node.js berkembang pesat hari ini, kami sudah boleh menggunakannya untuk melakukan pelbagai perkara. Beberapa waktu lalu, pemilik UP telah mengambil bahagian dalam acara Geek Song Dalam acara ini, kami menyasarkan untuk membuat permainan yang membolehkan "orang rendah" untuk berkomunikasi dengan lebih banyak Fungsi teras ialah interaksi berbilang pemain masa nyata konsep Parti Lan. Pertandingan Geek Pine hanya berlangsung selama 36 jam yang singkat dan memerlukan segala-galanya dengan pantas dan pantas. Di bawah premis sedemikian, persiapan awal kelihatan agak "semula jadi". Sebagai penyelesaian untuk aplikasi merentas platform, kami memilih nod-webkit, yang cukup mudah dan memenuhi keperluan kami.
Mengikut keperluan, pembangunan kami boleh dijalankan secara berasingan mengikut modul. Artikel ini menerangkan secara terperinci proses membangunkan Spaceroom (rangka kerja permainan berbilang pemain masa nyata kami), termasuk satu siri penerokaan dan percubaan, serta menyelesaikan beberapa pengehadan platform Node.js dan WebKit itu sendiri, dan mencadangkan penyelesaian.
Bermula
Ruang angkasa sekilas pandang
Sejak awal lagi, reka bentuk Spaceroom pastinya dipacu keperluan. Kami berharap rangka kerja ini dapat menyediakan fungsi asas berikut:
Dapat membezakan sekumpulan pengguna dalam unit bilik (atau saluran)
Dapat menerima arahan daripada pengguna dalam kumpulan koleksi
Penyegerakan masa antara setiap pelanggan boleh menyiarkan data permainan dengan tepat mengikut selang waktu yang ditentukan
Boleh meminimumkan impak yang disebabkan oleh kelewatan rangkaian
Sudah tentu, pada peringkat akhir pengekodan, kami menyediakan lebih banyak fungsi untuk Spaceroom, termasuk menjeda permainan, menjana nombor rawak yang konsisten antara pelbagai pelanggan, dll. (Sudah tentu, ini boleh dilaksanakan dalam rangka kerja logik permainan mengikut keperluan, tetapi tidak semestinya Anda perlu menggunakan Spaceroom, rangka kerja yang lebih berfungsi pada tahap komunikasi).
API
Ruang angkasa terbahagi kepada dua bahagian: bahagian hadapan dan hujung belakang. Kerja yang diperlukan di bahagian pelayan termasuk mengekalkan senarai bilik dan menyediakan fungsi mencipta bilik dan menyertai bilik. API pelanggan kami kelihatan seperti ini:
ruang ruang.sambung(alamat, panggil balik) – sambung ke pelayan
spaceroom.createRoom(callback) – Buat bilik
spaceroom.joinRoom(roomId) – sertai bilik
spaceroom.on(event, callback) – dengar acara
…
Selepas pelanggan menyambung ke pelayan, ia akan menerima pelbagai acara. Sebagai contoh, pengguna dalam bilik mungkin menerima acara yang telah disertai pemain baharu atau permainan telah bermula. Kami telah memberikan "kitaran hayat" kepada pelanggan dan ia akan berada dalam salah satu daripada keadaan berikut pada bila-bila masa:
Anda boleh mendapatkan keadaan semasa pelanggan melalui spaceroom.state.
Menggunakan rangka kerja sebelah pelayan agak mudah Jika anda menggunakan fail konfigurasi lalai, anda boleh menjalankan rangka kerja sebelah pelayan secara langsung. Kami mempunyai keperluan asas: kod pelayan boleh dijalankan terus dalam klien, tanpa memerlukan pelayan yang berasingan. Anda yang pernah bermain di PS atau PSP pasti tahu apa yang saya maksudkan. Sudah tentu, ia boleh dijalankan pada pelayan khusus, yang sememangnya sangat baik.
Pelaksanaan kod logik dipermudahkan di sini. Generasi pertama Spaceroom melengkapkan fungsi pelayan Socket Ia mengekalkan senarai bilik, termasuk status bilik, dan komunikasi masa permainan (koleksi arahan, siaran baldi, dll.) yang sepadan dengan setiap bilik. Untuk pelaksanaan khusus, sila rujuk kod sumber.
Algoritma penyegerakan
Jadi, bagaimanakah kita boleh menjadikan perkara yang dipaparkan antara setiap pelanggan konsisten dalam masa nyata?
Perkara ini kelihatan menarik. Fikirkan dengan teliti, apakah yang kita perlukan pelayan untuk membantu kita lulus? Adalah wajar untuk memikirkan perkara yang boleh menyebabkan ketidakkonsistenan logik antara pelbagai pelanggan: arahan pengguna. Memandangkan kod yang mengendalikan logik permainan semuanya sama, memandangkan keadaan yang sama, keputusan kod akan sama. Satu-satunya perbezaan ialah pelbagai arahan pemain yang diterima semasa permainan. Sudah tentu, kita memerlukan cara untuk menyegerakkan arahan ini. Jika semua pelanggan boleh mendapatkan arahan yang sama, maka semua pelanggan secara teorinya boleh mempunyai keputusan yang sama.
Algoritma penyegerakan permainan dalam talian adalah semua jenis pelik, dan senario yang berkenaan juga berbeza. Algoritma penyegerakan yang digunakan oleh Spaceroom adalah serupa dengan konsep kunci bingkai. Kami membahagikan garis masa kepada selang, dan setiap selang dipanggil baldi. Baldi digunakan untuk memuatkan arahan dan diselenggara oleh pelayan. Pada akhir setiap tempoh masa baldi, pelayan menyiarkan baldi kepada semua pelanggan Selepas klien mendapat baldi, ia mengambil arahan daripadanya dan melaksanakannya selepas pengesahan.
Untuk mengurangkan kesan kelewatan rangkaian, setiap arahan yang diterima oleh pelayan daripada klien akan dihantar ke baldi yang sepadan mengikut algoritma tertentu, secara khusus, ikuti langkah berikut:
Andaikan order_start ialah masa kejadian arahan yang dibawa oleh arahan, dan t ialah masa mula baldi di mana order_start terletak
Jika t delay_time <= masa mula baldi sedang mengumpulkan arahan, hantar arahan kepada baldi yang sedang mengumpulkan arahan, jika tidak, teruskan ke langkah 3
Hantar arahan kepada baldi yang sepadan dengan t delay_time
Antaranya, delay_time ialah masa tunda pelayan yang dipersetujui, yang boleh diambil sebagai purata kelewatan antara pelanggan Nilai lalai dalam Spaceroom ialah 80, dan nilai lalai bagi panjang baldi ialah 48. Pada akhir setiap tempoh masa baldi,. pelayan menyiarkan baldi ini kepada Semua pelanggan mula menerima arahan untuk baldi seterusnya. Pelanggan secara automatik melakukan pelarasan masa dalam logik berdasarkan selang baldi yang diterima untuk mengawal ralat masa dalam julat yang boleh diterima.
Ini bermakna dalam keadaan biasa, pelanggan akan menerima baldi daripada pelayan setiap 48ms Apabila masa untuk memproses baldi dicapai, pelanggan akan memprosesnya dengan sewajarnya. Dengan mengandaikan bahawa klien FPS=60, baldi akan diterima setiap 3 bingkai atau lebih, dan logik akan dikemas kini berdasarkan baldi ini. Jika baldi tidak diterima selepas masa melebihi kerana turun naik rangkaian, pelanggan menggantung logik permainan dan menunggu. Dalam baldi, kemas kini logik boleh menggunakan kaedah lerp.
Dalam kes delay_time = 80, bucket_size = 48, sebarang arahan akan ditangguhkan selama sekurang-kurangnya 96ms. Menukar kedua-dua parameter ini, sebagai contoh, dalam kes delay_time = 60, bucket_size = 32, sebarang arahan akan ditangguhkan selama sekurang-kurangnya 64ms.
Pembunuhan yang disebabkan oleh pemasa
Melihat keseluruhannya, rangka kerja kami perlu mempunyai pemasa yang tepat semasa berjalan. Laksanakan siaran baldi pada selang masa yang tetap. Sudah tentu, kami mula-mula terfikir untuk menggunakan setInterval(), tetapi detik seterusnya kami menyedari betapa tidak boleh dipercayai idea ini: setInterval() yang nakal kelihatan mempunyai ralat yang sangat serius. Dan perkara yang mengerikan ialah setiap kesilapan akan terkumpul, menyebabkan akibat yang lebih serius.
Jadi kami segera terfikir untuk menggunakan setTimeout() untuk membetulkan masa ketibaan seterusnya secara dinamik untuk memastikan logik kami stabil secara kasar sekitar selang waktu yang ditentukan. Sebagai contoh, setTimeout() kali ini adalah 5ms kurang daripada yang dijangkakan, maka kami akan menjadikannya 5ms lebih awal pada masa hadapan Walau bagaimanapun, keputusan ujian tidak memuaskan, dan ini tidak cukup elegan.
Jadi kita kena ubah pemikiran kita semula. Adakah mungkin untuk membuat setTimeout() tamat secepat mungkin dan kemudian kami menyemak sama ada masa semasa mencapai masa sasaran. Sebagai contoh, dalam gelung kami, menggunakan setTimeout(panggilan balik, 1) untuk sentiasa menyemak masa kelihatan seperti idea yang baik.
Pemasa yang mengecewakan
Kami segera menulis sekeping kod untuk menguji idea kami, dan hasilnya mengecewakan. Dalam versi stabil terkini node.js (v0.10.32) dan platform Windows, jalankan kod ini:
Seperti yang dijangkakan! Melihat pada manual node.js, kita boleh melihat perihalan setTimeout ini:
Kelewatan sebenar bergantung pada faktor luaran seperti kebutiran pemasa OS dan beban sistem.
Walau bagaimanapun, keputusan ujian menunjukkan bahawa kelewatan sebenar ini ialah selang pemasa maksimum (perhatikan bahawa selang pemasa semasa sistem pada masa ini hanya 1.001ms), yang tidak boleh diterima lagi, rasa ingin tahu kami yang kuat mendorong kami untuk melihat kod sumber daripada node.js.
BUG dalam Node.js
Saya percaya bahawa sebahagian besar daripada anda dan saya mempunyai pemahaman tertentu tentang mekanisme gelung genap Node.js Melihat pada kod sumber pelaksanaan pemasa, kita boleh memahami secara kasar prinsip pelaksanaan pemasa gelung gelung peristiwa:
Pelaksanaan dalaman fungsi ini menggunakan fungsi Windows GetTickCount() untuk menetapkan masa semasa. Ringkasnya, selepas memanggil fungsi setTimeout, selepas beberapa siri perjuangan, pemasa dalaman->due akan ditetapkan kepada tamat masa gelung semasa. Dalam gelung acara, mula-mula kemas kini masa gelung semasa melalui uv_update_time, dan kemudian semak sama ada pemasa telah tamat tempoh dalam uv_process_timers. Jika ya, masukkan dunia JavaScript. Selepas membaca keseluruhan artikel, gelung acara mungkin mempunyai proses ini:
Kemas kini masa global
Semak pemasa, jika mana-mana pemasa tamat tempoh, laksanakan panggilan balik
Semak baris gilir reqs dan laksanakan permintaan yang belum selesai
Masukkan fungsi tinjauan pendapat untuk mengumpul acara IO Jika acara IO tiba, tambahkan fungsi pemprosesan yang sepadan pada baris gilir reqs untuk pelaksanaan dalam gelung acara seterusnya. Di dalam fungsi tinjauan pendapat, kaedah sistem dipanggil untuk mengumpul peristiwa IO. Kaedah ini akan menyekat proses sehingga peristiwa IO tiba atau tamat masa yang ditetapkan dicapai. Apabila kaedah ini dipanggil, tamat masa ditetapkan kepada masa tamat pemasa terkini. Ini bermakna pengumpulan acara IO disekat, dan masa penyekatan maksimum ialah masa tamat pemasa seterusnya.
Kod sumber salah satu fungsi tinjauan pendapat di bawah Windows:
Mengikuti langkah di atas, dengan mengandaikan kita menetapkan pemasa dengan tamat masa = 1ms, fungsi tinjauan pendapat akan menyekat sehingga 1ms dan kemudian menyambung semula (jika tiada acara IO dalam tempoh tersebut). Apabila terus memasuki gelung acara, uv_update_time akan mengemas kini masa, dan kemudian uv_process_timers akan mendapati bahawa pemasa kami telah tamat tempoh dan melaksanakan panggilan balik. Jadi analisis awal ialah sama ada terdapat masalah dengan uv_update_time (masa semasa tidak dikemas kini dengan betul), atau fungsi tinjauan pendapat menunggu selama 1ms dan kemudian pulih, dan terdapat sesuatu yang tidak kena dengan menunggu 1ms ini.
Mencari MSDN, secara mengejutkan kami menemui penerangan tentang fungsi GetTickCount:
Leraian fungsi GetTickCount terhad kepada resolusi pemasa sistem, yang biasanya dalam julat 10 milisaat hingga 16 milisaat.
Ketepatan GetTickCount sangat kasar! Andaikan bahawa fungsi tinjauan pendapat menyekat dengan betul selama 1ms, tetapi kali seterusnya uv_update_time dilaksanakan, masa gelung semasa tidak dikemas kini dengan betul! Jadi pemasa kami tidak dinilai telah tamat tempoh, jadi tinjauan menunggu 1ms lagi dan memasuki gelung acara seterusnya. Sehinggalah GetTickCount akhirnya dikemas kini dengan betul (kononnya dikemas kini setiap 15.625ms) dan masa semasa gelung dikemas kini barulah pemasa kami dinilai telah tamat tempoh dalam uv_process_timers.
Minta bantuan WebKit
Kod sumber Node.js ini sangat tidak berdaya: ia menggunakan fungsi masa ketepatan rendah tanpa sebarang pemprosesan. Tetapi kami serta-merta berfikir bahawa kerana kami menggunakan Node-WebKit, sebagai tambahan kepada setTimeout Node.js, kami juga mempunyai setTimeout Chromium. Tulis kod ujian dan jalankan dengan penyemak imbas kami atau Node-WebKit: http://marks.lrednight.com/test.html#1 (Nombor berikut # menunjukkan selang yang perlu diukur) , hasilnya adalah seperti di bawah:
Mengikut spesifikasi HTML5, hasil teori hendaklah 1ms untuk 5 kali pertama dan 4ms untuk keputusan berikutnya. Keputusan yang dipaparkan dalam kes ujian bermula dari kali ketiga, yang bermaksud bahawa data pada jadual secara teorinya mestilah 1ms untuk tiga kali pertama, dan keputusan seterusnya adalah semua 4ms. Hasilnya mempunyai ralat tertentu, dan mengikut peraturan, hasil teori terkecil yang boleh kita perolehi ialah 4ms. Walaupun kami tidak berpuas hati, ini jelas lebih memuaskan daripada hasil node.js. Aliran Rasa Ingin Tahu Berkuasa Mari kita lihat kod sumber Chromium untuk melihat cara ia dilaksanakan. (https://chromium.googlesource.com/chromium/src.git/ /38.0.2125.101/base/time/time_win.cc)
Pertama, untuk menentukan masa semasa gelung, Chromium menggunakan fungsi timeGetTime(). Berunding dengan MSDN, anda boleh mendapati bahawa ketepatan fungsi ini dipengaruhi oleh selang pemasa semasa sistem. Pada mesin ujian kami, secara teorinya ialah 1.001ms yang dinyatakan di atas. Walau bagaimanapun, secara lalai dalam sistem Windows, selang pemasa ialah nilai maksimumnya (15.625ms pada mesin ujian), melainkan aplikasi mengubah suai selang pemasa global.
Jika anda mengikuti berita dalam industri IT, anda pasti pernah melihat berita sebegitu. Nampaknya Chromium kami telah menetapkan selang pemasa sangat kecil! Nampaknya kita tidak perlu risau tentang selang pemasa sistem lagi? Jangan terlalu cepat gembira, pembaikan ini memberi tamparan. Malah, isu ini telah dibetulkan dalam Chrome 38. Adakah kita perlu menggunakan pembaikan Node-WebKit sebelumnya? Ini jelas tidak elegan dan menghalang kami daripada menggunakan versi Chromium yang lebih berprestasi.
Melihat lebih jauh pada kod sumber Chromium, kita dapati bahawa apabila terdapat pemasa dan tamat masa pemasa
其中,kMinTimerIntervalLowResMs = 4,kMinTimerIntervalHighResMs = 1。timeBeginPeriod 以及timeEndPeriod 是 Windows 提供的用来修改系系篜修改是接电源时,我们能拿到的最小的 selang pemasa 是1ms,而使用电池时,是4ms。由于我们的循环不断地调用了 setTimeout,根据 W3C 规范,最小的间范,最小的间隔,不是义,使是义,使一之,根据 W3C个对我们的影响不大。
又一个精度问题
回到开头,我们发现测试结果显示,setTimeout 的间隔并不是稳定在 4ms 的,而是在在在在在定在 4ms 的,而是在动。 .lrednight.com/test.html#48 测试结果也显示,间隔在 48ms 和 49ms 之间跳动。原因是,在 Chromium 和 Node.js 的 gelung peristiwa Windows 丅下下中中中的 Windows函数调用的精度,受当前系统的计时器影响。游戏逻辑的实现需要用到 requestAnimationFrame 函数(不停更新画布),这个函数可以帮我们小网计为 kMinTimerIntervalLowResMs(因为他需要一个16ms的计时器,触发了高精度计时器的要求)。测计使用电源的时候,系统的 selang pemasa 是 1ms,所以测试结果有 ±1ms 的误差。如果你的电脑没所以测试结果有 ±1ms 的误差。如果你的电脑没有紗脑没有紗被隔,运行上面那个#48的测试,max可能会到达48 16=64ms。
使用 Chromium 的 setTimeout 实现,我们可以将 setTimeout(fn, 1) 的误差控制在 4ms 左右,而 setTimeout(fn, 48) 的误差控制在 4ms 左右,而 setTimeout(fn, 48差)左右。于是,我们的心中有了一幅新的蓝图,它让我们的代码看起来像是这样:
Kod di atas membolehkan kita menunggu satu masa apabila ralat kurang daripada saiz baldi (saiz_baldi – sisihan) dan bukannya menyamai saiz_baldi secara langsung Walaupun ralat maksimum berlaku dengan kelewatan 46ms, menurut teori di atas. selang sebenar adalah kurang daripada 48ms. Selebihnya kami menggunakan kaedah menunggu yang sibuk untuk memastikan bahawa gameLoop kami dilaksanakan pada selang waktu yang cukup tepat.
Walaupun kami menyelesaikan masalah itu pada tahap tertentu menggunakan Chromium, ia jelas tidak cukup elegan.
Ingat permintaan asal kami? Kod sebelah pelayan kami sepatutnya boleh dijalankan secara bebas daripada klien Node-Webkit dan dijalankan terus pada komputer dengan persekitaran Node.js. Jika anda menjalankan kod di atas secara langsung, nilai sisihan ialah sekurang-kurangnya 16ms, bermakna dalam setiap 48ms, kita perlu menunggu 16ms. Penggunaan CPU terus meningkat.
Kejutan yang tidak dijangka
Ia benar-benar menjengkelkan Terdapat BUG yang begitu besar dalam Node.js, dan tiada siapa yang menyedarinya? Jawapannya sangat mengejutkan kami. BUG ini telah ditetapkan dalam v.0.11.3. Anda juga boleh melihat hasil yang diubah suai dengan melihat terus cawangan induk kod libuv. Kaedah khusus ialah menambah tamat masa pada masa semasa gelung selepas fungsi tinjauan pendapat menunggu untuk selesai. Dengan cara ini, walaupun GetTickCount tidak bertindak balas, selepas menunggu tinjauan pendapat, kami masih menambah masa menunggu ini. Jadi pemasa boleh tamat tempoh dengan lancar.
Dengan kata lain, masalah yang mengambil masa yang lama untuk diselesaikan telah diselesaikan dalam v.0.11.3. Namun, usaha kita tidak sia-sia. Kerana walaupun pengaruh fungsi GetTickCount dihapuskan, fungsi tinjauan pendapat itu sendiri juga dipengaruhi oleh pemasa sistem. Satu penyelesaian ialah menulis pemalam Node.js untuk menukar selang pemasa sistem.
Namun, untuk permainan kami kali ini, tetapan awal ialah tiada pelayan. Selepas pelanggan mencipta bilik, ia menjadi pelayan. Kod pelayan boleh dijalankan dalam persekitaran Node-WebKit, jadi isu pemasa di bawah sistem Windows bukanlah keutamaan tertinggi. Mengikut penyelesaian yang kami berikan di atas, hasilnya sudah cukup untuk memuaskan hati kami.
Tamat
Selepas menyelesaikan masalah pemasa, pada dasarnya tiada halangan untuk pelaksanaan rangka kerja kami. Kami menyediakan sokongan WebSocket (dalam persekitaran HTML5 tulen), dan juga menyesuaikan protokol komunikasi untuk melaksanakan sokongan Socket berprestasi lebih tinggi (dalam persekitaran Node-WebKit). Sudah tentu, fungsi Spaceroom agak asas pada mulanya, tetapi apabila permintaan meningkat dan masa meningkat, kami menambah baik rangka kerja secara beransur-ansur.
Sebagai contoh, apabila kami mendapati bahawa kami perlu menjana nombor rawak yang konsisten dalam permainan kami, kami menambahkan fungsi sedemikian pada Spaceroom. Spaceroom akan mengedarkan benih nombor rawak apabila permainan dimulakan Ruang Ruang pelanggan menyediakan kaedah untuk menggunakan rawak md5 untuk menjana nombor rawak dengan bantuan benih nombor rawak.
Nampak sangat gembira. Dalam proses menulis rangka kerja sebegini, banyak juga yang saya pelajari. Jika anda berminat dengan Spaceroom, anda juga boleh menyertainya. Saya percaya bahawa Spaceroom akan menunjukkan kekuatannya di lebih banyak tempat.